1package com.peachpasswords.passkeys23import android.app.PendingIntent4import android.content.Context5import android.content.Intent6import android.os.Build7import android.os.Bundle8import android.view.WindowManager9import androidx.activity.ComponentActivity10import androidx.credentials.GetCredentialResponse11import androidx.credentials.GetPublicKeyCredentialOption12import androidx.credentials.CreatePublicKeyCredentialRequest13import androidx.credentials.CreatePublicKeyCredentialResponse14import androidx.credentials.PublicKeyCredential15import androidx.credentials.exceptions.CreateCredentialUnsupportedException16import androidx.credentials.exceptions.GetCredentialUnsupportedException17import androidx.credentials.provider.PendingIntentHandler18import com.peachpasswords.BuildConfig19import com.peachpasswords.shared.extractDomain20import androidx.lifecycle.lifecycleScope21import com.peachpasswords.vault.VaultRepository22import com.peachpasswords.vault.isLogin23import dagger.hilt.android.AndroidEntryPoint24import javax.inject.Inject25import kotlinx.coroutines.launch2627@AndroidEntryPoint28class PasskeyCredentialSelectionActivity : ComponentActivity() {29 @Inject lateinit var vault: VaultRepository3031 override fun onCreate(savedInstanceState: Bundle?) {32 super.onCreate(savedInstanceState)33 34 35 if (!BuildConfig.DEBUG) {36 window.setFlags(37 WindowManager.LayoutParams.FLAG_SECURE,38 WindowManager.LayoutParams.FLAG_SECURE,39 )40 }41 lifecycleScope.launch { completeSelection() }42 }4344 private suspend fun completeSelection() {45 if (intent.getStringExtra(EXTRA_MODE) == MODE_CREATE) {46 completeCreate()47 return48 }49 val result = Intent()50 val entryId = intent.getStringExtra(EXTRA_ENTRY_ID)51 val providerRequest = runCatching {52 PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)53 }.getOrNull()54 val option = providerRequest?.credentialOptions55 ?.filterIsInstance<GetPublicKeyCredentialOption>()56 ?.firstOrNull()57 val requestJson = option?.requestJson ?: intent.getStringExtra(EXTRA_REQUEST_JSON)58 val entry = entryId?.let(vault::getEntry)59 val origin = providerRequest?.callingAppInfo?.let { info ->60 runCatching { info.getOrigin(info.packageName) }.getOrNull()61 }62 val assertion = if (entry != null && requestJson != null) {63 PasskeyAssertion.build(64 entry = entry,65 requestJson = requestJson,66 origin = origin,67 clientDataHashOverride = option?.clientDataHash,68 )69 } else {70 null71 }7273 if (entry != null && assertion != null) {74 val passkey = entry.passkey75 val persisted = if (passkey != null) {76 val now = System.currentTimeMillis()77 runCatching {78 vault.updateEntry(79 entry.copy(80 passkey = passkey.copy(signCount = assertion.nextSignCount),81 signCount = assertion.nextSignCount,82 lastUsedAt = now,83 ),84 )85 }.isSuccess86 } else {87 false88 }89 if (persisted) {90 PendingIntentHandler.setGetCredentialResponse(91 result,92 GetCredentialResponse(PublicKeyCredential(assertion.responseJson)),93 )94 } else {95 PendingIntentHandler.setGetCredentialException(96 result,97 GetCredentialUnsupportedException("Peach could not update this passkey."),98 )99 }100 } else {101 PendingIntentHandler.setGetCredentialException(102 result,103 GetCredentialUnsupportedException("Peach cannot sign this passkey yet."),104 )105 }106 setResult(RESULT_OK, result)107 finish()108 }109110 private suspend fun completeCreate() {111 val result = Intent()112 val providerRequest = runCatching {113 PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)114 }.getOrNull()115 val request = providerRequest?.callingRequest as? CreatePublicKeyCredentialRequest116 val requestJson = request?.requestJson ?: intent.getStringExtra(EXTRA_REQUEST_JSON)117 val origin = request?.origin ?: providerRequest?.callingAppInfo?.let { info ->118 runCatching { info.getOrigin(info.packageName) }.getOrNull()119 }120 val registration = requestJson?.let { PasskeyRegistration.build(it, origin) }121 val stored = registration?.let { storeRegistration(it.entryDraft) } ?: false122 if (registration != null && stored) {123 PendingIntentHandler.setCreateCredentialResponse(124 result,125 CreatePublicKeyCredentialResponse(registration.responseJson),126 )127 } else {128 PendingIntentHandler.setCreateCredentialException(129 result,130 CreateCredentialUnsupportedException("Peach cannot create this passkey yet."),131 )132 }133 setResult(RESULT_OK, result)134 finish()135 }136137 private suspend fun storeRegistration(draft: com.peachpasswords.vault.VaultEntry): Boolean {138 val rpId = draft.passkey?.rpId ?: return false139 val target = vault.getEntries().firstOrNull { entry ->140 entry.deletedAt == null &&141 entry.passkey == null &&142 entry.isLogin() &&143 listOfNotNull(entry.url, *entry.additionalUrls.orEmpty().toTypedArray())144 .any { extractDomain(it) == rpId }145 }146 return runCatching {147 if (target != null) {148 vault.updateEntry(149 target.copy(150 passkey = draft.passkey,151 username = target.username ?: draft.username,152 ),153 )154 } else {155 vault.addEntry(draft)156 }157 }.isSuccess158 }159160 companion object {161 private const val EXTRA_ENTRY_ID = "com.peachpasswords.passkeys.EXTRA_ENTRY_ID"162 private const val EXTRA_REQUEST_JSON = "com.peachpasswords.passkeys.EXTRA_REQUEST_JSON"163 private const val EXTRA_MODE = "com.peachpasswords.passkeys.EXTRA_MODE"164 private const val MODE_CREATE = "create"165166 fun pendingIntent(context: Context, entryId: String, requestJson: String): PendingIntent {167 val intent = Intent(context, PasskeyCredentialSelectionActivity::class.java)168 .putExtra(EXTRA_ENTRY_ID, entryId)169 .putExtra(EXTRA_REQUEST_JSON, requestJson)170 val mutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0171 return PendingIntent.getActivity(172 context,173 "$entryId:$requestJson".hashCode(),174 intent,175 PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag,176 )177 }178179 fun createPendingIntent(context: Context, requestJson: String): PendingIntent {180 val intent = Intent(context, PasskeyCredentialSelectionActivity::class.java)181 .putExtra(EXTRA_MODE, MODE_CREATE)182 .putExtra(EXTRA_REQUEST_JSON, requestJson)183 val mutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0184 return PendingIntent.getActivity(185 context,186 requestJson.hashCode(),187 intent,188 PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag,189 )190 }191 }192}