שילוב של 'מנהל פרטי הכניסה' עם הפתרון של ספק פרטי הכניסה

‫Credential Manager הוא קבוצה של ממשקי API שהושקו ב-Android 14 ותומכים בכמה שיטות כניסה, כמו שם משתמש וסיסמה, מפתחות גישה ופתרונות כניסה מאוחדים (כמו כניסה באמצעות חשבון Google). כשמופעל Credential Manager API, מערכת Android צוברת את פרטי הכניסה מכל ספקי פרטי הכניסה שמותקנים במכשיר. במסמך הזה מתוארת קבוצת ממשקי ה-API שמספקים נקודות קצה לשילוב של ספקי פרטי הכניסה האלה.

הגדרה

לפני שאתם מטמיעים פונקציונליות בספק פרטי הכניסה, אתם צריכים לבצע את שלבי ההגדרה שמופיעים בקטעים הבאים.

הצהרה על יחסי תלות

כדי להשתמש בגרסה העדכנית של ספריית Credential Manager, מוסיפים את יחסי התלות הבאים לסקריפט ה-build של מודול האפליקציה:

Kotlin

dependencies {
    implementation("androidx.credentials:credentials:1.7.0-alpha02")
}

Groovy

dependencies {
    implementation "androidx.credentials:credentials:1.7.0-alpha02"
}

הצהרה על רכיב השירות בקובץ המניפסט

בקובץ המניפסט של האפליקציה AndroidManifest.xml, מוסיפים הצהרה של <service> למחלקת שירות שמרחיבה את המחלקה CredentialProviderService מהספרייה androidx.credentials, כמו בדוגמה הבאה.

<service android:name=".MyCredentialProviderService"
    android:enabled="true"
    android:exported="true"
    android:label="My Credential Provider"
    android:icon="@mipmap/ic_launcher"
    android:permission="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE"
    tools:targetApi="upside_down_cake">
    <intent-filter>
        <action android:name="android.service.credentials.CredentialProviderService"/>
    </intent-filter>
    <meta-data
        android:name="android.credentials.provider"
        android:resource="@xml/provider"/>
</service>

ההרשאה ומסנן ה-Intent שמוצגים בדוגמה הקודמת הם חלק בלתי נפרד מהתהליך של Credential Manager, כדי שהוא יפעל כמו שצריך. ההרשאה נדרשת כדי שרק מערכת Android תוכל להיות מקושרת לשירות הזה. מסנן ה-Intent משמש כדי לגלות את השירות הזה כספק פרטי כניסה לשימוש של Credential Manager.

הצהרה על סוגי פרטי הכניסה הנתמכים

בספרייה res/xml, יוצרים קובץ חדש בשם provider.xml. בקובץ הזה, מצהירים על סוגי פרטי הכניסה שהשירות תומך בהם, באמצעות קבועים שמוגדרים לכל סוג של פרטי כניסה בספרייה. בדוגמה הבאה, השירות תומך בסיסמאות רגילות וגם במפתחות גישה, שהקבועים שלהם מוגדרים בתור TYPE_PASSWORD_CREDENTIAL ו-TYPE_PUBLIC_KEY_CREDENTIAL:

<credential-provider xmlns:android="http://schemas.android.com/apk/res/android">
    <capabilities>
        <capability name="android.credentials.TYPE_PASSWORD_CREDENTIAL" />
        <capability name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" />
    </capabilities>
</credential-provider>

ברמות API קודמות, ספקי פרטי הכניסה משתלבים עם ממשקי API כמו מילוי אוטומטי של סיסמאות ונתונים אחרים. הספקים האלה יכולים להשתמש באותה תשתית פנימית כדי לאחסן את סוגי פרטי הכניסה הקיימים, ולהרחיב אותה כדי לתמוך בסוגים אחרים, כולל מפתחות גישה.

גישה דו-שלבית לאינטראקציה עם ספקים

‫Credential Manager יוצר אינטראקציה עם ספקי פרטי הכניסה בשני שלבים:

  1. השלב הראשון הוא שלב ההתחלה/השאילתה, שבו המערכת נקשרת לשירותים של ספק פרטי הכניסה ומפעילה את השיטות onBeginGetCredentialRequest(), ‏ onBeginCreateCredentialRequest() או onClearCredentialStateRequest() עם בקשות Begin…. הספקים צריכים לעבד את הבקשות האלה ולהגיב עם תגובות מסוג Begin…, ולאכלס אותן ברשומות שמייצגות אפשרויות חזותיות שיוצגו בבורר החשבונות. לכל רשומה צריך להיות ערך PendingIntent.
  2. אחרי שהמשתמש בוחר רשומה, שלב הבחירה מתחיל וה-PendingIntent שמשויך לרשומה מופעל, מה שגורם לפעילות של הספק המתאים להופיע. אחרי שהמשתמש מסיים את האינטראקציה עם הפעילות הזו, ספק פרטי הכניסה צריך להגדיר את התגובה לתוצאה של הפעילות לפני שהיא מסתיימת. התגובה הזו נשלחת לאפליקציית הלקוח שהפעילה את Credential Manager.

טיפול ביצירת מפתחות גישה

טיפול בשאילתות ליצירת מפתחות גישה

כשאפליקציית לקוח רוצה ליצור מפתח גישה ולשמור אותו אצל ספק פרטי כניסה, היא שולחת קריאה ל-API של createCredential. כדי לטפל בבקשה הזו בשירות של ספק פרטי הכניסה, כך שמפתח הגישה יישמר בפועל באחסון שלכם, בצעו את השלבים שמופיעים בקטעים הבאים.

  1. מבטלים את השיטה onBeginCreateCredentialRequest() בשירות שנוסף ל-CredentialProviderService.
  2. מטפלים ב-BeginCreateCredentialRequest על ידי יצירת BeginCreateCredentialResponse תואם והעברה שלו דרך הקריאה החוזרת (callback).
  3. כשיוצרים את BeginCreateCredentialResponse, מוסיפים את ה-CreateEntries הנדרש. כל CreateEntry צריך להתאים לחשבון שבו אפשר לשמור את פרטי הכניסה, וצריך להגדיר לו PendingIntent יחד עם מטא-נתונים נדרשים אחרים.

בדוגמה הבאה אפשר לראות איך להטמיע את השלבים האלה.

override fun onBeginCreateCredentialRequest(
    request: BeginCreateCredentialRequest,
    cancellationSignal: CancellationSignal,
    callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException>,
) {
    val response: BeginCreateCredentialResponse? = processCreateCredentialRequest(request)
    if (response != null) {
        callback.onResult(response)
    } else {
        callback.onError(CreateCredentialUnknownException())
    }
}

fun processCreateCredentialRequest(request: BeginCreateCredentialRequest): BeginCreateCredentialResponse? {
    when (request) {
        is BeginCreatePublicKeyCredentialRequest -> {
            // Request is passkey type
            return handleCreatePasskeyQuery(request)
        }
    }
    // Request not supported
    return null
}

private fun handleCreatePasskeyQuery(
    request: BeginCreatePublicKeyCredentialRequest
): BeginCreateCredentialResponse {

    // Adding two create entries - one for storing credentials to the 'Personal'
    // account, and one for storing them to the 'Family' account. These
    // accounts are local to this sample app only.
    val createEntries: MutableList<CreateEntry> = mutableListOf()
    createEntries.add(
        CreateEntry(
            PERSONAL_ACCOUNT_ID,
            createNewPendingIntent(PERSONAL_ACCOUNT_ID, CREATE_PASSKEY_INTENT)
        )
    )

    createEntries.add(
        CreateEntry(
            FAMILY_ACCOUNT_ID,
            createNewPendingIntent(FAMILY_ACCOUNT_ID, CREATE_PASSKEY_INTENT)
        )
    )

    return BeginCreateCredentialResponse(createEntries)
}

private fun createNewPendingIntent(accountId: String, action: String): PendingIntent {
    val intent = Intent(action).setPackage(PACKAGE_NAME)

    // Add your local account ID as an extra to the intent, so that when
    // user selects this entry, the credential can be saved to this
    // account
    intent.putExtra(EXTRA_KEY_ACCOUNT_ID, accountId)

    return PendingIntent.getActivity(
        applicationContext, UNIQUE_REQ_CODE,
        intent,
        (
            PendingIntent.FLAG_MUTABLE
                or PendingIntent.FLAG_UPDATE_CURRENT
            )
    )
}

מבנה ה-PendingIntent צריך לעמוד בדרישות הבאות:

  • צריך להגדיר את ה-Activity המתאים כדי להציג את כל ההנחיות, האישורים או הבחירות הביומטריות הנדרשות.
  • כל הנתונים הנדרשים שהספק צריך כשהפעילות המתאימה מופעלת צריכים להיות מוגדרים כתוספת ל-Intent שמשמש ליצירת ה-PendingIntent, כמו accountId בתהליך היצירה.
  • צריך ליצור את PendingIntent עם הדגל PendingIntent.FLAG_MUTABLE כדי שהמערכת תוכל לצרף את הבקשה הסופית ל-intent extra.
  • אסור ליצור את PendingIntent עם הדגל PendingIntent.FLAG_ONE_SHOT, כי המשתמש יכול לבחור רשומה, לחזור אחורה ולבחור אותה שוב, מה שיגרום להפעלת PendingIntent פעמיים.
  • צריך ליצור את PendingIntent עם קוד בקשה ייחודי כדי שלכל רשומה יהיה PendingIntent משלה.

טיפול בבחירת רשומה לבקשות ליצירת מפתח גישה

  1. כשהמשתמש בוחר CreateEntry שאוכלס בעבר, מופעלת הפונקציה PendingIntent המתאימה ונוצר הספק המשויך Activity.
  2. אחרי שהשיטה onCreate של ה-Activity מופעלת, ניגשים ל-Intent המשויך ומעבירים אותו למחלקה PendingIntentHander כדי לקבל את ProviderCreateCredentialRequest.
  3. מחלצים את requestJson, callingAppInfo ו-clientDataHash מהבקשה.
  4. מחלצים את ה-accountId המקומי מה-intent extra. זו הטמעה ספציפית לאפליקציה לדוגמה, והיא לא נדרשת. אפשר להשתמש במזהה החשבון הזה כדי לאחסן את פרטי הכניסה האלה בחשבון הספציפי הזה.
  5. מאמתים את ה-requestJson. בדוגמה הבאה נעשה שימוש במחלקות של נתונים מקומיים כמו PublicKeyCredentialCreationOptions כדי להמיר את קלט ה-JSON למחלקה מובנית בהתאם למפרט של WebAuthn. כספקי פרטי כניסה, אתם יכולים להחליף את זה במנתח משלכם.
  6. אם השיחה מגיעה מאפליקציית Android מקורית, בודקים את הקישור לנכס של אפליקציית השיחות.
  7. הצגת בקשה לאימות. בדוגמה הבאה נעשה שימוש ב-API Biometric של Android.
  8. כשהאימות מצליח, יוצרים credentialId וזוג מפתחות.
  9. שומרים את המפתח הפרטי במסד הנתונים המקומי שלכם, מול callingAppInfo.packageName.
  10. יוצרים תגובת JSON של Web Authentication API שמורכבת מהמפתח הציבורי ומ-credentialId. בדוגמה הבאה נעשה שימוש במחלקות שירות מקומיות כמו AuthenticatorAttestationResponse ו-FidoPublicKeyCredential, שעוזרות ליצור קובץ JSON על סמך המפרט שצוין קודם.כספקי פרטי כניסה, אתם יכולים להחליף את המחלקות האלה ב-builders משלכם.
  11. יוצרים CreatePublicKeyCredentialResponse באמצעות קובץ ה-JSON שנוצר למעלה.
  12. מגדירים את CreatePublicKeyCredentialResponse כתוספת ב-Intent באמצעות PendingIntentHander.setCreateCredentialResponse(), ומגדירים את ה-Intent הזה כתוצאה של ה-Activity.
  13. מסיימים את הפעילות.

הקוד לדוגמה שבהמשך ממחיש את השלבים האלה. צריך לטפל בקוד הזה במחלקת הפעילות (Activity) אחרי שמפעילים את onCreate().

override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
    super.onCreate(savedInstanceState, persistentState)
    // ...

    val request =
        PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)

    val accountId = intent.getStringExtra(CredentialsRepo.EXTRA_KEY_ACCOUNT_ID)
    if (request != null && request.callingRequest is CreatePublicKeyCredentialRequest) {
        val publicKeyRequest: CreatePublicKeyCredentialRequest =
            request.callingRequest as CreatePublicKeyCredentialRequest
        createPasskey(
            publicKeyRequest.requestJson,
            request.callingAppInfo,
            publicKeyRequest.clientDataHash,
            accountId
        )
    }
}

@SuppressLint("RestrictedApi")
fun createPasskey(
    requestJson: String,
    callingAppInfo: CallingAppInfo?,
    clientDataHash: ByteArray?,
    accountId: String?
) {
    val request = PublicKeyCredentialCreationOptions(requestJson)

    val biometricPrompt = BiometricPrompt(
        this,
        { }, // Pass in your own executor
        object : AuthenticationCallback() {
            override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                super.onAuthenticationError(errorCode, errString)
                finish()
            }

            override fun onAuthenticationFailed() {
                super.onAuthenticationFailed()
                finish()
            }

            @RequiresApi(VERSION_CODES.P)
            override fun onAuthenticationSucceeded(
                result: AuthenticationResult
            ) {
                super.onAuthenticationSucceeded(result)

                // Generate a credentialId
                val credentialId = ByteArray(32)
                SecureRandom().nextBytes(credentialId)

                // Generate a credential key pair
                val spec = ECGenParameterSpec("secp256r1")
                val keyPairGen = KeyPairGenerator.getInstance("EC")
                keyPairGen.initialize(spec)
                val keyPair = keyPairGen.genKeyPair()

                // Save passkey in your database as per your own implementation

                // Create AuthenticatorAttestationResponse object to pass to
                // FidoPublicKeyCredential

                val response = AuthenticatorAttestationResponse(
                    requestOptions = request,
                    credentialId = credentialId,
                    credentialPublicKey = getPublicKeyFromKeyPair(keyPair),
                    origin = appInfoToOrigin(callingAppInfo!!),
                    up = true,
                    uv = true,
                    be = true,
                    bs = true,
                    packageName = callingAppInfo.packageName
                )

                val credential = FidoPublicKeyCredential(
                    rawId = credentialId,
                    response = response,
                    authenticatorAttachment = "", // Add your authenticator attachment
                )
                val result = Intent()

                val createPublicKeyCredResponse =
                    CreatePublicKeyCredentialResponse(credential.json())

                // Set the CreateCredentialResponse as the result of the Activity
                PendingIntentHandler.setCreateCredentialResponse(
                    result,
                    createPublicKeyCredResponse
                )
                setResult(RESULT_OK, result)
                finish()
            }
        }
    )

    val promptInfo = BiometricPrompt.PromptInfo.Builder()
        .setTitle("Use your screen lock")
        .setSubtitle("Create passkey for ${request.rp.name}")
        .setAllowedAuthenticators(
            BiometricManager.Authenticators.BIOMETRIC_STRONG
            /* or BiometricManager.Authenticators.DEVICE_CREDENTIAL */
        )
        .build()
    biometricPrompt.authenticate(promptInfo)
}

@RequiresApi(VERSION_CODES.P)
fun appInfoToOrigin(info: CallingAppInfo): String {
    val cert = info.signingInfo.apkContentsSigners[0].toByteArray()
    val md = MessageDigest.getInstance("SHA-256")
    val certHash = md.digest(cert)
    // This is the format for origin
    return "android:apk-key-hash:${b64Encode(certHash)}"
}

טיפול בשאילתות לבקשות ליצירת סיסמה

כדי לטפל בשאילתות של בקשות ליצירת סיסמה:

  • בתוך ה-method‏ processCreateCredentialRequest() שצוין בקטע הקודם, מוסיפים עוד case בתוך הבלוק switch כדי לטפל בבקשות לסיסמאות.
  • בזמן יצירת ה-BeginCreateCredentialResponse, מוסיפים את ה-CreateEntries הנדרש.
  • כל CreateEntry צריך להתאים לחשבון שבו אפשר לשמור את פרטי הכניסה, וצריך להגדיר בו PendingIntent יחד עם מטא-נתונים אחרים.

בדוגמה הבאה אפשר לראות איך להטמיע את השלבים האלה:

fun processCreateCredentialRequest(
    request: BeginCreateCredentialRequest
): BeginCreateCredentialResponse? {
    when (request) {
        is BeginCreatePublicKeyCredentialRequest -> {
            // Request is passkey type
            return handleCreatePasskeyQuery(request)
        }

        is BeginCreatePasswordCredentialRequest -> {
            // Request is password type
            return handleCreatePasswordQuery(request)
        }
    }
    return null
}

@RequiresApi(VERSION_CODES.M)
private fun handleCreatePasswordQuery(
    request: BeginCreatePasswordCredentialRequest
): BeginCreateCredentialResponse {
    val createEntries: MutableList<CreateEntry> = mutableListOf()

    // Adding two create entries - one for storing credentials to the 'Personal'
    // account, and one for storing them to the 'Family' account. These
    // accounts are local to this sample app only.
    createEntries.add(
        CreateEntry(
            PERSONAL_ACCOUNT_ID,
            createNewPendingIntent(PERSONAL_ACCOUNT_ID, CREATE_PASSWORD_INTENT)
        )
    )
    createEntries.add(
        CreateEntry(
            FAMILY_ACCOUNT_ID,
            createNewPendingIntent(FAMILY_ACCOUNT_ID, CREATE_PASSWORD_INTENT)
        )
    )

    return BeginCreateCredentialResponse(createEntries)
}

טיפול בבחירת רשומה לבקשות ליצירת סיסמה

כשהמשתמש בוחר CreateEntry מאוכלס, ה-PendingIntent התואם מופעל ומציג את הפעילות המשויכת. ניגשים ל-Intent המשויך שמועבר ב-onCreate ומעבירים אותו למחלקה PendingIntentHander כדי לקבל את ה-method‏ ProviderCreateCredentialRequest.

הדוגמה הבאה ממחישה איך להטמיע את התהליך הזה. צריך לטפל בקוד הזה ב-method onCreate() של הפעילות.

val createRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
val accountId = intent.getStringExtra(CredentialsRepo.EXTRA_KEY_ACCOUNT_ID)

if (createRequest == null) {
    return
}

val request: CreatePasswordRequest = createRequest.callingRequest as CreatePasswordRequest

// Fetch the ID and password from the request and save it in your database
mDatabase.addNewPassword(
    PasswordInfo(
        request.id,
        request.password,
        createRequest.callingAppInfo.packageName
    )
)

// Set the final response back
val result = Intent()
val response = CreatePasswordResponse()
PendingIntentHandler.setCreateCredentialResponse(result, response)
setResult(Activity.RESULT_OK, result)
finish()

טיפול בכניסת משתמשים לחשבון

כניסת המשתמשים לחשבון מתבצעת לפי השלבים הבאים:

  • כשאפליקציית לקוח מנסה להכניס משתמש, היא מכינה מכונה של GetCredentialRequest.
  • מסגרת Android מפיצה את הבקשה הזו לכל ספקי פרטי הכניסה הרלוונטיים על ידי קישור לשירותים האלה.
  • לאחר מכן, שירות הספק מקבל BeginGetCredentialRequest שמכיל רשימה של BeginGetCredentialOption, שכל אחד מהם מכיל פרמטרים שאפשר להשתמש בהם כדי לאחזר פרטי כניסה תואמים.

כדי לטפל בבקשה הזו בשירות של ספק פרטי הכניסה, צריך לבצע את השלבים הבאים:

  1. מבטלים את השיטה onBeginGetCredentialRequest() כדי לטפל בבקשה. שימו לב: אם פרטי הכניסה שלכם נעולים, אתם יכולים להגדיר מיד AuthenticationAction בתגובה ולהפעיל את הקריאה החוזרת (callback).

    private val unlockEntryTitle = "Authenticate to continue"
    
    override fun onBeginGetCredentialRequest(
        request: BeginGetCredentialRequest,
        cancellationSignal: CancellationSignal,
        callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,
    ) {
        if (isAppLocked()) {
            callback.onResult(
                BeginGetCredentialResponse(
                    authenticationActions = mutableListOf(
                        AuthenticationAction(
                            unlockEntryTitle, createUnlockPendingIntent()
                        )
                    )
                )
            )
            return
        }
        try {
            response = processGetCredentialRequest(request)
            callback.onResult(response)
        } catch (e: GetCredentialException) {
            callback.onError(GetCredentialUnknownException())
        }
    }
    

    ספקים שדורשים לבטל את הנעילה של פרטי הכניסה לפני שהם מחזירים credentialEntries, צריכים להגדיר Intent בהמתנה שמנווט את המשתמש לתהליך ביטול הנעילה של האפליקציה:

    private fun createUnlockPendingIntent(): PendingIntent {
        val intent = Intent(UNLOCK_INTENT).setPackage(PACKAGE_NAME)
        return PendingIntent.getActivity(
            applicationContext, UNIQUE_REQUEST_CODE, intent,
            (
                PendingIntent.FLAG_MUTABLE
                    or PendingIntent.FLAG_UPDATE_CURRENT
                )
        )
    }
    
  2. אחזור פרטי הכניסה ממסד הנתונים המקומי והגדרתם באמצעות CredentialEntries כדי שיוצגו בבורר. במפתחות גישה, אפשר להגדיר את credentialId כתוספת ל-Intent כדי לדעת לאיזה פרטי כניסה הוא ממופה כשהמשתמש בוחר ברשומה הזו.

    companion object {
        // These intent actions are specified for corresponding activities
        // that are to be invoked through the PendingIntent(s)
        private const val GET_PASSKEY_INTENT_ACTION = "PACKAGE_NAME.GET_PASSKEY"
        private const val GET_PASSWORD_INTENT_ACTION = "PACKAGE_NAME.GET_PASSWORD"
    }
    
    fun processGetCredentialRequest(
        request: BeginGetCredentialRequest
    ): BeginGetCredentialResponse {
        val callingPackageInfo = request.callingAppInfo
        val callingPackageName = callingPackageInfo?.packageName.orEmpty()
        val credentialEntries: MutableList<CredentialEntry> = mutableListOf()
    
        for (option in request.beginGetCredentialOptions) {
            when (option) {
                is BeginGetPasswordOption -> {
                    credentialEntries.addAll(
                        populatePasswordData(
                            callingPackageName,
                            option
                        )
                    )
                }
                is BeginGetPublicKeyCredentialOption -> {
                    credentialEntries.addAll(
                        populatePasskeyData(
                            callingPackageInfo,
                            option
                        )
                    )
                } else -> {
                    Log.i(TAG, "Request not supported")
                }
            }
        }
        return BeginGetCredentialResponse(credentialEntries)
    }
    
  3. שליפת פרטי כניסה ממסד הנתונים, יצירת רשומות של מפתחות גישה וסיסמאות למילוי.

    private fun populatePasskeyData(
        callingAppInfo: CallingAppInfo?,
        option: BeginGetPublicKeyCredentialOption
    ): List<CredentialEntry> {
        val passkeyEntries: MutableList<CredentialEntry> = mutableListOf()
        val request = PublicKeyCredentialRequestOptions(option.requestJson)
        // Get your credentials from database where you saved during creation flow
        val creds = getCredentialsFromInternalDb(request.rpId)
        val passkeys = creds.passkeys
        for (passkey in passkeys) {
            val data = Bundle()
            data.putString("credId", passkey.credId)
            passkeyEntries.add(
                PublicKeyCredentialEntry(
                    context = applicationContext,
                    username = passkey.username,
                    pendingIntent = createNewPendingIntent(
                        GET_PASSKEY_INTENT_ACTION,
                        data
                    ),
                    beginGetPublicKeyCredentialOption = option,
                    displayName = passkey.displayName,
                    icon = passkey.icon
                )
            )
        }
        return passkeyEntries
    }
    
    // Fetch password credentials and create password entries to populate to the user
    private fun populatePasswordData(
        callingPackage: String,
        option: BeginGetPasswordOption
    ): List<CredentialEntry> {
        val passwordEntries: MutableList<CredentialEntry> = mutableListOf()
    
        // Get your password credentials from database where you saved during
        // creation flow
        val creds = getCredentialsFromInternalDb(callingPackage)
        val passwords = creds.passwords
        for (password in passwords) {
            passwordEntries.add(
                PasswordCredentialEntry(
                    context = applicationContext,
                    username = password.username,
                    pendingIntent = createNewPendingIntent(
                        GET_PASSWORD_INTENT
                    ),
                    beginGetPasswordOption = option,
                    displayName = password.username,
                    icon = password.icon
                )
            )
        }
        return passwordEntries
    }
    
    private fun createNewPendingIntent(
        action: String,
        extra: Bundle? = null
    ): PendingIntent {
        val intent = Intent(action).setPackage(PACKAGE_NAME)
        if (extra != null) {
            intent.putExtra("CREDENTIAL_DATA", extra)
        }
    
        return PendingIntent.getActivity(
            applicationContext, UNIQUE_REQUEST_CODE, intent,
            (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
        )
    }
    
  4. אחרי ששולחים שאילתה ומאכלסים את פרטי הכניסה, צריך לטפל בשלב הבחירה של פרטי הכניסה שהמשתמש בוחר, בין אם מדובר במפתח גישה או בסיסמה.

טיפול בבחירת המשתמש למפתחות גישה

  1. ב-method‏ onCreate של ה-Activity המתאימה, מאחזרים את ה-Intent המשויך ומעבירים אותו אל PendingIntentHandler.retrieveProviderGetCredentialRequest().
  2. מחלצים את GetPublicKeyCredentialOption מהבקשה שאוחזרה למעלה. לאחר מכן, מחלצים את requestJson ו-clientDataHash מהאפשרות הזו.
  3. מחלצים את ה-credentialId מה-intent extra, שאוכלס על ידי ספק פרטי הכניסה כשה-PendingIntent התואם הוגדר.
  4. מחלצים את מפתח הגישה ממסד הנתונים המקומי באמצעות הפרמטרים של הבקשה שאליהם ניגשתם בשלב הקודם.
  5. מוודאים שמפתח הגישה תקין באמצעות המטא-נתונים שחולצו ואימות המשתמש.

    val getRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
    val publicKeyRequest = getRequest?.credentialOptions?.first() as GetPublicKeyCredentialOption
    
    val requestInfo = intent.getBundleExtra("CREDENTIAL_DATA")
    val credIdEnc = requestInfo?.getString("credId").orEmpty()
    
    // Get the saved passkey from your database based on the credential ID from the PublicKeyRequest
    val passkey = mDatabase.getPasskey(credIdEnc)
    
    // Decode the credential ID, private key and user ID
    val credId = b64Decode(credIdEnc)
    val privateKey = b64Decode(passkey.credPrivateKey)
    val uid = b64Decode(passkey.uid)
    
    val origin = appInfoToOrigin(getRequest.callingAppInfo)
    val packageName = getRequest.callingAppInfo.packageName
    
    validatePasskey(
        publicKeyRequest.requestJson,
        origin,
        packageName,
        uid,
        passkey.username,
        credId,
        privateKey
    )
    
  6. כדי לאמת את המשתמש, מציגים בקשה לאימות ביומטרי (או שיטת אימות אחרת). קטע הקוד הבא משתמש ב-Android Biometric API.

  7. אחרי שהאימות מצליח, יוצרים תשובת JSON על סמך מפרט טענת הנכוֹנוּת (assertion) של אימות האינטרנט של W3. בקטע הקוד שבהמשך, מחלקות נתונים של רכיבי עזר כמו AuthenticatorAssertionResponse משמשות לקבלת פרמטרים מובְנים ולהמרה שלהם לפורמט ה-JSON הנדרש. התשובה מכילה חתימה דיגיטלית מהמפתח הפרטי של פרטי הכניסה של WebAuthn. השרת של הצד המסתמך יכול לאמת את החתימה הזו כדי לאמת את המשתמש לפני הכניסה.

  8. יוצרים PublicKeyCredential באמצעות ה-JSON שנוצר למעלה, ומגדירים אותו ב-GetCredentialResponse סופי. הגדירו את התשובה הסופית הזו על סמך התוצאה של הפעילות הזו.

הדוגמה הבאה ממחישה איך אפשר להטמיע את השלבים האלה:

val request = PublicKeyCredentialRequestOptions(requestJson)
val privateKey: ECPrivateKey = convertPrivateKey(privateKeyBytes)

val biometricPrompt = BiometricPrompt(
    this,
    { }, // Pass in your own executor
    object : BiometricPrompt.AuthenticationCallback() {
        override fun onAuthenticationError(
            errorCode: Int,
            errString: CharSequence
        ) {
            super.onAuthenticationError(errorCode, errString)
            finish()
        }

        override fun onAuthenticationFailed() {
            super.onAuthenticationFailed()
            finish()
        }

        override fun onAuthenticationSucceeded(
            result: BiometricPrompt.AuthenticationResult
        ) {
            super.onAuthenticationSucceeded(result)
            val response = AuthenticatorAssertionResponse(
                requestOptions = request,
                credentialId = credId,
                origin = origin,
                up = true,
                uv = true,
                be = true,
                bs = true,
                userHandle = uid,
                packageName = packageName
            )

            val sig = Signature.getInstance("SHA256withECDSA")
            sig.initSign(privateKey)
            sig.update(response.dataToSign())
            response.signature = sig.sign()

            val credential = FidoPublicKeyCredential(
                rawId = credId,
                response = response,
                authenticatorAttachment = "", // Add your authenticator attachment
            )
            val result = Intent()
            val passkeyCredential = PublicKeyCredential(credential.json())
            PendingIntentHandler.setGetCredentialResponse(
                result, GetCredentialResponse(passkeyCredential)
            )
            setResult(RESULT_OK, result)
            finish()
        }
    }
)

val promptInfo = BiometricPrompt.PromptInfo.Builder()
    .setTitle("Use your screen lock")
    .setSubtitle("Use passkey for ${request.rpId}")
    .setAllowedAuthenticators(
        BiometricManager.Authenticators.BIOMETRIC_STRONG
        /* or BiometricManager.Authenticators.DEVICE_CREDENTIAL */
    )
    .build()
biometricPrompt.authenticate(promptInfo)

טיפול בבחירת המשתמש לאימות באמצעות סיסמה

  1. בפעילות המתאימה, ניגשים ל-intent שמועבר אל onCreate ומחלצים את ProviderGetCredentialRequest באמצעות PendingIntentHandler.
  2. משתמשים ב-GetPasswordOption בבקשה כדי לאחזר את פרטי הכניסה באמצעות סיסמה של שם החבילה הנכנסת.

    val getRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
    
    val passwordOption = getRequest?.credentialOptions?.first() as GetPasswordOption
    
    val username = passwordOption.allowedUserIds.first()
    // Fetch the credentials for the calling app package name
    val creds = mDatabase.getCredentials(callingAppInfo.packageName)
    val passwords = creds.passwords
    val it = passwords.iterator()
    var password = ""
    while (it.hasNext()) {
        val passwordItemCurrent = it.next()
        if (passwordItemCurrent.username == username) {
            password = passwordItemCurrent.password
            break
        }
    }
    
  3. אחרי שהפרטים מאוחזרים, מגדירים את התגובה לפרטי הכניסה של הסיסמה שנבחרה.

    // Set the response back
    val result = Intent()
    val passwordCredential = PasswordCredential(username, password)
    PendingIntentHandler.setGetCredentialResponse(
        result, GetCredentialResponse(passwordCredential)
    )
    setResult(Activity.RESULT_OK, result)
    finish()
    

טיפול בבחירה של רשומת פעולת אימות

כמו שציינו קודם, ספק פרטי כניסה יכול להגדיר AuthenticationAction אם פרטי הכניסה נעולים. אם המשתמש בוחר ברשומה הזו, מופעלת ה-Activity שתואמת לפעולת ה-Intent שהוגדרה ב-PendingIntent. לאחר מכן, ספקי פרטי הכניסה יכולים להציג תהליך אימות ביומטרי או מנגנון דומה כדי לבטל את הנעילה של פרטי הכניסה. אם הפעולה מצליחה, ספק פרטי הכניסה צריך ליצור BeginGetCredentialResponse, בדומה לאופן שבו מתואר למעלה הטיפול בכניסת משתמשים, כי עכשיו פרטי הכניסה לא נעולים. לאחר מכן, צריך להגדיר את התשובה הזו באמצעות ה-method‏ PendingIntentHandler.setBeginGetCredentialResponse() לפני שה-Intent המוכן מוגדר כתוצאה וה-Activity מסתיימת.

מחיקת בקשות לפרטי כניסה

אפליקציית לקוח יכולה לבקש לנקות כל מצב שנשמר לבחירת פרטי כניסה, למשל ספק פרטי כניסה יכול לזכור את פרטי הכניסה שנבחרו בעבר ולהחזיר רק אותם בפעם הבאה. אפליקציית לקוח קוראת ל-API הזה ומצפה שהבחירה הקבועה תבוטל. שירות ספק פרטי הכניסה יכול לטפל בבקשה הזו על ידי שינוי של ה-method‏ onClearCredentialStateRequest():

override fun onClearCredentialStateRequest(
    request: ProviderClearCredentialStateRequest,
    cancellationSignal: CancellationSignal,
    callback: OutcomeReceiver<Void?, ClearCredentialException>
) {
    // Delete any maintained state as appropriate.
}

כדי לאפשר למשתמשים לפתוח את ההגדרות של הספק במסך סיסמאות, מפתחות גישה ומילוי אוטומטי, אפליקציות של ספקי פרטי כניסה צריכות להטמיע את מאפיין המניפסט credential-provider settingsActivity ב-res/xml/provider.xml. המאפיין הזה מאפשר לכם להשתמש ב-Intent כדי לפתוח את מסך ההגדרות של האפליקציה שלכם אם משתמש לוחץ על שם של ספק ברשימת השירותים סיסמאות, מפתחות גישה ומילוי אוטומטי. צריך להגדיר את ערך המאפיין הזה בתור שם הפעילות שתושק ממסך ההגדרות.

<credential-provider
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:settingsSubtitle="Example settings provider name"
    android:settingsActivity="com.example.SettingsActivity">
    <capabilities>
        <capability name="android.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" />
    </capabilities>
</credential-provider>
תרשים שמראה את הפונקציות של הכפתורים &quot;שינוי&quot; ו&quot;פתיחה&quot;
איור 1: הלחצן שינוי פותח את תיבת הדו-שיח לבחירה קיימת, ומאפשר למשתמש לבחור את ספק פרטי הכניסה המועדף עליו. הלחצן פתיחה מפעיל את פעילות ההגדרות שהוגדרה בשינוי המניפסט, ופותח דף הגדרות ספציפי לספק הזה.

הגדרות של כוונות

פתיחת ההגדרות: ה-intent‏ android.settings.CREDENTIAL_PROVIDER פותח מסך הגדרות שבו המשתמש יכול לבחור את ספקי פרטי הכניסה המועדפים וספקים נוספים.

מסך ההגדרות של סיסמאות, מפתחות גישה ומילוי אוטומטי
תמונה 2: מסך ההגדרות של סיסמאות, מפתחות גישה ומילוי אוטומטי.

שירות פרטי הכניסה המועדף: ה-intent‏ ACTION_REQUEST_SET_AUTOFILL_SERVICE מפנה את המשתמש למסך הבחירה של הספק המועדף. הספק שנבחר במסך הזה הופך לספק המועדף של פרטי הכניסה והמילוי האוטומטי.

תרשים שמראה את הפונקציות של הכפתורים &quot;שינוי&quot; ו&quot;פתיחה&quot;
איור 3: מסך ההגדרות של השירות המועדף לסיסמאות, מפתחות גישה ומילוי אוטומטי.

קבלת רשימת היתרים של אפליקציות עם הרשאות

אפליקציות עם הרשאות, כמו דפדפני אינטרנט, מבצעות קריאות ל-Credential Manager בשם גורמים אחרים שמסתמכים על פרטי הכניסה. לשם כך, הן מגדירות את הפרמטר origin ב-Credential Manager בשיטות GetCredentialRequest() ו-CreatePublicKeyCredentialRequest(). כדי לעבד את הבקשות האלה, ספק פרטי הכניסה מאחזר את origin באמצעות ה-API של getOrigin().

כדי לאחזר את origin, האפליקציה של ספק פרטי הכניסה צריכה להעביר רשימה של גורמים מורשים ומהימנים שמבצעים קריאות ל-API של androidx.credentials.provider.CallingAppInfo's getOrigin(). רשימת ההיתרים הזו חייבת להיות אובייקט JSON תקין. הערך origin מוחזר אם הערך של packageName וטביעות אצבע לאישור שהתקבלו מ-signingInfo תואמים לאלה של אפליקציה שנמצאת ב-privilegedAllowlist שהועבר ל-API של getOrigin(). אחרי שמקבלים את הערך של origin, אפליקציית הספק צריכה להתייחס לקריאה הזו כקריאה עם הרשאות ולהגדיר את origin בנתוני הלקוח ב-AuthenticatorResponse, במקום לחשב את origin באמצעות החתימה של אפליקציית השיחות.

אם אתם מאחזרים origin, השתמשו ב-clientDataHash שסופק ישירות ב-CreatePublicKeyCredentialRequest() או ב-GetPublicKeyCredentialOption(), במקום להרכיב ולגבב את clientDataJSON במהלך בקשת החתימה. כדי למנוע בעיות בניתוח JSON, צריך להגדיר ערך זמני לשמירת מקום (פלייסהולדר) בשביל clientDataJSON בתגובה לאימות ולטענה. מנהל הסיסמאות של Google משתמש ברשימת היתרים שזמינה באופן גלוי לכולם לקריאות אל getOrigin(). כספקי פרטי כניסה, אתם יכולים להשתמש ברשימה הזו או לספק רשימה משלכם בפורמט JSON שמתואר ב-API. הספק בוחר באיזו רשימה להשתמש. כדי לקבל גישה עם הרשאות באמצעות ספקי פרטי כניסה של צד שלישי, עיינו במסמכי התיעוד של הצד השלישי.

הפעלת ספקים במכשיר

המשתמשים צריכים להפעיל את הספק דרך הגדרות המכשיר > סיסמאות וחשבונות > הספק שלך > הפעלה או השבתה.

fun createSettingsPendingIntent(): PendingIntent