Eseguire una richiesta API classica

Se prevedi di effettuare solo richieste API standard, adatte alla maggior parte degli sviluppatori, puoi passare direttamente ai test dell'integrità. In questa pagina vengono descritte le richieste API classiche per gli 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 anti-abuso della tua app. Le richieste standard sono adatte a tutti i giochi e le app e possono essere utilizzate per verificare che qualsiasi azione o chiamata al server sia autentica, delegando al contempo una protezione contro la rigiocabilità e l'esfiltrazione a Google Play. Le richieste classiche sono più costose da effettuare ed è tua responsabilità implementarle correttamente per proteggerti dall'esfiltrazione e da determinati tipi di attacchi. Le richieste classiche devono essere effettuate meno frequentemente rispetto a quelle standard, ad esempio una tantum occasionale per verificare se un'azione sensibile o molto utile è 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 obbligatoria 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
Preparazione dell'API obbligatoria ✔️ (pochi secondi)
Latenza tipica delle richieste Alcune centinaia di millisecondi Alcuni secondi
Frequenza potenziale delle richieste Frequente (controllo on demand di qualsiasi azione o richiesta) Non frequente (controllo una tantum delle azioni di maggior valore o delle richieste più sensibili)
Timeout La maggior parte dei periodi di riscaldamento è inferiore a 10 secondi, ma prevede una chiamata al server, quindi è consigliato un timeout lungo, ad esempio 1 minuto. Le richieste di verdetto avvengono lato client La maggior parte delle richieste ha meno di 10 secondi, ma prevede una chiamata al server, quindi è consigliato un timeout lungo, ad esempio 1 minuto.
Token dell'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
Decriptare e verificare il token tramite il server di Google Play ✔️ ✔️
Tipica latenza delle richieste server-server per la decrittografia Decine di millisecondi con disponibilità di tre nove Decine di millisecondi con disponibilità di tre nove
Decriptare e verificare il token in locale in un ambiente server sicuro ✔️
Decriptare e verificare il token lato client
Aggiornamento dell'esito dell'integrità Memorizzazione nella cache e aggiornamento automatico da parte di Google Play Tutti gli esiti 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 le manomissioni e attacchi simili Utilizza campo requestHash Utilizza il campo nonce con l'associazione dei contenuti in base ai dati della richiesta
Mitigare contro attacchi di ripetizione e simili Mitigazione automatica da parte di Google Play Utilizza il campo nonce con la logica lato server

* Tutte le richieste, incluse quelle senza limiti pubblici, sono soggette a limiti difensivi non pubblici a valori elevati

Effettua richieste nella versione classica non di frequente

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, ti conviene effettuare le richieste classiche per verificare che il valore massimo o le azioni più sensibili siano autentiche solo quando vuoi un'ulteriore garanzia per una richiesta standard. Non devi effettuare richieste nella versione classica per azioni ad alta 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 chiamare contemporaneamente da un numero elevato di dispositivi. 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

La memorizzazione di un esito 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 considerando di effettuare una richiesta classica e poi memorizzarla nella cache per utilizzarla in seguito, ti consigliamo di eseguire una richiesta standard on demand. Le richieste standard prevedono una certa memorizzazione nella cache sul dispositivo, ma Google Play utilizza tecniche di protezione aggiuntive per ridurre il rischio di attacchi di ripetizione ed esfiltrazione.

Utilizza il campo nonce per proteggere le richieste classiche

L'API Play Integrity offre un campo chiamato nonce, che può essere utilizzato per proteggere ulteriormente la tua app da determinati attacchi, ad esempio attacchi di ripetizione e 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 nonce per proteggere la tua app dagli attacchi.

Riprova le richieste classiche con backoff esponenziale

Condizioni ambientali, come una connessione a internet instabile o un dispositivo sovraccarico, possono causare il mancato superamento dei controlli di integrità del dispositivo. Ciò può impedire la generazione di etichette per un dispositivo altrimenti attendibile. Per attenuare questi scenari, includi un'opzione di nuovo tentativo con backoff esponenziale.

Panoramica

Diagramma di sequenza che mostra la progettazione di alto livello
dell'API Play Integrity

Quando l'utente esegue nella tua app un'azione di alto valore che vuoi proteggere con un controllo di integrità, completa i seguenti passaggi:

  1. 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".
  2. L'app crea l'nonce dal valore univoco e dai contenuti dell'azione di valore elevato. Quindi chiama l'API Play Integrity, passando nonce.
  3. La tua app riceve un esito firmato e criptato dall'API Play Integrity.
  4. L'app trasmette l'esito firmato e criptato al backend dell'app.
  5. Il backend dell'app invia l'esito a un server di Google Play. Il server di Google Play decripta e verifica l'esito, restituendo i risultati al backend dell'app.
  6. Il backend dell'app decide come procedere in base agli indicatori contenuti nel payload del token.
  7. Il backend dell'app invia i risultati relativi alle decisioni all'app.

Genera un nonce

Quando proteggi un'azione nella tua app con l'API Play Integrity, puoi sfruttare il campo nonce per limitare determinati tipi di attacchi, come gli attacchi di manomissione personali in-the-middle (PITM) e gli attacchi di ripetizione. L'API Play Integrity restituisce il valore impostato in questo campo all'interno della risposta relativa all'integrità firmata.

Il valore impostato nel campo nonce deve essere formattato correttamente:

  • String
  • Sicurezza 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 delle richieste per proteggerti dalle manomissioni

Puoi usare 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 dalle manomissioni.

Quando richiedi un esito relativo all'integrità:

  1. Calcola un digest di tutti i parametri di richiesta critici (ad es. SHA256 di una serializzazione di una richiesta stabile) dall'azione utente o dalla richiesta del server in corso.
  2. Usa setNonce per impostare il campo nonce sul valore del digest calcolato.

Quando ricevi un esito relativo all'integrità:

  1. Decodifica e verifica il token di integrità e ottieni il digest dal campo nonce.
  2. Calcola un digest della richiesta come nell'app (ad es. SHA256 di una serializzazione di richiesta stabile).
  3. Confronta i digest lato app e lato server. Se non corrispondono, la richiesta non è attendibile.

Includi valori univoci per proteggerti dagli attacchi ripetuti

Per impedire a utenti malintenzionati di riutilizzare risposte precedenti dell'API Play Integrity, puoi usare il campo nonce per identificare in modo univoco ogni messaggio.

Quando richiedi un esito relativo all'integrità:

  1. ottenere un valore globalmente univoco, non prevedibile da utenti malintenzionati. Ad esempio, un numero casuale con protezione criptata 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 è generare un numero casuale sul dispositivo. Consigliamo di creare valori di almeno 128 bit.
  2. Richiama setNonce() per impostare il campo nonce sul valore univoco del passaggio 1.

Quando ricevi un esito relativo all'integrità:

  1. Decodifica e verifica il token di integrità e ottieni il valore univoco dal campo nonce.
  2. 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 un periodo di tempo adeguato). Se il valore univoco ricevuto è già stato utilizzato o non è visualizzato nel record, rifiuta
  3. In caso contrario, se sul dispositivo è stato generato il valore univoco, verifica che il valore ricevuto venga utilizzato per la prima volta (il server deve conservare un record dei valori già visualizzati per un periodo di tempo adeguato). Se il valore univoco ricevuto è già stato utilizzato, rifiuta la richiesta.

Combinare entrambe le protezioni contro le manomissioni e gli attacchi di ripetizione (consigliato)

Puoi utilizzare il campo nonce per proteggerti contemporaneamente da attacchi di manomissione e replica. Per farlo, genera il valore univoco descritto sopra e includilo nella richiesta. Calcola quindi l'hash della richiesta, assicurandoti di includere il valore univoco come parte dell'hash. Di seguito è riportata un'implementazione che combina entrambi gli approcci:

Quando richiedi un esito relativo all'integrità:

  1. L'utente avvia l'azione di valore elevato.
  2. Ottieni un valore univoco per questa azione come descritto nella sezione Includi valori univoci per proteggerti dagli attacchi ripetuti.
  3. Prepara un messaggio che vuoi proteggere. Includi nel messaggio il valore univoco del passaggio 2.
  4. La tua app calcola una sintesi del messaggio che vuole proteggere, come descritto nella sezione Includere un hash delle richieste per evitare le manomissioni. Poiché il messaggio contiene il valore univoco, questo valore fa parte dell'hash.
  5. Usa setNonce() per impostare il campo nonce sul digest calcolato dal passaggio precedente.

Quando ricevi un esito relativo all'integrità:

  1. Ottenere il valore univoco dalla richiesta
  2. Decodifica e verifica il token di integrità e ottieni il digest dal campo nonce.
  3. Come descritto nella sezione Includere un hash della richiesta per evitare le manomissioni, ricalcola il digest sul lato server e verifica che corrisponda al digest ottenuto dal token di integrità.
  4. Come descritto nella sezione Includi valori univoci per proteggerti dagli attacchi di ripetizione, verifica la validità del valore univoco.

Il seguente diagramma sequenza illustra questi passaggi con un elemento nonce lato server:

Diagramma di sequenza che mostra come proteggerti sia dalle manomissioni sia dagli attacchi di ripetizione

Richiedi un esito relativo all'integrità

Dopo aver generato un valore nonce, puoi richiedere un esito relativo all'integrità a Google Play. Per farlo, segui questi passaggi:

  1. Crea un oggetto IntegrityManager, come mostrato negli esempi seguenti.
  2. Crea un IntegrityTokenRequest, fornendo nonce tramite il metodo setNonce() nel generatore associato. Anche le app distribuite esclusivamente al di fuori di Google Play e degli SDK devono specificare il numero di progetto Google Cloud tramite il metodo setCloudProjectNumber(). 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.
  3. Utilizza il gestore per chiamare requestIntegrityToken(), fornendo il IntegrityTokenRequest.

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 parametro nonce che includi nella richiesta diventa parte del token di risposta.

Formato token

Il token è un JSON Web Token (JWT) nidificato, vale a dire 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 in varie implementazioni JWT:

  • JWE utilizza A256KW per alg e A256GCM per enc

  • JWS utilizza ES256.

Decriptare e verificare 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, il che migliora la sicurezza della tua app. Per farlo, segui questi passaggi:

  1. Crea un account di servizio all'interno del progetto Google Cloud collegato alla tua app. Durante questo processo di creazione dell'account, devi concedere all'account di servizio i ruoli Utente account di servizio e Consumatore Service Usage.
  2. Sul server dell'app, recupera il token di accesso dalle credenziali dell'account di servizio utilizzando l'ambito playintegrity ed effettua la seguente richiesta:

    playintegrity.googleapis.com/v1/PACKAGE_NAME:decodeIntegrityToken -d \
    '{ "integrity_token": "INTEGRITY_TOKEN" }'
  3. 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 pubblica codificata DER per la verifica della firma da Play Console a chiavi specifiche del linguaggio (nel nostro caso, il linguaggio di programmazione Java) 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));

Successivamente, 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 in testo normale che contiene verdetti dell'integrità.