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

API การโอนข้อมูลเข้าสู่ระบบของ Credential Manager ช่วยให้สามารถโอนข้อมูลเข้าสู่ระบบของผู้ใช้ระหว่างผู้ให้บริการข้อมูลเข้าสู่ระบบในอุปกรณ์เดียวกันได้อย่างปลอดภัย คู่มือนี้จะอธิบายรายละเอียดวิธีที่ผู้ให้บริการข้อมูลเข้าสู่ระบบใน Android สามารถผสานรวมกับ API ที่ไลบรารี androidx.credentials:providerevents มีให้ ฟีเจอร์นี้รองรับรหัสผ่าน พาสคีย์ ข้อมูลที่อยู่ และช่องที่กำหนดเองโดยใช้ FIDO Credential Exchange Format (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 เนื้อหาที่จำเป็น

หากต้องการป้องกันการเรียกใช้ที่ไม่ได้รับอนุญาตและการโจมตีแบบ Confused Deputy ให้ประกาศ 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) เพื่อแยกวิเคราะห์คำขอ ยืนยันคอมโพเนนต์การเรียกและข้อมูลเมตาของคำขอ ดำเนินการตรวจสอบสิทธิ์ด้วยข้อมูลไบโอเมตริกที่จำเป็น และเขียนเพย์โหลด FIDO CXF กลับไปยัง URI เนื้อหาที่ระบุ

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

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

  1. ยืนยัน Nonce ของเซสชัน credId (การป้องกันหลัก): เมื่อลงทะเบียน ExportEntry ให้สร้าง id (Nonce) แบบสุ่มที่เข้ารหัสและมีเอนโทรปีสูง แล้วเชื่อมโยงกับเซสชันที่มีอายุสั้น ในกิจกรรม ให้ยืนยันว่า request.credId ตรงกับ Nonce ของเซสชันที่ใช้งานอยู่และยังไม่หมดอายุซึ่งผู้ให้บริการของคุณออกให้ก่อนหน้านี้ เนื่องจากแอปของบุคคลที่สามไม่สามารถคาดเดา Nonce นี้ได้ จึงช่วยให้มั่นใจได้ว่า Intent สอดคล้องกับการเลือกของผู้ใช้ที่แท้จริงในเครื่องมือเลือกของระบบ
  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 หรือการเรียกใช้ในระดับระบบปฏิบัติการที่ไม่รองรับ)
  • 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

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

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

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

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