1package com.peachpasswords.credentials23import android.app.PendingIntent4import android.content.Context5import android.content.Intent6import android.os.Build7import android.os.Bundle8import androidx.credentials.CreatePasswordResponse9import androidx.credentials.GetCredentialResponse10import androidx.credentials.PasswordCredential11import androidx.credentials.exceptions.CreateCredentialUnsupportedException12import androidx.credentials.exceptions.GetCredentialUnsupportedException13import androidx.credentials.provider.CallingAppInfo14import androidx.credentials.provider.PendingIntentHandler15import androidx.fragment.app.FragmentActivity16import androidx.lifecycle.lifecycleScope17import com.peachpasswords.BuildConfig18import com.peachpasswords.autofill.AutofillSaveDraft19import com.peachpasswords.autofill.AutofillSavePlan20import com.peachpasswords.autofill.appLinkDomains21import com.peachpasswords.autofill.planAutofillSave22import com.peachpasswords.passkeys.PasskeyCallerOrigin23import com.peachpasswords.shared.extractDomain24import com.peachpasswords.ui.components.QaFlags25import com.peachpasswords.ui.components.SecureWindowPolicy26import com.peachpasswords.vault.VaultRepository27import dagger.hilt.android.AndroidEntryPoint28import javax.inject.Inject29import kotlinx.coroutines.launch3031323334353637383940414243@AndroidEntryPoint44class PasswordCredentialSelectionActivity : FragmentActivity() {45 @Inject lateinit var vault: VaultRepository46 private var debugSecureLease = false4748 override fun onCreate(savedInstanceState: Bundle?) {49 super.onCreate(savedInstanceState)50 51 52 if (!BuildConfig.DEBUG) {53 SecureWindowPolicy.setUserSecure(window, true)54 } else if (!qaSecureCaptureBypass()) {55 SecureWindowPolicy.acquire(window)56 debugSecureLease = true57 }58 lifecycleScope.launch { completeSelection() }59 }6061 override fun onDestroy() {62 if (debugSecureLease) {63 SecureWindowPolicy.release(window)64 debugSecureLease = false65 }66 super.onDestroy()67 }6869 private fun qaSecureCaptureBypass(): Boolean =70 BuildConfig.DEBUG && (71 intent.getBooleanExtra(QaFlags.EXTRA_DISABLE_SECURE_CAPTURE, false) ||72 QaFlags.disableSecureCaptureForQa73 )7475 private suspend fun completeSelection() {76 if (intent.getStringExtra(EXTRA_MODE) == MODE_CREATE) {77 completeCreate()78 return79 }80 val result = Intent()81 val entryId = intent.getStringExtra(EXTRA_ENTRY_ID)82 val entry = entryId?.let(vault::getEntry)83 val providerRequest = runCatching {84 PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)85 }.getOrNull()86 val callerMatches = providerRequest?.callingAppInfo?.let { info ->87 if (entry == null) {88 false89 } else {90 val allowlist = PasskeyCallerOrigin.privilegedAllowlist(this)91 val webOrigin = runCatching { info.getOrigin(allowlist) }.getOrNull()92 if (!webOrigin.isNullOrBlank()) {93 val domain = extractDomain(webOrigin)94 domain != null && PasswordCredentialPlanner95 .candidates(listOf(entry), setOf(domain), null)96 .isNotEmpty()97 } else {98 PasswordCredentialPlanner.candidates(99 listOf(entry),100 appLinkDomains(this, info.packageName).toSet(),101 packageName = info.packageName,102 ).isNotEmpty()103 }104 }105 } ?: false106 val username = entry?.username ?: entry?.email107 val password = entry?.password108 if (entry == null || entry.deletedAt != null || !callerMatches ||109 username.isNullOrBlank() || password.isNullOrEmpty()110 ) {111 PendingIntentHandler.setGetCredentialException(112 result,113 GetCredentialUnsupportedException("Peach cannot fill this login."),114 )115 } else {116 runCatching { vault.updateEntry(entry.copy(lastUsedAt = System.currentTimeMillis())) }117 PendingIntentHandler.setGetCredentialResponse(118 result,119 GetCredentialResponse(PasswordCredential(username, password)),120 )121 }122 setResult(RESULT_OK, result)123 finish()124 }125126 private suspend fun completeCreate() {127 val result = Intent()128 val id = intent.getStringExtra(EXTRA_CREATE_ID)129 val password = intent.getStringExtra(EXTRA_CREATE_PASSWORD)130 val providerRequest = runCatching {131 PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)132 }.getOrNull()133 val draft = buildSaveDraft(providerRequest?.callingAppInfo, id, password)134 if (draft == null) {135 PendingIntentHandler.setCreateCredentialException(136 result,137 CreateCredentialUnsupportedException("Peach cannot save this password."),138 )139 } else {140 val saved = runCatching {141 when (val plan = planAutofillSave(vault.getEntries(), draft)) {142 is AutofillSavePlan.Add -> vault.addEntry(plan.entry)143 is AutofillSavePlan.Update -> vault.updateEntry(plan.entry)144 }145 }.isSuccess146 if (saved) {147 PendingIntentHandler.setCreateCredentialResponse(result, CreatePasswordResponse())148 } else {149 PendingIntentHandler.setCreateCredentialException(150 result,151 CreateCredentialUnsupportedException("Peach could not save this password."),152 )153 }154 }155 setResult(RESULT_OK, result)156 finish()157 }158159 160161162163164165166167 private fun buildSaveDraft(info: CallingAppInfo?, id: String?, password: String?): AutofillSaveDraft? {168 if (info == null || id.isNullOrBlank() || password.isNullOrEmpty()) return null169 val identity = id.trim()170 val identityIsEmail = identity.contains("@")171 val allowlist = PasskeyCallerOrigin.privilegedAllowlist(this)172 val webOrigin = runCatching { info.getOrigin(allowlist) }.getOrNull()173 if (!webOrigin.isNullOrBlank()) {174 return AutofillSaveDraft(webOrigin, true, identity, identityIsEmail, password, null)175 }176 val packageName = info.packageName?.trim().takeUnless { it.isNullOrEmpty() } ?: return null177 return AutofillSaveDraft(packageName, false, identity, identityIsEmail, password, null)178 }179180 companion object {181 private const val EXTRA_ENTRY_ID = "com.peachpasswords.credentials.EXTRA_ENTRY_ID"182 private const val EXTRA_MODE = "com.peachpasswords.credentials.EXTRA_MODE"183 private const val MODE_CREATE = "create"184 private const val EXTRA_CREATE_ID = "com.peachpasswords.credentials.EXTRA_CREATE_ID"185 private const val EXTRA_CREATE_PASSWORD = "com.peachpasswords.credentials.EXTRA_CREATE_PASSWORD"186187 fun pendingIntent(context: Context, entryId: String): PendingIntent {188 val intent = Intent(context, PasswordCredentialSelectionActivity::class.java)189 .putExtra(EXTRA_ENTRY_ID, entryId)190 val mutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0191 return PendingIntent.getActivity(192 context,193 "cm-password:$entryId".hashCode(),194 intent,195 PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag,196 )197 }198199 fun createPendingIntent(context: Context, id: String, password: String): PendingIntent {200 val intent = Intent(context, PasswordCredentialSelectionActivity::class.java)201 .putExtra(EXTRA_MODE, MODE_CREATE)202 .putExtra(EXTRA_CREATE_ID, id)203 .putExtra(EXTRA_CREATE_PASSWORD, password)204 val mutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0205 return PendingIntent.getActivity(206 context,207 "cm-password-create:$id".hashCode(),208 intent,209 PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag,210 )211 }212 }213}