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.appLabel21import com.peachpasswords.autofill.appLinkDomains22import com.peachpasswords.autofill.planAutofillSave23import com.peachpasswords.passkeys.PasskeyCallerOrigin24import com.peachpasswords.shared.extractDomain25import com.peachpasswords.ui.components.QaFlags26import com.peachpasswords.ui.components.SecureWindowPolicy27import com.peachpasswords.vault.VaultRepository28import dagger.hilt.android.AndroidEntryPoint29import javax.inject.Inject30import kotlinx.coroutines.launch3132333435363738394041424344@AndroidEntryPoint45class PasswordCredentialSelectionActivity : FragmentActivity() {46 @Inject lateinit var vault: VaultRepository47 private var debugSecureLease = false4849 override fun onCreate(savedInstanceState: Bundle?) {50 super.onCreate(savedInstanceState)51 52 53 if (!BuildConfig.DEBUG) {54 SecureWindowPolicy.setUserSecure(window, true)55 } else if (!qaSecureCaptureBypass()) {56 SecureWindowPolicy.acquire(window)57 debugSecureLease = true58 }59 lifecycleScope.launch { completeSelection() }60 }6162 override fun onDestroy() {63 if (debugSecureLease) {64 SecureWindowPolicy.release(window)65 debugSecureLease = false66 }67 super.onDestroy()68 }6970 private fun qaSecureCaptureBypass(): Boolean =71 BuildConfig.DEBUG && (72 intent.getBooleanExtra(QaFlags.EXTRA_DISABLE_SECURE_CAPTURE, false) ||73 QaFlags.disableSecureCaptureForQa74 )7576 private suspend fun completeSelection() {77 if (intent.getStringExtra(EXTRA_MODE) == MODE_CREATE) {78 completeCreate()79 return80 }81 val result = Intent()82 val entryId = intent.getStringExtra(EXTRA_ENTRY_ID)83 val entry = entryId?.let(vault::getEntry)84 val providerRequest = runCatching {85 PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)86 }.getOrNull()87 val callerMatches = providerRequest?.callingAppInfo?.let { info ->88 if (entry == null) {89 false90 } else {91 val allowlist = PasskeyCallerOrigin.privilegedAllowlist(this)92 val webOrigin = runCatching { info.getOrigin(allowlist) }.getOrNull()93 if (!webOrigin.isNullOrBlank()) {94 val domain = extractDomain(webOrigin)95 domain != null && PasswordCredentialPlanner96 .candidates(listOf(entry), setOf(domain), null)97 .isNotEmpty()98 } else {99 PasswordCredentialPlanner.candidates(100 listOf(entry),101 appLinkDomains(this, info.packageName).toSet(),102 appLabel(this, info.packageName),103 ).isNotEmpty()104 }105 }106 } ?: false107 val username = entry?.username ?: entry?.email108 val password = entry?.password109 if (entry == null || entry.deletedAt != null || !callerMatches ||110 username.isNullOrBlank() || password.isNullOrEmpty()111 ) {112 PendingIntentHandler.setGetCredentialException(113 result,114 GetCredentialUnsupportedException("Peach cannot fill this login."),115 )116 } else {117 runCatching { vault.updateEntry(entry.copy(lastUsedAt = System.currentTimeMillis())) }118 PendingIntentHandler.setGetCredentialResponse(119 result,120 GetCredentialResponse(PasswordCredential(username, password)),121 )122 }123 setResult(RESULT_OK, result)124 finish()125 }126127 private suspend fun completeCreate() {128 val result = Intent()129 val id = intent.getStringExtra(EXTRA_CREATE_ID)130 val password = intent.getStringExtra(EXTRA_CREATE_PASSWORD)131 val providerRequest = runCatching {132 PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)133 }.getOrNull()134 val draft = buildSaveDraft(providerRequest?.callingAppInfo, id, password)135 if (draft == null) {136 PendingIntentHandler.setCreateCredentialException(137 result,138 CreateCredentialUnsupportedException("Peach cannot save this password."),139 )140 } else {141 val saved = runCatching {142 when (val plan = planAutofillSave(vault.getEntries(), draft)) {143 is AutofillSavePlan.Add -> vault.addEntry(plan.entry)144 is AutofillSavePlan.Update -> vault.updateEntry(plan.entry)145 }146 }.isSuccess147 if (saved) {148 PendingIntentHandler.setCreateCredentialResponse(result, CreatePasswordResponse())149 } else {150 PendingIntentHandler.setCreateCredentialException(151 result,152 CreateCredentialUnsupportedException("Peach could not save this password."),153 )154 }155 }156 setResult(RESULT_OK, result)157 finish()158 }159160 161162163164165166 private fun buildSaveDraft(info: CallingAppInfo?, id: String?, password: String?): AutofillSaveDraft? {167 if (info == null || id.isNullOrBlank() || password.isNullOrEmpty()) return null168 val identity = id.trim()169 val identityIsEmail = identity.contains("@")170 val allowlist = PasskeyCallerOrigin.privilegedAllowlist(this)171 val webOrigin = runCatching { info.getOrigin(allowlist) }.getOrNull()172 if (!webOrigin.isNullOrBlank()) {173 return AutofillSaveDraft(webOrigin, true, identity, identityIsEmail, password, null)174 }175 val title = appLabel(this, info.packageName) ?: return null176 return AutofillSaveDraft(title, false, identity, identityIsEmail, password, null)177 }178179 companion object {180 private const val EXTRA_ENTRY_ID = "com.peachpasswords.credentials.EXTRA_ENTRY_ID"181 private const val EXTRA_MODE = "com.peachpasswords.credentials.EXTRA_MODE"182 private const val MODE_CREATE = "create"183 private const val EXTRA_CREATE_ID = "com.peachpasswords.credentials.EXTRA_CREATE_ID"184 private const val EXTRA_CREATE_PASSWORD = "com.peachpasswords.credentials.EXTRA_CREATE_PASSWORD"185186 fun pendingIntent(context: Context, entryId: String): PendingIntent {187 val intent = Intent(context, PasswordCredentialSelectionActivity::class.java)188 .putExtra(EXTRA_ENTRY_ID, entryId)189 val mutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0190 return PendingIntent.getActivity(191 context,192 "cm-password:$entryId".hashCode(),193 intent,194 PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag,195 )196 }197198 fun createPendingIntent(context: Context, id: String, password: String): PendingIntent {199 val intent = Intent(context, PasswordCredentialSelectionActivity::class.java)200 .putExtra(EXTRA_MODE, MODE_CREATE)201 .putExtra(EXTRA_CREATE_ID, id)202 .putExtra(EXTRA_CREATE_PASSWORD, password)203 val mutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0204 return PendingIntent.getActivity(205 context,206 "cm-password-create:$id".hashCode(),207 intent,208 PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag,209 )210 }211 }212}