Перенос учетных данных

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 контента.

Выполните аутентификацию запроса

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

  1. Проверьте одноразовый код сеанса credId (основная защита). При регистрации ExportEntry сгенерируйте криптографически случайный id (одноразовый код) с высокой энтропией и свяжите его с кратковременным сеансом. В своем действии убедитесь, что request.credId соответствует активному, неистекшему одноразовому коду сеанса, ранее выданному вашим поставщиком. Поскольку сторонние приложения не могут угадать этот nonce, это гарантирует, что интент соответствует подлинному выбору пользователя в системном селекторе.
  2. Проверка Activity.getCallingPackage() (проверка вызывающего абонента). Поскольку селектор учетных данных запускает ваше действие с помощью startActivityForResult, ваше действие может проверить getCallingPackage(), чтобы подтвердить, что непосредственным вызывающим абонентом, запускающим пользовательский интерфейс, является доверенная системная среда выбора учетных данных (на устройствах Android с сервисами Google Play, "com.google.android.gms"). Отклоните запрос, если вызывающий пакет не распознан.
  3. Проверка 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.

Как реализовать импортер

Чтобы импортировать учетные данные в приложение (например, во время регистрации или импорта поставщика), реализуйте роль импортера и запустите процесс, вызвав 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.

Поддерживаемые типы учетных данных и расширения

Объект 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)