Se prevedi di effettuare solo richieste API standard, adatte alla maggior parte degli sviluppatori, puoi passare direttamente ai verdetti di integrità. Questa pagina descrive come effettuare richieste API classiche per i giudizi di integrità, che sono supportati su Android 4.4 (livello API 19) o versioni successive.
Considerazioni
Confrontare le richieste standard e quelle classiche
Puoi effettuare richieste standard, richieste classiche o una combinazione delle due, a seconda delle esigenze di sicurezza e anti-abuso della tua app. Le richieste standard sono adatte a tutte le app e a tutti i giochi e possono essere utilizzate per verificare che qualsiasi azione o chiamata al server sia autentica, delegando al contempo a Google Play una certa protezione contro la rigiocabilità e l'esfiltrazione. Le richieste classiche sono più costose da effettuare e è tua responsabilità implementarle correttamente per proteggerti dalla esfiltrazione e da determinati tipi di attacchi. Le richieste classiche devono essere effettuate meno frequentemente rispetto alle richieste standard, ad esempio come azione una tantum occasionale per verificare se un'azione di grande valore o sensibile è autentica.
La seguente tabella evidenzia le principali differenze tra i due tipi di richieste:
Richiesta API standard | Richiesta API classica | |
---|---|---|
Prerequisiti | ||
Versione minima dell'SDK Android richiesta | Android 5.0 (livello API 21) o versioni successive | Android 4.4 (livello API 19) o versioni successive |
Requisiti di Google Play | Google Play Store e Google Play Services | Google Play Store e Google Play Services |
Dettagli sull'integrazione | ||
È necessario il riscaldamento dell'API | ✔️ (pochi secondi) | ❌ |
Latenza di richiesta tipica | Poche centinaia di millisecondi | Pochi secondi |
Frequenza potenziale delle richieste | Frequente (controllo su richiesta per qualsiasi azione o richiesta) | Infrequenti (controllo una tantum per le azioni di valore più elevato o le richieste più sensibili) |
Timeout | La maggior parte dei warm-up dura meno di 10 secondi, ma prevede una chiamata al server, pertanto è consigliato un timeout lungo (ad es. 1 minuto). Le richieste di verdetto avvengono lato client | La maggior parte delle richieste dura meno di 10 secondi, ma prevede una chiamata al server, pertanto è consigliabile un timeout lungo (ad es.1 minuto). |
Token relativo all'esito relativo all'integrità | ||
Contiene dettagli su dispositivo, app e account | ✔️ | ✔️ |
Memorizzazione nella cache dei token | Memorizzazione nella cache sul dispositivo protetta da Google Play | Selezione sconsigliata |
Decrittografare e verificare il token tramite il server di Google Play | ✔️ | ✔️ |
Latenza tipica della richiesta di decrittografia server-to-server | 10 millisecondi con disponibilità a tre nove | 10 millisecondi con disponibilità a tre nove |
Decriptare e verificare il token localmente in un ambiente server sicuro | ❌ | ✔️ |
Decriptare e verificare il token lato client | ❌ | ❌ |
Aggiornamento dell'esito relativo all'integrità | Alcuni processi di memorizzazione nella cache e aggiornamento automatici da parte di Google Play | Tutti i giudizi vengono ricalcolati per ogni richiesta |
limiti | ||
Richieste per app al giorno | 10.000 per impostazione predefinita (è possibile richiedere un aumento) | 10.000 per impostazione predefinita (è possibile richiedere un aumento) |
Richieste per istanza di app al minuto | Attività di riscaldamento: 5 al minuto Token di integrità: nessun limite pubblico* |
Token di integrità: 5 al minuto |
Protezione | ||
Mitigare le manomissioni e gli attacchi simili | Utilizzare il campo requestHash |
Utilizzare il campo nonce con l'associazione dei contenuti in base ai dati della richiesta |
Mitigare gli attacchi di replay e simili | Mitigazione automatica da parte di Google Play | Utilizzare il campo nonce con la logica lato server |
* Tutte le richieste, incluse quelle senza limiti pubblici, sono soggette a valori elevati di limiti difensivi non pubblici
Invia richieste classiche di rado
La generazione di un token di integrità richiede tempo, dati e batteria e ogni app ha un numero massimo di richieste classiche che può effettuare al giorno. Pertanto, dovresti effettuare richieste classiche solo per verificare che le azioni più sensibili o di valore più elevato siano autentiche quando vuoi una garanzia aggiuntiva rispetto a una richiesta standard. Non dovresti effettuare richieste classiche per azioni ad alta frequenza o di basso valore. Non effettuare richieste classiche ogni volta che l'app passa in primo piano né ogni pochi minuti in background ed evita di effettuare chiamate da un numero elevato di dispositivi contemporaneamente. Un'app che effettua troppe chiamate di richieste classiche potrebbe essere limitata per proteggere gli utenti da implementazioni errate.
Evitare la memorizzazione nella cache dei verdetti
La memorizzazione nella cache di un verdetto aumenta il rischio di attacchi come l'esfiltrazione e la riproduzione, in cui un verdetto positivo viene riutilizzato da un ambiente non attendibile. Se stai valutando la possibilità di effettuare una richiesta classica e poi memorizzarla nella cache per utilizzarla in un secondo momento, ti consigliamo di eseguire una richiesta standard on demand. Le richieste standard comportano un po' di memorizzazione nella cache sul dispositivo, ma Google Play utilizza tecniche di protezione aggiuntive per ridurre il rischio di attacchi di replay ed esfiltrazione.
Utilizzare il campo nonce per proteggere le richieste classiche
L'API Play Integrity offre un campo denominato nonce
, che può essere utilizzato per difendere ulteriormente la tua app da determinati attacchi, come quelli di replay e di manomissione. L'API Play Integrity restituisce il valore impostato in questo campo all'interno della risposta di integrità firmata. Segui attentamente le indicazioni su come generare valori nonce per proteggere la tua app dagli attacchi.
Riprova le richieste classiche con backoff esponenziale
Le condizioni ambientali, ad esempio una connessione a internet instabile o un dispositivo sovraccaricato, possono causare l'interruzione dei controlli di integrità del dispositivo. Ciò può portare alla generazione di nessuna etichetta per un dispositivo altrimenti attendibile. Per mitigare questi scenari, includi un'opzione di nuovo tentativo con backoff esponenziale.
Panoramica
Quando l'utente esegue un'azione di alto valore nella tua app che vuoi proteggere con un controllo dell'integrità, svolgi i seguenti passaggi:
- Il backend lato server dell'app genera e invia un valore univoco alla logica lato client. Nei passaggi rimanenti, questa logica viene indicata come "app".
- La tua app crea il valore
nonce
dal valore univoco e dai contenuti della tua azione di alto valore. Quindi chiama l'API Play Integrity, passando il valorenonce
. - La tua app riceve un verdetto firmato e criptato dall'API Play Integrity.
- L'app passa il verdetto firmato e criptato al backend dell'app.
- Il backend dell'app invia il verdetto a un server di Google Play. Il server di Google Play decripta e verifica il verdetto, restituendo i risultati al backend della tua app.
- Il backend dell'app decide come procedere in base agli indicatori contenuti nel payload del token.
- Il backend dell'app invia i risultati della decisione all'app.
Genera un nonce
Quando proteggi un'azione nella tua app con l'API Play Integrity, puoi sfruttare il campo nonce
per mitigare determinati tipi di attacchi, come gli attacchi di manomissione con attacco intermediario (PITM) e gli attacchi di replay. L'API Play Integrity restituisce il valore impostato in questo campo all'interno della risposta di integrità firmata.
Il valore impostato nel campo nonce
deve essere formattato correttamente:
String
- Adatto per gli URL
- Codificata in Base64 e senza a capo
- Minimo 16 caratteri
- Massimo 500 caratteri
Di seguito sono riportati alcuni modi comuni per utilizzare il campo nonce
nell'API Play Integrity. Per ottenere la protezione più efficace da nonce
, puoi combinare i metodi riportati di seguito.
Includi un hash della richiesta per proteggerti da manomissioni
Puoi utilizzare il parametro nonce
in una richiesta API classica in modo simile al parametro requestHash
in una richiesta API standard per proteggere i contenuti di una richiesta da manomissioni.
Quando richiedi un esito relativo all'integrità:
- Calcola un digest di tutti i parametri di richiesta critici (ad es. SHA256 di una serializzazione della richiesta stabile) dall'azione utente o dalla richiesta del server in corso.
- Utilizza
setNonce
per impostare il campononce
sul valore del digest calcolato.
Quando ricevi un esito relativo all'integrità:
- Decodifica e verifica il token di integrità e ottieni il digest dal
nonce
campo. - Calcola un digest della richiesta nello stesso modo in cui viene fatto nell'app (ad es. SHA256 di una serializzazione della richiesta stabile).
- Confronta i digest lato app e lato server. Se non corrispondono, la richiesta non è attendibile.
Includi valori univoci per proteggerti dagli attacchi di replay
Per impedire agli utenti malintenzionati di riutilizzare le risposte precedenti dell'API Play Integrity, puoi utilizzare il campo nonce
per identificare in modo univoco ogni messaggio.
Quando richiedi un esito relativo all'integrità:
- Ottieni un valore univoco a livello globale in un modo che gli utenti malintenzionati non possono prevedere. Ad esempio, un numero casuale con crittografia sicura generato lato server può essere un valore di questo tipo oppure un ID preesistente, ad esempio una sessione o un ID transazione. Una variante più semplice e meno sicura consiste nel generare un numero random sul dispositivo. Ti consigliamo di creare valori di almeno 128 bit.
- Chiama
setNonce()
per impostare il campononce
sul valore univoco del passaggio 1.
Quando ricevi un esito relativo all'integrità:
- Decodifica e verifica il token di integrità e ottieni il valore univoco dal
nonce
campo. - Se il valore del passaggio 1 è stato generato sul server, controlla che il valore univoco ricevuto sia uno dei valori generati e che venga utilizzato per la prima volta (il server dovrà conservare un record dei valori generati per una durata adeguata). Se il valore univoco ricevuto è già stato utilizzato o non compare nel record, rifiuta la richiesta
- In caso contrario, se il valore univoco è stato generato sul dispositivo, verifica che il valore ricevuto venga utilizzato per la prima volta (il server deve conservare un record dei valori già visti per una durata adeguata). Se il valore unico ricevuto è già stato utilizzato, rifiuta la richiesta.
Combina entrambe le protezioni contro gli attacchi di manomissione e di replay (opzione consigliata)
È possibile utilizzare il campo nonce
per proteggersi contemporaneamente sia da attacchi di manomissione sia da attacchi di replay. A questo scopo, genera il valore univoco come descritto sopra e includilo nella richiesta. Poi calcola l'hash della richiesta, assicurandoti di includere il valore univoco nell'hash. Un'implementazione che combina entrambi gli approcci è la seguente:
Quando richiedi un esito relativo all'integrità:
- L'utente avvia l'azione di alto valore.
- Ottieni un valore univoco per questa azione come descritto nella sezione Includi valori unici per proteggerti dagli attacchi di replay.
- Prepara un messaggio che vuoi proteggere. Includi il valore univoco del passaggio 2 nel messaggio.
- L'app calcola un digest del messaggio che vuole proteggere, come descritto nella sezione Includi un hash della richiesta per proteggerti dalle manomissioni. Poiché il messaggio contiene il valore unico, questo fa parte dell'hash.
- Utilizza
setNonce()
per impostare il campononce
sul digest calcolato nel passaggio precedente.
Quando ricevi un esito relativo all'integrità:
- Ottieni il valore univoco dalla richiesta
- Decodifica e verifica il token di integrità e ottieni il digest dal
nonce
campo. - Come descritto nella sezione Includi un hash della richiesta per proteggerti dalle manomissioni, calcola di nuovo il digest lato server e verifica che corrisponda al digest ottenuto dal token di integrità.
- Come descritto nella sezione Includi valori univoci per proteggerti dagli attacchi di replay, controlla la validità del valore univoco.
Il seguente diagramma di sequenza illustra questi passaggi con un nonce
lato server:
Richiedere un esito relativo all'integrità
Dopo aver generato un nonce
, puoi richiedere un verdetto di integrità da Google Play. Per farlo, segui questi passaggi:
- Crea un
IntegrityManager
, come mostrato negli esempi seguenti. - Costruisci un
IntegrityTokenRequest
, fornendo ilnonce
tramite il metodosetNonce()
nel builder associato. Anche le app distribuite esclusivamente al di fuori di Google Play e gli SDK devono specificare il numero del progetto Google Cloud tramite il metodosetCloudProjectNumber()
. Le app su Google Play sono collegate a un progetto Cloud in Play Console e non è necessario impostare il numero del progetto Cloud nella richiesta. Utilizza il gestore per chiamare
requestIntegrityToken()
, fornendo il valoreIntegrityTokenRequest
.
Kotlin
// Receive the nonce from the secure server. val nonce: String = ... // Create an instance of a manager. val integrityManager = IntegrityManagerFactory.create(applicationContext) // Request the integrity token by providing a nonce. val integrityTokenResponse: Task<IntegrityTokenResponse> = integrityManager.requestIntegrityToken( IntegrityTokenRequest.builder() .setNonce(nonce) .build())
Java
import com.google.android.gms.tasks.Task; ... // Receive the nonce from the secure server. String nonce = ... // Create an instance of a manager. IntegrityManager integrityManager = IntegrityManagerFactory.create(getApplicationContext()); // Request the integrity token by providing a nonce. Task<IntegrityTokenResponse> integrityTokenResponse = integrityManager .requestIntegrityToken( IntegrityTokenRequest.builder().setNonce(nonce).build());
Unity
IEnumerator RequestIntegrityTokenCoroutine() { // Receive the nonce from the secure server. var nonce = ... // Create an instance of a manager. var integrityManager = new IntegrityManager(); // Request the integrity token by providing a nonce. var tokenRequest = new IntegrityTokenRequest(nonce); var requestIntegrityTokenOperation = integrityManager.RequestIntegrityToken(tokenRequest); // Wait for PlayAsyncOperation to complete. yield return requestIntegrityTokenOperation; // Check the resulting error code. if (requestIntegrityTokenOperation.Error != IntegrityErrorCode.NoError) { AppendStatusLog("IntegrityAsyncOperation failed with error: " + requestIntegrityTokenOperation.Error); yield break; } // Get the response. var tokenResponse = requestIntegrityTokenOperation.GetResult(); }
Unreal Engine
// .h void MyClass::OnRequestIntegrityTokenCompleted( EIntegrityErrorCode ErrorCode, UIntegrityTokenResponse* Response) { // Check the resulting error code. if (ErrorCode == EIntegrityErrorCode::Integrity_NO_ERROR) { // Get the token. FString Token = Response->Token; } } // .cpp void MyClass::RequestIntegrityToken() { // Receive the nonce from the secure server. FString Nonce = ... // Create the Integrity Token Request. FIntegrityTokenRequest Request = { Nonce }; // Create a delegate to bind the callback function. FIntegrityOperationCompletedDelegate Delegate; // Bind the completion handler (OnRequestIntegrityTokenCompleted) to the delegate. Delegate.BindDynamic(this, &MyClass::OnRequestIntegrityTokenCompleted); // Initiate the integrity token request, passing the delegate to handle the result. GetGameInstance() ->GetSubsystem<UIntegrityManager>() ->RequestIntegrityToken(Request, Delegate); }
Nativo
/// Create an IntegrityTokenRequest opaque object. const char* nonce = RequestNonceFromServer(); IntegrityTokenRequest* request; IntegrityTokenRequest_create(&request); IntegrityTokenRequest_setNonce(request, nonce); /// Prepare an IntegrityTokenResponse opaque type pointer and call /// IntegerityManager_requestIntegrityToken(). IntegrityTokenResponse* response; IntegrityErrorCode error_code = IntegrityManager_requestIntegrityToken(request, &response); /// ... /// Proceed to polling iff error_code == INTEGRITY_NO_ERROR if (error_code != INTEGRITY_NO_ERROR) { /// Remember to call the *_destroy() functions. return; } /// ... /// Use polling to wait for the async operation to complete. /// Note, the polling shouldn't block the thread where the IntegrityManager /// is running. IntegrityResponseStatus response_status; /// Check for error codes. IntegrityErrorCode error_code = IntegrityTokenResponse_getStatus(response, &response_status); if (error_code == INTEGRITY_NO_ERROR && response_status == INTEGRITY_RESPONSE_COMPLETED) { const char* integrity_token = IntegrityTokenResponse_getToken(response); SendTokenToServer(integrity_token); } /// ... /// Remember to free up resources. IntegrityTokenRequest_destroy(request); IntegrityTokenResponse_destroy(response); IntegrityManager_destroy();
Decripta e verifica l'esito relativo all'integrità
Quando richiedi un esito relativo all'integrità, l'API Play Integrity fornisce un token di risposta firmato. Il valore nonce
incluso nella richiesta diventa parte del
token di risposta.
Formato del token
Il token è un JSON Web Token (JWT) nidificato, ovvero JSON Web Encryption (JWE) di JSON Web Signature (JWS). I componenti JWE e JWS sono rappresentati utilizzando la serializzazione compatta.
Gli algoritmi di crittografia / firma sono ben supportati nelle varie implementazioni JWT:
Decripta e verifica sui server di Google (opzione consigliata)
L'API Play Integrity ti consente di decriptare e verificare il verdetto di integrità sui server di Google, migliorando la sicurezza della tua app. Per farlo, segui questi passaggi:
- Crea un account di servizio nel progetto Google Cloud collegato alla tua app.
Sul server dell'app, recupera il token di accesso dalle credenziali del tuo account di servizio utilizzando l'ambito
playintegrity
e fai la seguente richiesta:playintegrity.googleapis.com/v1/PACKAGE_NAME:decodeIntegrityToken -d \ '{ "integrity_token": "INTEGRITY_TOKEN" }'
Leggi la risposta JSON.
Decriptare e verificare in locale
Se scegli di gestire e scaricare le chiavi di crittografia delle risposte, puoi decriptare e verificare il token restituito all'interno del tuo ambiente server sicuro.
Puoi ottenere il token restituito utilizzando il metodo IntegrityTokenResponse#token()
.
L'esempio seguente mostra come decodificare la chiave AES e la chiave EC pubblica con codifica DER per la verifica della firma da Play Console alle chiavi specifiche per lingua (nel nostro caso il linguaggio di programmazione Java) nel backend dell'app. Tieni presente che le chiavi sono codificate in base64 utilizzando i flag predefiniti.
Kotlin
// base64OfEncodedDecryptionKey is provided through Play Console. var decryptionKeyBytes: ByteArray = Base64.decode(base64OfEncodedDecryptionKey, Base64.DEFAULT) // Deserialized encryption (symmetric) key. var decryptionKey: SecretKey = SecretKeySpec( decryptionKeyBytes, /* offset= */ 0, AES_KEY_SIZE_BYTES, AES_KEY_TYPE ) // base64OfEncodedVerificationKey is provided through Play Console. var encodedVerificationKey: ByteArray = Base64.decode(base64OfEncodedVerificationKey, Base64.DEFAULT) // Deserialized verification (public) key. var verificationKey: PublicKey = KeyFactory.getInstance(EC_KEY_TYPE) .generatePublic(X509EncodedKeySpec(encodedVerificationKey))
Java
// base64OfEncodedDecryptionKey is provided through Play Console. byte[] decryptionKeyBytes = Base64.decode(base64OfEncodedDecryptionKey, Base64.DEFAULT); // Deserialized encryption (symmetric) key. SecretKey decryptionKey = new SecretKeySpec( decryptionKeyBytes, /* offset= */ 0, AES_KEY_SIZE_BYTES, AES_KEY_TYPE); // base64OfEncodedVerificationKey is provided through Play Console. byte[] encodedVerificationKey = Base64.decode(base64OfEncodedVerificationKey, Base64.DEFAULT); // Deserialized verification (public) key. PublicKey verificationKey = KeyFactory.getInstance(EC_KEY_TYPE) .generatePublic(new X509EncodedKeySpec(encodedVerificationKey));
Successivamente, utilizza queste chiavi per decriptare prima il token di integrità (parte JWE) e poi verificare ed estrarre la parte JWS nidificata.
Kotlin
val jwe: JsonWebEncryption = JsonWebStructure.fromCompactSerialization(integrityToken) as JsonWebEncryption jwe.setKey(decryptionKey) // This also decrypts the JWE token. val compactJws: String = jwe.getPayload() val jws: JsonWebSignature = JsonWebStructure.fromCompactSerialization(compactJws) as JsonWebSignature jws.setKey(verificationKey) // This also verifies the signature. val payload: String = jws.getPayload()
Java
JsonWebEncryption jwe = (JsonWebEncryption)JsonWebStructure .fromCompactSerialization(integrityToken); jwe.setKey(decryptionKey); // This also decrypts the JWE token. String compactJws = jwe.getPayload(); JsonWebSignature jws = (JsonWebSignature) JsonWebStructure.fromCompactSerialization(compactJws); jws.setKey(verificationKey); // This also verifies the signature. String payload = jws.getPayload();
Il payload risultante è un token in testo normale che contiene verdict sull'integrità.