Compatibilidade de versões

A restauração de credenciais do Gerenciador de credenciais funciona em dispositivos com o Android 9 (nível 28 da API) e versões mais recentes, o Google Play Services (GMS) versão principal 242200000 (v24.22) ou mais recente e a versão 1.5.0 ou mais recente da biblioteca androidx.credentials. Em dispositivos sem o Google Play Services ou com uma versão mais antiga do GMS, capture CreateCredentialProviderConfigurationException e GetCredentialProviderConfigurationException para que o app volte normalmente ao login padrão.

Pré-requisitos

Configure um servidor de parte confiável da WebAuthn semelhante ao servidor para chaves de acesso. Se você já tiver um servidor configurado para processar a autenticação com chaves de acesso, poderá reutilizar os mesmos endpoints de registro e declaração da WebAuthn para as chaves de restauração.

Dependências

Adicione as seguintes dependências ao arquivo build.gradle do módulo do app:

Kotlin

dependencies {
    implementation("androidx.credentials:credentials:1.7.0-alpha02")
    implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha02")
}

Groovy

dependencies {
    implementation "androidx.credentials:credentials:1.7.0-alpha02"
    implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha02"
}

A Restore Credentials está disponível na versão 1.5.0 e mais recentes da biblioteca androidx.credentials. No entanto, recomendamos usar as versões estáveis mais recentes das dependências sempre que possível.

Como o android:allowBackup afeta o Restore Credentials

O atributo de manifesto android:allowBackup controla o backup de dados do app, que interage com a API Restore Credentials de três maneiras distintas:

  • Transferência de chave de restauração (independente de allowBackup): o Google Play Services transfere o par de chaves RestoreCredential usando backup na nuvem criptografado de ponta a ponta ou transferência local entre dispositivos (D2D, na sigla em inglês), independentemente de android:allowBackup ser true ou false. Para transferências D2D, a chave de restauração é sempre transferida. A única condição em que ela não é transferida é durante um backup na nuvem quando o usuário desativou o backup na nuvem nas configurações do Android.
  • Restauração em segundo plano no BackupAgent (requer android:allowBackup="true"): o login silencioso em segundo plano antes que o usuário abra o app no novo dispositivo (BackupAgent.onRestoreFinished()) só é executado quando android:allowBackup="true" e android:backupAgent estão configurados no manifesto. Se for android:allowBackup="false", restaure a sessão quando o usuário iniciar a Activity principal pela primeira vez. Também é possível implementar as duas abordagens para tornar a restauração mais robusta.
  • Teste de backup na nuvem do Android Studio (requer android:allowBackup="true"): para testar o tipo de backup na nuvem na ferramenta de backup e restauração do Android Studio, é necessário usar o android:allowBackup="true", enquanto o teste de dispositivo para dispositivo funciona independentemente do allowBackup.

Visão geral

A API Restore Credentials segue um ciclo de vida de três etapas nos dispositivos antigos e novos do usuário:

  1. Criar uma chave de restauração no dispositivo anterior: depois que o usuário fizer login (ou na inicialização do app, se o usuário já estiver conectado e nenhuma chave de restauração estiver registrada no armazenamento local), busque as opções de criação no servidor da parte confiável e chame credentialManager.createCredential().
  2. Fazer login com a chave de restauração no novo dispositivo: recupere a RestoreCredential transferida usando uma estratégia de dois níveis: primeiro, silenciosamente em BackupAgent.onRestoreFinished() (quando android:allowBackup="true") para que a sincronização em segundo plano e as notificações push sejam retomadas imediatamente e, em segundo lugar, como um substituto no inicializador Activity.onCreate() na primeira inicialização do app.
  3. Excluir a chave de restauração ao sair: quando o usuário sair ou a sessão dele for invalidada pelo servidor, chame credentialManager.clearCredentialState() com ClearCredentialStateRequest(ClearCredentialStateRequest.TYPE_CLEAR_RESTORE_CREDENTIAL) e limpe a flag de sincronização local.

Criar uma chave de restauração

O app precisa abranger todos os casos de login de um usuário para garantir que os usuários ativos tenham uma chave de restauração criada. Crie a chave de restauração nos seguintes cenários:

  • Se o usuário estiver conectado e uma chave de restauração ainda não tiver sido criada (como no método onCreate() para o Activity principal).
  • Quando o usuário está fazendo login ou concluindo um fluxo de registro de nova conta.

Para evitar chamar createCredential() sempre que o app for iniciado, armazene uma flag de sincronização local, como has_synced_restore_credential, no armazenamento local. Não armazene essa flag em SharedPreferences ou DataStore com backup padrão, a menos que esse arquivo seja excluído em data_extraction_rules.xml. Caso contrário, o Backup automático vai copiar has_synced_restore_credential = true para o novo dispositivo e impedir que ele gere ou alterne a própria chave de restauração.

Instanciar o Credential Manager

Use o contexto de atividade do app para instanciar um objeto CredentialManager.

// Use your app or activity context to instantiate a client instance of
// CredentialManager.
private val credentialManager = CredentialManager.create(context)

Receber opções de criação de credenciais do servidor de apps

Use uma biblioteca compatível com FIDO no servidor do app para enviar ao app cliente o JSON PublicKeyCredentialCreationOptions necessário para criar a credencial de restauração. Para mais informações sobre a implementação do lado do servidor, consulte Orientação do lado do servidor.

O JSON das opções de criação segue a mesma estrutura do registro de chave de acesso:

{
  "challenge": "<base64url-encoded challenge>",
  "rp": {
    "name": "<relying party name>",
    "id": "<relying party host name>"
  },
  "user": {
    "id": "<base64url-encoded user ID>",
    "name": "<user name>",
    "displayName": "<user display name>"
  },
  "pubKeyCredParams": [
    {
      "type": "public-key",
      "alg": -7
    }
  ],
  "attestation": "none",
  "excludeCredentials": [
    {
        "id": "<base64url-encoded credential ID to exclude>", 
        "type": "public-key"
    }
  ],
  "authenticatorSelection": {
    "requireResidentKey": true,
    "residentKey": "required",
    "userVerification": "required"
  }
}

Verifique se o JSON das opções de criação atende a estes requisitos:

  • challenge e user.id: codifique os dois valores como strings Base64URL sem preenchimento (substituindo + por -, / por _ e removendo o = final). Caracteres Base64 padrão ou IDs de números inteiros brutos causam CreateRestoreCredentialDomException ou IllegalArgumentException.
  • rp.id: defina como o domínio da parte confiável (por exemplo, example.com).
  • pubKeyCredParams: inclua {"type": "public-key", "alg": -7} (ES256) e {"type": "public-key", "alg": -257} (RS256).

Criar a chave de restauração

Depois de receber o JSON de opções de criação de chave pública do servidor, envolva o JSON em um objeto CreateRestoreCredentialRequest e chame createCredential() no objeto CredentialManager.

// requestJson contains the credential creation options sent by the server
val createRestoreRequest = CreateRestoreCredentialRequest(requestJson)
val response = try {
    credentialManager.createCredential(context, createRestoreRequest)
} catch (e: E2eeUnavailableException) {
    // Fall back to local device-to-device (D2D) restore if cloud E2EE is unavailable
    credentialManager.createCredential(
        context,
        CreateRestoreCredentialRequest(requestJson, isCloudBackupEnabled = false),
    )
}

Pontos principais sobre o código

  • O objeto CreateRestoreCredentialRequest contém estes campos:

    • requestJson: as opções de criação de credenciais enviadas pelo servidor do app no formato da API Web Authentication para PublicKeyCredentialCreationOptionsJSON.
    • isCloudBackupEnabled: campo Boolean para determinar se a chave de restauração precisa ser armazenada em backup na nuvem. Por padrão, essa flag é true. Esse campo tem os seguintes valores:

      • true: (recomendado) esse valor permite o backup de chaves de restauração na nuvem se o usuário tiver o Backup do Google e a criptografia de ponta a ponta, como um bloqueio de tela, ativados.
      • false: esse valor salva a chave localmente, e não na nuvem. A chave não estará disponível no novo dispositivo se o usuário optar por restaurar da nuvem, mas ainda será transferida durante uma configuração local de cabo ou Wi-Fi de dispositivo para dispositivo (D2D).

Processar a resposta de criação de credenciais

A API Credential Manager retorna uma resposta do tipo CreateRestoreCredentialResponse. Essa resposta contém a resposta de registro da credencial de chave pública no formato JSON.

Envie response.registrationResponseJson do app para o servidor da parte confiável para verificar e armazenar a chave pública junto com a conta do usuário e, em seguida, registre o marcador de sincronização local no armazenamento local. Para mais informações sobre a verificação do lado do servidor, consulte as orientações para chaves de acesso.

Durante o processo de criação da chave de restauração, processe estas exceções:

  • CreateRestoreCredentialDomException: essa exceção ocorre se requestJson for inválido e não seguir o formato WebAuthn para PublicKeyCredentialCreationOptionsJSON.
  • E2eeUnavailableException: essa exceção ocorre se isCloudBackupEnabled for true, mas o dispositivo do usuário não tiver backup de dados ou criptografia de ponta a ponta, como um bloqueio de tela.
    Para garantir que as credenciais de restauração sejam criadas para transferências locais de dispositivo para dispositivo (D2D), mesmo quando a E2EE na nuvem não estiver disponível, capture E2eeUnavailableException e tente novamente createCredential() com CreateRestoreCredentialRequest(requestJson, isCloudBackupEnabled = false).
  • IllegalArgumentException: essa exceção ocorre se requestJson estiver vazio, não for um JSON válido ou não tiver um user.id codificado em Base64URL válido que esteja em conformidade com as especificações do WebAuthn.

Fazer login com uma chave de restauração

Use as credenciais de restauração para fazer login silenciosamente durante o processo de configuração do dispositivo.

Receber opções de recuperação de credenciais do servidor de apps

Antes de chamar getCredential() no novo dispositivo, solicite um desafio JSON PublicKeyCredentialRequestOptions da WebAuthn no servidor do app. Como o app no novo dispositivo ainda não sabe qual usuário está fazendo login, esse endpoint precisa ser não autenticado (descoberta sem usuário). Para orientações relacionadas ao servidor, consulte Fazer login com uma chave de acesso e o guia de autenticação do lado do servidor.

O JSON das opções de recuperação tem a seguinte estrutura:

{
  "challenge": "<your app challenge>",
  "allowCredentials": [],
  "rpId": "<your app server domain>"
}

Configure o JSON das opções de recuperação no servidor da seguinte maneira:

  • allowCredentials: omita esse campo ou transmita uma matriz vazia ([]) porque a conta do usuário não é conhecida antes da restauração. Depois que getCredential() retornar, o servidor vai identificar o usuário no campo response.userHandle (que corresponde ao user.id registrado durante a criação) e vai procurar a chave pública por id (credentialId).
  • userVerification: definido como "discouraged" (ou "preferred"). Como a restauração de credenciais é executada silenciosamente sem a interface de biometria ou de bloqueio de tela, o Android substitui "required" por "discouraged", e os dados do autenticador retornados ao servidor têm a flag Usuário verificado (uv) definida como false. Configure o verificador do servidor WebAuthn para aceitar uv = false para declarações de chave de restauração.

Receber a chave de restauração

Para receber a chave de restauração no novo dispositivo, envolva o requestJson do servidor em um GetRestoreCredentialOption dentro de um GetCredentialRequest e chame getCredential() no objeto CredentialManager.

Implemente uma estratégia de restauração em duas etapas para que os usuários façam login, independentemente da execução da restauração de dados do app em segundo plano:

  • Nível 1 (restauração em segundo plano no BackupAgent.onRestoreFinished()): se o backup de dados do app estiver ativado (android:allowBackup="true"), crie uma subclasse de BackupAgent e substitua onRestoreFinished(). Não use onRestore(), que é chamado apenas para backups de chave-valor legados. Como onRestoreFinished() é síncrono e getCredential() é uma função suspend do Kotlin, faça a ponte da chamada usando runBlocking(Dispatchers.IO). Capturar NoCredentialException silenciosamente quando não houver uma chave de restauração.
  • Nível 2 (substituição em primeiro plano no inicializador Activity.onCreate()): na primeira inicialização do app, quando não houver uma sessão local, chame getCredential() de lifecycleScope antes de mostrar a tela de login. Isso processa apps com android:allowBackup="false" e se recupera se o dispositivo estava off-line quando onRestoreFinished() foi executado.

Se você adicionar um novo BackupAgent a um app que já usa o Backup Automático com android:allowBackup="true", defina também android:fullBackupOnly="true" no elemento <application> em AndroidManifest.xml para que o Android continue usando o Backup Automático em vez de mudar para o backup de chave-valor legado.

    suspend fun getRestoreKey(
        fetchAuthenticationJson: () -> String,
    ) {
        // Fetch the options required to get the restore key
        val authenticationJson = fetchAuthenticationJson()

        // Create the GetRestoreCredentialOption object
        val options = GetRestoreCredentialOption(authenticationJson)
        val getRequest = GetCredentialRequest(listOf(options))

        try {
            val response = credentialManager.getCredential(context, getRequest)

            // Type-check and extract the restore credential
            val credential = response.credential
            if (credential is RestoreCredential) {
                val responseJson = credential.authenticationResponseJson
            }
        } catch (e: NoCredentialException) {
            // No restore key is present on this device
        }
    }

class MyBackupAgent : BackupAgent() {

    override fun onRestoreFinished() {
        super.onRestoreFinished()
        val credentialManager = CredentialManager.create(this)
        val restoreFunctions = RestoreCredentialsFunctions(this, credentialManager)

        runBlocking(Dispatchers.IO) {
            restoreFunctions.getRestoreKey(
                fetchAuthenticationJson = {
                    /* Fetch challenge JSON from server */ ""
                }
            )
        }
    }
}

A API Credential Manager retorna um GetCredentialResponse com a propriedade credential como um RestoreCredential (não um PublicKeyCredential) que contém authenticationResponseJson.

Processar a resposta de login

Envie restoreCredential.authenticationResponseJson do app para o servidor da parte confiável para verificar a declaração criptográfica e emitir um novo token de sessão para o usuário. Para detalhes sobre a verificação do lado do servidor, consulte Fazer login com uma chave de acesso.

Ao projetar o armazenamento de back-end e as políticas de ciclo de vida para chaves de restauração, siga estas diretrizes:

  • Diferencie as chaves de restauração das chaves de acesso do usuário: marque as chaves de restauração no seu banco de dados para que elas fiquem ocultas nas telas de gerenciamento de chaves de acesso voltadas ao usuário e isentas de verificações de usuário (uv = true).
  • Suporte a vários dispositivos ativos por usuário: um usuário pode ter vários dispositivos (como um smartphone e um tablet). Permita várias chaves de restauração ativas por conta de usuário (por exemplo, com chave por dispositivo ou ID de instalação) em vez de substituir uma única linha.
  • Defina um TTL adequado e limpe as chaves órfãs: desinstalar um app ou limpar os dados do app exclui a chave de restauração local sem notificar o servidor. Retenha as chaves de restauração por tempo suficiente para sobreviver às janelas de upgrade do dispositivo (mesmo que o usuário saia do dispositivo antigo após a transferência) e remova as chaves desatualizadas com base no TTL de inatividade ou nos limites de chaves por usuário.

Excluir a chave de restauração

O Credential Manager não tem estado e não tem conhecimento da atividade do usuário. Por isso, ele não exclui automaticamente as chaves de restauração após o uso. Para excluir uma chave de restauração, chame o método clearCredentialState(). Por segurança, exclua a chave sempre que um usuário sair para que ele não seja conectado silenciosamente na próxima inicialização do app.

As chaves de restauração são removidas apenas nas seguintes situações:

  • Ações no nível do sistema: os usuários desinstalam o app ou limpam os dados dele.
  • Chamadas no nível do app: exclua a chave de forma programática chamando clearCredentialState() ao processar a saída do usuário no código do app.

Como um ClearCredentialStateRequest() padrão só limpa o estado da sessão padrão e deixa a chave de restauração intacta, faça uma limpeza dupla ao sair:

  1. Chame credentialManager.clearCredentialState(ClearCredentialStateRequest()) para limpar o estado da sessão do provedor de credenciais ativo.
  2. Chame credentialManager.clearCredentialState() com ClearCredentialStateRequest(ClearCredentialStateRequest.TYPE_CLEAR_RESTORE_CREDENTIAL) para excluir o RestoreCredential armazenado.
  3. Exclua o marcador has_synced_restore_credential local do armazenamento local.
// Clear active credential provider session state
credentialManager.clearCredentialState(ClearCredentialStateRequest())

// When the user logs out, delete the restore key
val clearRequest = ClearCredentialStateRequest(TYPE_CLEAR_RESTORE_CREDENTIAL)
val response = credentialManager.clearCredentialState(clearRequest)

Próximas etapas