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

'מנהל פרטי הכניסה' הוא קבוצה של ממשקי 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">
    <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:

<?xml version="1.0" encoding="utf-8"?>
<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 תואם והעברה שלו באמצעות הפונקציה הלא סטטית להפעלה חוזרת.
  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 צריך לעמוד בדרישות הבאות:

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

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

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

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

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

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

  val biometricPrompt = BiometricPrompt(
    this,
    <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)

        // 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
        )
        val result = Intent()

        val createPublicKeyCredResponse =
          CreatePublicKeyCredentialResponse(credential.json())

        // Set the CreateCredentialResponse as the result of the Activity
        PendingIntentHandler.setCreateCredentialResponse(
          result, createPublicKeyCredResponse
        )
        setResult(Activity.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)
}

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
}

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 כדי לקבל את השיטה ProviderCreateCredentialRequest.

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

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

val request: CreatePasswordRequest = createRequest.callingRequest as CreatePasswordRequest

// Fetch the ID and password from the request and save it in your database
<your_database>.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)
this@<activity>.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, צריכים להגדיר כוונה בהמתנה (pending 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 processGetCredentialsRequest(
    request: BeginGetCredentialRequest
    ): BeginGetCredentialResponse {
        val callingPackage = request.callingAppInfo?.packageName
        val credentialEntries: MutableList<CredentialEntry> = mutableListOf()
    
        for (option in request.beginGetCredentialOptions) {
            when (option) {
                is BeginGetPasswordOption -> {
                    credentialEntries.addAll(
                            populatePasswordData(
                                callingPackage,
                                option
                            )
                        )
                    }
                    is BeginGetPublicKeyCredentialOption -> {
                        credentialEntries.addAll(
                            populatePasskeyData(
                                callingPackage,
                                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
                  ),
                  beginPublicKeyCredentialOption = 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 של הפעילות המתאימה, מאחזרים את הכוונה המשויכת ומעבירים אותה אל PendingIntentHandler.retrieveProviderGetCredentialRequest().
  2. מחלצים את GetPublicKeyCredentialOption מהבקשה שאוחזרה למעלה. לאחר מכן, מחלצים את requestJson ו-clientDataHash מהאפשרות הזו.
  3. מחלצים את credentialId מהפרמטר הנוסף של הכוונה, שספק פרטי הכניסה מילא כשהגדרתם את PendingIntent התואם.
  4. מחלצים את מפתח הגישה ממסד הנתונים המקומי באמצעות פרמטרים של הבקשה שאליהם ניגשים למעלה.
  5. בודקים שהמפתח תקף באמצעות המטא-נתונים שחולצו ואימות המשתמש.

    val getRequest =
        PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
    val publicKeyRequest =
    getRequest.credentialOption as GetPublicKeyCredentialOption
    
    val requestInfo = intent.getBundleExtra("CREDENTIAL_DATA")
    val credIdEnc = requestInfo.getString("credId")
    
    // Get the saved passkey from your database based on the credential ID
    // from the publickeyRequest
    val passkey = <your database>.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 לאימות באינטרנט. בקטע הקוד שבהמשך, נעשה שימוש בקטגוריות נתונים מסייעות כמו AuthenticatorAssertionResponse כדי לקבל פרמטרים מובְנים ולהמיר אותם לפורמט הנדרש של JSON. התשובה מכילה חתימה דיגיטלית מהמפתח הפרטי של פרטי הכניסה ל-WebAuthn. השרת של הצד הנסמך יכול לאמת את החתימה הזו כדי לאמת משתמש לפני הכניסה.

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

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

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

val biometricPrompt = BiometricPrompt(
    this,
    <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
        )
        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. בפעילות המתאימה, ניגשים לכוונה שהועברה אל onCreate ומחפשים את ProviderGetCredentialRequest באמצעות PendingIntentHandler.
  2. משתמשים ב-GetPasswordOption בבקשה כדי לאחזר את פרטי הכניסה של הסיסמה בשם החבילה הנכנסת.

    val getRequest =
    PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
    
    val passwordOption = getRequest.credentialOption as GetPasswordCredentialOption
    
    val username = passwordOption.username
    // Fetch the credentials for the calling app package name
    val creds = <your_database>.getCredentials(callingAppInfo.packageName)
    val passwords = creds.passwords
    val it = passwords.iterator()
    var password = ""
    while (it.hasNext() == true) {
        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, בדומה לאופן שבו מתוארת הטיפול בכניסה של משתמשים למעלה, כי עכשיו אפשר לפתוח את נעילת פרטי הכניסה. לאחר מכן צריך להגדיר את התשובה הזו באמצעות השיטה PendingIntentHandler.setBeginGetCredentialResponse(), לפני שהכוונה שהוגדרה מוגדרת כתוצאה והפעילות מסתיימת.

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

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

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

כדי לאפשר למשתמשים לפתוח את ההגדרות של הספק מהמסך סיסמאות, מפתחות גישה ומילוי אוטומטי, אפליקציות של ספקי פרטי כניסה צריכות להטמיע את מאפיין המניפסט settingsActivity credential-provider ב-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>
תרשים שבו מוצגות הפונקציות של לחצן השינוי והלחצן הפתוח
איור 1: לחיצה על הלחצן Change פותחת את תיבת הדו-שיח הקיימת לבחירה, ומאפשרת למשתמש לבחור את ספק פרטי הכניסה המועדף עליו. לחיצה על הלחצן Open מפעילה את פעילות ההגדרות שמוגדרת בשינוי המניפסט, ופותחת דף הגדרות ספציפי לספק הזה.

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

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

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

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

תרשים שבו מוצגות הפונקציות של לחצן השינוי והלחצן הפתוח
איור 3: המסך 'השירות המועדף עבור סיסמאות, מפתחות גישה ומילוי אוטומטי'.

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

אפליקציות עם הרשאות, כמו דפדפני אינטרנט, מבצעות קריאות ל-Credential Manager בשם צדדים נסמכים אחרים על ידי הגדרת הפרמטר origin בשיטות GetCredentialRequest() ו-CreatePublicKeyCredentialRequest() של Credential Manager. כדי לעבד את הבקשות האלה, ספק פרטי הכניסה מאחזר את 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, מגדירים ערך placeholder ל-clientDataJSON בתגובת האימות והטענה הנכוֹנוּת. מנהל הסיסמאות של Google משתמש ברשימת ההיתרים שזמינה לכולם, לקריאות ל-getOrigin(). כספק פרטי כניסה, אתם יכולים להשתמש ברשימה הזו או לספק רשימה משלכם בפורמט JSON שמתואר ב-API. הבחירה באיזו רשימה להשתמש היא בידי הספק. כדי לקבל גישה עם הרשאות באמצעות ספקי פרטי כניסה של צד שלישי, יש לעיין במסמכים שספק הצד השלישי מספק.

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

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

fun createSettingsPendingIntent(): PendingIntent