Fazer uma solicitação de API clássica

Se você planeja fazer solicitações de API padrão, que são adequadas para a maioria dos desenvolvedores, pule para a seção sobre vereditos de integridade. Esta página descreve como fazer solicitações de API clássicas para vereditos de integridade, que têm suporte no Android 4.4 (nível 19 da API) e mais recentes.

Considerações

Comparar solicitações padrão e clássicas

É possível fazer solicitações padrão, clássicas ou uma combinação das duas, dependendo das necessidades de segurança e antiabuso do app. As solicitações padrão são adequadas para todos os apps e jogos e podem ser usadas para verificar se uma ação ou chamada de servidor é genuína, enquanto delegam proteção contra repetibilidade e exfiltração ao Google Play. As solicitações clássicas são mais computacionalmente caras, e você é responsável pela implementação correta delas para se proteger contra exfiltração e alguns tipos de ataques. As solicitações clássicas precisam ser feitas com menos frequência que as solicitações padrão, por exemplo, como uma ocasião única para verificar se uma ação altamente valiosa ou sensível é genuína.

A tabela abaixo destaca as principais diferenças entre os dois tipos de solicitações:

Solicitação de API padrão Solicitação de API clássica
Pré-requisitos
Versão mínima do SDK do Android exigida Android 5.0 (nível 21 da API) ou mais recente Android 4.4 (nível 19 da API) ou mais recente
Requisitos do Google Play Google Play Store e Google Play Services Google Play Store e Google Play Services
Detalhes da integração
Aquecimento da API necessário ✔️ (alguns segundos)
Latência de solicitação típica Algumas centenas de milissegundos Alguns segundos
Possível frequência de solicitação Frequente (verificação sob demanda de qualquer ação ou solicitação) Infrequente (verificação única para ações de maior valor ou solicitações mais sensíveis)
Tempos limite A maioria dos aquecimentos leva menos de 10 segundos, mas envolve uma chamada de servidor, portanto, recomenda-se um tempo limite longo (por exemplo, um minuto). As solicitações de veredito acontecem no lado do cliente A maioria das solicitações leva menos de 10 segundos, mas envolve uma chamada de servidor. Por isso, recomenda-se um tempo limite longo (por exemplo, um minuto)
Token de veredito de integridade
Contém detalhes do dispositivo, do app e da conta ✔️ ✔️
Armazenamento de tokens em cache Armazenamento em cache no dispositivo protegido pelo Google Play Não recomendado
Descriptografar e verificar o token pelo servidor do Google Play ✔️ ✔️
Latência de descriptografia típica de servidor para servidor Vários milissegundos, no máximo 999 Vários milissegundos, no máximo 999
Descriptografar e verificar o token localmente em um ambiente de servidor seguro ✔️
Descriptografar e verificar o token do lado do cliente
Atualização do veredito de integridade Pouco armazenamento em cache automático e atualizações do Google Play Todos os vereditos recalculados em cada solicitação
Limites
Solicitações por app por dia 10.000 por padrão (é possível solicitar um aumento) 10.000 por padrão (é possível solicitar um aumento)
Solicitações por instância do app por minuto Aquecimentos: 5 por minuto
Tokens de integridade: sem limite público*
Tokens de integridade: 5 por minuto
Proteção
Redução de adulterações e ataques semelhantes Use o campo requestHash Use o campo nonce com vinculação de conteúdo com base nos dados da solicitação
Redução da repetição e ataques semelhantes Mitigação automática do Google Play Use o campo nonce com a lógica do lado do servidor

* Todas as solicitações, incluindo aquelas sem limites públicos, estão sujeitas a limites defensivos não públicos em valores altos

Faça solicitações clássicas com pouca frequência

A geração de um token de integridade consome tempo, dados e bateria, e cada app tem um número máximo de solicitações clássicas que podem ser feitas por dia. Portanto, só faça solicitações clássicas para verificar se o valor mais alto ou as ações mais confidenciais são genuínos quando você quiser uma garantia extra em relação a uma solicitação padrão. Não faça solicitações clássicas para ações de alta frequência ou de baixo valor. Não faça solicitações clássicas toda vez que o app for para o primeiro plano ou em intervalos de poucos minutos em segundo plano. Além disso, evite fazer chamadas de um grande número de dispositivos ao mesmo tempo. Um app que faz muitas chamadas de solicitações clássicas pode ser limitado para proteger os usuários contra implementações incorretas.

Evite armazenar vereditos em cache

O armazenamento em cache de um veredito aumenta o risco de ataques, como exfiltração e reprodução, em que um veredito legítimo é reutilizado em um ambiente não confiável. Se você estiver pensando em fazer uma solicitação clássica e armazená-la em cache para uso futuro, é recomendável realizar uma solicitação padrão sob demanda. As solicitações padrão envolvem um pouco de armazenamento em cache no dispositivo, mas o Google Play usa outras técnicas de proteção para reduzir o risco de ataques de repetição e exfiltração.

Use o campo "nonce" para proteger solicitações clássicas

A API Play Integrity oferece um campo chamado nonce (valor de uso único), que pode ser usado para proteger ainda mais o app contra determinados ataques, como ataques de repetição e adulteração. A API Play Integrity retorna o valor definido nesse campo na resposta de integridade assinada. Siga com cuidado as orientações sobre como gerar valores de uso único para proteger o app contra ataques.

Repita solicitações clássicas com espera exponencial

Condições do ambiente, como uma conexão de Internet instável ou um dispositivo sobrecarregado, podem causar falhas nas verificações de integridade do dispositivo. Essas condições podem fazer com que nenhum marcador seja gerado para um dispositivo que é confiável. Para mitigar esses cenários, inclua uma opção de repetição com espera exponencial.

Visão geral

Diagrama de sequência que mostra o design de alto nível da API Play
Integrity

Quando o usuário realizar uma ação de alto valor no app que você queira proteger com uma verificação de integridade, siga as etapas abaixo:

  1. O back-end do servidor do app gera e envia um valor exclusivo à lógica do lado do cliente. As outras etapas se referem a essa lógica como o "app".
  2. O app cria o nonce usando o valor exclusivo e o conteúdo da ação de alto valor. Em seguida, ele chama a API Play Integrity, transmitindo o nonce.
  3. O app recebe um veredito assinado e criptografado da API Play Integrity.
  4. O app transmite o veredito assinado e criptografado ao back-end.
  5. O back-end do app envia o veredito a um servidor do Google Play. O servidor do Google Play descriptografa e verifica o veredito, retornando os resultados ao back-end do app.
  6. O back-end do app decide como proceder, de acordo com os sinais contidos no payload do token.
  7. O back-end do app envia os resultados da decisão para o app.

Gerar um valor de uso único

Ao proteger uma ação no app com a API Play Integrity, você pode aproveitar o campo nonce para reduzir certos tipos de ataque, como os de repetição e de person-in-the-middle (PITM). A API Play Integrity retorna o valor definido nesse campo na resposta de integridade assinada.

O valor definido no campo do nonce precisa ser formatado corretamente:

  • String
  • Seguro para URL
  • Codificado como Base64 e sem wrapper
  • Mínimo de 16 caracteres
  • Máximo de 500 caracteres

Confira abaixo algumas formas comuns de usar o campo do nonce na API Play Integrity. Para ter a proteção mais forte do nonce, é possível combinar os métodos abaixo.

Inclua um hash de solicitação para proteger contra adulterações

Você pode usar o parâmetro nonce em uma solicitação de API clássica de maneira semelhante ao parâmetro requestHash em uma solicitação de API padrão para proteger o conteúdo de uma solicitação contra adulteração.

Quando você solicita um veredito de integridade:

  1. Calcule um resumo de todos os parâmetros de solicitação essenciais (por exemplo, SHA256 de uma serialização de solicitações estável) da ação do usuário ou da solicitação do servidor que estiver acontecendo.
  2. Use setNonce para definir o campo nonce como o valor do resumo calculado.

Quando você recebe um veredito de integridade:

  1. Decodifique e verifique o token de integridade e extraia o resumo do campo nonce.
  2. Calcule um resumo da solicitação da mesma maneira que no app (por exemplo SHA256 de uma serialização de solicitações estável).
  3. Compare os resumos do lado do app e do servidor. Se eles não corresponderem, a solicitação não será confiável.

Inclua valores exclusivos para se proteger contra ataques de repetição

Para evitar que usuários mal-intencionados reutilizem respostas anteriores da API Play Integrity, use o campo nonce para identificar exclusivamente cada mensagem.

Quando você solicita um veredito de integridade:

  1. Extraia um valor globalmente exclusivo de maneira que usuários mal-intencionados não possam prever. Por exemplo, um número aleatório com segurança criptográfica gerado no lado do servidor pode ser esse valor ou um ID preexistente, como uma sessão ou um ID de transação. Uma variante mais simples e menos segura é gerar um número aleatório no dispositivo. Recomendamos a criação de valores de 128 bits ou maiores.
  2. Chame setNonce() para definir o campo nonce com o valor exclusivo da etapa 1.

Quando você recebe um veredito de integridade:

  1. Decodifique e verifique o token de integridade e extraia o valor exclusivo do campo nonce.
  2. Se o valor da etapa 1 foi gerado no servidor, verifique se o valor exclusivo recebido foi um dos valores gerados e se ele está sendo usado pela primeira vez. Seu servidor precisará manter um registro de valores gerados por uma duração adequada. Se o valor exclusivo recebido tiver sido usado ou não aparecer no registro, recuse a solicitação
  3. Caso contrário, se o valor único foi gerado no dispositivo, verifique se o valor recebido está sendo usado pela primeira vez. Seu servidor precisa manter um registro de valores já usados por um período adequado. Se o valor exclusivo recebido já tiver sido usado, rejeite a solicitação.

Combine as duas proteções contra adulteração e ataques de repetição (recomendado)

É possível usar o campo nonce para se proteger contra ataques de adulteração e repetição ao mesmo tempo. Para fazer isso, gere o valor exclusivo conforme descrito acima e inclua-o como parte da solicitação. Em seguida, calcule o hash de solicitação e inclua o valor exclusivo como parte do hash. Uma implementação que combina as duas abordagens segue este modelo:

Quando você solicita um veredito de integridade:

  1. O usuário inicia a ação de alto valor.
  2. Extraia um valor exclusivo para essa ação, conforme descrito na seção Inclua valores exclusivos para se proteger contra ataques de repetição.
  3. Prepare uma mensagem que você quer proteger. Inclua o valor exclusivo da etapa 2 na mensagem.
  4. O app calcula um resumo da mensagem que quer proteger, conforme descrito na seção Inclua um hash de solicitação para proteger contra adulterações. Como a mensagem contém o valor exclusivo, esse valor faz parte do hash.
  5. Use setNonce() para definir o campo nonce para o resumo calculado na etapa anterior.

Quando você recebe um veredito de integridade:

  1. Extraia o valor exclusivo da solicitação
  2. Decodifique e verifique o token de integridade e extraia o resumo do campo nonce.
  3. Conforme descrito na seção Inclua um hash de solicitação para proteger contra adulterações, recalcule o resumo no lado do servidor e verifique se ele corresponde ao resumo extraído do token de integridade.
  4. Conforme descrito na seção Inclua valores exclusivos para proteção contra ataques de repetição, verifique a validade do valor exclusivo.

O diagrama de sequência abaixo ilustra essas etapas com um nonce no lado do servidor:

Diagrama de sequência que mostra como se proteger contra adulterações e ataques
de repetição

Solicitar um veredito de integridade

Após gerar um nonce, é possível solicitar um veredito de integridade do Google Play. Para isso, siga estas etapas:

  1. Crie um IntegrityManager, conforme mostrado nos exemplos abaixo.
  2. Crie um IntegrityTokenRequest, fornecendo o nonce com o método setNonce() no builder associado. Apps que são distribuídos exclusivamente fora do Google Play e dos SDKs também precisam especificar um número de projeto do Google Cloud usando o método setCloudProjectNumber(). No Google Play, os apps são vinculados a um projeto do Google Cloud no Play Console. Por isso, não é necessário definir o número do projeto na solicitação.
  3. Use o gerenciador para chamar requestIntegrityToken(), fornecendo a 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();

Descriptografar e verificar o veredito de integridade

Ao solicitar um veredito de integridade, a API Play Integrity fornece um token de resposta assinado. O nonce incluído na solicitação se torna parte do token de resposta.

Formato do token

O token é um JSON Web Token (JWT) aninhado, que é a Criptografia JSON da Web (JWE) da JSON Web Signature (JWS). Os componentes JWE e JWS são representados por comandos compact serialização ,

Os algoritmos de criptografia e assinatura têm suporte em várias implementações de JWT:

  • A JWE usa A256KW para alg e A256GCM para enc (links em inglês)

  • A JWS usa ES256.

Descriptografar e verificar nos servidores do Google (recomendado)

A API Play Integrity permite descriptografar e verificar o veredito de integridade nos servidores do Google, o que melhora a segurança do app. Para fazer isso, siga estas etapas:

  1. Criar uma conta de serviço no projeto do Google Cloud vinculado ao seu app.
  2. No servidor do app, busque o token de acesso nas credenciais da conta de serviço usando o escopo playintegrity e faça a seguinte solicitação:

    playintegrity.googleapis.com/v1/PACKAGE_NAME:decodeIntegrityToken -d \
    '{ "integrity_token": "INTEGRITY_TOKEN" }'
  3. Leia a resposta JSON.

Descriptografar e verificar localmente

Se você optar por gerenciar e fazer o download das chaves de criptografia de resposta, vai poder descriptografar e verificar o token retornado no seu próprio ambiente de servidor seguro. Você pode extrair o token retornado usando o método IntegrityTokenResponse#token().

O exemplo abaixo mostra como decodificar a chave AES e a chave pública EC codificada em DER para verificar a assinatura do Play Console no caso de chaves específicas da linguagem (aqui, a linguagem de programação Java) no back-end do app. Observe que as chaves são codificadas em base64 usando sinalizações padrão.

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));

Em seguida, use essas chaves para descriptografar o token de integridade (parte JWE) e, em seguida, verificar e extrair a parte JWS aninhada.

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();

O payload resultante é um token de texto simples que contém vereditos de integridade.