인증 관리자의 사용자 인증 정보 전송 API를 사용하면 사용자 인증 정보 제공업체 간에 사용자 인증 정보를 동일한 기기에서 안전하게 전송할 수 있습니다. 이 가이드에서는 Android의 사용자 인증 정보 제공업체가 androidx.credentials:providerevents 라이브러리에서 제공하는 API와 통합하는 방법을 자세히 설명합니다. 이 기능은 표준화된 FIDO 사용자 인증 정보 교환 형식 (CXF)을 사용하여 비밀번호, 패스키, 주소 정보, 맞춤 입력란을 지원합니다.
핵심 개념
사용자 인증 정보 전송 프레임워크는 원시 사용자 인증 정보를 Android OS 또는 인증되지 않은 앱에 노출하지 않고 동일한 기기에서 P2P 사용자 인증 정보 전송을 지원합니다.
이 프레임워크는 두 가지 기본 역할을 정의합니다.
- 내보내기 도구 (소스 제공업체): 현재 사용자 인증 정보를 보유하고 있는 인증 정보 제공업체입니다. 내보낼 수 있는 계정 (
ExportEntry)에 관한 메타데이터를 시스템에 사전 등록하고 사용자가 선택하면 전송 요청에 응답합니다. - 가져오기 도구 (클라이언트 제공자 또는 설정 마법사): 수신할 수 있는 사용자 인증 정보 및 확장 프로그램 유형을 지정하는 가져오기 요청(
ImportCredentialsRequest)을 시작하는 사용자 인증 정보 제공자 또는 설정 마법사입니다.
Android 버전 호환성
Credentials Transfer API는 Android 8 (API 수준 26) 이상을 실행하는 기기에서 작동합니다.
종속 항목 추가
모듈의 build.gradle 또는 build.gradle.kts에 androidx.credentials:providerevents 종속 항목을 추가합니다.
dependencies {
implementation("androidx.credentials:providerevents:1.0.0-alpha06")
}
필수 클래스 인스턴스화
필요한 ProviderEventsManager의 인스턴스를 만듭니다.
val providerEventsManager = ProviderEventsManager.create(context)
내보내기 도구 구현
사용자가 앱에서 기기의 다른 인증 정보 제공업체로 인증 정보를 내보낼 수 있도록 하려면 내보내기 도구 역할을 구현하세요.
내보내기 항목 등록
사용자 인증 정보 제공자가 변경되면 (예: 사용자가 로그인하거나 사용자 인증 정보를 추가하거나 계정을 수정하는 경우) ProviderEventsManager.registerExport()를 사용하여 ExportEntry 항목을 등록하거나 업데이트합니다.
각 ExportEntry에는 다음이 필요합니다.
id: 이 내보내기 항목을 고유하게 나타내는 비밀의 무작위로 생성된 문자열 식별자입니다. 이 ID를 안전하게 저장해야 합니다. 나중에 수신 전송 요청을 확인하는 데 필요합니다.accountDisplayName: 선택사항인 계정 라벨입니다 (예:"Personal Account").userDisplayName: 사용자의 기본 식별자입니다 (예:"alice@example.com").icon: 제공업체 또는 계정을 나타내는Bitmap아이콘입니다 (라이브러리에서 자동으로 32x32 PNG로 조정됨).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) } }
매니페스트 파일에서 내보내기 도구 활동 선언
사용자가 시스템 선택기 UI에서 ExportEntry를 선택하면 시스템은 지정된 처리 Activity를 실행합니다. 필수 인텐트 작업 및 콘텐츠 URI 스킴을 사용하여 이 활동을 선언합니다.
승인되지 않은 호출과 혼동된 대리인 공격을 방지하려면 Android 14 (API 수준 34) 이상에서 android:requireContentUriPermissionFromCaller="write"를 선언하세요. 이렇게 하면 다음 예와 같이 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>
활동에서 전송 인텐트 처리
CredentialExportActivity에서 IntentHandler.retrieveProviderImportCredentialsRequest(intent)을 사용하여 요청을 파싱하고, 호출 구성요소와 요청 메타데이터를 확인하고, 필요한 생체 인식 인증을 실행하고, FIDO CXF 페이로드를 제공된 콘텐츠 URI에 다시 작성합니다.
요청 인증
활동을 내보내므로 다음 레이어를 각각 확인하여 사용자 인증 정보 내보내기 흐름을 보호합니다.
credId세션 nonce 확인 (기본 방어):ExportEntry를 등록할 때 암호화 방식으로 무작위인 높은 엔트로피id(nonce)를 생성하고 이를 단기 세션과 연결합니다. 활동에서request.credId가 이전에 제공업체에서 발급한 활성 상태의 만료되지 않은 세션 nonce와 일치하는지 확인합니다. 서드 파티 앱은 이 nonce를 추측할 수 없으므로 인텐트가 시스템 선택기에서 실제 사용자 선택에 상응합니다.Activity.getCallingPackage()확인 (호출자 확인): 사용자 인증 정보 선택 도구는startActivityForResult를 사용하여 활동을 실행하므로 활동은getCallingPackage()를 검사하여 UI를 실행하는 즉각적인 호출자가 신뢰할 수 있는 시스템 사용자 인증 정보 선택 도구 프레임워크 (Google Play 서비스가 설치된 Android 기기에서는"com.google.android.gms")인지 확인할 수 있습니다. 호출 패키지가 인식되지 않으면 요청을 거부합니다.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: 제공업체 설정 문제 (예:ExportEntry의 빈supportedCredentialTypes또는 지원되지 않는 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: 구성 오류(예:ImportCredentialsRequest에 설정된 빈credentialTypes또는 누락된 권한)ImportCredentialsInvalidJsonException: 내보내기 도구에서 요청 검증에 실패한 잘못된 형식의 JSON 페이로드 또는 빈 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" |
사용자 ID 및 이름 세부정보입니다. |
CREDENTIAL_TYPE_SSH_KEY |
"ssh-key" |
SSH 공개 키 및 비공개 키 쌍 |
CREDENTIAL_TYPE_TOTP |
"totp" |
시간 기반 일회용 비밀번호 (2FA) 보안 비밀 |
CREDENTIAL_TYPE_WIFI |
"wifi" |
Wi-Fi 네트워크 SSID 및 암호문 |
커스텀 WASM (WebAssembly) 일치자 (고급)
기본적으로 RegisterExportRequest.create(context, entries)를 호출하면 라이브러리 애셋에서 시스템 기본 credential_transfer_matcher.wasm가 번들로 묶여 supportedCredentialTypes의 교차점을 기준으로 항목을 필터링합니다.
사용자 인증 정보 제공업체에 복잡한 일치 로직 (예: 동적 기능 검사 또는 맞춤 필드를 기반으로 한 조건부 필터링)이 필요한 경우 WASM 사용자 인증 정보 전송 API와 일치하는 자체 WebAssembly 모듈을 컴파일하고 원시 바이트 배열을 직접 전달할 수 있습니다.
// 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)