1package com.peachpasswords.passkeys23import android.app.PendingIntent4import android.content.Context5import android.content.Intent6import android.os.Build7import android.os.Bundle8import androidx.activity.ComponentActivity9import androidx.credentials.GetCredentialResponse10import androidx.credentials.GetPublicKeyCredentialOption11import androidx.credentials.PublicKeyCredential12import androidx.credentials.exceptions.GetCredentialUnsupportedException13import androidx.credentials.provider.PendingIntentHandler14import androidx.lifecycle.lifecycleScope15import com.peachpasswords.vault.VaultRepository16import dagger.hilt.android.AndroidEntryPoint17import javax.inject.Inject18import kotlinx.coroutines.launch1920@AndroidEntryPoint21class PasskeyCredentialSelectionActivity : ComponentActivity() {22 @Inject lateinit var vault: VaultRepository2324 override fun onCreate(savedInstanceState: Bundle?) {25 super.onCreate(savedInstanceState)26 lifecycleScope.launch { completeSelection() }27 }2829 private suspend fun completeSelection() {30 val result = Intent()31 val entryId = intent.getStringExtra(EXTRA_ENTRY_ID)32 val providerRequest = runCatching {33 PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)34 }.getOrNull()35 val option = providerRequest?.credentialOptions36 ?.filterIsInstance<GetPublicKeyCredentialOption>()37 ?.firstOrNull()38 val requestJson = option?.requestJson ?: intent.getStringExtra(EXTRA_REQUEST_JSON)39 val entry = entryId?.let(vault::getEntry)40 val origin = providerRequest?.callingAppInfo?.let { info ->41 runCatching { info.getOrigin(info.packageName) }.getOrNull()42 }43 val assertion = if (entry != null && requestJson != null) {44 PasskeyAssertion.build(45 entry = entry,46 requestJson = requestJson,47 origin = origin,48 clientDataHashOverride = option?.clientDataHash,49 )50 } else {51 null52 }5354 if (entry != null && assertion != null) {55 val passkey = entry.passkey56 val persisted = if (passkey != null) {57 val now = System.currentTimeMillis()58 runCatching {59 vault.updateEntry(60 entry.copy(61 passkey = passkey.copy(signCount = assertion.nextSignCount),62 signCount = assertion.nextSignCount,63 lastUsedAt = now,64 ),65 )66 }.isSuccess67 } else {68 false69 }70 if (persisted) {71 PendingIntentHandler.setGetCredentialResponse(72 result,73 GetCredentialResponse(PublicKeyCredential(assertion.responseJson)),74 )75 } else {76 PendingIntentHandler.setGetCredentialException(77 result,78 GetCredentialUnsupportedException("Peach could not update this passkey."),79 )80 }81 } else {82 PendingIntentHandler.setGetCredentialException(83 result,84 GetCredentialUnsupportedException("Peach cannot sign this passkey yet."),85 )86 }87 setResult(RESULT_OK, result)88 finish()89 }9091 companion object {92 private const val EXTRA_ENTRY_ID = "com.peachpasswords.passkeys.EXTRA_ENTRY_ID"93 private const val EXTRA_REQUEST_JSON = "com.peachpasswords.passkeys.EXTRA_REQUEST_JSON"9495 fun pendingIntent(context: Context, entryId: String, requestJson: String): PendingIntent {96 val intent = Intent(context, PasskeyCredentialSelectionActivity::class.java)97 .putExtra(EXTRA_ENTRY_ID, entryId)98 .putExtra(EXTRA_REQUEST_JSON, requestJson)99 val mutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0100 return PendingIntent.getActivity(101 context,102 "$entryId:$requestJson".hashCode(),103 intent,104 PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag,105 )106 }107 }108}