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
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:
- 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".
- 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 ononce
. - O app recebe um veredito assinado e criptografado da API Play Integrity.
- O app transmite o veredito assinado e criptografado ao back-end.
- 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.
- O back-end do app decide como proceder, de acordo com os sinais contidos no payload do token.
- 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:
- 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.
- Use
setNonce
para definir o campononce
como o valor do resumo calculado.
Quando você recebe um veredito de integridade:
- Decodifique e verifique o token de integridade e extraia o resumo do
campo
nonce
. - Calcule um resumo da solicitação da mesma maneira que no app (por exemplo SHA256 de uma serialização de solicitações estável).
- 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:
- 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.
- Chame
setNonce()
para definir o campononce
com o valor exclusivo da etapa 1.
Quando você recebe um veredito de integridade:
- Decodifique e verifique o token de integridade e extraia o valor exclusivo do
campo
nonce
. - 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
- 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:
- O usuário inicia a ação de alto valor.
- Extraia um valor exclusivo para essa ação, conforme descrito na seção Inclua valores exclusivos para se proteger contra ataques de repetição.
- Prepare uma mensagem que você quer proteger. Inclua o valor exclusivo da etapa 2 na mensagem.
- 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.
- Use
setNonce()
para definir o campononce
para o resumo calculado na etapa anterior.
Quando você recebe um veredito de integridade:
- Extraia o valor exclusivo da solicitação
- Decodifique e verifique o token de integridade e extraia o resumo do
campo
nonce
. - 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.
- 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:
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:
- Crie um
IntegrityManager
, conforme mostrado nos exemplos abaixo. - Crie um
IntegrityTokenRequest
, fornecendo ononce
com o métodosetNonce()
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étodosetCloudProjectNumber()
. 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. Use o gerenciador para chamar
requestIntegrityToken()
, fornecendo aIntegrityTokenRequest
.
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:
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:
- Criar uma conta de serviço no projeto do Google Cloud vinculado ao seu app.
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" }'
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.