'מנהל פרטי הכניסה' הוא קבוצה של ממשקי API שהוצגו ב-Android 14 ותומכים בכמה שיטות כניסה, כמו שם משתמש וסיסמה, מפתחות גישה ופתרונות מאוחדים לכניסה (כמו 'כניסה באמצעות חשבון Google'). מתי ממשק ה-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>
ההרשאה ומסנן ה-Intent שמוצגים למעלה הם חלק בלתי נפרד לפרטי הכניסה תהליך העבודה של המנהל כמצופה. ההרשאה נדרשת כדי שרק מערכת 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 כמו מילוי אוטומטי של סיסמאות ונתונים אחרים. הספקים האלה יכולים להשתמש באותה תשתית פנימית כדי לאחסן את סוגי פרטי הכניסה הקיימים, ולהרחיב אותה כדי לתמוך בסוגי פרטי כניסה אחרים, כולל מפתחות גישה.
גישה דו-שלבית לאינטראקציה עם ספקים
האינטראקציה של מנהל פרטי הכניסה עם ספקים של פרטי הכניסה כוללת שני שלבים:
- השלב הראשון הוא שלב ההתחלה/השאילתה שבו המערכת מקשרת
שירותים והפעלות של ספקי פרטי כניסה
onBeginGetCredentialRequest()
,onBeginCreateCredentialRequest()
, או שיטותonClearCredentialStateRequest()
עם בקשותBegin…
. הספקים צריכים לעבד את הבקשות האלה ולהשיב בתשובות מסוגBegin…
, תוך מילוי שלהן ברשאות שמייצגות אפשרויות חזותיות שיוצגו בבורר החשבונות. לכל רשומה צריך להיות ערךPendingIntent
. - אחרי שהמשתמש בוחר רשומה, מתחילה שלב הבחירה והאירוע
PendingIntent
שמשויך לרשומה מופעל, וכתוצאה מכך מוצגת פעילות הספק המתאימה. לאחר שהמשתמש יסיים את האינטראקציה איתו פעילות, ספק פרטי הכניסה חייב להגדיר את התגובה לתוצאה של לפני הסיום. התגובה הזו נשלחת לאפליקציית הלקוח שהפעילה את Credential Manager.
איך מטפלים ביצירת מפתחות גישה
טיפול בשאילתות ליצירת מפתחות גישה
כשאפליקציית לקוח רוצה ליצור מפתח גישה ולאחסן אותו אצל ספק פרטי כניסה, היא מבצעת קריאה ל-API createCredential
. כדי לטפל בבקשה הזו בשירות של ספק פרטי הכניסה, כך שמפתח הגישה יישמר בפועל באחסון, צריך לבצע את השלבים שמפורטים בקטעים הבאים.
- שינוי השיטה
onBeginCreateCredentialRequest()
בשירות הורחב מ-CredentialProviderService
. - מטפלים באירוע
BeginCreateCredentialRequest
על ידי יצירה של אירועBeginCreateCredentialResponse
תואם והעברה שלו באמצעות הפונקציה הלא סטטית להפעלה חוזרת. - במהלך היצירה של
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
כדי שהמערכת תוכל לצרף את הטקסט בקשה ל-Intent הנוסף. - אסור ליצור את
PendingIntent
עם הדגלPendingIntent.FLAG_ONE_SHOT
, כי המשתמש עשוי לבחור רשומה, לחזור אחורה ולבחור אותה שוב, וכתוצאה מכך האירועPendingIntent
יופעל פעמיים. - עליכם ליצור את
PendingIntent
עם קוד בקשה ייחודי, כדי שכל רשומה תוכל לכלולPendingIntent
תואם משלה.
טיפול בבחירת רשומה לבקשות ליצירת מפתחות גישה
- כשהמשתמש בוחר בשדה
CreateEntry
שאוכלס קודם לכן, הפרמטרPendingIntent
התואם מופעל והספק המשויךActivity
נוצר. - אחרי שמפעילים את השיטה
onCreate
ב-Activity, ניגשים ל-intent המשויך ומעבירים אותו לכיתהPendingIntentHander
כדי לקבל אתProviderCreateCredentialRequest
. - מחלצים את הערכים
requestJson
,callingAppInfo
ו-clientDataHash
מהבקשה. - חילוץ הערך המקומי של
accountId
מהתוסף של כוונת החיפוש. זוהי הטמעה לדוגמה ספציפית לאפליקציה, והיא לא חובה. אפשר להשתמש במספר החשבון הזה כדי לאחסן את פרטי הכניסה האלה מול מספר החשבון הספציפי הזה. - מאמתים את
requestJson
. בדוגמה הבאה נעשה שימוש בקטגוריות נתונים מקומיות כמוPublicKeyCredentialCreationOptions
כדי להמיר את קלט ה-JSON לקטגוריה מובנית בהתאם למפרט של WebAuthn. כספק פרטי כניסה, תוכלו להחליף את הקטגוריה הזו במנתח משלכם. - כדאי לבדוק את הקישור לנכס של אפליקציית השיחות, אם השיחה בוצעה אפליקציית נייטיב ל-Android.
- הצגת בקשה לאימות. בדוגמה הבאה נעשה שימוש ב-Android Biometric API.
- כשהאימות מסתיים בהצלחה, יוצרים
credentialId
וצמד מפתחות. - לשמור את המפתח הפרטי במסד הנתונים המקומי כנגד
callingAppInfo.packageName
. - בונים תגובת JSON של Web Authentication API
מורכב ממפתח ציבורי ומ-
credentialId
. הדוגמה שבהמשך משתמש במחלקות שירותים מקומיים כמוAuthenticatorAttestationResponse
וFidoPublicKeyCredential
שעוזרים לבנות קובץ JSON על סמך המפרט שצוין.כספק פרטי כניסה, אפשר להחליף את המחלקות האלה בנאים משלכם. - בונים
CreatePublicKeyCredentialResponse
עם ה-JSON שנוצר למעלה. - הגדרת
CreatePublicKeyCredentialResponse
כתוספת תשלום ב-Intent
עדPendingIntentHander.setCreateCredentialResponse()
, ומגדירים את הכוונה שלנו לתוצאה של הפעילות. - מסיימים את הפעילות.
דוגמת הקוד שבהמשך ממחישה את השלבים האלה. צריך לטפל בקוד הזה בכיתה Activity אחרי שמפעילים את 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()
שמוזכרת ב בקטע הקודם, מוסיפים עוד פנייה בתוך בלוק המתג לטיפול בקשות לסיסמאות. - במהלך היצירה של
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
, שכל אחד מהם מכיל פרמטרים שאפשר להשתמש בהם כדי לאחזר פרטי כניסה תואמים.
כדי לטפל בבקשה הזו בשירות של ספק פרטי הכניסה, צריך להשלים את את השלבים הבאים:
משנים את השיטה
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 ) ) }
אחזור פרטי הכניסה מהמסד הנתונים המקומי והגדרתם באמצעות
CredentialEntries
כדי שיוצגו בבורר. למפתחות גישה, אפשר להגדירcredentialId
כתוספת לכוונה, כדי לדעת מה פרטי הכניסה שלה ממופה כאשר המשתמש בוחר רשומה זו.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) }
שולחים שאילתה לפרטי הכניסה ממסד הנתונים, ליצור רשומות של מפתחות גישה וסיסמאות לאכלס.
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) ) }
אחרי ששולחים שאילתה ומאכלסים את פרטי הכניסה, צריך לטפל בשלב הבחירה של פרטי הכניסה שהמשתמש בוחר, בין אם מדובר במפתח גישה או בסיסמה.
טיפול בבחירת המשתמש למפתחות גישה
- ב-method
onCreate
של הפעילות המתאימה, מאחזרים את כוונת המשתמש המשויכת, ומעביריםPendingIntentHandler.retrieveProviderGetCredentialRequest()
. - מחלצים את
GetPublicKeyCredentialOption
מהבקשה שאוחזרה למעלה. לאחר מכן, מחלצים את השדותrequestJson
ו-clientDataHash
מהאפשרות הזו. - יש לחלץ את ה-
credentialId
מה-Intent הנוסף, שאוכלס על ידי של פרטי הכניסה של הספק כשהערך של ה-PendingIntent
התואם. - מחלצים את מפתח הגישה ממסד הנתונים המקומי באמצעות פרמטרים של הבקשה שאליהם ניגשים למעלה.
טענה שמפתח הגישה חוקי עם המטא-נתונים שחולצו, ועם המשתמש אימות.
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 )
כדי לאמת את המשתמש, מציגים הנחיה לביצוע אימות ביומטרי (או שיטת טענת נכוֹנוּת אחרת). קטע הקוד הבא משתמש ב-Android Biometric API.
לאחר שהאימות יצליח, יש ליצור תגובת JSON שמבוססת על W3 מפרט הטענה של אימות האימות באינטרנט. בקטע הקוד בהמשך, סיווגי נתונים עוזרים כמו
AuthenticatorAssertionResponse
משמשים לוקחים פרמטרים מובנים וממירים אותם לקובץ ה-JSON הנדרש הפורמט. התשובה מכילה חתימה דיגיטלית מהמפתח הפרטי של פרטי הכניסה ל-WebAuthn. השרת של הצד הנסמך יכול לבצע אימות החתימה הזו כדי לאמת משתמש לפני כניסה.בונים
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)
טיפול בבחירת המשתמש לאימות באמצעות סיסמה
- בפעילות המתאימה, נכנסים ל-Intent שהועבר אל
onCreate
ומחלצים את הקודProviderGetCredentialRequest
באמצעותPendingIntentHandler
בבקשה לאחזר את הסיסמה, צריך להשתמש ב-
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 } }
לאחר האחזור, מגדירים את התגובה לפרטי הכניסה שנבחרו לסיסמה.
// Set the response back val result = Intent() val passwordCredential = PasswordCredential(username, password) PendingIntentHandler.setGetCredentialResponse( result, GetCredentialResponse(passwordCredential) ) setResult(Activity.RESULT_OK, result) finish()
טיפול בבחירה של רשומה של פעולת אימות
כפי שצוין קודם, ספק פרטי הכניסה יכול להגדיר AuthenticationAction
אם פרטי הכניסה נעולים. אם המשתמש בוחר באפשרות הזאת
הרשומה, הפעילות שתואמת לפעולת ה-Intent שהוגדרה
בוצעה הפעלה של 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.
}
הוספת יכולת לקישור לדף ההגדרות של הספק
כדי לאפשר למשתמשים לפתוח את הגדרות הספק דרך סיסמאות,
מפתחות גישה מילוי אוטומטי, באפליקציות של ספקי פרטי הכניסה צריכים להטמיע
מאפיין מניפסט 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>
הגדרות Intent
פתיחת ההגדרות: כאשר משתמשים בכוונה android.settings.CREDENTIAL_PROVIDER
, מוצג מסך הגדרות שבו המשתמש יכול לבחור את ספקי פרטי הכניסה המועדפים עליו וספקים נוספים.
השירות המועדף לפרטי כניסה:
ACTION_REQUEST_SET_AUTOFILL_SERVICE
ה-Intent מפנה את המשתמשים אל
מסך לבחירת ספק מועדף. הספק שנבחר במסך הזה
הופך לספק פרטי הכניסה וספק המילוי האוטומטי המועדפים.
קבלת רשימת היתרים של אפליקציות מורשות
אפליקציות שקיבלו הרשאות, כמו דפדפני אינטרנט, מבצעות הפעלות של 'מנהל פרטי הכניסה' מטעם
גורמים מסתמכים אחרים על ידי הגדרה של הפרמטר origin
בפרטי הכניסה
המנהל GetCredentialRequest()
וגם
CreatePublicKeyCredentialRequest()
. כדי לעבד את הבקשות האלה, ספק פרטי הכניסה מאחזר את origin
באמצעות ה-API של getOrigin()
.
כדי לאחזר את origin
, האפליקציה של ספק פרטי הכניסה צריכה להעביר רשימה של
מתקשרים מהימנים ובעלי הרשאות
androidx.credentials.provider.CallingAppInfo's getOrigin()
API. רשימת ההיתרים חייבת להיות אובייקט JSON חוקי. הערך origin
מוחזר אם packageName
וטביעות האצבע של האישור שהתקבלו מ-signingInfo
תואמים לאלה של אפליקציה שנמצאת ב-privilegedAllowlist
שהועברה ל-API של getOrigin()
. אחרי
התקבל ערך של origin
, אפליקציית הספק צריכה להתייחס לזה כאל הרשאה
קוראים ומגדירים את origin
בנתוני הלקוח
בAuthenticatorResponse
, במקום לחשב
origin
באמצעות החתימה של אפליקציית השיחות.
אם מאחזרים origin
, צריך להשתמש ב-clientDataHash
שסופק ישירות
ב-CreatePublicKeyCredentialRequest()
או
GetPublicKeyCredentialOption()
במקום להרכיב ולגבב
clientDataJSON
במהלך בקשת החתימה. כדי להימנע מבעיות בניתוח JSON, צריך להגדיר
ערך placeholder ל-clientDataJSON
באימות (attestation) ובטענת נכוֹנוּת (assertion)
תשובה.
במנהל הסיסמאות של Google משתמשים ברשימת היתרים פתוחה עבור
שיחות אל getOrigin()
. כספק פרטי כניסה, אתם יכולים להשתמש ברשימה הזו או לספק רשימה משלכם בפורמט JSON שמתואר ב-API. הספק הוא זה שבוחר באיזו רשימה להשתמש. כדי לקבל גישה מוגבלת באמצעות צד שלישי
ספקים של פרטי כניסה צריכים לעיין במסמכים שהצד השלישי סיפק.
הפעלת ספקים במכשיר
המשתמשים חייבים להפעיל את הספק דרך הגדרות מכשיר > סיסמאות חשבונות > הספק שלך > הפעלה או השבתה.
fun createSettingsPendingIntent(): PendingIntent