Przenoszenie certyfikatów

Interfejsy API Credential Managera do przenoszenia danych logowania umożliwiają bezpieczne przenoszenie danych logowania użytkownika między dostawcami danych uwierzytelniających na tym samym urządzeniu. Z tego przewodnika dowiesz się, jak dostawcy danych logowania na Androidzie mogą integrować się z interfejsami API udostępnianymi przez bibliotekę androidx.credentials:providerevents. Ta funkcja obsługuje hasła, klucze dostępu, informacje o adresie i pola niestandardowe przy użyciu standardowego formatu wymiany danych logowania FIDO (CXF).

Podstawowe pojęcia

Platforma przenoszenia danych logowania ułatwia przenoszenie danych logowania między urządzeniami peer-to-peer na tym samym urządzeniu bez ujawniania nieprzetworzonych danych logowania systemowi operacyjnemu Android ani nieuwierzytelnionym aplikacjom.

W ramach tego modelu definiowane są 2 główne role:

  • Eksporter (dostawca źródłowy): dostawca danych uwierzytelniających, który obecnie przechowuje dane logowania użytkownika. Wstępnie rejestruje w systemie metadane dotyczące dostępnych kont, które można wyeksportować (ExportEntry), i odpowiada na prośby o przeniesienie, gdy użytkownik je wybierze.
  • Importer (dostawca klienta lub kreator konfiguracji): dostawca danych logowania lub kreator konfiguracji, który inicjuje żądanie importu (ImportCredentialsRequest) i określa, jakie typy danych logowania i rozszerzeń może otrzymywać.

Zgodność z wersją Androida

Interfejs Credentials Transfer API działa na urządzeniach z Androidem 8 (poziom interfejsu API 26) lub nowszym.

Dodawanie zależności

Dodaj zależność androidx.credentials:providerevents do pliku build.gradle lub build.gradle.kts modułu:

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

Utworzenie instancji wymaganej klasy

Utwórz instancję wymaganej usługi ProviderEventsManager.

val providerEventsManager = ProviderEventsManager.create(context)

Implementowanie eksportera

Aby umożliwić użytkownikom eksportowanie danych logowania z aplikacji do innych dostawców danych logowania na urządzeniu, zaimplementuj rolę eksportera.

Rejestrowanie wpisów eksportu

Gdy dostawca danych logowania ulegnie zmianie (np. użytkownik zaloguje się, doda dane logowania lub zmodyfikuje konta), zarejestruj lub zaktualizuj elementy ExportEntry za pomocą ProviderEventsManager.registerExport().

Każdy ExportEntry wymaga:

  • id: tajny, losowo wygenerowany identyfikator ciągu znaków, który jednoznacznie reprezentuje ten wpis eksportu. Musisz bezpiecznie przechowywać ten identyfikator, ponieważ będzie on potrzebny później do weryfikacji przychodzących próśb o przeniesienie.
  • accountDisplayName: opcjonalna etykieta konta (np. "Personal Account").
  • userDisplayName: główny identyfikator użytkownika (np. "alice@example.com").
  • icon: ikona Bitmap reprezentująca dostawcę lub konto (automatycznie skalowana przez bibliotekę do formatu PNG 32 x 32).
  • supportedCredentialTypes: zestaw stałych ciągów znaków z CredentialTypes reprezentujących typy, które zawiera ten wpis.

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

Zadeklaruj aktywność eksportera w pliku manifestu

Gdy użytkownik wybierze ExportEntry w interfejsie selektora systemu, system uruchomi wyznaczony przez Ciebie element obsługi Activity. Zadeklaruj tę aktywność za pomocą obowiązkowego działania intencji i schematu identyfikatora URI treści.

Aby chronić aplikację przed nieautoryzowanymi wywołaniami i atakami typu „confused deputy”, zadeklaruj android:requireContentUriPermissionFromCaller="write" na Androidzie 14 (API na poziomie 34) i nowszych wersjach. Dzięki temu system operacyjny Android sprawdza, czy element wywołujący, który inicjuje uruchomienie, ma uprawnienia do zapisu w docelowym identyfikatorze URI treści, zanim zezwoli na rozpoczęcie aktywności, jak pokazano w tym przykładzie:

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

Obsługa intencji przeniesienia w aktywności

W CredentialExportActivity użyj IntentHandler.retrieveProviderImportCredentialsRequest(intent), aby przeanalizować żądanie, zweryfikować komponent wywołujący i metadane żądania, przeprowadzić wymagane uwierzytelnianie biometryczne i zapisać ładunek FIDO CXF z powrotem w podanym identyfikatorze URI treści.

Uwierzytelnianie żądania

Ponieważ aktywność jest eksportowana, zabezpiecz proces eksportowania danych logowania, sprawdzając każdą z tych warstw:

  1. Sprawdź liczbę jednorazową sesji credId (główna ochrona): podczas rejestrowania ExportEntry wygeneruj kryptograficznie losową liczbę jednorazową o wysokiej entropii (id) i powiąż ją z krótkotrwałą sesją. W aktywności sprawdź, czy request.credId pasuje do aktywnej, niewygasłej liczby jednorazowej sesji wydanej wcześniej przez dostawcę. Aplikacje innych firm nie mogą odgadnąć tej liczby jednorazowej, co gwarantuje, że intencja odpowiada autentycznemu wyborowi użytkownika w selektorze systemowym.
  2. Weryfikacja Activity.getCallingPackage() (sprawdzanie elementu wywołującego): selektor danych logowania uruchamia aktywność za pomocą startActivityForResult, więc aktywność może sprawdzić getCallingPackage(), aby potwierdzić, że bezpośrednim elementem wywołującym, który uruchamia interfejs, jest zaufana platforma selektora danych logowania systemu (na urządzeniach z Androidem z Usługami Google Play jest to "com.google.android.gms"). Jeśli pakiet wywołujący nie zostanie rozpoznany, odrzuć żądanie.
  3. Sprawdź request.callingAppInfo (informacje o aplikacji docelowej): callingAppInfo identyfikuje aplikację docelową, która otrzyma zaimportowane dane logowania. Możesz wykorzystać te informacje, aby zapewnić przejrzystość (np. wyświetlając nazwę i ikonę aplikacji docelowej w interfejsie potwierdzenia) lub sprawdzić, czy aplikacja docelowa jest zaufana.

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()
    }
}

Wyjątki dotyczące rejestracji i rozliczania eksportu

Wszystkie wyjątki w tym module są podklasami ImportCredentialsException, RegisterExportException lub ClearExportException.

Implementowanie importera

Aby zaimportować dane logowania do aplikacji (np. podczas wprowadzania lub importowania dostawcy), zaimplementuj rolę importera i rozpocznij proces, wywołując metodę ProviderEventsManager.importCredentials().

Tworzenie żądania importu i procesu uruchamiania

Określ, które CredentialTypes i KnownExtensions obsługuje Twój importer:

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) {}

Wyjątki dotyczące procesu importowania

Wszystkie wyjątki w tym module są podklasami ImportCredentialsException, RegisterExportException lub ClearExportException.

Obsługiwane typy i rozszerzenia danych logowania

Obiekt androidx.credentials.providerevents.transfer.CredentialTypes definiuje standardowe stałe ciągów znaków odpowiadające typom elementów FIDO CXF:

Stała Wartość (typ cxf) Opis
CREDENTIAL_TYPE_BASIC_AUTH "basic-auth" Dane logowania (nazwa użytkownika i hasło).
CREDENTIAL_TYPE_PUBLIC_KEY "passkey" Dane uwierzytelniające klucza publicznego klucza dostępu FIDO2 lub WebAuthn.
CREDENTIAL_TYPE_ADDRESS "address" adres pocztowy lub adres dostawy na potrzeby autouzupełniania formularzy;
CREDENTIAL_TYPE_API_KEY "api-key" klucze i tokeny dostępu do interfejsu API;
CREDENTIAL_TYPE_CREDIT_CARD "credit-card" informacje o płatnościach kartą kredytową i debetową;
CREDENTIAL_TYPE_CUSTOM_FIELDS "custom-fields" niestandardowe grupowania lub pola zdefiniowane przez użytkownika;
CREDENTIAL_TYPE_DRIVERS_LICENSE "drivers-license" Dane prawa jazdy.
CREDENTIAL_TYPE_FILE "file" metadane i odwołania do obiektów zastępczych plików binarnych;
CREDENTIAL_TYPE_GENERATED_PASSWORD "generated-password" Bezpieczne hasła generowane przez komputer.
CREDENTIAL_TYPE_IDENTITY_DOCUMENT "identity-document" dowody osobiste, numery SSN, numery TIN lub numery paszportów;
CREDENTIAL_TYPE_ITEM_REFERENCE "item-reference" Linki logiczne wskazujące inny element w ładunku.
CREDENTIAL_TYPE_NOTE "note" Zdefiniowane przez użytkownika bezpieczne notatki (ciąg znaków UTF-8).
CREDENTIAL_TYPE_PASSPORT "passport" Dane paszportu.
CREDENTIAL_TYPE_PERSON_NAME "person-name" Dane dotyczące tożsamości i nazwy osoby.
CREDENTIAL_TYPE_SSH_KEY "ssh-key" pary kluczy publicznych i prywatnych SSH,
CREDENTIAL_TYPE_TOTP "totp" Obiekty tajne haseł jednorazowych na określony czas (2FA).
CREDENTIAL_TYPE_WIFI "wifi" identyfikator SSID i hasła sieci Wi-Fi,

Niestandardowe dopasowania WASM (WebAssembly) (zaawansowane)

Domyślnie wywołanie RegisterExportRequest.create(context, entries) grupuje domyślny systemowy credential_transfer_matcher.wasm z zasobów biblioteki, który filtruje wpisy wyłącznie na podstawie przecięcia supportedCredentialTypes.

Jeśli dostawca danych logowania wymaga złożonej logiki dopasowywania (np. dynamicznych kontroli możliwości lub filtrowania warunkowego na podstawie pól niestandardowych), możesz skompilować własny moduł WebAssembly pasujący do interfejsu WASM Credential Transfer API i przekazać bezpośrednio nieprzetworzoną tablicę bajtów:

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