API переноса учетных данных в Менеджере учетных данных позволяют безопасно переносить учетные данные пользователей между поставщиками учетных данных на одном устройстве. В этом руководстве рассказывается, как поставщики учетных данных на Android могут интегрировать API, предоставляемые библиотекой androidx.credentials:providerevents. Эта функция поддерживает пароли, ключи доступа, адреса и пользовательские поля, используя стандартизированный формат обмена учетными данными FIDO (CXF).
Основные понятия
Фреймворк для передачи учетных данных позволяет переносить их между приложениями на одном устройстве, не раскрывая необработанные учетные данные ОС Android или неаутентифицированным приложениям.
В рамках этой модели определены две основные роли:
- Экспортер (поставщик источника). Поставщик учетных данных, у которого в настоящее время хранятся учетные данные пользователя. Он предварительно регистрирует в системе метаданные о доступных для экспорта аккаунтах (
ExportEntry) и отвечает на запросы о переносе, когда пользователь выбирает его. - Импортер (поставщик учетных данных или мастер настройки). Поставщик учетных данных или мастер настройки, который инициирует запрос на импорт (
ImportCredentialsRequest), указывая, какие типы учетных данных и расширений он может получать.
Совместимость с версиями Android
Credentials Transfer API работает на устройствах с Android 8 (уровень API 26) и более поздних версий.
Добавьте зависимости
Добавьте зависимость androidx.credentials:providerevents в файл build.gradle или build.gradle.kts модуля:
dependencies {
implementation("androidx.credentials:providerevents:1.0.0-alpha06")
}
Создайте экземпляр нужного класса
Создайте экземпляр нужного класса ProviderEventsManager.
val providerEventsManager = ProviderEventsManager.create(context)
Реализуйте экспортер
Чтобы пользователи могли экспортировать учетные данные из вашего приложения в другие поставщики учетных данных на устройстве, реализуйте роль экспортера.
Регистрация записей экспорта
Когда поставщик учетных данных меняется (например, пользователь входит в систему, добавляет учетные данные или изменяет аккаунты), зарегистрируйте или обновите элементы ExportEntry с помощью ProviderEventsManager.registerExport().
Для каждого ExportEntry требуется:
id– секретный, случайно сгенерированный строковый идентификатор, уникально представляющий эту запись экспорта. Сохраните этот идентификатор в надежном месте. Он понадобится вам позже для проверки входящих запросов на перенос.accountDisplayName– необязательный ярлык аккаунта (например,"Personal Account").userDisplayName– основной идентификатор пользователя (например,"alice@example.com").icon– значокBitmap, представляющий поставщика или аккаунт (автоматически масштабируется библиотекой до PNG размером 32 x 32).supportedCredentialTypes– набор строковых констант изCredentialTypes, представляющих типы, которые содержит эта запись.
suspend fun registerMyProviderForExport( providerEventsManager: ProviderEventsManager, providerIcon: Bitmap, // Randomly generated and stored in encrypted storage secretEntryId: String ) { val entry = ExportEntry( id = secretEntryId, accountDisplayName = "MyProvider Personal", userDisplayName = "alice@example.com", icon = providerIcon, supportedCredentialTypes = setOf( CredentialTypes.CREDENTIAL_TYPE_BASIC_AUTH, // Passwords CredentialTypes.CREDENTIAL_TYPE_PUBLIC_KEY, // Passkeys CredentialTypes.CREDENTIAL_TYPE_ADDRESS, CredentialTypes.CREDENTIAL_TYPE_CREDIT_CARD ) ) // RegisterExportRequest.create() attaches the default WASM matcher from assets val request = RegisterExportRequest.create(context, listOf(entry)) try { val response = providerEventsManager.registerExport(request) // Registration successful } catch (e: Exception) { // Handle registration exceptions (e.g., RegisterExportProviderConfigurationException) } }
Объявите действие экспортера в файле манифеста
Когда пользователь выбирает ваше приложение ExportEntry в интерфейсе выбора, система запускает назначенное вами действие Activity. Объявите это действие с обязательным действием намерения и схемой URI контента.
Чтобы защититься от несанкционированных вызовов и атак типа "запутанный заместитель", объявите android:requireContentUriPermissionFromCaller="write" в Android 14 (уровень API 34) и более поздних версиях. Это указывает операционной системе Android, что перед запуском действия нужно проверить, есть ли у вызывающего абонента, инициирующего запуск, разрешения на запись в URI целевого контента, как показано в следующем примере:
<activity
android:name="com.example.CredentialExportActivity"
android:exported="true"
android:requireContentUriPermissionFromCaller="write"
android:label="@string/export_activity_label">
<intent-filter>
<!-- This intent action is required for Credential Manager to invoke this activity -->
<action android:name="androidx.identitycredentials.action.IMPORT_CREDENTIALS" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="content" />
</intent-filter>
</activity>
Обработка намерения передачи в действии
В CredentialExportActivity используйте IntentHandler.retrieveProviderImportCredentialsRequest(intent), чтобы проанализировать запрос, проверить вызывающий компонент и метаданные запроса, выполнить необходимую биометрическую аутентификацию и записать полезную нагрузку FIDO CXF обратно в предоставленный URI контента.
Выполните аутентификацию запроса
Поскольку ваши действия экспортируются, защитите поток экспорта учетных данных, проверив каждый из следующих уровней:
- Проверьте одноразовый код сеанса
credId(основная защита). При регистрацииExportEntryсгенерируйте криптографически случайныйid(одноразовый код) с высокой энтропией и свяжите его с кратковременным сеансом. В своем действии убедитесь, чтоrequest.credIdсоответствует активному, неистекшему одноразовому коду сеанса, ранее выданному вашим поставщиком. Поскольку сторонние приложения не могут угадать этот nonce, это гарантирует, что интент соответствует подлинному выбору пользователя в системном селекторе. - Проверка
Activity.getCallingPackage()(проверка вызывающего абонента). Поскольку селектор учетных данных запускает ваше действие с помощьюstartActivityForResult, ваше действие может проверитьgetCallingPackage(), чтобы подтвердить, что непосредственным вызывающим абонентом, запускающим пользовательский интерфейс, является доверенная системная среда выбора учетных данных (на устройствах Android с сервисами Google Play,"com.google.android.gms"). Отклоните запрос, если вызывающий пакет не распознан. - Проверка
request.callingAppInfo(информация о целевом приложении).callingAppInfoидентифицирует целевое приложение, которое получит импортированные учетные данные. Вы можете использовать эту информацию по своему усмотрению, например показывать название и значок целевого приложения в интерфейсе подтверждения или проверять, является ли целевое приложение надежным.
class CredentialExportActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 1. Extract the transfer request from the incoming Intent val request: ProviderImportCredentialsRequest? = IntentHandler.retrieveProviderImportCredentialsRequest(intent) if (request == null) { finishWithError() return } // 2. Validate CallingAppInfo and secret `credId` val callingAppPackage = request.callingAppInfo.packageName val receivedCredId = request.credId if (!verifySecretEntryId(receivedCredId) || !isTrustedImporter(callingAppPackage)) { // Secret ID mismatch or untrusted caller -> abort sendExceptionAndFinish(ImportCredentialsNoExportOptionException("Unauthorized request")) return } // 3. Optional: Prompt user for Biometric / PIN authentication before exporting authenticateUserThenExport(request) } private fun authenticateUserThenExport(request: ProviderImportCredentialsRequest) { // ... Biometric prompt logic ... // Once authenticated, generate the FIDO CXF JSON string matching the requested types val cxfJsonPayload = buildFidoCxfJsonPayload( requestedTypes = request.request.credentialTypes, requestedExtensions = request.request.knownExtensions ) val response = ImportCredentialsResponse(cxfJsonPayload) // 4. Write the JSON payload to the Content URI and set Activity result IntentHandler.setImportCredentialsResponse( context = this, uri = request.uri, intent = intent, response = response ) setResult(Activity.RESULT_OK, intent) finish() } private fun sendExceptionAndFinish(exception: androidx.credentials.providerevents.exception.ImportCredentialsException) { IntentHandler.setImportCredentialsException(intent, exception) setResult(Activity.RESULT_OK, intent) finish() } private fun verifySecretEntryId(credentialId: String): Boolean { // Check if credentialId matches what you stored when calling RegisterExportRequest return credentialId == getStoredSecretEntryId() } private fun isTrustedImporter(packageName: String): Boolean { // Implement any specific allowlisting / caller checks if required return true } private fun finishWithError() { setResult(Activity.RESULT_CANCELED) finish() } }
Исключения для регистрации экспорта и клиринга
Все исключения в этом модуле являются подклассами ImportCredentialsException, RegisterExportException или ClearExportException.
RegisterExportProviderConfigurationExceptionилиClearExportProviderConfigurationException. Возникает, когда регистрация или очистка не удается из-за проблем с настройкой поставщика (например, пустое полеsupportedCredentialTypesвExportEntryили вызов на неподдерживаемом уровне ОС).RegisterExportUnknownErrorExceptionилиClearExportUnknownErrorException– при обновлении реестра экспорта произошла неклассифицированная ошибка системы или хранилища.
Как реализовать импортер
Чтобы импортировать учетные данные в приложение (например, во время регистрации или импорта поставщика), реализуйте роль импортера и запустите процесс, вызвав ProviderEventsManager.importCredentials().
Создайте запрос на импорт и запустите процесс
Укажите, какие CredentialTypes и KnownExtensions поддерживает ваш импортер:
suspend fun startCredentialImport( activityContext: Context, providerEventsManager: ProviderEventsManager ) { val importRequest = ImportCredentialsRequest( credentialTypes = setOf( CredentialTypes.CREDENTIAL_TYPE_BASIC_AUTH, CredentialTypes.CREDENTIAL_TYPE_PUBLIC_KEY, CredentialTypes.CREDENTIAL_TYPE_ADDRESS, CredentialTypes.CREDENTIAL_TYPE_NOTE ), knownExtensions = setOf( KnownExtensions.KNOWN_EXTENSION_SHARED ) ) try { // Launches the system Selector UI; suspends until user selects a provider and completes transfer val response = providerEventsManager.importCredentials(activityContext, importRequest) // 1. Inspect the source exporter's package info val exporterPackageName = response.callingAppInfo.packageName // 2. Parse the FIDO CXF JSON string val cxfJsonString = response.response.responseJson parseAndSaveImportedCredentials(cxfJsonString) } catch (e: ImportCredentialsException) { // Handle specific import exceptions (e.g., ImportCredentialsCancellationException) handleImportFailure(e) } } private fun parseAndSaveImportedCredentials(cxfJsonString: String) { val rootJson = JSONObject(cxfJsonString) // Parse according to FIDO Credential Exchange Format (CXF v1.0) specification: // https://fidoalliance.org/specs/cx/cxf-v1.0-ps-20250814.html } // Helper function to make it compile private fun handleImportFailure(e: ImportCredentialsException) {}
Исключения при импорте потока
Все исключения в этом модуле являются подклассами ImportCredentialsException, RegisterExportException или ClearExportException.
ImportCredentialsCancellationException– пользователь закрыл интерфейс выбора или отменил экспорт (Activity.RESULT_CANCELED).ImportCredentialsNoExportOptionException– ни одна из зарегистрированных записей экспорта не соответствует запрошенным типам учетных данных или выбранный экспортер выдал исключение отклонения.ImportCredentialsProviderConfigurationException– ошибка конфигурации (например, пустое значениеcredentialTypesвImportCredentialsRequestили отсутствие разрешений).ImportCredentialsInvalidJsonException– экспортер вернул пустую или неправильно сформированную полезную нагрузку JSON, которая не прошла проверку запроса.ImportCredentialsSystemErrorException– во время импорта произошла внутренняя ошибка системы Android или передачи Binder.ImportCredentialsUnknownCallerException– фреймворку не удалось проверить вызывающее приложение.ImportCredentialsUnknownErrorException– неклассифицированная или непредвиденная ошибка в процессе импорта.
Поддерживаемые типы учетных данных и расширения
Объект androidx.credentials.providerevents.transfer.CredentialTypes определяет стандартизированные строковые константы, соответствующие типам элементов FIDO CXF:
| Константа | Значение (тип cxf) |
Описание |
|---|---|---|
CREDENTIAL_TYPE_BASIC_AUTH |
"basic-auth" |
Учетные данные для входа (имя пользователя и пароль). |
CREDENTIAL_TYPE_PUBLIC_KEY |
"passkey" |
Учетные данные открытого ключа доступа FIDO2 или WebAuthn. |
CREDENTIAL_TYPE_ADDRESS |
"address" |
Почтовый адрес или адрес доставки для автозаполнения форм. |
CREDENTIAL_TYPE_API_KEY |
"api-key" |
ключи и токены доступа к API; |
CREDENTIAL_TYPE_CREDIT_CARD |
"credit-card" |
Платежные данные кредитных и дебетовых карт. |
CREDENTIAL_TYPE_CUSTOM_FIELDS |
"custom-fields" |
Специальные группы или поля, заданные пользователем. |
CREDENTIAL_TYPE_DRIVERS_LICENSE |
"drivers-license" |
Сведения о водительском удостоверении. |
CREDENTIAL_TYPE_FILE |
"file" |
Метаданные и ссылки на плейсхолдеры для двоичных файлов. |
CREDENTIAL_TYPE_GENERATED_PASSWORD |
"generated-password" |
Надежные пароли, созданные компьютером. |
CREDENTIAL_TYPE_IDENTITY_DOCUMENT |
"identity-document" |
Удостоверения личности государственного образца, номера социального страхования (SSN), идентификационные номера налогоплательщика (TIN) или паспортные данные. |
CREDENTIAL_TYPE_ITEM_REFERENCE |
"item-reference" |
Логические ссылки, указывающие на другой элемент в полезной нагрузке. |
CREDENTIAL_TYPE_NOTE |
"note" |
Пользовательские защищенные заметки (строка UTF-8). |
CREDENTIAL_TYPE_PASSPORT |
"passport" |
Сведения о паспорте или проездном документе. |
CREDENTIAL_TYPE_PERSON_NAME |
"person-name" |
Идентификационные данные и имена. |
CREDENTIAL_TYPE_SSH_KEY |
"ssh-key" |
Пары открытых и закрытых ключей SSH. |
CREDENTIAL_TYPE_TOTP |
"totp" |
Секретные ключи для одноразовых кодов, ограниченных по времени (двухфакторная аутентификация). |
CREDENTIAL_TYPE_WIFI |
"wifi" |
SSID и кодовые фразы сетей Wi-Fi. |
Специальные сопоставители WASM (WebAssembly) (расширенные настройки)
По умолчанию вызов RegisterExportRequest.create(context, entries) объединяет системный credential_transfer_matcher.wasm из объектов библиотеки, который фильтрует записи исключительно на основе пересечения supportedCredentialTypes.
Если вашему поставщику учетных данных требуется сложная логика сопоставления (например, динамические проверки возможностей или условная фильтрация на основе настраиваемых полей), вы можете скомпилировать собственный модуль WebAssembly, соответствующий API передачи учетных данных WASM, и передать необработанный массив байтов напрямую:
// Loading a custom WASM matcher val customMatcherBytes = context.assets.open("my_custom_matcher.wasm").use { it.readBytes() } val customRequest = RegisterExportRequest( entries = myEntries, exportMatcher = customMatcherBytes ) providerEventsManager.registerExport(customRequest)