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: ikonaBitmapreprezentująca dostawcę lub konto (automatycznie skalowana przez bibliotekę do formatu PNG 32 x 32).supportedCredentialTypes: zestaw stałych ciągów znaków zCredentialTypesreprezentują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:
- Sprawdź liczbę jednorazową sesji
credId(główna ochrona): podczas rejestrowaniaExportEntrywygeneruj kryptograficznie losową liczbę jednorazową o wysokiej entropii (id) i powiąż ją z krótkotrwałą sesją. W aktywności sprawdź, czyrequest.credIdpasuje 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. - 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. - Sprawdź
request.callingAppInfo(informacje o aplikacji docelowej):callingAppInfoidentyfikuje 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.
RegisterExportProviderConfigurationExceptionlubClearExportProviderConfigurationException: błąd pojawia się, gdy rejestracja lub czyszczenie nie powiedzie się z powodu problemów z konfiguracją dostawcy (np. puste polesupportedCredentialTypeswExportEntrylub wywołanie na nieobsługiwanym poziomie systemu operacyjnego).RegisterExportUnknownErrorExceptionlubClearExportUnknownErrorException: podczas aktualizowania rejestru eksportu wystąpił niesklasyfikowany błąd systemu lub pamięci masowej.
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.
ImportCredentialsCancellationException: użytkownik zamknął interfejs selektora lub anulował działanie eksportu (Activity.RESULT_CANCELED).ImportCredentialsNoExportOptionException: żaden zarejestrowany wpis eksportu nie pasował do żądanych typów danych logowania lub wybrany eksporter zgłosił wyjątek odrzucenia.ImportCredentialsProviderConfigurationException: błąd konfiguracji (np. pusty zbiórcredentialTypeswImportCredentialsRequestlub brak uprawnień).ImportCredentialsInvalidJsonException: eksporter zwrócił nieprawidłowo sformułowany lub pusty ładunek JSON, który nie przeszedł weryfikacji żądania.ImportCredentialsSystemErrorException: podczas importowania wystąpił błąd wewnętrzny systemu Android lub błąd przesyłania Binder.ImportCredentialsUnknownCallerException: platforma nie mogła zweryfikować aplikacji do dzwonienia.ImportCredentialsUnknownErrorException: niesklasyfikowany lub nieoczekiwany błąd podczas procesu importowania.
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)