Prezentowanie dokumentów za pomocą aplikacji do ich przechowywania

Interfejs Credential Manager Holder API umożliwia aplikacji posiadacza na Androidzie (nazywanej też „portfelem”) zarządzanie cyfrowymi dokumentami tożsamości i przedstawianie ich weryfikatorom. Z tego przewodnika dowiesz się, jak zarejestrować dane logowania aplikacji posiadacza w Menedżerze danych logowania i jak obsłużyć prośbę o prezentację, gdy użytkownik wybierze dane logowania. Interfejs Holder API jest obsługiwany na Androidzie 6 (poziom interfejsu API 23) i nowszych wersjach.

Dodawanie zależności

Aby używać interfejsu Credential Manager Holder API, dodaj te zależności do skryptu build.gradle modułu aplikacji:

dependencies {
    // Used to implement credential registries
    implementation "androidx.credentials.registry:registry-digitalcredentials-mdoc:1.0.0-alpha05"
    implementation "androidx.credentials.registry:registry-digitalcredentials-openid:1.0.0-alpha05"
    implementation "androidx.credentials.registry:registry-digitalcredentials-sdjwtvc:1.0.0-alpha05"
    implementation "androidx.credentials.registry:registry-provider:1.0.0-alpha05"
    implementation "androidx.credentials.registry:registry-provider-play-services:1.0.0-alpha05"
}

Rejestrowanie danych logowania

Najpierw utwórz instancję RegistryManager, której użyjesz do zarejestrowania żądania OpenId4VpRegistry.

// Create the registry manager
val registryManager = RegistryManager.create(context)

Tworzenie żądania OpenId4VpRegistry

Aby obsługiwać żądania OpenID4VP od weryfikatorów, musisz zarejestrować OpenId4VpRegistry. Jeśli masz typy danych lokalnych wczytane z danymi logowania do portfela (np. sdJwtsFromStorage i mdocsFromStorage), musisz przekonwertować je na ich odpowiedniki w Jetpacku DigitalCredentialEntry: SdJwtEntry i MdocEntry.

Dodawanie tokenów SD-JWT do rejestru

Zmapuj każde lokalne dane uwierzytelniające SD-JWT na SdJwtEntry rejestru:

fun mapToSdJwtEntries(sdJwtsFromStorage: List<StoredSdJwtEntry>): List<SdJwtEntry> {
    val list = mutableListOf<SdJwtEntry>()

    for (sdJwt in sdJwtsFromStorage) {
        list.add(
            SdJwtEntry(
                verifiableCredentialType = sdJwt.getVCT(),
                claims = sdJwt.getClaimsList(),
                entryDisplayPropertySet = sdJwt.toDisplayProperties(),
                id = sdJwt.getId() // Make sure this cannot be readily guessed
            )
        )
    }
    return list
}

Dodawanie dokumentów mdoc do rejestru

Zmapuj lokalne dane uwierzytelniające mdoc na typ Jetpack MdocEntry:

fun mapToMdocEntries(mdocsFromStorage: List<StoredMdocEntry>): List<MdocEntry> {
    val list = mutableListOf<MdocEntry>()

    for (mdoc in mdocsFromStorage) {
        list.add(
            MdocEntry(
                docType = mdoc.retrieveDocType(),
                fields = mdoc.getFields(),
                entryDisplayPropertySet = mdoc.toDisplayProperties(),
                id = mdoc.getId() // Make sure this cannot be readily guessed
            )
        )
    }
    return list
}

Najważniejsze informacje o kodzie

  • Zaszyfrowane identyfikatory: jedną z metod konfigurowania pola ID jest zarejestrowanie zaszyfrowanego identyfikatora danych logowania, dzięki czemu tylko Ty możesz odszyfrować wartość.
  • Lokalizacja: pola wyświetlane w interfejsie w przypadku obu formatów powinny być zlokalizowane.

Łączenie i rejestrowanie prośby

Połącz przekonwertowane wpisy i zarejestruj żądanie za pomocą RegistryManager:

val credentialEntries =
    mapToSdJwtEntries(sdJwtsFromStorage) + mapToMdocEntries(mdocsFromStorage)

val openidRegistryRequest = OpenId4VpRegistry(
    credentialEntries = credentialEntries,
    id = "my-wallet-openid-registry-v1" // A stable, unique ID to identify your registry record.
)

Opcjonalnie: wydawanie wbudowane

Jeśli użytkownik nie ma dostępnych danych uwierzytelniających żądanego typu, możesz wyświetlić opcję dodania danych uwierzytelniających do aplikacji przechowującej dokumenty. Aby to zrobić, dodaj do pliku OpenId4VpRegistry listę inlineIssuanceEntries zawierającą obsługiwane dane logowania:

val openidRegistryRequest = OpenId4VpRegistry(
    credentialEntries = credentialEntries,
    inlineIssuanceEntries = listOf(
        MdocInlineIssuanceEntry(
            id = "Issuance",
            display = InlineIssuanceEntry.InlineIssuanceDisplayProperties(
                subtitle = "Mobile Drivers License, State Id, and Others",
            ),
            supportedMdocs = setOf(
                MdocInlineIssuanceEntry.SupportedMdoc(
                    "eu.europa.ec.eudi.pid.1"
                ),
                MdocInlineIssuanceEntry.SupportedMdoc(
                    "org.iso.18013.5.1.mDL1"
                ),
            )
        ),
        SdJwtInlineIssuanceEntry(
            id = "sd-jwt-issuance",
            display = InlineIssuanceEntry.InlineIssuanceDisplayProperties(
                subtitle = "Mobile Drivers License, State Id, and Others",
            ),
            supportedSdJwts = setOf(
                SdJwtInlineIssuanceEntry.SupportedSdJwt("urn:openid:interop:id:1")
            )
        )
    ),
    id = "my-wallet-openid-registry-v1"
)

Zarejestruj swoje dane logowania w CredentialManager.

try {
    val response = registryManager.registerCredentials(openidRegistryRequest)
} catch (e: Exception) {
    // Handle failure
}

Zarządzanie metadanymi aplikacji

Metadane, które aplikacja przechowująca rejestruje w CredentialManager, mają te właściwości:

  • Trwałość: informacje są zapisywane lokalnie i zachowywane po ponownym uruchomieniu.
  • Odizolowane miejsce na dane: wpisy rejestru każdej aplikacji są przechowywane oddzielnie, co oznacza, że jedna aplikacja nie może zmieniać wpisów rejestru innej aplikacji.
  • Aktualizacje z kluczem: rekordy rejestru każdej aplikacji są kluczowane za pomocą id, co umożliwia reidentyfikację, aktualizowanie i usuwanie rekordów.
  • Aktualizowanie metadanych: dobrą praktyką jest aktualizowanie zapisanych metadanych przy każdej zmianie lub pierwszym wczytaniu aplikacji. Jeśli rejestr jest wywoływany wielokrotnie w ramach tego samego id, ostatnie wywołanie zastępuje wszystkie poprzednie rekordy. Aby zaktualizować, zarejestruj się ponownie bez konieczności wcześniejszego czyszczenia starego rekordu.

Opcjonalnie: utwórz dopasowanie

Dopasowanie to plik binarny Wasm, który Menedżer danych logowania uruchamia w piaskownicy, aby filtrować zarejestrowane dane logowania pod kątem przychodzącego żądania weryfikatora.

  • Dopasowanie domyślne: klasa OpenId4VpRegistry automatycznie uwzględnia domyślne dopasowanie OpenId4VP (OpenId4VpDefaults.DEFAULT_MATCHER) podczas tworzenia instancji. W przypadku wszystkich standardowych przypadków użycia OpenID4VP biblioteka obsługuje dopasowywanie.
  • Niestandardowy moduł dopasowujący: niestandardowy moduł dopasowujący należy wdrożyć tylko wtedy, gdy obsługujesz niestandardowy protokół, który wymaga własnej logiki dopasowywania.

Obsługa wybranych danych logowania

Gdy użytkownik wybierze dokument, aplikacja musi obsłużyć żądanie. Musisz zdefiniować aktywność, która nasłuchuje filtra intencji androidx.credentials.registry.provider.action.GET_CREDENTIAL. Ten proces jest przedstawiony w przykładowym portfelu.

Intencja uruchamia aktywność z żądaniem weryfikatora i wywołującym źródłem, które wyodrębniasz za pomocą funkcji PendingIntentHandler.retrieveProviderGetCredentialRequest. Zwraca on obiekt ProviderGetCredentialRequest zawierający wszystkie informacje powiązane z żądaniem weryfikatora. Składa się z 3 kluczowych elementów:

Żądanie prezentacji możesz przetworzyć za pomocą tego kodu:

request.credentialOptions.forEach { option ->
    if (option is GetDigitalCredentialOption) {
        Log.i(TAG, "Got DC request: ${option.requestJson}")
        processRequest(option.requestJson)
    }
}

Przykład znajdziesz w przykładowym portfelu.

Sprawdzanie tożsamości weryfikatora

Najpierw wyodrębnij ProviderGetCredentialRequest z intencji:

val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)

Sprawdź, czy nie ma aplikacji uprzywilejowanych: aplikacje uprzywilejowane (np. przeglądarki internetowe) mogą wykonywać połączenia w imieniu innych weryfikatorów, ustawiając parametr origin. Aby pobrać to źródło, przekaż listę uprzywilejowanych i zaufanych rozmówców (listę dozwolonych w formacie JSON) do interfejsu API CallingAppInfo getOrigin.

val origin = request?.callingAppInfo?.getOrigin(
    privilegedAppsJson // Your allow list JSON
)

Jeśli pole origin nie jest puste: źródło jest zwracane, jeśli packageName i odciski cyfrowe certyfikatu pasują do aplikacji na liście dozwolonych. Potraktuj to jako wywołanie uprzywilejowane i ustaw to źródło w odpowiedzi OpenID4VP zamiast obliczać źródło za pomocą podpisu aplikacji wywołującej.

Jeśli pole origin jest puste: żądanie weryfikatora pochodzi z aplikacji na Androida. Pochodzenie zmapowane na odpowiedź OpenID4VP powinno być obliczane jako android:apk-key-hash:<encoded SHA 256 fingerprint>.

val appSigningInfo =
    request?.callingAppInfo?.signingInfoCompat?.signingCertificateHistory?.firstOrNull()?.toByteArray()
val md = MessageDigest.getInstance("SHA-256")
val certHash =
    Base64.encodeToString(md.digest(requireNotNull(appSigningInfo)), Base64.NO_WRAP or Base64.NO_PADDING)
val computedOrigin = "android:apk-key-hash:$certHash"

Renderowanie interfejsu posiadacza

Po wybraniu dokumentu tożsamości wywoływana jest aplikacja, która prowadzi użytkownika przez interfejs. Można to zrobić na 2 standardowe sposoby:

  • Jeśli do udostępnienia danych uwierzytelniających wymagane jest dodatkowe uwierzytelnienie użytkownika, użyj interfejsu BiometricPrompt API. Zostało to przedstawione w przykładzie.
  • W przeciwnym razie wiele portfeli decyduje się na cichy zwrot, renderując pustą aktywność, która natychmiast przekazuje dane z powrotem do aplikacji wywołującej. Minimalizuje to liczbę kliknięć użytkownika i zapewnia płynniejsze działanie.

Zwracanie odpowiedzi z danymi logowania

Gdy aplikacja przechowująca będzie gotowa do odesłania wyniku, zakończ aktywność z odpowiedzią dotyczącą danych uwierzytelniających:

PendingIntentHandler.setGetCredentialResponse(
    resultData,
    GetCredentialResponse(DigitalCredential(response.responseJson))
)
setResult(RESULT_OK, resultData)
finish()

Jeśli wystąpi wyjątek, możesz wysłać wyjątek dotyczący danych logowania:

PendingIntentHandler.setGetCredentialException(
    resultData,
    GetCredentialUnknownException() // Configure the proper exception
)
setResult(RESULT_OK, resultData)
finish()

Pełny przykład zwracania odpowiedzi z danymi logowania w kontekście znajdziesz w przykładowej aplikacji.