การโอนข้อมูลเข้าสู่ระบบ

API การโอนข้อมูลเข้าสู่ระบบของ Credential Manager ช่วยให้โอนข้อมูลเข้าสู่ระบบของผู้ใช้ระหว่างผู้ให้บริการข้อมูลเข้าสู่ระบบได้อย่างปลอดภัยในอุปกรณ์เดียวกัน คู่มือนี้จะอธิบายรายละเอียดเกี่ยวกับวิธีที่ผู้ให้บริการข้อมูลเข้าสู่ระบบใน Android สามารถผสานรวมกับ API ที่จัดทำโดยไลบรารี androidx.credentials:providerevents ฟีเจอร์นี้รองรับรหัสผ่าน พาสคีย์ ข้อมูลที่อยู่ และฟิลด์ที่กำหนดเองโดยใช้รูปแบบการแลกเปลี่ยนข้อมูลเข้าสู่ระบบ FIDO (CXF) ที่ได้มาตรฐาน

แนวคิดหลัก

เฟรมเวิร์กการโอนข้อมูลเข้าสู่ระบบช่วยอำนวยความสะดวกในการโอนข้อมูลเข้าสู่ระบบแบบเพียร์ทูเพียร์ ในอุปกรณ์เดียวกันโดยไม่เปิดเผยข้อมูลเข้าสู่ระบบดิบต่อระบบปฏิบัติการ Android หรือ แอปที่ไม่ได้รับการตรวจสอบสิทธิ์

เฟรมเวิร์กกำหนดบทบาทหลัก 2 บทบาท ได้แก่

  • ผู้ส่งออก (ผู้ให้บริการต้นทาง): ผู้ให้บริการข้อมูลเข้าสู่ระบบที่ปัจจุบันถือ ข้อมูลเข้าสู่ระบบของผู้ใช้ โดยจะลงทะเบียนข้อมูลเมตาล่วงหน้าเกี่ยวกับบัญชีที่ส่งออกได้ (ExportEntry) กับระบบ และตอบกลับคำขอโอนเมื่อผู้ใช้เลือก
  • ผู้นำเข้า (ผู้ให้บริการไคลเอ็นต์หรือวิซาร์ดการตั้งค่า): ผู้ให้บริการข้อมูลเข้าสู่ระบบหรือ วิซาร์ดการตั้งค่าที่เริ่มคำขอนำเข้า (ImportCredentialsRequest) โดยระบุประเภทข้อมูลเข้าสู่ระบบและ ส่วนขยายที่รับได้

ความเข้ากันได้กับเวอร์ชัน Android

Credentials Transfer API ใช้งานได้ในอุปกรณ์ที่ใช้ Android 8 (ระดับ API 26) ขึ้นไป

เพิ่มทรัพยากร Dependency

เพิ่มทรัพยากร Dependency androidx.credentials:providerevents ลงใน build.gradle หรือ build.gradle.kts ของโมดูล

dependencies {
    implementation("androidx.credentials:providerevents:1.0.0-alpha06")
}

สร้างอินสแตนซ์ของคลาสที่จำเป็น

สร้างอินสแตนซ์ของ ProviderEventsManager ที่จำเป็น

val providerEventsManager = ProviderEventsManager.create(context)

ติดตั้งใช้งานโปรแกรมส่งออก

หากต้องการอนุญาตให้ผู้ใช้ส่งออกข้อมูลเข้าสู่ระบบจากแอปของคุณไปยังผู้ให้บริการข้อมูลเข้าสู่ระบบรายอื่น ในอุปกรณ์ ให้ใช้บทบาทผู้ส่งออก

ลงทะเบียนรายการส่งออก

เมื่อผู้ให้บริการข้อมูลเข้าสู่ระบบมีการเปลี่ยนแปลง (เช่น ผู้ใช้ลงชื่อเข้าใช้ เพิ่มข้อมูลเข้าสู่ระบบ หรือแก้ไขบัญชี) ให้ลงทะเบียนหรืออัปเดตExportEntryรายการ โดยใช้ ProviderEventsManager.registerExport()

ExportEntry แต่ละรายการต้องมีข้อมูลต่อไปนี้

  • id: ตัวระบุสตริงลับที่สร้างขึ้นแบบสุ่มซึ่งแสดงถึงรายการส่งออกนี้โดยเฉพาะ คุณต้องเก็บรหัสนี้อย่างปลอดภัย คุณจะต้องใช้รหัสนี้ในภายหลัง เพื่อยืนยันคำขอโอนที่เข้ามา
  • accountDisplayName: ป้ายกำกับบัญชีที่ไม่บังคับ (เช่น "Personal Account")
  • userDisplayName: ตัวระบุหลักของผู้ใช้ (เช่น "alice@example.com")
  • icon: Bitmap ไอคอนที่แสดงถึงผู้ให้บริการหรือบัญชี (ไลบรารีจะปรับขนาดเป็น PNG 32x32 โดยอัตโนมัติ)
  • supportedCredentialTypes: ชุดค่าคงที่สตริงจาก CredentialTypes ที่แสดงประเภทที่รายการนี้มี

suspend fun registerMyProviderForExport(
    providerEventsManager: ProviderEventsManager,
    providerIcon: Bitmap,
    // Randomly generated and stored in encrypted storage
    secretEntryId: String
) {
    val entry = ExportEntry(
        id = secretEntryId,
        accountDisplayName = "MyProvider Personal",
        userDisplayName = "alice@example.com",
        icon = providerIcon,
        supportedCredentialTypes = setOf(
            CredentialTypes.CREDENTIAL_TYPE_BASIC_AUTH, // Passwords
            CredentialTypes.CREDENTIAL_TYPE_PUBLIC_KEY, // Passkeys
            CredentialTypes.CREDENTIAL_TYPE_ADDRESS,
            CredentialTypes.CREDENTIAL_TYPE_CREDIT_CARD
        )
    )

    // RegisterExportRequest.create() attaches the default WASM matcher from assets
    val request = RegisterExportRequest.create(context, listOf(entry))

    try {
        val response = providerEventsManager.registerExport(request)
        // Registration successful
    } catch (e: Exception) {
        // Handle registration exceptions (e.g., RegisterExportProviderConfigurationException)
    }
}

ประกาศกิจกรรมการส่งออกในไฟล์ Manifest

เมื่อผู้ใช้เลือก ExportEntry ใน UI ตัวเลือกของระบบ ระบบจะเปิดActivityการจัดการที่กำหนด ประกาศกิจกรรมนี้ด้วยการดำเนินการผ่าน Intent และสคีม URI เนื้อหาที่จำเป็น

หากต้องการป้องกันการเรียกใช้ที่ไม่ได้รับอนุญาตและการโจมตีแบบสับสนในหน้าที่ ให้ประกาศ android:requireContentUriPermissionFromCaller="write" ใน Android 14 (ระดับ API 34) ขึ้นไป ซึ่งจะสั่งให้ระบบปฏิบัติการ Android ตรวจสอบว่าผู้เรียกที่เริ่มการเปิดใช้มีสิทธิ์เขียนไปยัง URI ของเนื้อหาปลายทางก่อนที่จะอนุญาตให้กิจกรรมเริ่มต้น ดังที่แสดงในตัวอย่างต่อไปนี้

<activity
    android:name="com.example.CredentialExportActivity"
    android:exported="true"
    android:requireContentUriPermissionFromCaller="write"
    android:label="@string/export_activity_label">
    <intent-filter>
        <!-- This intent action is required for Credential Manager to invoke this activity -->
        <action android:name="androidx.identitycredentials.action.IMPORT_CREDENTIALS" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:scheme="content" />
    </intent-filter>
</activity>

จัดการ Intent การโอนในกิจกรรม

ใน CredentialExportActivity ให้ใช้ IntentHandler.retrieveProviderImportCredentialsRequest(intent) เพื่อแยกวิเคราะห์ คำขอ ยืนยันคอมโพเนนต์ที่เรียกใช้และข้อมูลเมตาของคำขอ ดำเนินการ การตรวจสอบสิทธิ์ด้วยข้อมูลไบโอเมตริกที่จำเป็น และเขียนเพย์โหลด CXF ของ FIDO กลับไปยัง URI เนื้อหาที่ระบุ

ตรวจสอบสิทธิ์คำขอ

เนื่องจากระบบจะส่งออกกิจกรรมของคุณ ให้ปกป้องขั้นตอนการส่งออกข้อมูลเข้าสู่ระบบโดย ยืนยันแต่ละเลเยอร์ต่อไปนี้

  1. ยืนยัน credId nonce ของเซสชัน (การป้องกันหลัก): เมื่อลงทะเบียน ExportEntry ให้สร้าง id (nonce) ที่สุ่มแบบเข้ารหัส ที่มีเอนโทรปีสูง แล้วเชื่อมโยงกับเซสชันที่มีอายุสั้น ในกิจกรรมของคุณ ให้ตรวจสอบว่า request.credId ตรงกับค่า Nonce ของเซสชันที่ใช้งานอยู่และยังไม่หมดอายุ ซึ่งผู้ให้บริการออกให้ก่อนหน้านี้ เนื่องจากแอปของบุคคลที่สาม ไม่สามารถคาดเดานอนซ์นี้ได้ จึงมั่นใจได้ว่าความตั้งใจตรงกับการเลือกของผู้ใช้จริงในตัวเลือกของระบบ
  2. ยืนยัน Activity.getCallingPackage() (การตรวจสอบผู้โทร): เนื่องจากตัวเลือกข้อมูลเข้าสู่ระบบจะเปิดใช้งานกิจกรรมของคุณโดยใช้ startActivityForResult กิจกรรมจึงตรวจสอบ getCallingPackage() เพื่อ ยืนยันว่าผู้โทรที่เปิด UI โดยตรงคือเฟรมเวิร์กตัวเลือกข้อมูลเข้าสู่ระบบที่เชื่อถือได้ (ในอุปกรณ์ Android ที่มีบริการ Google Play, "com.google.android.gms") ปฏิเสธคำขอหากแพ็กเกจที่เรียกใช้ไม่รู้จัก
  3. ตรวจสอบ request.callingAppInfo (ข้อมูลแอปเป้าหมาย): callingAppInfo ระบุแอปพลิเคชันปลายทางที่จะ รับข้อมูลเข้าสู่ระบบที่นำเข้า คุณสามารถใช้ข้อมูลนี้ตามที่เห็นสมควรเพื่อความโปร่งใส (เช่น การแสดงชื่อและไอคอนแอปปลายทางใน UI การยืนยัน) หรือเพื่อยืนยันว่าแอปพลิเคชันปลายทางเชื่อถือได้หรือไม่

class CredentialExportActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 1. Extract the transfer request from the incoming Intent
        val request: ProviderImportCredentialsRequest? =
            IntentHandler.retrieveProviderImportCredentialsRequest(intent)

        if (request == null) {
            finishWithError()
            return
        }

        // 2. Validate CallingAppInfo and secret `credId`
        val callingAppPackage = request.callingAppInfo.packageName
        val receivedCredId = request.credId
        if (!verifySecretEntryId(receivedCredId) || !isTrustedImporter(callingAppPackage)) {
            // Secret ID mismatch or untrusted caller -> abort
            sendExceptionAndFinish(ImportCredentialsNoExportOptionException("Unauthorized request"))
            return
        }

        // 3. Optional: Prompt user for Biometric / PIN authentication before exporting
        authenticateUserThenExport(request)
    }

    private fun authenticateUserThenExport(request: ProviderImportCredentialsRequest) {
        // ... Biometric prompt logic ...
        // Once authenticated, generate the FIDO CXF JSON string matching the requested types
        val cxfJsonPayload = buildFidoCxfJsonPayload(
            requestedTypes = request.request.credentialTypes,
            requestedExtensions = request.request.knownExtensions
        )

        val response = ImportCredentialsResponse(cxfJsonPayload)

        // 4. Write the JSON payload to the Content URI and set Activity result
        IntentHandler.setImportCredentialsResponse(
            context = this,
            uri = request.uri,
            intent = intent,
            response = response
        )
        setResult(Activity.RESULT_OK, intent)
        finish()
    }

    private fun sendExceptionAndFinish(exception: androidx.credentials.providerevents.exception.ImportCredentialsException) {
        IntentHandler.setImportCredentialsException(intent, exception)
        setResult(Activity.RESULT_OK, intent)
        finish()
    }

    private fun verifySecretEntryId(credentialId: String): Boolean {
        // Check if credentialId matches what you stored when calling RegisterExportRequest
        return credentialId == getStoredSecretEntryId()
    }

    private fun isTrustedImporter(packageName: String): Boolean {
        // Implement any specific allowlisting / caller checks if required
        return true
    }

    private fun finishWithError() {
        setResult(Activity.RESULT_CANCELED)
        finish()
    }
}

ข้อยกเว้นในการส่งออกการจดทะเบียนและการล้างข้อมูล

ข้อยกเว้นทั้งหมดในโมดูลนี้เป็นคลาสย่อยของ ImportCredentialsException, RegisterExportException หรือ ClearExportException

  • RegisterExportProviderConfigurationException หรือ ClearExportProviderConfigurationException: เกิดขึ้นเมื่อการลงทะเบียน หรือการล้างข้อมูลไม่สำเร็จเนื่องจากปัญหาการตั้งค่าของผู้ให้บริการ (เช่น supportedCredentialTypes ว่างเปล่าใน ExportEntry หรือการเรียกใช้ในระดับ OS ที่ไม่รองรับ)
  • RegisterExportUnknownErrorException หรือ ClearExportUnknownErrorException: เกิดข้อผิดพลาดของระบบหรือพื้นที่เก็บข้อมูลที่ไม่ได้จัดประเภท ขณะอัปเดตรีจิสทรีการส่งออก

ติดตั้งใช้งานโปรแกรมนำเข้า

หากต้องการนำเข้าข้อมูลเข้าสู่ระบบไปยังแอป (เช่น ระหว่างการเริ่มต้นใช้งานหรือการนำเข้าผู้ให้บริการ) ให้ใช้บทบาทผู้นำเข้าและเริ่มขั้นตอนโดยการเรียกใช้ ProviderEventsManager.importCredentials()

สร้างคำขอนำเข้าและขั้นตอนการเปิดตัว

ระบุCredentialTypesและ KnownExtensions ที่โปรแกรมนำเข้า รองรับ

suspend fun startCredentialImport(
    activityContext: Context,
    providerEventsManager: ProviderEventsManager
) {
    val importRequest = ImportCredentialsRequest(
        credentialTypes = setOf(
            CredentialTypes.CREDENTIAL_TYPE_BASIC_AUTH,
            CredentialTypes.CREDENTIAL_TYPE_PUBLIC_KEY,
            CredentialTypes.CREDENTIAL_TYPE_ADDRESS,
            CredentialTypes.CREDENTIAL_TYPE_NOTE
        ),
        knownExtensions = setOf(
            KnownExtensions.KNOWN_EXTENSION_SHARED
        )
    )

    try {
        // Launches the system Selector UI; suspends until user selects a provider and completes transfer
        val response = providerEventsManager.importCredentials(activityContext, importRequest)

        // 1. Inspect the source exporter's package info
        val exporterPackageName = response.callingAppInfo.packageName

        // 2. Parse the FIDO CXF JSON string
        val cxfJsonString = response.response.responseJson
        parseAndSaveImportedCredentials(cxfJsonString)
    } catch (e: ImportCredentialsException) {
        // Handle specific import exceptions (e.g., ImportCredentialsCancellationException)
        handleImportFailure(e)
    }
}

private fun parseAndSaveImportedCredentials(cxfJsonString: String) {
    val rootJson = JSONObject(cxfJsonString)
    // Parse according to FIDO Credential Exchange Format (CXF v1.0) specification:
    // https://fidoalliance.org/specs/cx/cxf-v1.0-ps-20250814.html
}

// Helper function to make it compile
private fun handleImportFailure(e: ImportCredentialsException) {}

ข้อยกเว้นของโฟลว์การนำเข้า

ข้อยกเว้นทั้งหมดในโมดูลนี้เป็นคลาสย่อยของ ImportCredentialsException, RegisterExportException หรือ ClearExportException

  • ImportCredentialsCancellationException: ผู้ใช้ปิด UI ตัวเลือกหรือยกเลิกกิจกรรมการส่งออก (Activity.RESULT_CANCELED)
  • ImportCredentialsNoExportOptionException: ไม่มีรายการส่งออกที่ลงทะเบียนซึ่งตรงกับประเภทข้อมูลเข้าสู่ระบบที่ขอ หรือโปรแกรมส่งออกที่เลือก ส่งข้อยกเว้นการปฏิเสธ
  • ImportCredentialsProviderConfigurationException: ข้อผิดพลาดในการกำหนดค่า (เช่น ตั้งค่า credentialTypes เป็นค่าว่างใน ImportCredentialsRequest หรือไม่มีสิทธิ์)
  • ImportCredentialsInvalidJsonException: ผู้ส่งออกแสดงเพย์โหลด JSON ที่มีรูปแบบไม่ถูกต้องหรือว่างเปล่า ซึ่งทำให้การตรวจสอบคำขอล้มเหลว
  • ImportCredentialsSystemErrorException: เกิดข้อผิดพลาดในระบบ Android ภายในหรือข้อผิดพลาดในการโอน Binder ระหว่างการนำเข้า
  • ImportCredentialsUnknownCallerException: เฟรมเวิร์กยืนยันแอปการโทรไม่ได้
  • ImportCredentialsUnknownErrorException: ข้อผิดพลาดที่ไม่คาดคิดหรือไม่ได้จัดประเภทระหว่างขั้นตอนการนำเข้า

ประเภทและส่วนขยายของข้อมูลเข้าสู่ระบบที่รองรับ

ออบเจ็กต์ androidx.credentials.providerevents.transfer.CredentialTypes กำหนดค่าคงที่สตริงที่ได้มาตรฐานซึ่งสอดคล้องกับประเภทรายการ FIDO CXF ดังนี้

ค่าคงที่ ค่า (cxf ประเภท) คำอธิบาย
CREDENTIAL_TYPE_BASIC_AUTH "basic-auth" ข้อมูลเข้าสู่ระบบชื่อผู้ใช้และรหัสผ่าน
CREDENTIAL_TYPE_PUBLIC_KEY "passkey" ข้อมูลเข้าสู่ระบบคีย์สาธารณะของพาสคีย์ FIDO2 หรือ WebAuthn
CREDENTIAL_TYPE_ADDRESS "address" ข้อมูลที่อยู่ไปรษณีย์หรือที่อยู่สำหรับจัดส่งสำหรับการป้อนข้อมูลแบบฟอร์มโดยอัตโนมัติ
CREDENTIAL_TYPE_API_KEY "api-key" คีย์และโทเค็นการเข้าถึง API
CREDENTIAL_TYPE_CREDIT_CARD "credit-card" ข้อมูลการชำระเงินด้วยบัตรเครดิตและบัตรเดบิต
CREDENTIAL_TYPE_CUSTOM_FIELDS "custom-fields" การจัดกลุ่มที่กำหนดเองหรือฟิลด์ที่ผู้ใช้กำหนด
CREDENTIAL_TYPE_DRIVERS_LICENSE "drivers-license" รายละเอียดใบขับขี่
CREDENTIAL_TYPE_FILE "file" ข้อมูลเมตาและการอ้างอิงตัวยึดตำแหน่งสำหรับไฟล์ไบนารี
CREDENTIAL_TYPE_GENERATED_PASSWORD "generated-password" รหัสผ่านที่ปลอดภัยซึ่งเครื่องสร้างขึ้น
CREDENTIAL_TYPE_IDENTITY_DOCUMENT "identity-document" บัตรประจำตัวประชาชน, SSN, TIN หรือข้อมูลอ้างอิงหนังสือเดินทาง
CREDENTIAL_TYPE_ITEM_REFERENCE "item-reference" ลิงก์เชิงตรรกะที่ชี้ไปยังรายการอื่นในเพย์โหลด
CREDENTIAL_TYPE_NOTE "note" โน้ตที่ปลอดภัยซึ่งผู้ใช้กำหนด (สตริง UTF-8)
CREDENTIAL_TYPE_PASSPORT "passport" รายละเอียดเอกสารการเดินทางในหนังสือเดินทาง
CREDENTIAL_TYPE_PERSON_NAME "person-name" รายละเอียดเกี่ยวกับตัวบุคคลและการตั้งชื่อ
CREDENTIAL_TYPE_SSH_KEY "ssh-key" คู่คีย์สาธารณะและส่วนตัวของ SSH
CREDENTIAL_TYPE_TOTP "totp" ความลับของรหัสผ่านที่สามารถใช้งานได้เพียงครั้งเดียวตามเวลา (2FA)
CREDENTIAL_TYPE_WIFI "wifi" SSID และรหัสผ่านของเครือข่าย Wi-Fi

ตัวเทียบ WASM (WebAssembly) ที่กำหนดเอง (ขั้นสูง)

โดยค่าเริ่มต้น การเรียกใช้ RegisterExportRequest.create(context, entries) จะรวมcredential_transfer_matcher.wasmเริ่มต้นของระบบจากชิ้นงานในคลัง ซึ่งจะกรองรายการตามส่วนตัดกันของ supportedCredentialTypes โดยเฉพาะ

หากผู้ให้บริการข้อมูลเข้าสู่ระบบกำหนดให้ใช้ตรรกะการจับคู่ที่ซับซ้อน (เช่น การตรวจสอบความสามารถแบบไดนามิกหรือการกรองแบบมีเงื่อนไขตามช่องที่กำหนดเอง) คุณ สามารถคอมไพล์โมดูล WebAssembly ของคุณเองที่ตรงกับ API การโอนข้อมูลเข้าสู่ระบบ WASM และส่งอาร์เรย์ไบต์ดิบได้โดยตรง

// Loading a custom WASM matcher
val customMatcherBytes = context.assets.open("my_custom_matcher.wasm").use { it.readBytes() }
val customRequest = RegisterExportRequest(
    entries = myEntries,
    exportMatcher = customMatcherBytes
)
providerEventsManager.registerExport(customRequest)