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 android.view.WindowManager import androidx.activity.ComponentActivity import androidx.credentials.GetCredentialResponse import androidx.credentials.GetPublicKeyCredentialOption import androidx.credentials.CreatePublicKeyCredentialRequest import androidx.credentials.CreatePublicKeyCredentialResponse import androidx.credentials.PublicKeyCredential import androidx.credentials.exceptions.CreateCredentialUnsupportedException import androidx.credentials.exceptions.GetCredentialUnsupportedException import androidx.credentials.provider.PendingIntentHandler import com.peachpasswords.BuildConfig import com.peachpasswords.shared.extractDomain import androidx.lifecycle.lifecycleScope import com.peachpasswords.vault.VaultRepository import com.peachpasswords.vault.isLogin 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) // H-05: separate window from MainActivity — sign assertion and registration // operate on passkey material. Block screenshots / recents in release. if (!BuildConfig.DEBUG) { window.setFlags( WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE, ) } lifecycleScope.launch { completeSelection() } } private suspend fun completeSelection() { if (intent.getStringExtra(EXTRA_MODE) == MODE_CREATE) { completeCreate() return } 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() } private suspend fun completeCreate() { val result = Intent() val providerRequest = runCatching { PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent) }.getOrNull() val request = providerRequest?.callingRequest as? CreatePublicKeyCredentialRequest val requestJson = request?.requestJson ?: intent.getStringExtra(EXTRA_REQUEST_JSON) val origin = request?.origin ?: providerRequest?.callingAppInfo?.let { info -> runCatching { info.getOrigin(info.packageName) }.getOrNull() } val registration = requestJson?.let { PasskeyRegistration.build(it, origin) } val stored = registration?.let { storeRegistration(it.entryDraft) } ?: false if (registration != null && stored) { PendingIntentHandler.setCreateCredentialResponse( result, CreatePublicKeyCredentialResponse(registration.responseJson), ) } else { PendingIntentHandler.setCreateCredentialException( result, CreateCredentialUnsupportedException("Peach cannot create this passkey yet."), ) } setResult(RESULT_OK, result) finish() } private suspend fun storeRegistration(draft: com.peachpasswords.vault.VaultEntry): Boolean { val rpId = draft.passkey?.rpId ?: return false val target = vault.getEntries().firstOrNull { entry -> entry.deletedAt == null && entry.passkey == null && entry.isLogin() && listOfNotNull(entry.url, *entry.additionalUrls.orEmpty().toTypedArray()) .any { extractDomain(it) == rpId } } return runCatching { if (target != null) { vault.updateEntry( target.copy( passkey = draft.passkey, username = target.username ?: draft.username, ), ) } else { vault.addEntry(draft) } }.isSuccess } 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" private const val EXTRA_MODE = "com.peachpasswords.passkeys.EXTRA_MODE" private const val MODE_CREATE = "create" 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, ) } fun createPendingIntent(context: Context, requestJson: String): PendingIntent { val intent = Intent(context, PasskeyCredentialSelectionActivity::class.java) .putExtra(EXTRA_MODE, MODE_CREATE) .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, requestJson.hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag, ) } } }