Se prevedi di effettuare solo richieste API standard, che sono adatte alla maggior parte degli sviluppatori, puoi passare direttamente ai verdetti sull'integrità. In questa pagina viene descritta l'esecuzione di richieste API classiche per esiti relativi all'integrità, supportate su Android 4.4 (livello API 19) o versioni successive.
considerazioni
Confronta le richieste standard e classiche
Puoi effettuare richieste standard, richieste classiche o una combinazione delle due a seconda delle esigenze di sicurezza e antiabuso della tua app. Le richieste standard sono adatte per tutti i giochi e le app e possono essere utilizzate per verificare che qualsiasi azione o chiamata al server sia autentica, delega una certa protezione contro la rigiocabilità e l'esfiltrazione a Google Play. Le richieste classiche sono più costose da fare ed è tua responsabilità implementarle correttamente per proteggerti dall'esfiltrazione e da determinati tipi di attacchi. Le richieste classiche dovrebbero essere effettuate meno frequenti rispetto a quelle standard, ad esempio una tantum, per verificare se un'azione sensibile o molto redditizia è 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 dell'integrazione | ||
Riscaldamento dell'API richiesto | ✔️ (pochi secondi) | ❌ |
Latenza tipica delle richieste | Poche centinaia di millisecondi | Pochi secondi |
Frequenza potenziale delle richieste | Frequente (controllo su richiesta per qualsiasi azione o richiesta) | Raro (controllo una tantum delle azioni di valore più elevato o della maggior parte delle richieste sensibili) |
Timeout | La maggior parte delle fasi di riscaldamento dura meno di 10 secondi ma prevede una chiamata al server, quindi è consigliato un timeout lungo (ad es. 1 minuto). Le richieste di verdetto avvengono lato client | La maggior parte delle richieste è inferiore a 10 secondi ma comporta una chiamata al server, quindi si consiglia un timeout lungo (ad es. 1 minuto) |
Token di esito dell'integrità | ||
Contiene i dettagli del dispositivo, dell'app e dell'account | ✔️ | ✔️ |
Memorizzazione nella cache dei token | Memorizzazione nella cache on-device protetta da Google Play | Selezione sconsigliata |
Decripta e verifica il token tramite il server di Google Play | ✔️ | ✔️ |
Latenza tipica delle richieste server-server di decriptazione | Decine di millisecondi con disponibilità di tre nove | Decine di millisecondi con disponibilità di tre nove |
Decripta e verifica il token localmente in un ambiente server sicuro | ❌ | ✔️ |
Decripta e verifica il token sul lato client | ❌ | ❌ |
Aggiornamento dell'esito relativo all'integrità | Memorizzazione nella cache e aggiornamento automatici da parte di Google Play | Tutti gli esiti sono 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 | Riscaldamento: 5 al minuto Token di integrità: nessun limite pubblico* |
Token di integrità: 5 al minuto |
Protezione | ||
Mitigare contro manomissioni e attacchi simili | Utilizza il campo requestHash |
Utilizza il campo nonce con associazione di contenuti basata sui dati della richiesta |
Mitigare contro le repliche e gli attacchi simili | Mitigazione automatica di Google Play | Utilizza il campo nonce con la logica lato server |
* Tutte le richieste, comprese quelle senza limiti pubblici, sono soggette a limiti difensivi non pubblici in valori elevati.
Effettua richieste classiche raramente
La generazione di un token di integrità richiede tempo, dati e batteria. Inoltre, ogni app ha un numero massimo di richieste classiche che può effettuare al giorno. Di conseguenza, dovresti effettuare richieste classiche per verificare il valore più alto o le azioni più sensibili sono autentiche solo quando vuoi una garanzia aggiuntiva per una richiesta standard. Non devi effettuare richieste classiche per azioni ad alta frequenza o di scarso valore. Non effettuare richieste classiche ogni volta che l'app passa in primo piano o a intervalli di 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.
Evita gli esiti della memorizzazione nella cache
Memorizzare un verdetto nella cache aumenta il rischio di attacchi come esfiltrazione e riproduzione, in cui un esito positivo viene riutilizzato da un ambiente non attendibile. Se stai prendendo in considerazione l'esecuzione di una richiesta classica e la memorizzazione nella cache per l'utilizzo 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 ripetizione 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
proteggere ulteriormente la tua app da determinati attacchi, come la ripetizione e
le manomissioni. L'API Play Integrity restituisce il valore impostato in questo campo, all'interno
della risposta relativa all'integrità firmata. Segui attentamente le indicazioni su come generare
nonce per proteggere la tua app da attacchi.
Riprova le richieste classiche con backoff esponenziale
Condizioni ambientali, come una connessione a internet instabile o un dispositivo sovraccarico, possono causare la mancata riuscita dei controlli di integrità del dispositivo. Ciò può comportare la generazione di etichette per un dispositivo altrimenti attendibile. Per mitigare questi scenari, includi un'opzione di nuovo tentativo con backoff esponenziale.
Panoramica
Quando l'utente esegue nella tua app un'azione di alto valore che vuoi proteggere con un controllo di integrità, completa i seguenti passaggi:
- Il backend lato server dell'app genera e invia un valore univoco alla logica lato client. I passaggi rimanenti fanno riferimento a questa logica come "app".
- L'app crea
nonce
dal valore univoco e dai contenuti dell'azione di alto valore. Quindi chiama l'API Play Integrity, passandononce
. - La tua app riceve un esito firmato e criptato dall'API Play Integrity.
- L'app trasmette l'esito firmato e criptato al relativo backend.
- Il backend dell'app invia l'esito a un server Google Play. Il server di Google Play decripta e verifica l'esito, restituendo i risultati al backend dell'app.
- Il backend della tua 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, ad esempio
le manomissioni degli attacchi PITM (person in the middle) e la riproduzione degli attacchi. 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
- Sicuro per URL
- Codificato come Base64 e senza wrapping
- 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 massima protezione da nonce
, puoi combinare
i metodi riportati di seguito.
Includi un hash della richiesta per proteggere 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 critici della richiesta (ad es. SHA256 di una serializzazione stabile) dall'azione utente o dalla richiesta del server in corso.
- Usa
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 campo
nonce
. - Calcola un digest della richiesta come nell'app (ad es. SHA256 di una serializzazione di richiesta stabile).
- Confronta le sintesi lato app e lato server. Se non corrispondono, la richiesta non è attendibile.
Includi valori univoci per la protezione dagli attacchi di ripetizione
Per impedire a 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 globalmente univoco in modo da non essere in grado di prevedere gli utenti malintenzionati. Ad esempio, un numero casuale con crittografia sicuro generato sul lato server può essere un valore di questo tipo o un ID preesistente, come una sessione o un ID transazione. Una variante più semplice e meno sicura è la generazione di un numero casuale sul dispositivo. Consigliamo di creare valori di almeno 128 bit.
- Richiama
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 campo
nonce
. - Se il valore del passaggio 1 è stato generato sul server, verifica 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 adatta). Se il valore univoco ricevuto è già stato utilizzato o non appare 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à visualizzati per una durata adatta). Se il valore univoco ricevuto è già stato utilizzato, rifiuta la richiesta.
Combina entrambe le protezioni contro gli attacchi di manomissione e ripetizione (opzione consigliata)
È possibile utilizzare il campo nonce
per proteggere contemporaneamente da attacchi di manomissione e ripetizione. Per farlo, genera il valore univoco descritto in precedenza e includilo nella richiesta. Poi calcola l'hash della richiesta, assicurandoti di includere il valore univoco come parte dell'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 univoci per proteggerti dagli attacchi di ripetizione.
- Prepara un messaggio che vuoi proteggere. Includi nel messaggio il valore univoco del passaggio 2.
- La tua app calcola una sintesi del messaggio che vuole proteggere, come descritto nella sezione Includi un hash della richiesta per proteggere dalle manomissione. Poiché il messaggio contiene il valore univoco, il valore univoco fa parte dell'hash.
- Utilizza
setNonce()
per impostare il campononce
sul digest calcolato dal passaggio precedente.
Quando ricevi un esito relativo all'integrità:
- Ottieni il valore unico dalla richiesta
- Decodifica e verifica il token di integrità e ottieni il digest dal campo
nonce
. - Come descritto nella sezione Includere un hash della richiesta per la protezione dalle manomissioni, ricalcola il digest sul lato server e controlla che corrisponda al digest ottenuto dal token di integrità.
- Come descritto nella sezione Includi valori univoci per proteggerti dagli attacchi di ripetizione, controlla la validità del valore univoco.
Il seguente diagramma di sequenza illustra questi passaggi con un nonce
lato server:
Richiedi un esito relativo all'integrità
Dopo aver generato un nonce
, puoi richiedere un esito relativo all'integrità da Google
Play. Per farlo, segui questi passaggi:
- Crea un'
IntegrityManager
, come mostrato negli esempi seguenti. - Crea un
IntegrityTokenRequest
, fornendo il valorenonce
tramite il metodosetNonce()
nel builder associato. Le app distribuite esclusivamente all'esterno di Google Play e degli SDK devono specificare anche il numero di 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 di progetto Cloud nella richiesta. Utilizza l'amministratore per chiamare
requestIntegrityToken()
, fornendo ilIntegrityTokenRequest
.
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(); }
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 nonce
che includi nella richiesta diventa parte del token di risposta.
Formato 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 serie compatta.
Gli algoritmi di crittografia / firma sono ben supportati in varie implementazioni di JWT:
Decripta e verifica sui server di Google (opzione consigliata)
L'API Play Integrity ti consente di decriptare e verificare l'esito relativo all'integrità sui server di Google, migliorando la sicurezza della tua app. Per farlo, completa questi passaggi:
- Crea un account di servizio all'interno del progetto Google Cloud collegato alla tua app.
Sul server dell'app, recupera il token di accesso dalle credenziali dell'account di servizio utilizzando l'ambito
playintegrity
ed effettua la richiesta seguente:playintegrity.googleapis.com/v1/PACKAGE_NAME:decodeIntegrityToken -d \ '{ "integrity_token": "INTEGRITY_TOKEN" }'
Leggi la risposta JSON.
Decripta e verifica 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 a chiavi specifiche del linguaggio (il linguaggio di programmazione Java, nel nostro caso) nel backend dell'app. Tieni presente che le chiavi sono codificate in base64 utilizzando 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));
Quindi, utilizza queste chiavi per decriptare prima il token di integrità (parte JWE), quindi 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 di testo normale che contiene verdetti di integrità.