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

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

הגדרה

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

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

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

implementation "androidx.credentials:credentials:1.2.0-{latest}"

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

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

<service android:name=".MyCredentialProviderService"
    android:enabled="true"
    android:exported="true"
    android:label="My Credential Provider"
    android:icon="<any drawable icon>"
    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>

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

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

בספרייה 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 כמו מילוי אוטומטי של סיסמאות ונתונים אחרים. הספקים האלה יכולים להשתמש באותה תשתית פנימית כדי לאחסן את סוגי פרטי הכניסה הקיימים, וגם להרחיב אותה כדי לתמוך בסוגים אחרים, כולל מפתחות גישה.

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

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

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

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

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

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

  1. מבטלים את השיטה onBeginCreateCredentialRequest() בשירות שהורחב מ-CredentialProviderService.
  2. מטפלים ב-BeginCreateCredentialRequest על ידי יצירת BeginCreateCredentialResponse תואם והעברתו דרך הקריאה החוזרת.
  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 צריכה לעמוד בדרישות הבאות:

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

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

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

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

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

  • בתוך השיטה processCreateCredentialRequest() שצוינה בקטע הקודם, מוסיפים עוד מקרה בתוך בלוק ה-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.

הדוגמה הבאה ממחישה איך ליישם את התהליך הזה. צריך לטפל בקוד הזה בשיטה 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 בתגובה ולהפעיל את הקריאה החוזרת.

    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. בשיטה onCreate של הפעילות המתאימה, מאחזרים את ה-Intent המשויך ומעבירים אותו אל PendingIntentHandler.retrieveProviderGetCredentialRequest().
  2. תחלץ את GetPublicKeyCredentialOption מהבקשה שאוחזרה למעלה. לאחר מכן, מחלצים את requestJson וclientDataHash מהאפשרות הזו.
  3. מחולצים את credentialId מהנתונים הנוספים של הכוונה, שאוכלסו על ידי ספק האישורים כשהוגדר 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 על סמך מפרט האישור של W3 Web Authentication. בקטע הקוד שלמטה, נעשה שימוש במחלקות עזר של נתונים כמו 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 אם פרטי הכניסה נעולים. אם המשתמש בוחר באפשרות הזו, מופעלת הפעילות שמתאימה לפעולת הכוונה שמוגדרת ב-PendingIntent. ספקי פרטי הכניסה יכולים להציג תהליך אימות ביומטרי או מנגנון דומה כדי לבטל את הנעילה של פרטי הכניסה. אם הפעולה מצליחה, ספק פרטי הכניסה צריך ליצור BeginGetCredentialResponse, בדומה לאופן הטיפול בכניסת משתמשים שמתואר למעלה, כי פרטי הכניסה לא נעולים יותר. לאחר מכן צריך להגדיר את התשובה הזו באמצעות method‏ PendingIntentHandler.setBeginGetCredentialResponse() לפני שהכוונה המוכנה מוגדרת כתוצאה והפעילות מסתיימת.

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

אפליקציית לקוח עשויה לבקש לנקות את כל המצב שנשמר לבחירת אישורים, למשל ספק אישורים עשוי לזכור את האישור שנבחר קודם ולהחזיר אותו רק בפעם הבאה. אפליקציית לקוח קוראת ל-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>
תרשים שמציג את הפונקציות של הכפתורים &#39;שינוי&#39; ו&#39;פתיחה&#39;
איור 1: הלחצן שינוי פותח את תיבת הדו-שיח הקיימת לבחירה, ומאפשר למשתמש לבחור את ספק האישורים המועדף עליו. הלחצן פתיחה מפעיל את פעילות ההגדרות שמוגדרת בשינוי המניפסט, ופותח דף הגדרות שספציפי לספק הזה.

התעניינות בהגדרות

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

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

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

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

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

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

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

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

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

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

fun createSettingsPendingIntent(): PendingIntent