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.CreatePublicKeyCredentialRequest12import androidx.credentials.CreatePublicKeyCredentialResponse13import androidx.credentials.PublicKeyCredential14import androidx.credentials.exceptions.CreateCredentialUnsupportedException15import androidx.credentials.exceptions.GetCredentialUnsupportedException16import androidx.credentials.provider.PendingIntentHandler17import com.peachpasswords.shared.extractDomain18import androidx.lifecycle.lifecycleScope19import com.peachpasswords.vault.VaultRepository20import com.peachpasswords.vault.isLogin21import dagger.hilt.android.AndroidEntryPoint22import javax.inject.Inject23import kotlinx.coroutines.launch2425@AndroidEntryPoint26class PasskeyCredentialSelectionActivity : ComponentActivity() {27 @Inject lateinit var vault: VaultRepository2829 override fun onCreate(savedInstanceState: Bundle?) {30 super.onCreate(savedInstanceState)31 lifecycleScope.launch { completeSelection() }32 }3334 private suspend fun completeSelection() {35 if (intent.getStringExtra(EXTRA_MODE) == MODE_CREATE) {36 completeCreate()37 return38 }39 val result = Intent()40 val entryId = intent.getStringExtra(EXTRA_ENTRY_ID)41 val providerRequest = runCatching {42 PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)43 }.getOrNull()44 val option = providerRequest?.credentialOptions45 ?.filterIsInstance<GetPublicKeyCredentialOption>()46 ?.firstOrNull()47 val requestJson = option?.requestJson ?: intent.getStringExtra(EXTRA_REQUEST_JSON)48 val entry = entryId?.let(vault::getEntry)49 val origin = providerRequest?.callingAppInfo?.let { info ->50 runCatching { info.getOrigin(info.packageName) }.getOrNull()51 }52 val assertion = if (entry != null && requestJson != null) {53 PasskeyAssertion.build(54 entry = entry,55 requestJson = requestJson,56 origin = origin,57 clientDataHashOverride = option?.clientDataHash,58 )59 } else {60 null61 }6263 if (entry != null && assertion != null) {64 val passkey = entry.passkey65 val persisted = if (passkey != null) {66 val now = System.currentTimeMillis()67 runCatching {68 vault.updateEntry(69 entry.copy(70 passkey = passkey.copy(signCount = assertion.nextSignCount),71 signCount = assertion.nextSignCount,72 lastUsedAt = now,73 ),74 )75 }.isSuccess76 } else {77 false78 }79 if (persisted) {80 PendingIntentHandler.setGetCredentialResponse(81 result,82 GetCredentialResponse(PublicKeyCredential(assertion.responseJson)),83 )84 } else {85 PendingIntentHandler.setGetCredentialException(86 result,87 GetCredentialUnsupportedException("Peach could not update this passkey."),88 )89 }90 } else {91 PendingIntentHandler.setGetCredentialException(92 result,93 GetCredentialUnsupportedException("Peach cannot sign this passkey yet."),94 )95 }96 setResult(RESULT_OK, result)97 finish()98 }99100 private suspend fun completeCreate() {101 val result = Intent()102 val providerRequest = runCatching {103 PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)104 }.getOrNull()105 val request = providerRequest?.callingRequest as? CreatePublicKeyCredentialRequest106 val requestJson = request?.requestJson ?: intent.getStringExtra(EXTRA_REQUEST_JSON)107 val origin = request?.origin ?: providerRequest?.callingAppInfo?.let { info ->108 runCatching { info.getOrigin(info.packageName) }.getOrNull()109 }110 val registration = requestJson?.let { PasskeyRegistration.build(it, origin) }111 val stored = registration?.let { storeRegistration(it.entryDraft) } ?: false112 if (registration != null && stored) {113 PendingIntentHandler.setCreateCredentialResponse(114 result,115 CreatePublicKeyCredentialResponse(registration.responseJson),116 )117 } else {118 PendingIntentHandler.setCreateCredentialException(119 result,120 CreateCredentialUnsupportedException("Peach cannot create this passkey yet."),121 )122 }123 setResult(RESULT_OK, result)124 finish()125 }126127 private suspend fun storeRegistration(draft: com.peachpasswords.vault.VaultEntry): Boolean {128 val rpId = draft.passkey?.rpId ?: return false129 val target = vault.getEntries().firstOrNull { entry ->130 entry.deletedAt == null &&131 entry.passkey == null &&132 entry.isLogin() &&133 listOfNotNull(entry.url, *entry.additionalUrls.orEmpty().toTypedArray())134 .any { extractDomain(it) == rpId }135 }136 return runCatching {137 if (target != null) {138 vault.updateEntry(139 target.copy(140 passkey = draft.passkey,141 username = target.username ?: draft.username,142 ),143 )144 } else {145 vault.addEntry(draft)146 }147 }.isSuccess148 }149150 companion object {151 private const val EXTRA_ENTRY_ID = "com.peachpasswords.passkeys.EXTRA_ENTRY_ID"152 private const val EXTRA_REQUEST_JSON = "com.peachpasswords.passkeys.EXTRA_REQUEST_JSON"153 private const val EXTRA_MODE = "com.peachpasswords.passkeys.EXTRA_MODE"154 private const val MODE_CREATE = "create"155156 fun pendingIntent(context: Context, entryId: String, requestJson: String): PendingIntent {157 val intent = Intent(context, PasskeyCredentialSelectionActivity::class.java)158 .putExtra(EXTRA_ENTRY_ID, entryId)159 .putExtra(EXTRA_REQUEST_JSON, requestJson)160 val mutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0161 return PendingIntent.getActivity(162 context,163 "$entryId:$requestJson".hashCode(),164 intent,165 PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag,166 )167 }168169 fun createPendingIntent(context: Context, requestJson: String): PendingIntent {170 val intent = Intent(context, PasskeyCredentialSelectionActivity::class.java)171 .putExtra(EXTRA_MODE, MODE_CREATE)172 .putExtra(EXTRA_REQUEST_JSON, requestJson)173 val mutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0174 return PendingIntent.getActivity(175 context,176 requestJson.hashCode(),177 intent,178 PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag,179 )180 }181 }182}