1package com.peachpasswords.passkeys23import android.app.PendingIntent4import android.content.Context5import android.content.Intent6import android.os.Build7import android.os.Bundle8import android.view.WindowManager9import androidx.credentials.GetCredentialResponse10import androidx.credentials.GetPublicKeyCredentialOption11import androidx.credentials.CreatePublicKeyCredentialRequest12import androidx.credentials.CreatePublicKeyCredentialResponse13import androidx.credentials.PublicKeyCredential14import androidx.credentials.exceptions.CreateCredentialUnsupportedException15import androidx.credentials.exceptions.GetCredentialUnsupportedException16import androidx.credentials.provider.PendingIntentHandler17import androidx.fragment.app.FragmentActivity18import androidx.lifecycle.lifecycleScope19import com.peachpasswords.BuildConfig20import com.peachpasswords.shared.extractDomain21import com.peachpasswords.ui.lock.Biometrics22import com.peachpasswords.vault.VaultRepository23import com.peachpasswords.vault.isLogin24import dagger.hilt.android.AndroidEntryPoint25import javax.inject.Inject26import kotlinx.coroutines.launch27import kotlinx.coroutines.suspendCancellableCoroutine28import kotlinx.serialization.json.Json29import kotlinx.serialization.json.contentOrNull30import kotlinx.serialization.json.jsonObject31import kotlinx.serialization.json.jsonPrimitive32import kotlin.coroutines.resume3334@AndroidEntryPoint35class PasskeyCredentialSelectionActivity : FragmentActivity() {36 @Inject lateinit var vault: VaultRepository3738 override fun onCreate(savedInstanceState: Bundle?) {39 super.onCreate(savedInstanceState)40 41 42 if (!BuildConfig.DEBUG) {43 window.setFlags(44 WindowManager.LayoutParams.FLAG_SECURE,45 WindowManager.LayoutParams.FLAG_SECURE,46 )47 }48 lifecycleScope.launch { completeSelection() }49 }5051 private suspend fun completeSelection() {52 if (intent.getStringExtra(EXTRA_MODE) == MODE_CREATE) {53 completeCreate()54 return55 }56 val result = Intent()57 val entryId = intent.getStringExtra(EXTRA_ENTRY_ID)58 val providerRequest = runCatching {59 PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)60 }.getOrNull()61 val option = providerRequest?.credentialOptions62 ?.filterIsInstance<GetPublicKeyCredentialOption>()63 ?.firstOrNull()64 val requestJson = option?.requestJson ?: intent.getStringExtra(EXTRA_REQUEST_JSON)65 val entry = entryId?.let(vault::getEntry)66 val origin = providerRequest?.callingAppInfo?.let { info ->67 runCatching { info.getOrigin(info.packageName) }.getOrNull()68 }69 70 71 72 val userVerified = if (entry != null && requestJson != null &&73 userVerificationRequirement(requestJson) == UV_REQUIRED74 ) {75 runUserVerificationPrompt()76 } else {77 false78 }79 val assertion = if (entry != null && requestJson != null) {80 PasskeyAssertion.build(81 entry = entry,82 requestJson = requestJson,83 origin = origin,84 clientDataHashOverride = option?.clientDataHash,85 userVerified = userVerified,86 )87 } else {88 null89 }9091 if (entry != null && assertion != null) {92 val passkey = entry.passkey93 val persisted = if (passkey != null) {94 val now = System.currentTimeMillis()95 runCatching {96 vault.updateEntry(97 entry.copy(98 passkey = passkey.copy(signCount = assertion.nextSignCount),99 signCount = assertion.nextSignCount,100 lastUsedAt = now,101 ),102 )103 }.isSuccess104 } else {105 false106 }107 if (persisted) {108 PendingIntentHandler.setGetCredentialResponse(109 result,110 GetCredentialResponse(PublicKeyCredential(assertion.responseJson)),111 )112 } else {113 PendingIntentHandler.setGetCredentialException(114 result,115 GetCredentialUnsupportedException("Peach could not update this passkey."),116 )117 }118 } else {119 PendingIntentHandler.setGetCredentialException(120 result,121 GetCredentialUnsupportedException("Peach cannot sign this passkey yet."),122 )123 }124 setResult(RESULT_OK, result)125 finish()126 }127128 private suspend fun completeCreate() {129 val result = Intent()130 val providerRequest = runCatching {131 PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)132 }.getOrNull()133 val request = providerRequest?.callingRequest as? CreatePublicKeyCredentialRequest134 val requestJson = request?.requestJson ?: intent.getStringExtra(EXTRA_REQUEST_JSON)135 val origin = request?.origin ?: providerRequest?.callingAppInfo?.let { info ->136 runCatching { info.getOrigin(info.packageName) }.getOrNull()137 }138 val registration = requestJson?.let { PasskeyRegistration.build(it, origin) }139 val stored = registration?.let { storeRegistration(it.entryDraft) } ?: false140 if (registration != null && stored) {141 PendingIntentHandler.setCreateCredentialResponse(142 result,143 CreatePublicKeyCredentialResponse(registration.responseJson),144 )145 } else {146 PendingIntentHandler.setCreateCredentialException(147 result,148 CreateCredentialUnsupportedException("Peach cannot create this passkey yet."),149 )150 }151 setResult(RESULT_OK, result)152 finish()153 }154155 private fun userVerificationRequirement(requestJson: String): String? =156 runCatching {157 Json.parseToJsonElement(requestJson).jsonObject["userVerification"]158 ?.jsonPrimitive?.contentOrNull159 }.getOrNull()160161 private suspend fun runUserVerificationPrompt(): Boolean {162 if (!Biometrics.isAvailable(this)) return false163 return suspendCancellableCoroutine { cont ->164 Biometrics.authenticate(165 activity = this,166 title = "Verify it's you",167 subtitle = "Confirm to use this passkey",168 onSuccess = { if (cont.isActive) cont.resume(true) },169 onFail = { if (cont.isActive) cont.resume(false) },170 )171 }172 }173174 private suspend fun storeRegistration(draft: com.peachpasswords.vault.VaultEntry): Boolean {175 val rpId = draft.passkey?.rpId ?: return false176 val target = vault.getEntries().firstOrNull { entry ->177 entry.deletedAt == null &&178 entry.passkey == null &&179 entry.isLogin() &&180 listOfNotNull(entry.url, *entry.additionalUrls.orEmpty().toTypedArray())181 .any { extractDomain(it) == rpId }182 }183 return runCatching {184 if (target != null) {185 vault.updateEntry(186 target.copy(187 passkey = draft.passkey,188 username = target.username ?: draft.username,189 ),190 )191 } else {192 vault.addEntry(draft)193 }194 }.isSuccess195 }196197 companion object {198 private const val EXTRA_ENTRY_ID = "com.peachpasswords.passkeys.EXTRA_ENTRY_ID"199 private const val EXTRA_REQUEST_JSON = "com.peachpasswords.passkeys.EXTRA_REQUEST_JSON"200 private const val EXTRA_MODE = "com.peachpasswords.passkeys.EXTRA_MODE"201 private const val MODE_CREATE = "create"202 203 204 205 private const val UV_REQUIRED = "required"206207 fun pendingIntent(context: Context, entryId: String, requestJson: String): PendingIntent {208 val intent = Intent(context, PasskeyCredentialSelectionActivity::class.java)209 .putExtra(EXTRA_ENTRY_ID, entryId)210 .putExtra(EXTRA_REQUEST_JSON, requestJson)211 val mutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0212 return PendingIntent.getActivity(213 context,214 "$entryId:$requestJson".hashCode(),215 intent,216 PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag,217 )218 }219220 fun createPendingIntent(context: Context, requestJson: String): PendingIntent {221 val intent = Intent(context, PasskeyCredentialSelectionActivity::class.java)222 .putExtra(EXTRA_MODE, MODE_CREATE)223 .putExtra(EXTRA_REQUEST_JSON, requestJson)224 val mutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0225 return PendingIntent.getActivity(226 context,227 requestJson.hashCode(),228 intent,229 PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag,230 )231 }232 }233}