package com.peachpasswords.passkeys import android.app.PendingIntent import android.content.Context import android.content.Intent import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity import androidx.credentials.GetCredentialResponse import androidx.credentials.GetPublicKeyCredentialOption import androidx.credentials.PublicKeyCredential import androidx.credentials.exceptions.GetCredentialUnsupportedException import androidx.credentials.provider.PendingIntentHandler import androidx.lifecycle.lifecycleScope import com.peachpasswords.vault.VaultRepository import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import kotlinx.coroutines.launch @AndroidEntryPoint class PasskeyCredentialSelectionActivity : ComponentActivity() { @Inject lateinit var vault: VaultRepository override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycleScope.launch { completeSelection() } } private suspend fun completeSelection() { val result = Intent() val entryId = intent.getStringExtra(EXTRA_ENTRY_ID) val providerRequest = runCatching { PendingIntentHandler.retrieveProviderGetCredentialRequest(intent) }.getOrNull() val option = providerRequest?.credentialOptions ?.filterIsInstance() ?.firstOrNull() val requestJson = option?.requestJson ?: intent.getStringExtra(EXTRA_REQUEST_JSON) val entry = entryId?.let(vault::getEntry) val origin = providerRequest?.callingAppInfo?.let { info -> runCatching { info.getOrigin(info.packageName) }.getOrNull() } val assertion = if (entry != null && requestJson != null) { PasskeyAssertion.build( entry = entry, requestJson = requestJson, origin = origin, clientDataHashOverride = option?.clientDataHash, ) } else { null } if (entry != null && assertion != null) { val passkey = entry.passkey val persisted = if (passkey != null) { val now = System.currentTimeMillis() runCatching { vault.updateEntry( entry.copy( passkey = passkey.copy(signCount = assertion.nextSignCount), signCount = assertion.nextSignCount, lastUsedAt = now, ), ) }.isSuccess } else { false } if (persisted) { PendingIntentHandler.setGetCredentialResponse( result, GetCredentialResponse(PublicKeyCredential(assertion.responseJson)), ) } else { PendingIntentHandler.setGetCredentialException( result, GetCredentialUnsupportedException("Peach could not update this passkey."), ) } } else { PendingIntentHandler.setGetCredentialException( result, GetCredentialUnsupportedException("Peach cannot sign this passkey yet."), ) } setResult(RESULT_OK, result) finish() } companion object { private const val EXTRA_ENTRY_ID = "com.peachpasswords.passkeys.EXTRA_ENTRY_ID" private const val EXTRA_REQUEST_JSON = "com.peachpasswords.passkeys.EXTRA_REQUEST_JSON" fun pendingIntent(context: Context, entryId: String, requestJson: String): PendingIntent { val intent = Intent(context, PasskeyCredentialSelectionActivity::class.java) .putExtra(EXTRA_ENTRY_ID, entryId) .putExtra(EXTRA_REQUEST_JSON, requestJson) val mutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0 return PendingIntent.getActivity( context, "$entryId:$requestJson".hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag, ) } } }