Resposta rápida: use a Play Integrity API para o backend avaliar se uma requisição sensível provavelmente veio do app reconhecido pelo Google Play, instalado em um ambiente confiável e associado a uma conta ou licença compatível. No Android, prepare o provedor de token, gere um requestHash a partir da operação e solicite um token perto da ação protegida. Envie esse token ao seu servidor, decodifique-o pela API oficial do Google e aplique uma decisão baseada no risco. Nunca valide apenas no celular, nunca coloque credenciais de serviço no APK e não bloqueie todo usuário ao primeiro sinal: combine os veredictos com autenticação, rate limiting, histórico e uma resposta proporcional.
Apps Android que distribuem cupons, conteúdo pago, pontos, ingressos, benefícios, partidas competitivas ou operações financeiras atraem automação e adulteração. Um atacante pode modificar o APK, executar o app em um ambiente comprometido, repetir uma chamada capturada ou criar um cliente próprio para conversar diretamente com sua API.
A Play Integrity API ajuda a responder uma pergunta específica: qual é o nível de confiança desta interação com o app? Ela não é um antivírus e não transforma o cliente em ambiente seguro. Seu valor aparece quando o token é verificado no backend e participa de uma política em camadas.
Este guia explica a arquitetura recomendada, o fluxo da Standard API, o vínculo da requisição com requestHash, a validação no servidor, os principais veredictos, estratégias de resposta e cuidados de privacidade e operação. Para proteger também o binário distribuído, combine a solução com R8 e ProGuard no Android e com o guia de segurança de dados locais em apps Kotlin.
O que a Play Integrity API verifica?
A API produz um token assinado e criptografado com sinais calculados pelo ecossistema Google Play. Depois de decodificado no servidor, o resultado pode incluir grupos de veredictos sobre:
- integridade do app: se o pacote e o certificado reconhecidos correspondem à versão esperada;
- integridade do dispositivo: se o ambiente atende a níveis de confiança informados pelo Google;
- licenciamento ou conta: se a instalação e a conta têm relação esperada com a distribuição pelo Google Play, quando aplicável;
- atividade recente do dispositivo: sinal adicional para reconhecer volume anormal de solicitações;
- risco de acesso por outros apps: quando disponível e habilitado, ajuda a detectar situações como captura, sobreposição ou controle por aplicativos de risco;
- Play Protect: sinal opcional relacionado ao estado de proteção e a apps potencialmente nocivos.
A presença exata dos campos depende da configuração do projeto, da disponibilidade do recurso e da modalidade usada. Por isso, seu backend deve tolerar campos opcionais e tratar um veredicto ausente de maneira diferente de um veredicto explicitamente negativo.
Também é importante separar sinal de decisão. A API informa evidências. Quem decide liberar, pedir nova autenticação, limitar ou bloquear é o seu serviço.
Standard API ou Classic API: qual escolher?
Para a maioria das integrações novas e frequentes, a Standard API é a escolha normal. Ela prepara um provedor de token com antecedência e depois solicita tokens sob demanda, reduzindo a latência percebida em ações do usuário.
A Classic API usa outro modelo de solicitação e pode aparecer em integrações existentes ou cenários específicos. Não escolha Classic apenas porque um tutorial antigo mostra nonce. Para um projeto novo, confirme a recomendação atual na documentação oficial e comece pela Standard API, a menos que seu caso exija explicitamente o fluxo clássico.
| Critério | Standard API | Classic API |
|---|---|---|
| Uso típico | ações recorrentes e integração moderna | compatibilidade e casos específicos |
| Preparação | provedor preparado antes da ação | solicitação mais autocontida |
| Vínculo da operação | requestHash | nonce |
| Estratégia | baixa latência depois do warm-up | avaliar conforme requisitos e quotas |
Não misture exemplos dos dois fluxos. Um nonce de Classic não é substituto automático para o requestHash de Standard.
Arquitetura segura de ponta a ponta
O desenho recomendado tem quatro participantes:
- app Android: solicita o token de integridade;
- seu backend: recebe a operação e o token;
- serviço da Play Integrity: decodifica o token para o backend autenticado;
- motor de decisão: combina veredictos, usuário, valor, histórico e risco.
O fluxo para resgatar um cupom, por exemplo, pode ser:
- o usuário autentica normalmente;
- o app monta os dados essenciais do resgate;
- o app calcula um hash determinístico desses dados;
- a Standard API emite um token ligado ao hash;
- o app envia operação e token ao backend;
- o backend recalcula o mesmo hash;
- o backend decodifica o token pelo Google;
- o backend compara pacote, certificado, horário, hash e veredictos;
- uma transação atômica registra o uso do cupom e impede repetição.
A Play Integrity API não substitui a última etapa. Se dois requests válidos conseguem consumir o mesmo benefício, existe uma falha de concorrência no servidor, não uma falha de atestado.
Configuração inicial no Google Play Console
Antes do código, faça a configuração administrativa:
- publique ou associe o app no Google Play Console;
- vincule o projeto do Google Cloud usado pela integração;
- habilite a Play Integrity API no projeto correto;
- configure as respostas e os sinais necessários no Play Console;
- crie a identidade de serviço usada somente pelo backend;
- conceda apenas as permissões necessárias;
- registre ambientes e contas de teste para validar respostas antes de bloquear produção.
Guarde o número do projeto do Google Cloud, pois ele é usado na preparação do provedor Standard. Não confunda esse número com o ID textual do projeto.
As credenciais do backend devem ficar em um secret manager ou identidade de workload. Nunca adicione um JSON de service account em res/raw, assets, BuildConfig ou repositório Git. Tudo que entra no APK pode ser extraído.
Dependência no Gradle Kotlin DSL
Use a versão estável atual da biblioteca no catálogo de versões do projeto:
# gradle/libs.versions.toml
[versions]
play-integrity = "<versao-estavel>"
[libraries]
play-integrity = {
module = "com.google.android.play:integrity",
version.ref = "play-integrity"
}
// app/build.gradle.kts
dependencies {
implementation(libs.play.integrity)
}
Confira a versão na documentação e no repositório oficial antes de atualizar. Centralizar dependências em um Version Catalog do Gradle facilita revisão e rollback.
Preparando o provedor Standard
Prepare o provedor antes do momento crítico, mas não necessariamente no primeiro milissegundo do processo. Uma boa opção é iniciar quando o usuário entra na área autenticada ou se aproxima de uma funcionalidade protegida.
class IntegrityTokenProvider(
context: Context,
private val cloudProjectNumber: Long,
) {
private val manager = IntegrityManagerFactory
.createStandard(context.applicationContext)
@Volatile
private var provider: StandardIntegrityManager.StandardIntegrityTokenProvider? = null
suspend fun prepare() {
provider = manager.prepareIntegrityToken(
PrepareIntegrityTokenRequest.builder()
.setCloudProjectNumber(cloudProjectNumber)
.build(),
).await()
}
suspend fun tokenFor(requestHash: String): String {
val current = provider ?: error("Provedor de integridade não preparado")
return current.request(
StandardIntegrityTokenRequest.builder()
.setRequestHash(requestHash)
.build(),
).await().token()
}
}
O exemplo usa a extensão await() de integração entre Tasks e coroutines. Adapte imports e tratamento de erros às versões adotadas pelo projeto. Mantenha um único responsável pelo provider e evite preparar uma nova instância a cada clique.
Se o processo for encerrado, o objeto em memória desaparece. Seu código deve conseguir preparar novamente. Não persista token pronto em DataStore ou banco local para reutilização futura.
Como criar um requestHash corretamente
O requestHash liga o token a uma operação. Sem esse vínculo, um token obtido para uma ação de baixo risco pode ser reaproveitado em outro request.
Escolha apenas campos estáveis e relevantes. Para um resgate:
data class CouponRedemption(
val couponId: String,
val userId: String,
val campaignId: String,
)
fun CouponRedemption.canonicalPayload(): String = buildString {
append("couponId=").append(couponId)
append("&userId=").append(userId)
append("&campaignId=").append(campaignId)
}
fun sha256Base64Url(value: String): String {
val digest = MessageDigest.getInstance("SHA-256")
.digest(value.toByteArray(StandardCharsets.UTF_8))
return Base64.encodeToString(
digest,
Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING,
)
}
Uso:
val operation = CouponRedemption(
couponId = couponId,
userId = session.userId,
campaignId = campaignId,
)
val requestHash = sha256Base64Url(operation.canonicalPayload())
val integrityToken = integrityTokenProvider.tokenFor(requestHash)
api.redeemCoupon(
RedeemCouponRequest(
couponId = couponId,
campaignId = campaignId,
integrityToken = integrityToken,
),
)
O backend precisa gerar exatamente a mesma representação canônica. JSON serializado livremente pode mudar ordem de campos, espaços ou formato numérico. Defina o contrato e crie testes com vetores conhecidos para Android e servidor.
Não inclua senha, token de sessão ou dado pessoal bruto no hash. Hash não é criptografia de conteúdo sensível e pode permitir correlação. Use identificadores internos mínimos e mantenha a comunicação sob HTTPS.
Validação obrigatória no backend
O servidor recebe o token como dado não confiável. Em seguida, chama o endpoint oficial de decodificação da Play Integrity usando uma identidade autorizada. A URL, a biblioteca cliente e os nomes dos tipos podem evoluir; prefira o SDK oficial ou a especificação atual em vez de montar uma integração antiga por cópia.
Um serviço Kotlin pode separar decodificação de política:
data class IntegrityEvidence(
val packageName: String?,
val requestHash: String?,
val timestampMillis: Long?,
val appRecognized: Boolean,
val deviceLabels: Set<String>,
val licensed: Boolean?,
)
interface PlayIntegrityDecoder {
suspend fun decode(packageName: String, token: String): IntegrityEvidence
}
class RedeemCouponUseCase(
private val decoder: PlayIntegrityDecoder,
private val policy: IntegrityPolicy,
private val coupons: CouponRepository,
) {
suspend fun execute(command: RedeemCouponCommand): RedeemResult {
val evidence = decoder.decode(
packageName = EXPECTED_PACKAGE,
token = command.integrityToken,
)
val expectedHash = sha256Base64Url(command.canonicalPayload())
val decision = policy.evaluate(evidence, expectedHash, command)
if (!decision.allow) {
return RedeemResult.Rejected(decision.publicReason)
}
return coupons.redeemAtomically(command)
}
}
Depois de decodificar, valide no mínimo:
- package name esperado;
- reconhecimento e certificado/versão do app conforme sua política;
requestHashigual ao recalculado;- timestamp dentro de uma janela curta e coerente;
- nível de integridade do dispositivo exigido para a operação;
- licenciamento, quando relevante para o produto;
- autenticação e autorização do usuário;
- idempotência e regras de negócio no banco.
Não confie em um booleano enviado pelo app como isDeviceTrusted. A única evidência útil para a decisão é a resposta decodificada no servidor.
Como interpretar os veredictos sem bloquear usuários legítimos
Uma política binária global costuma causar problemas. Nem toda tela exige a mesma confiança.
| Ação | Resposta razoável |
|---|---|
| Ler conteúdo público | normalmente não solicitar token |
| Fazer login | observar risco, aplicar rate limiting e desafio adicional |
| Resgatar cupom único | exigir app reconhecido, hash válido e política de device definida |
| Alterar dado crítico | pedir reautenticação e avaliar integridade |
| Sincronizar preferência | permitir com limites, mesmo sob sinal inconclusivo |
Crie três resultados internos, por exemplo:
- permitir: sinais suficientes e regras atendidas;
- desafiar ou limitar: resposta ausente, transitória ou risco moderado;
- negar: token inválido, hash divergente, app adulterado ou replay confirmado.
Isso oferece uma saída para aparelhos com problemas temporários, indisponibilidade de rede, Google Play desatualizado ou configuração ainda em rollout. A mensagem ao usuário também deve ser útil: “Não foi possível confirmar a segurança deste dispositivo. Atualize o Google Play e tente novamente” é melhor que “erro -17”.
Tratamento de erros no app
Falhas podem ocorrer ao preparar o provider ou solicitar o token. Modele-as como parte do fluxo:
sealed interface IntegrityTokenResult {
data class Success(val token: String) : IntegrityTokenResult
data class Retryable(val cause: Throwable) : IntegrityTokenResult
data class Unavailable(val cause: Throwable) : IntegrityTokenResult
}
suspend fun requestIntegrityToken(hash: String): IntegrityTokenResult =
try {
IntegrityTokenResult.Success(
integrityTokenProvider.tokenFor(hash),
)
} catch (error: IntegrityServiceException) {
// Classifique pelos códigos documentados na versão usada.
IntegrityTokenResult.Retryable(error)
} catch (error: Exception) {
IntegrityTokenResult.Unavailable(error)
}
Não faça retry infinito. Use backoff, limite de tentativas e cancelamento ligado ao ciclo de vida. Se a tela fecha, a coroutine não precisa continuar. O guia de coroutines e structured concurrency ajuda a evitar jobs órfãos.
Registre métricas por etapa — preparação, emissão, chamada ao backend, decodificação e decisão — sem gravar o token inteiro. Tokens e payloads de segurança não devem aparecer em logs, analytics ou relatórios de crash.
Replay, idempotência e vínculo com a sessão
requestHash dificulta usar o token em outra operação, mas sua API ainda precisa de controles de replay:
- chave de idempotência por comando;
- janela curta para aceitar o timestamp;
- estado transacional no servidor;
- limite de tentativas por usuário, IP, dispositivo lógico e recurso;
- vínculo entre usuário autenticado e identificador incluído no payload canônico;
- registro de decisões para investigação, com retenção mínima.
Para pagamentos, inventário, pontos e cupons, a garantia final deve estar no banco. Use uma constraint única ou atualização condicional dentro de transação. Segurança do cliente não conserta uma operação não atômica.
Testes antes de ativar bloqueio
Implemente em etapas:
- modo observação: decodifique e registre apenas categorias agregadas;
- dashboard: meça taxa de sucesso, ausência e divergência por versão do app;
- testes controlados: valide instalação pela Play Store, trilhas internas e cenários previstos;
- política moderada: aplique desafio ou limite nas ações de maior risco;
- enforcement: bloqueie apenas combinações de sinais bem compreendidas;
- rollback: mantenha feature flag no backend para relaxar a política sem publicar outro APK.
Teste também rotação, processo recriado, troca de conta, rede instável e múltiplos cliques. Um teste feliz em aparelho de desenvolvimento não representa a variedade da base Android.
Feature flags ajudam a separar deploy de ativação, mas a decisão precisa continuar no servidor. Veja como organizar esse controle no guia de feature flags com Kotlin no Android e backend.
Erros comuns na implementação
Validar o token no próprio app
O cliente pode ser modificado para pular a validação. A decisão real pertence ao backend.
Reutilizar um token para várias operações
Solicite o token perto da ação e vincule-o ao conteúdo com requestHash. Não crie um “token da sessão” genérico.
Colocar service account no APK
Credencial embutida é credencial vazada. Apenas o servidor deve autenticar a chamada de decodificação.
Bloquear pelo modelo do aparelho ou por root detectado localmente
Heurísticas locais podem complementar telemetria, mas são contornáveis e geram falsos positivos. Use os veredictos oficiais e uma política proporcional.
Logar token ou resposta completa
Além do risco de segurança e privacidade, logs podem parar em serviços de terceiros. Armazene apenas o necessário para auditoria, de preferência categorias agregadas e IDs de decisão.
Tratar Play Integrity como única defesa
Continue usando autenticação, autorização, TLS, rate limiting, detecção de automação, validação de entrada e controles transacionais. Para observar abuso e falhas sem expor dados, integre uma estratégia de Crashlytics e diagnóstico de ANR com regras explícitas de sanitização.
Checklist de produção
Antes de ativar enforcement, confirme:
- projeto Cloud e app do Play Console estão vinculados corretamente;
- backend usa identidade mínima e credenciais fora do APK;
- Standard API foi escolhida conscientemente;
- provider é preparado e recriado quando necessário;
- cada ação sensível recebe um
requestHashdeterminístico; - servidor recalcula e compara o hash;
- package, app, timestamp e device verdict são avaliados;
- tokens nunca aparecem em logs ou analytics;
- retries têm backoff e limite;
- banco impede replay e consumo duplicado;
- política distingue permitir, desafiar e negar;
- métricas foram observadas antes do bloqueio;
- feature flag permite rollback rápido;
- mensagens orientam o usuário sem revelar regras antifraude.
Conclusão
A Play Integrity API é mais útil quando deixa de ser tratada como um selo mágico e passa a ser uma fonte de sinais para o backend. O app Kotlin prepara o provider, cria um hash da operação e solicita o token. O servidor decodifica a resposta pelo Google, verifica se o token corresponde ao request e combina os veredictos com identidade, histórico, limites e regras transacionais.
Comece por uma ação realmente valiosa, rode em modo observação e conheça a distribuição dos resultados antes de negar acesso. Depois, aplique respostas graduais. Essa abordagem reduz fraude sem transformar diferenças legítimas do ecossistema Android em bloqueios indiscriminados.
Para completar a camada de distribuição, revise assinatura, shrinking e obfuscação com R8/ProGuard. Para operações protegidas que chamam serviços externos, aplique também timeouts, retries e circuit breaker no Ktor Client — disponibilidade e segurança precisam funcionar juntas.