Интегрируйте Credential Manager с вашим решением поставщика учетных данных

Диспетчер учетных данных — это набор API-интерфейсов, представленных в Android 14, которые поддерживают несколько методов входа, таких как имя пользователя-пароль, ключи доступа и решения для федеративного входа (например, вход с помощью Google). При вызове API Credential Manager система Android объединяет учетные данные всех поставщиков учетных данных, установленных на устройстве. В этом документе описывается набор API-интерфейсов, которые предоставляют конечные точки интеграции для этих поставщиков учетных данных.

Настраивать

Прежде чем реализовать функциональные возможности своего поставщика учетных данных, выполните действия по настройке, показанные в следующих разделах.

Объявить зависимости

В файле build.gradle вашего модуля объявите зависимость, используя последнюю версию библиотеки Credential Manager:

implementation "androidx.credentials:credentials:1.2.0-{latest}"

Объявить элемент службы в файле манифеста

В файле манифеста вашего приложения AndroidManifest.xml включите объявление <service> для класса обслуживания, который расширяет класс CredentialProviderService из библиотеки androidx.credentials, как показано в примере ниже.

<service android:name=".MyCredentialProviderService"
         android:enabled="true"
         android:exported="true"
         android:label="My Credential Provider"
         android:icon="<any drawable icon>"
         android:permission="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE">
    <intent-filter>
        <action android:name="android.service.credentials.CredentialProviderService"/>
    </intent-filter>
    <meta-data
         android:name="android.credentials.provider"
         android:resource="@xml/provider"/>
</service>

Разрешение и фильтр намерений, показанные выше, являются неотъемлемой частью потока диспетчера учетных данных для правильной работы. Разрешение необходимо, чтобы только система Android могла привязываться к этой службе. Фильтр намерений используется для обнаружения этой службы в качестве поставщика учетных данных, который будет использоваться диспетчером учетных данных.

Объявить поддерживаемые типы учетных данных

В каталоге res/xml создайте новый файл с именем provider.xml . В этом файле объявите типы учетных данных, которые поддерживает ваша служба, с помощью констант, определенных для каждого типа учетных данных в библиотеке. В следующем примере служба поддерживает традиционные пароли, а также ключи доступа, константы для которых определены как TYPE_PASSWORD_CREDENTIAL и TYPE_PUBLIC_KEY_CREDENTIAL :

<?xml version="1.0" encoding="utf-8"?>
<credential-provider xmlns:android="http://schemas.android.com/apk/res/android">
   <capabilities>
       <capability name="android.credentials.TYPE_PASSWORD_CREDENTIAL" />
       <capability name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" />
   </capabilities>
</credential-provider>

На предыдущих уровнях API поставщики учетных данных интегрируются с такими API, как автозаполнение паролей и других данных. Эти поставщики могут использовать одну и ту же внутреннюю инфраструктуру для хранения существующих типов учетных данных, одновременно расширяя ее для поддержки других, включая ключи доступа.

Двухэтапный подход к взаимодействию с провайдером

Диспетчер учетных данных взаимодействует с поставщиками учетных данных в два этапа:

  1. Первая фаза — это фаза начала/запроса , при которой система привязывается к службам поставщика учетных данных и вызывает методы onBeginGetCredentialRequest() , onBeginCreateCredentialRequest() или onClearCredentialStateRequest() с запросами Begin… . Поставщики должны обрабатывать эти запросы и отвечать ответами Begin… , заполняя их записями, представляющими визуальные параметры, которые будут отображаться в средстве выбора учетной записи. Каждая запись должна иметь установленный PendingIntent .
  2. Как только пользователь выбирает запись, начинается фаза выбора , и PendingIntent связанный с записью, активируется, вызывая соответствующую активность поставщика. Как только пользователь завершит взаимодействие с этим действием, поставщик учетных данных должен установить ответ на результат действия, прежде чем завершить его. Затем этот ответ отправляется клиентскому приложению, которое вызвало диспетчер учетных данных.

Обработка создания ключа доступа

Обработка запросов на создание ключа доступа

Когда клиентское приложение желает создать ключ доступа и сохранить его у поставщика учетных данных, оно вызывает API createCredential . Чтобы обработать этот запрос в службе поставщика учетных данных, чтобы ключ доступа фактически сохранялся в вашем хранилище, выполните действия, показанные в следующих разделах.

  1. Переопределите метод onBeginCreateCredentialRequest() в вашей службе, расширенный из CredentialProviderService .
  2. Обработайте BeginCreateCredentialRequest , создав соответствующий BeginCreateCredentialResponse и передав его через обратный вызов.
  3. При создании BeginCreateCredentialResponse добавьте необходимые CreateEntries . Каждый CreateEntry должен соответствовать учетной записи, в которой могут быть сохранены учетные данные, и должен иметь набор PendingIntent вместе с другими необходимыми метаданными.

Следующий пример иллюстрирует, как реализовать эти шаги.

override fun onBeginCreateCredentialRequest(
  request: BeginCreateCredentialRequest,
  cancellationSignal: CancellationSignal,
  callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException>,
) {
  val response: BeginCreateCredentialResponse? = processCreateCredentialRequest(request)
  if (response != null) {
    callback.onResult(response)
  } else {
    callback.onError(CreateCredentialUnknownException())
  }
}

fun processCreateCredentialRequest(request: BeginCreateCredentialRequest): BeginCreateCredentialResponse? {
  when (request) {
    is BeginCreatePublicKeyCredentialRequest -> {
      // Request is passkey type
      return handleCreatePasskeyQuery(request)
    }
  }
  // Request not supported
  return null
}

private fun handleCreatePasskeyQuery(
    request: BeginCreatePublicKeyCredentialRequest
    ): BeginCreateCredentialResponse {

    // Adding two create entries - one for storing credentials to the 'Personal'
    // account, and one for storing them to the 'Family' account. These
    // accounts are local to this sample app only.
    val createEntries: MutableList<CreateEntry> = mutableListOf()
    createEntries.add( CreateEntry(
        PERSONAL_ACCOUNT_ID,
        createNewPendingIntent(PERSONAL_ACCOUNT_ID, CREATE_PASSKEY_INTENT)
    ))

    createEntries.add( CreateEntry(
        FAMILY_ACCOUNT_ID,
        createNewPendingIntent(FAMILY_ACCOUNT_ID, CREATE_PASSKEY_INTENT)
    ))

    return BeginCreateCredentialResponse(createEntries)
}

private fun createNewPendingIntent(accountId: String, action: String): PendingIntent {
    val intent = Intent(action).setPackage(PACKAGE_NAME)

    // Add your local account ID as an extra to the intent, so that when
    // user selects this entry, the credential can be saved to this
    // account
    intent.putExtra(EXTRA_KEY_ACCOUNT_ID, accountId)

    return PendingIntent.getActivity(
        applicationContext, UNIQUE_REQ_CODE,
        intent, (
            PendingIntent.FLAG_MUTABLE
            or PendingIntent.FLAG_UPDATE_CURRENT
        )
    )
}

Ваша конструкция PendingIntent должна соответствовать следующему:

  • Соответствующее действие должно быть настроено для отображения любого необходимого биометрического запроса, подтверждения или выбора.
  • Любые необходимые данные, которые нужны поставщику при вызове соответствующего действия, должны быть установлены как дополнительные для намерения, которое используется для создания вашего PendingIntent , например accountId в потоке создания.
  • Ваш PendingIntent должен быть создан с флагом PendingIntent.FLAG_MUTABLE , чтобы система могла добавить окончательный запрос к дополнительному намерению.
  • Ваш PendingIntent не должен создаваться с флагом PendingIntent.FLAG_ONE_SHOT , поскольку пользователь может выбрать запись, вернуться и повторно выбрать ее, что приведет к двойному срабатыванию PendingIntent .
  • Ваш PendingIntent должен быть создан с использованием уникального кода запроса, чтобы каждая запись могла иметь собственный соответствующий PendingIntent .

Обработка выбора записи для запросов на создание ключа доступа

  1. Когда пользователь выбирает ранее заполненный CreateEntry , вызывается соответствующий PendingIntent и создается связанное с Activity поставщика.
  2. После вызова метода onCreate вашего Activity получите доступ к связанному намерению и передайте его в класс PendingIntentHander , чтобы получить ProviderCreateCredentialRequest .
  3. Извлеките requestJson , callingAppInfo и clientDataHash из запроса.
  4. Извлеките локальный accountId из дополнительного намерения. Это пример реализации для конкретного приложения, который не является обязательным. Этот идентификатор учетной записи можно использовать для хранения этих учетных данных относительно этого конкретного идентификатора учетной записи.
  5. Подтвердите requestJson . В приведенном ниже примере используются локальные классы данных, такие как PublicKeyCredentialCreationOptions для преобразования входного JSON в структурированный класс согласно спецификации WebAuthn. Как поставщик учетных данных вы можете заменить его собственным анализатором.
  6. Проверьте ссылку на актив для вызывающего приложения, если вызов исходит из собственного приложения Android.
  7. Отобразите запрос на аутентификацию. В приведенном ниже примере используется биометрический API Android.
  8. Если аутентификация прошла успешно, сгенерируйте credentialId и пару ключей .
  9. Сохраните закрытый ключ в своей локальной базе данных для callingAppInfo.packageName .
  10. Создайте ответ JSON API веб-аутентификации , состоящий из открытого ключа и credentialId . В приведенном ниже примере используются локальные служебные классы, такие как AuthenticatorAttestationResponse и FidoPublicKeyCredential , которые помогают создавать JSON на основе ранее упомянутой спецификации. Как поставщик учетных данных, вы можете заменить эти классы своими собственными сборщиками.
  11. Создайте CreatePublicKeyCredentialResponse с помощью JSON, созданного выше.
  12. Установите CreatePublicKeyCredentialResponse как дополнительное значение для Intent с помощью PendingIntentHander.setCreateCredentialResponse() и установите это намерение в качестве результата действия.
  13. Завершите действие.

Пример кода ниже иллюстрирует эти шаги. Этот код необходимо обрабатывать в вашем классе Activity после вызова onCreate() .

val request =
  PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)

val accountId = intent.getStringExtra(CredentialsRepo.EXTRA_KEY_ACCOUNT_ID)
if (request != null && request.callingRequest is CreatePublicKeyCredentialRequest) {
  val publicKeyRequest: CreatePublicKeyCredentialRequest =
    request.callingRequest as CreatePublicKeyCredentialRequest
  createPasskey(
    publicKeyRequest.requestJson,
    request.callingAppInfo,
    publicKeyRequest.clientDataHash,
    accountId
  )
}

fun createPasskey(
  requestJson: String,
  callingAppInfo: CallingAppInfo?,
  clientDataHash: ByteArray?,
  accountId: String?
) {
  val request = PublicKeyCredentialCreationOptions(requestJson)

  val biometricPrompt = BiometricPrompt(
    this,
    <executor>,
    object : BiometricPrompt.AuthenticationCallback() {
      override fun onAuthenticationError(
        errorCode: Int, errString: CharSequence
      ) {
        super.onAuthenticationError(errorCode, errString)
        finish()
      }

      override fun onAuthenticationFailed() {
        super.onAuthenticationFailed()
        finish()
      }

      override fun onAuthenticationSucceeded(
        result: BiometricPrompt.AuthenticationResult
      ) {
        super.onAuthenticationSucceeded(result)

        // Generate a credentialId
        val credentialId = ByteArray(32)
        SecureRandom().nextBytes(credentialId)

        // Generate a credential key pair
        val spec = ECGenParameterSpec("secp256r1")
        val keyPairGen = KeyPairGenerator.getInstance("EC");
        keyPairGen.initialize(spec)
        val keyPair = keyPairGen.genKeyPair()

        // Save passkey in your database as per your own implementation

        // Create AuthenticatorAttestationResponse object to pass to
        // FidoPublicKeyCredential

        val response = AuthenticatorAttestationResponse(
          requestOptions = request,
          credentialId = credentialId,
          credentialPublicKey = getPublicKeyFromKeyPair(keyPair),
          origin = appInfoToOrigin(callingAppInfo),
          up = true,
          uv = true,
          be = true,
          bs = true,
          packageName = callingAppInfo.packageName
        )

        val credential = FidoPublicKeyCredential(
          rawId = credentialId, response = response
        )
        val result = Intent()

        val createPublicKeyCredResponse =
          CreatePublicKeyCredentialResponse(credential.json())

        // Set the CreateCredentialResponse as the result of the Activity
        PendingIntentHandler.setCreateCredentialResponse(
          result, createPublicKeyCredResponse
        )
        setResult(Activity.RESULT_OK, result)
        finish()
      }
    }
  )

  val promptInfo = BiometricPrompt.PromptInfo.Builder()
    .setTitle("Use your screen lock")
    .setSubtitle("Create passkey for ${request.rp.name}")
    .setAllowedAuthenticators(
        BiometricManager.Authenticators.BIOMETRIC_STRONG
        /* or BiometricManager.Authenticators.DEVICE_CREDENTIAL */
      )
    .build()
  biometricPrompt.authenticate(promptInfo)
}

fun appInfoToOrigin(info: CallingAppInfo): String {
  val cert = info.signingInfo.apkContentsSigners[0].toByteArray()
  val md = MessageDigest.getInstance("SHA-256");
  val certHash = md.digest(cert)
  // This is the format for origin
  return "android:apk-key-hash:${b64Encode(certHash)}"
}

Обработка запросов на создание пароля

Для обработки запросов на создание пароля выполните следующие действия:

  • Внутри вашего методаprocessCreateCredentialRequest processCreateCredentialRequest() упомянутого в предыдущем разделе, добавьте еще один случай внутри блока переключателя для обработки запросов пароля.
  • При создании BeginCreateCredentialResponse добавьте необходимые CreateEntries .
  • Каждый CreateEntry должен соответствовать учетной записи, в которой могут быть сохранены учетные данные, и для него должен быть установлен PendingIntent вместе с другими метаданными.

Следующий пример иллюстрирует, как реализовать эти шаги:

fun processCreateCredentialRequest(
    request: BeginCreateCredentialRequest
  ): BeginCreateCredentialResponse? {
  when (request) {
    is BeginCreatePublicKeyCredentialRequest -> {
      // Request is passkey type
      return handleCreatePasskeyQuery(request)
    }

    is BeginCreatePasswordCredentialRequest -> {
    // Request is password type
      return handleCreatePasswordQuery(request)
    }
  }
  return null
}

private fun handleCreatePasswordQuery(
    request: BeginCreatePasswordCredentialRequest
  ): BeginCreateCredentialResponse {
  val createEntries: MutableList<CreateEntry> = mutableListOf()

  // Adding two create entries - one for storing credentials to the 'Personal'
  // account, and one for storing them to the 'Family' account. These
  // accounts are local to this sample app only.
  createEntries.add(
    CreateEntry(
      PERSONAL_ACCOUNT_ID,
      createNewPendingIntent(PERSONAL_ACCOUNT_ID, CREATE_PASSWORD_INTENT)
    )
  )
  createEntries.add(
    CreateEntry(
      FAMILY_ACCOUNT_ID,
      createNewPendingIntent(FAMILY_ACCOUNT_ID, CREATE_PASSWORD_INTENT)
    )
  )

  return BeginCreateCredentialResponse(createEntries)
}

Обработка выбора записи для запросов на создание пароля

Когда пользователь выбирает заполненный CreateEntry , соответствующий PendingIntent выполняется и вызывает связанное действие. Получите доступ к связанному намерению, переданному в onCreate , и передайте его в класс PendingIntentHander чтобы получить метод ProviderCreateCredentialRequest .

Пример ниже иллюстрирует, как реализовать этот процесс. Этот код необходимо обрабатывать в методе onCreate() вашего Activity.

val createRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
val accountId = intent.getStringExtra(CredentialsRepo.EXTRA_KEY_ACCOUNT_ID)

val request: CreatePasswordRequest = createRequest.callingRequest as CreatePasswordRequest

// Fetch the ID and password from the request and save it in your database
<your_database>.addNewPassword(
    PasswordInfo(
        request.id,
        request.password,
        createRequest.callingAppInfo.packageName
    )
)

//Set the final response back
val result = Intent()
val response = CreatePasswordResponse()
PendingIntentHandler.setCreateCredentialResponse(result, response)
setResult(Activity.RESULT_OK, result)
this@<activity>.finish()

Обработка входа пользователя в систему

Вход пользователя в систему осуществляется с помощью следующих шагов:

  • Когда клиентское приложение пытается войти в систему пользователя , оно подготавливает экземпляр GetCredentialRequest .
  • Платформа Android передает этот запрос всем применимым поставщикам учетных данных путем привязки к этим службам.
  • Затем служба поставщика получает BeginGetCredentialRequest , содержащий список BeginGetCredentialOption , каждый из которых содержит параметры, которые можно использовать для получения соответствующих учетных данных.

Чтобы обработать этот запрос в службе поставщика учетных данных, выполните следующие действия:

  1. Переопределите метод onBeginGetCredentialRequest() для обработки запроса. Обратите внимание: если ваши учетные данные заблокированы, вы можете немедленно установить AuthenticationAction для ответа и вызвать обратный вызов.

    private val unlockEntryTitle = "Authenticate to continue"
    
    override fun onBeginGetCredentialRequest(
        request: BeginGetCredentialRequest,
        cancellationSignal: CancellationSignal,
        callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,
    ) {
        if (isAppLocked()) {
            callback.onResult(BeginGetCredentialResponse(
                authenticationActions = mutableListOf(AuthenticationAction(
                    unlockEntryTitle, createUnlockPendingIntent())
                    )
                )
            )
            return
        }
        try {
            response = processGetCredentialRequest(request)
            callback.onResult(response)
        } catch (e: GetCredentialException) {
            callback.onError(GetCredentialUnknownException())
        }
    }
    

    Поставщики, которым требуется разблокировать учетные данные перед возвратом любых credentialEntries , должны настроить ожидающее намерение, которое направляет пользователя к потоку разблокировки приложения:

    private fun createUnlockPendingIntent(): PendingIntent {
        val intent = Intent(UNLOCK_INTENT).setPackage(PACKAGE_NAME)
        return PendingIntent.getActivity(
        applicationContext, UNIQUE_REQUEST_CODE, intent, (
            PendingIntent.FLAG_MUTABLE
            or PendingIntent.FLAG_UPDATE_CURRENT
            )
        )
    }
    
  2. Получите учетные данные из локальной базы данных и настройте их с помощью CredentialEntries для отображения в селекторе. Для ключей доступа вы можете установить credentialId в качестве дополнительного параметра намерения, чтобы знать, с какими учетными данными он сопоставляется, когда пользователь выбирает эту запись.

    companion object {
        // These intent actions are specified for corresponding activities
        // that are to be invoked through the PendingIntent(s)
        private const val GET_PASSKEY_INTENT_ACTION = "PACKAGE_NAME.GET_PASSKEY"
        private const val GET_PASSWORD_INTENT_ACTION = "PACKAGE_NAME.GET_PASSWORD"
    
    }
    
    fun processGetCredentialsRequest(
    request: BeginGetCredentialRequest
    ): BeginGetCredentialResponse {
        val callingPackage = request.callingAppInfo?.packageName
        val credentialEntries: MutableList<CredentialEntry> = mutableListOf()
    
        for (option in request.beginGetCredentialOptions) {
            when (option) {
                is BeginGetPasswordOption -> {
                    credentialEntries.addAll(
                            populatePasswordData(
                                callingPackage,
                                option
                            )
                        )
                    }
                    is BeginGetPublicKeyCredentialOption -> {
                        credentialEntries.addAll(
                            populatePasskeyData(
                                callingPackage,
                                option
                            )
                        )
                    )
                } else -> {
                    Log.i(TAG, "Request not supported")
                }
            }
        }
        return BeginGetCredentialResponse(credentialEntries)
    }
    
  3. Запросите учетные данные из вашей базы данных, создайте записи пароля и пароля для заполнения.

    private fun populatePasskeyData(
        callingAppInfo: CallingAppInfo,
        option: BeginGetPublicKeyCredentialOption
    ): List<CredentialEntry> {
      val passkeyEntries: MutableList<CredentialEntry> = mutableListOf()
      val request = PublicKeyCredentialRequestOptions(option.requestJson)
      // Get your credentials from database where you saved during creation flow
      val creds = <getCredentialsFromInternalDb(request.rpId)>
      val passkeys = creds.passkeys
      for (passkey in passkeys) {
          val data = Bundle()
          data.putString("credId", passkey.credId)
          passkeyEntries.add(
              PublicKeyCredentialEntry(
                  context = applicationContext,
                  username = passkey.username,
                  pendingIntent = createNewPendingIntent(
                      GET_PASSKEY_INTENT_ACTION,
                      data
                  ),
                  beginPublicKeyCredentialOption = option,
                  displayName = passkey.displayName,
                  icon = passkey.icon
              )
          )
      }
      return passkeyEntries
    }
    
    // Fetch password credentials and create password entries to populate to
    // the user
    private fun populatePasswordData(
    callingPackage: String,
    option: BeginGetPasswordOption
    ): List<CredentialEntry> {
        val passwordEntries: MutableList<CredentialEntry> = mutableListOf()
    
        // Get your password credentials from database where you saved during
        // creation flow
        val creds = <getCredentialsFromInternalDb(callingPackage)>
        val passwords = creds.passwords
        for (password in passwords) {
            passwordEntries.add(
                PasswordCredentialEntry(
                    context = applicationContext,
                    username = password.username,
                    pendingIntent = createNewPendingIntent(
                    GET_PASSWORD_INTENT
                    ),
                    beginGetPasswordOption = option
                        displayName = password.username,
                    icon = password.icon
                )
            )
        }
        return passwordEntries
    }
    
    private fun createNewPendingIntent(
        action: String,
        extra: Bundle? = null
    ): PendingIntent {
        val intent = Intent(action).setPackage(PACKAGE_NAME)
        if (extra != null) {
            intent.putExtra("CREDENTIAL_DATA", extra)
        }
    
        return PendingIntent.getActivity(
            applicationContext, UNIQUE_REQUEST_CODE, intent,
            (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
        )
    }
    
  4. После того как вы запросите и заполните учетные данные, теперь вам нужно обработать этап выбора учетных данных, выбранных пользователем, будь то ключ доступа или пароль.

Обработка выбора пользователем ключей доступа

  1. В методе onCreate соответствующего действия извлеките связанное намерение и перейдите к PendingIntentHandler.retrieveProviderGetCredentialRequest() .
  2. Извлеките GetPublicKeyCredentialOption из запроса, полученного выше. Впоследствии извлеките requestJson и clientDataHash из этой опции.
  3. Извлеките credentialId из дополнительного намерения, которое было заполнено поставщиком учетных данных при настройке соответствующего PendingIntent .
  4. Извлеките ключ доступа из локальной базы данных, используя параметры запроса, указанные выше.
  5. Подтвердите, что ключ доступа действителен с помощью извлеченных метаданных и проверки пользователя.

    val getRequest =
        PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
    val publicKeyRequest =
    getRequest.credentialOption as GetPublicKeyCredentialOption
    
    val requestInfo = intent.getBundleExtra("CREDENTIAL_DATA")
    val credIdEnc = requestInfo.getString("credId")
    
    // Get the saved passkey from your database based on the credential ID
    // from the publickeyRequest
    val passkey = <your database>.getPasskey(credIdEnc)
    
    // Decode the credential ID, private key and user ID
    val credId = b64Decode(credIdEnc)
    val privateKey = b64Decode(passkey.credPrivateKey)
    val uid = b64Decode(passkey.uid)
    
    val origin = appInfoToOrigin(getRequest.callingAppInfo)
    val packageName = getRequest.callingAppInfo.packageName
    
    validatePasskey(
        publicKeyRequest.requestJson,
        origin,
        packageName,
        uid,
        passkey.username,
        credId,
        privateKey
    )
    
  6. Чтобы подтвердить пользователя, откройте биометрическое приглашение (или другой метод подтверждения). В приведенном ниже фрагменте кода используется биометрический API Android.

  7. После успешной аутентификации создайте ответ JSON на основе спецификации W3 Web Authentication Assertion . В приведенном ниже фрагменте кода вспомогательные классы данных, такие как AuthenticatorAssertionResponse используются для приема структурированных параметров и преобразования их в необходимый формат JSON. Ответ содержит цифровую подпись на основе закрытого ключа учетных данных WebAuthn. Сервер проверяющей стороны может проверить эту подпись для аутентификации пользователя перед входом в систему.

  8. Создайте PublicKeyCredential , используя сгенерированный выше JSON, и установите его в финальном GetCredentialResponse . Установите этот окончательный ответ на результат этого действия.

Следующий пример иллюстрирует, как можно реализовать эти шаги:

val request = PublicKeyCredentialRequestOptions(requestJson)
val privateKey: ECPrivateKey = convertPrivateKey(privateKeyBytes)

val biometricPrompt = BiometricPrompt(
    this,
    <executor>,
    object : BiometricPrompt.AuthenticationCallback() {
        override fun onAuthenticationError(
        errorCode: Int, errString: CharSequence
        ) {
            super.onAuthenticationError(errorCode, errString)
            finish()
        }

        override fun onAuthenticationFailed() {
            super.onAuthenticationFailed()
            finish()
        }

        override fun onAuthenticationSucceeded(
        result: BiometricPrompt.AuthenticationResult
        ) {
        super.onAuthenticationSucceeded(result)
        val response = AuthenticatorAssertionResponse(
            requestOptions = request,
            credentialId = credId,
            origin = origin,
            up = true,
            uv = true,
            be = true,
            bs = true,
            userHandle = uid,
            packageName = packageName
        )

        val sig = Signature.getInstance("SHA256withECDSA");
        sig.initSign(privateKey)
        sig.update(response.dataToSign())
        response.signature = sig.sign()

        val credential = FidoPublicKeyCredential(
            rawId = credId, response = response
        )
        val result = Intent()
        val passkeyCredential = PublicKeyCredential(credential.json)
        PendingIntentHandler.setGetCredentialResponse(
            result, GetCredentialResponse(passkeyCredential)
        )
        setResult(RESULT_OK, result)
        finish()
        }
    }
)

val promptInfo = BiometricPrompt.PromptInfo.Builder()
    .setTitle("Use your screen lock")
    .setSubtitle("Use passkey for ${request.rpId}")
    .setAllowedAuthenticators(
            BiometricManager.Authenticators.BIOMETRIC_STRONG
            /* or BiometricManager.Authenticators.DEVICE_CREDENTIAL */
        )
    .build()
biometricPrompt.authenticate(promptInfo)

Обработка выбора пользователя для аутентификации по паролю

  1. В соответствующем действии получите доступ к намерению, переданному в onCreate , и извлеките ProviderGetCredentialRequest с помощью PendingIntentHandler .
  2. Используйте GetPasswordOption в запросе, чтобы получить учетные данные пароля для имени входящего пакета.

    val getRequest =
    PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
    
    val passwordOption = getRequest.credentialOption as GetPasswordCredentialOption
    
    val username = passwordOption.username
    // Fetch the credentials for the calling app package name
    val creds = <your_database>.getCredentials(callingAppInfo.packageName)
    val passwords = creds.passwords
    val it = passwords.iterator()
    var password = ""
    while (it.hasNext() == true) {
        val passwordItemCurrent = it.next()
        if (passwordItemCurrent.username == username) {
           password = passwordItemCurrent.password
           break
        }
    }
    
  3. После получения установите ответ для выбранных учетных данных пароля.

    // Set the response back
    val result = Intent()
    val passwordCredential = PasswordCredential(username, password)
    PendingIntentHandler.setGetCredentialResponse(
    result, GetCredentialResponse(passwordCredential)
    )
    setResult(Activity.RESULT_OK, result)
    finish()
    

Обработка выбора записи действия аутентификации

Как упоминалось ранее , поставщик учетных данных может установить AuthenticationAction если учетные данные заблокированы. Если пользователь выбирает эту запись, вызывается действие, соответствующее действию намерения, установленному в PendingIntent . Поставщики учетных данных могут затем использовать поток биометрической аутентификации или аналогичный механизм для разблокировки учетных данных. В случае успеха поставщик учетных данных должен создать BeginGetCredentialResponse аналогично тому, как описана обработка входа пользователя в систему , поскольку теперь учетные данные разблокированы. Затем этот ответ необходимо установить с помощью метода PendingIntentHandler.setBeginGetCredentialResponse() прежде чем подготовленное намерение будет установлено как результат и действие будет завершено.

Очистить запросы учетных данных

Клиентское приложение может запросить очистку любого состояния, поддерживаемого для выбора учетных данных, например, поставщик учетных данных может запомнить ранее выбранные учетные данные и возвращать их только в следующий раз. Клиентское приложение вызывает этот API и ожидает, что этот закрепленный выбор будет очищен. Служба поставщика учетных данных может обработать этот запрос, переопределив метод onClearCredentialStateRequest() :

override fun onClearCredentialStateRequest(
    request: android.service.credentials.ClearCredentialStateRequest,
    cancellationSignal: CancellationSignal,
    callback: OutcomeReceiver<Void?, ClearCredentialException>,
  ) {
    // Delete any maintained state as appropriate.
}

Чтобы пользователи могли открывать настройки вашего поставщика на экране «Пароли, ключи доступа и автозаполнение» , приложения поставщика учетных данных должны реализовать атрибут манифеста credential-provider settingsActivity в res/xml/provider.xml . Этот атрибут позволяет использовать намерение открыть собственный экран настроек вашего приложения, если пользователь щелкает имя поставщика в списке служб «Пароли, ключи доступа и автозаполнение» . Установите значение этого атрибута на имя активности, которая будет запускаться с экрана настроек.

<credential-provider
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:settingsSubtitle="Example settings provider name"
    android:settingsActivity="com.example.SettingsActivity">
    <capabilities>
        <capability name="android.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" />
    </capabilities>
</credential-provider>
Схема, показывающая функции кнопок изменения и открытия.
Рис. 1. Кнопка «Изменить» открывает существующее диалоговое окно выбора, позволяя пользователю выбрать предпочитаемого поставщика учетных данных. Кнопка «Открыть» запускает действие настроек, определенное в изменении манифеста, и открывает страницу настроек специально для этого поставщика.

Намерения настроек

Открыть настройки : намерение android.settings.CREDENTIAL_PROVIDER открывает экран настроек, на котором пользователь может выбрать предпочтительных и дополнительных поставщиков учетных данных.

Экран настроек паролей, паролей и автозаполнения
Рисунок 2. Экран настроек паролей, паролей и автозаполнения.

Предпочитаемая служба учетных данных : намерение ACTION_REQUEST_SET_AUTOFILL_SERVICE перенаправляет пользователя на экран выбора предпочтительного поставщика. Выбранный на этом экране поставщик становится предпочтительным поставщиком учетных данных и автозаполнения.

Схема, показывающая функции кнопок изменения и открытия.
Рисунок 3: Предпочитаемый сервис для паролей, ключей доступа и экрана настроек автозаполнения.

Получить белый список привилегированных приложений

Привилегированные приложения, такие как веб-браузеры, выполняют вызовы диспетчера учетных данных от имени других проверяющих сторон, устанавливая параметр origin в методах GetCredentialRequest() и CreatePublicKeyCredentialRequest() диспетчера учетных данных. Для обработки этих запросов поставщик учетных данных получает origin с помощью API getOrigin() .

Чтобы получить origin , приложению поставщика учетных данных необходимо передать список привилегированных и доверенных вызывающих абонентов в API androidx.credentials.provider.CallingAppInfo's getOrigin() . Этот белый список должен быть допустимым объектом JSON. origin возвращается, если packageName и отпечатки сертификатов, полученные из signingInfo совпадают с отпечатками приложения, найденными в списке privilegedAllowlist переданном в API getOrigin() . После получения значения origin приложение-поставщик должно считать это привилегированным вызовом и установить это origin для данных клиента в AuthenticatorResponse вместо вычисления origin с использованием подписи вызывающего приложения.

Если вы извлекаете origin , используйте clientDataHash , предоставленный непосредственно в CreatePublicKeyCredentialRequest() или GetPublicKeyCredentialOption() вместо сборки и хеширования clientDataJSON во время запроса подписи. Чтобы избежать проблем с анализом JSON, установите значение-заполнитель для clientDataJSON в ответе на подтверждение и утверждение. Диспетчер паролей Google использует общедоступный список разрешений для вызовов getOrigin() . Как поставщик учетных данных вы можете использовать этот список или предоставить свой собственный в формате JSON, описанном API. Поставщик сам выбирает, какой список использовать. Чтобы получить привилегированный доступ к сторонним поставщикам учетных данных, обратитесь к документации, предоставленной третьей стороной.

Включить поставщиков на устройстве

Пользователи должны включить поставщика через настройки устройства > Пароли и учетные записи > Ваш провайдер > Включить или отключить .

fun createSettingsPendingIntent(): PendingIntent