Android 시스템 키 인증기

Android용 키 인증 도구는 사용자가 엔드 투 엔드 암호화 (E2EE) 앱에서 올바른 상대와 소통하고 있는지 확인할 수 있는 통합되고 안전한 방법을 제공합니다. 사용자가 신뢰할 수 있고 일관된 시스템 UI를 통해 연락처의 공개 암호화 키의 진위 여부를 확인할 수 있도록 하여 중간자 공격으로부터 사용자를 보호합니다.

이 기능은 Google 시스템 서비스의 일부이며 Play 스토어를 사용하여 배포되는 시스템 서비스인 키 확인자에 의해 제공됩니다. E2EE 공개 키의 중앙 집중식 온디바이스 저장소 역할을 합니다.

Key Verifier와 통합해야 하는 이유

  • 통합 UX 제공: 자체 인증 흐름을 빌드하는 대신 시스템의 표준 UI를 실행하여 모든 앱에서 사용자에게 일관되고 신뢰할 수 있는 환경을 제공할 수 있습니다.
  • 사용자 신뢰도 향상: 명확하고 시스템에서 지원하는 인증 상태는 사용자가 대화가 안전하고 비공개임을 확신할 수 있도록 합니다.
  • 개발 오버헤드 감소: 키 확인 UI, 저장소, 상태 관리의 복잡성을 시스템 서비스로 오프로드합니다.

주요 용어

  • lookupKey: 연락처의 불투명한 영구 식별자로, 연락처 제공자의 LOOKUP_KEY 열에 저장됩니다. contact ID와 달리 lookupKey는 기본 연락처 세부정보가 변경되거나 병합되더라도 안정적으로 유지되므로 연락처를 참조하는 데 권장되는 방법입니다.
  • accountId: 기기에서 사용자 계정의 앱별 식별자입니다. 이 ID는 앱에서 정의하며 단일 사용자가 보유할 수 있는 여러 계정을 구분하는 데 도움이 됩니다. 이는 UI에 사용자에게 표시되므로 전화번호, 이메일 주소, 사용자 핸들과 같이 의미 있는 것을 사용하는 것이 좋습니다.
  • deviceId: 사용자 계정과 연결된 특정 기기의 고유 식별자입니다. 이를 통해 사용자는 각각 자체 암호화 키 집합이 있는 여러 기기를 사용할 수 있습니다. 실제 기기를 나타내지 않을 수도 있지만 동일한 계정에 사용되는 여러 키를 구분하는 방법일 수 있습니다.

시작하기

시작하기 전에 키 확인자 서비스와 통신하도록 앱을 설정하세요.

권한 선언: AndroidManifest.xml에서 다음 권한을 선언합니다. 런타임에 사용자에게도 요청해야 합니다.

<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />

클라이언트 인스턴스 가져오기: API의 진입점인 ContactKeys의 인스턴스를 가져옵니다.

import com.google.android.gms.contactkeys.ContactKeys

val contactKeyClient = ContactKeys.getClient(context)

메시지 앱 개발자를 위한 안내

메시지 앱 개발자의 기본 역할은 사용자의 공개 키와 연락처의 키를 키 확인 도구 서비스에 게시하는 것입니다.

사용자의 공개 키 게시

다른 사용자가 사용자를 찾고 확인할 수 있도록 하려면 온디바이스 저장소에 공개 키를 게시하세요. 보안을 강화하려면 Android 키 저장소에서 키를 만드는 것이 좋습니다.

import com.google.android.gms.contactkeys.ContactKeyClient
import com.google.android.gms.tasks.Tasks

suspend fun publishSelfKey(
    contactKeyClient: ContactKeyClient,
    accountId: String,
    deviceId: String,
    publicKey: ByteArray
) {
    try {
        Tasks.await(
          contactKeyClient.updateOrInsertE2eeSelfKey(
            deviceId,
            accountId,
            publicKey
          )
        )
        // Self key published successfully.
    } catch (e: Exception) {
        // Handle error.
    }
}

공개 키를 연락처와 연결

앱이 사용자 연락처 중 하나의 공개 키를 수신하면 중앙 저장소에 저장하고 해당 연락처와 연결해야 합니다. 이렇게 하면 키를 인증할 수 있고 다른 앱에서 연락처의 인증 상태를 표시할 수 있습니다. 이렇게 하려면 Android 연락처 제공자의 연락처 lookupKey가 필요합니다. 이는 일반적으로 키 배포 서버에서 키를 가져오거나 로컬 키를 주기적으로 동기화하는 중에 트리거됩니다.

import com.google.android.gms.contactkeys.ContactKeyClient
import com.google.android.gms.tasks.Tasks

suspend fun storeContactKey(
    contactKeyClient: ContactKeyClient,
    contactLookupKey: String,
    contactAccountId: String,
    contactDeviceId: String,
    contactPublicKey: ByteArray
) {
    try {
        Tasks.await(
            contactKeyClient.updateOrInsertE2eeContactKey(
                contactLookupKey,
                contactDeviceId,
                contactAccountId,
                contactPublicKey
            )
        )
        // Contact's key stored successfully.
    } catch (e: Exception) {
        // Handle error.
    }
}

키 및 인증 상태 가져오기

키를 게시한 후 사용자는 오프라인 QR 코드 스캔을 통해 키를 확인할 수 있습니다. 앱의 UI는 대화에서 인증된 키를 사용하는지 여부를 반영해야 합니다. 각 키에는 UI에 알리는 데 사용할 수 있는 인증 상태가 있습니다.

인증 상태 이해하기:

  • UNVERIFIED: 모든 새 키의 기본 상태입니다. 키가 존재하지만 사용자가 아직 진위 여부를 확인하지 않았음을 의미합니다. UI에서 이를 중립 상태로 취급해야 하며 일반적으로 특수 표시기를 표시하지 않습니다.

  • VERIFIED: 이 상태는 높은 수준의 신뢰를 나타냅니다. 이는 사용자가 인증 흐름 (예: QR 코드 스캔)을 성공적으로 완료하고 키가 의도한 연락처에 속하는지 확인했음을 의미합니다. UI에 녹색 체크표시나 방패와 같은 명확하고 긍정적인 표시기를 표시해야 합니다.

  • VERIFICATION_FAILED: 경고 상태입니다. 연락처와 연결된 키가 이전에 인증된 키와 일치하지 않음을 의미합니다. 연락처가 새 기기를 사용하는 경우에도 이 문제가 발생할 수 있지만 잠재적인 보안 위험을 나타낼 수도 있습니다. UI에서 눈에 띄는 경고로 사용자에게 알리고 민감한 정보를 보내기 전에 다시 인증하도록 제안합니다.

연락처와 연결된 모든 키의 집계 상태를 가져올 수 있습니다. 여러 키가 있는 경우 VerificationState.leastVerifiedFrom()를 사용하여 상태를 확인하는 것이 좋습니다. VERIFICATION_FAILEDVERIFIED보다 올바른 우선순위가 부여되기 때문입니다.

  • 연락처 수준에서 집계 상태 가져오기
import com.google.android.gms.contactkeys.ContactKeyClient
import com.google.android.gms.contactkeys.constants.VerificationState
import com.google.android.gms.tasks.Tasks

suspend fun displayContactVerificationStatus(
    contactKeyClient: ContactKeyClient,
    contactLookupKey: String
) {
    try {
        val keysResult = Tasks.await(contactKeyClient.getAllE2eeContactKeys(contactLookupKey))
        val states =
          keysResult.keys.map { VerificationState.fromState(it.localVerificationState) }
        val contactStatus = VerificationState.leastVerifiedFrom(states)
        updateUi(contactLookupKey, contactStatus)
    } catch (e: Exception) {
        // Handle error.
    }
}
  • 계정 수준에서 집계 상태 가져오기
import com.google.android.gms.contactkeys.ContactKeyClient
import com.google.android.gms.contactkeys.constants.VerificationState
import com.google.android.gms.tasks.Tasks

suspend fun displayAccountVerificationStatus(
    contactKeyClient: ContactKeyClient,
    accountId: String
) {
    try {
        val keys = Tasks.await(contactKeyClient.getE2eeAccountKeysForAccount(accountId))
        val states = keys.map { VerificationState.fromState(it.localVerificationState) }
        val accountStatus = VerificationState.leastVerifiedFrom(states)
        updateUi(accountId, accountStatus)
    } catch (e: Exception) {
        // Handle error.
    }
}

주요 변경사항을 실시간으로 확인

앱의 UI가 항상 올바른 신뢰 상태를 표시하는지 확인하려면 업데이트를 수신해야 합니다. 구독 계정의 키가 추가, 삭제되거나 인증 상태가 변경될 때마다 새 키 목록을 내보내는 흐름 기반 API를 사용하는 것이 좋습니다. 이 기능은 특히 그룹 대화의 회원 목록을 최신 상태로 유지하는 데 유용합니다. 다음과 같은 경우 키의 인증 상태가 변경될 수 있습니다.

  • 사용자가 인증 과정을 완료합니다 (예: QR 코드 스캔).
  • 연락처의 키가 수정되어 이전에 인증된 값과 더 이상 일치하지 않습니다.
fun observeKeyUpdates(contactKeyClient: ContactKeyClient, accountIds: List<String>) {
    lifecycleScope.launch {
        contactKeyClient.getAccountContactKeysFlow(accountIds)
            .collect { updatedKeys ->
                // A key was added, removed, or updated.
                // Refresh your app's UI and internal state.
                refreshUi(updatedKeys)
            }
    }
}

대면 키 인증 실행

사용자가 키를 인증하는 가장 안전한 방법은 오프라인 인증입니다. 오프라인 인증은 보통 QR 코드를 스캔하거나 숫자 시퀀스를 비교하는 방식으로 이루어집니다. 키 검증 도구 앱은 이 프로세스를 위한 표준 UI 흐름을 제공하며 앱에서 이를 실행할 수 있습니다. 인증 시도 후 API는 키의 인증 상태를 자동으로 업데이트하며, 키 업데이트를 관찰하는 경우 앱에 알림이 전송됩니다.

  • 사용자가 선택한 연락처의 키 인증 프로세스 시작 선택한 연락처의 lookupKey를 사용하여 getScanQrCodeIntent에서 제공한 PendingIntent를 실행합니다. UI를 통해 사용자는 지정된 연락처의 모든 키를 확인할 수 있습니다.
import android.app.ActivityOptions
import android.app.PendingIntent
import com.google.android.gms.contactkeys.ContactKeyClient
import com.google.android.gms.tasks.Tasks

suspend fun initiateVerification(contactKeyClient: ContactKeyClient, lookupKey: String) {
    try {
        val pendingIntent = Tasks.await(contactKeyClient.getScanQrCodeIntent(lookupKey))
        val options =
          ActivityOptions.makeBasic()
            .setPendingIntentBackgroundActivityStartMode(
              ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
            )
            .toBundle()
        pendingIntent.send(options)
    } catch (e: Exception) {
        // Handle error.
    }
}
  • 사용자가 선택한 계정의 키 확인 프로세스 시작 사용자가 연락처에 직접 연결되지 않은 계정(또는 연락처의 특정 계정)을 확인하려는 경우 getScanQrCodeIntentForAccount에서 제공하는 PendingIntent를 실행하면 됩니다. 일반적으로 자체 앱의 패키지 이름과 계정 ID에 사용됩니다.
import android.app.ActivityOptions
import android.app.PendingIntent
import com.google.android.gms.contactkeys.ContactKeyClient
import com.google.android.gms.tasks.Tasks

suspend fun initiateVerification(contactKeyClient: ContactKeyClient, packageName: String, accountId: String) {
    try {
        val pendingIntent = Tasks.await(contactKeyClient.getScanQrCodeIntentForAccount(packageName, accountId))
        // Allow activity start from background on Android SDK34+
        val options =
          ActivityOptions.makeBasic()
            .setPendingIntentBackgroundActivityStartMode(
              ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
            )
            .toBundle()
        pendingIntent.send(options)
    } catch (e: Exception) {
        // Handle error.
    }
}