認証情報の転送

認証情報マネージャーの Credentials Transfer API を使用すると、認証情報プロバイダ間でユーザー認証情報を同じデバイスで安全に転送できます。このガイドでは、Android の認証情報プロバイダが androidx.credentials:providerevents ライブラリが提供する API と統合する方法について詳しく説明します。この機能は、標準化された FIDO Credential Exchange Format(CXF)を使用して、パスワード、パスキー、住所情報、カスタム フィールドをサポートします。

基本コンセプト

認証情報転送フレームワークは、Android OS や認証されていないアプリに未加工の認証情報を公開することなく、同じデバイスでのピアツーピアの認証情報転送を容易にします。

このフレームワークでは、次の 2 つの主なロールが定義されています。

  • エクスポート元(ソース プロバイダ): 現在ユーザーの認証情報を保持している認証情報プロバイダ。エクスポート可能なアカウント(ExportEntry)に関するメタデータをシステムに事前登録し、ユーザーが選択したときに転送リクエストに応答します。
  • インポータ(クライアント プロバイダまたは設定ウィザード): インポート リクエスト(ImportCredentialsRequest)を開始する認証情報プロバイダまたは設定ウィザード。受信できる認証情報と拡張機能のタイプを指定します。

Android バージョンの互換性

Credentials Transfer API は、Android 8(API レベル 26)以降を搭載したデバイスで動作します。

依存関係を追加する

モジュールの build.gradle または build.gradle.kts に androidx.credentials:providerevents 依存関係を追加します。

dependencies {
    implementation("androidx.credentials:providerevents:1.0.0-alpha06")
}

必要なクラスをインスタンス化する

必要な ProviderEventsManager のインスタンスを作成します。

val providerEventsManager = ProviderEventsManager.create(context)

エクスポータを実装する

ユーザーがアプリからデバイス上の他の認証情報プロバイダに認証情報をエクスポートできるようにするには、エクスポート ロールを実装します。

エクスポート エントリを登録する

認証情報プロバイダが変更された場合(ユーザーがログインした、認証情報を追加した、アカウントを変更したなど)、ProviderEventsManager.registerExport() を使用して ExportEntry アイテムを登録または更新します。

各 ExportEntry には、次のものが必要です。

  • id: このエクスポート エントリを一意に表す、ランダムに生成された秘密の文字列識別子。この ID は安全に保持する必要があります。後で受信転送リクエストを確認する際に必要になります。
  • accountDisplayName: 省略可能なアカウント ラベル(例: "Personal Account")。
  • userDisplayName: ユーザーのプライマリ ID(例: "alice@example.com")。
  • icon: プロバイダまたはアカウントを表す Bitmap アイコン(ライブラリによって 32x32 PNG に自動的にスケーリングされます)。
  • 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)
    }
}

マニフェスト ファイルでエクスポータ アクティビティを宣言する

ユーザーがシステム セレクタ UI で ExportEntry を選択すると、システムは指定された処理 Activity を起動します。このアクティビティを、必須のインテント アクションとコンテンツ URI スキームとともに宣言します。

不正な呼び出しや混乱した代理攻撃から保護するには、Android 14(API レベル 34)以降で android:requireContentUriPermissionFromCaller="write" を宣言します。これにより、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 が、プロバイダから以前に発行された有効な未期限のセッション ノンスと一致することを確認します。サードパーティ製アプリはこのノンスを推測できないため、インテントがシステム セレクタでの正規のユーザー選択に対応していることが保証されます。
  2. Activity.getCallingPackage() を検証する(呼び出し元の確認): 認証情報セレクタは startActivityForResult を使用してアクティビティを起動するため、アクティビティは getCallingPackage() を検査して、UI を起動する直接の呼び出し元が信頼できるシステム認証情報セレクタ フレームワーク(Google Play 開発者サービスを搭載した Android デバイスでは "com.google.android.gms")であることを確認できます。呼び出し元のパッケージが認識されない場合は、リクエストを拒否します。
  3. request.callingAppInfo を検査する(ターゲット アプリ情報): callingAppInfo は、インポートされた認証情報を受け取る宛先アプリケーションを識別します。この情報は、透明性を確保するため(確認 UI に宛先アプリの名前とアイコンを表示するなど)、または宛先アプリが信頼できるかどうかを確認するために、必要に応じて使用できます。

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" 国民 ID カード、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" 時間ベースのワンタイム パスワード(2FA)シークレット。
CREDENTIAL_TYPE_WIFI "wifi" Wi-Fi ネットワークの SSID とパスフレーズ。

カスタム WASM(WebAssembly)マッチャー(高度)

デフォルトでは、RegisterExportRequest.create(context, entries) を呼び出すと、ライブラリ アセットからシステム デフォルトの credential_transfer_matcher.wasm がバンドルされます。これは、supportedCredentialTypes の交差のみに基づいてエントリをフィルタします。

認証情報プロバイダに複雑なマッチング ロジック(動的な機能チェックやカスタム フィールドに基づく条件付きフィルタリングなど)が必要な場合は、WASM 認証情報転送 API に一致する独自の WebAssembly モジュールをコンパイルし、未加工のバイト配列を直接渡すことができます。

// 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)