1package com.peachpasswords.credentials23import android.app.PendingIntent4import android.content.Context5import android.content.Intent6import android.os.Build7import android.os.Bundle8import androidx.credentials.GetCredentialResponse9import androidx.credentials.PasswordCredential10import androidx.credentials.exceptions.GetCredentialUnsupportedException11import androidx.credentials.provider.PendingIntentHandler12import androidx.fragment.app.FragmentActivity13import androidx.lifecycle.lifecycleScope14import com.peachpasswords.BuildConfig15import com.peachpasswords.autofill.appLabel16import com.peachpasswords.autofill.appLinkDomains17import com.peachpasswords.passkeys.PasskeyCallerOrigin18import com.peachpasswords.shared.extractDomain19import com.peachpasswords.ui.components.QaFlags20import com.peachpasswords.ui.components.SecureWindowPolicy21import com.peachpasswords.vault.VaultRepository22import dagger.hilt.android.AndroidEntryPoint23import javax.inject.Inject24import kotlinx.coroutines.launch252627282930313233343536@AndroidEntryPoint37class PasswordCredentialSelectionActivity : FragmentActivity() {38 @Inject lateinit var vault: VaultRepository39 private var debugSecureLease = false4041 override fun onCreate(savedInstanceState: Bundle?) {42 super.onCreate(savedInstanceState)43 44 45 if (!BuildConfig.DEBUG) {46 SecureWindowPolicy.setUserSecure(window, true)47 } else if (!qaSecureCaptureBypass()) {48 SecureWindowPolicy.acquire(window)49 debugSecureLease = true50 }51 lifecycleScope.launch { completeSelection() }52 }5354 override fun onDestroy() {55 if (debugSecureLease) {56 SecureWindowPolicy.release(window)57 debugSecureLease = false58 }59 super.onDestroy()60 }6162 private fun qaSecureCaptureBypass(): Boolean =63 BuildConfig.DEBUG && (64 intent.getBooleanExtra(QaFlags.EXTRA_DISABLE_SECURE_CAPTURE, false) ||65 QaFlags.disableSecureCaptureForQa66 )6768 private suspend fun completeSelection() {69 val result = Intent()70 val entryId = intent.getStringExtra(EXTRA_ENTRY_ID)71 val entry = entryId?.let(vault::getEntry)72 val providerRequest = runCatching {73 PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)74 }.getOrNull()75 val callerMatches = providerRequest?.callingAppInfo?.let { info ->76 if (entry == null) {77 false78 } else {79 val allowlist = PasskeyCallerOrigin.privilegedAllowlist(this)80 val webOrigin = runCatching { info.getOrigin(allowlist) }.getOrNull()81 if (!webOrigin.isNullOrBlank()) {82 val domain = extractDomain(webOrigin)83 domain != null && PasswordCredentialPlanner84 .candidates(listOf(entry), setOf(domain), null)85 .isNotEmpty()86 } else {87 PasswordCredentialPlanner.candidates(88 listOf(entry),89 appLinkDomains(this, info.packageName).toSet(),90 appLabel(this, info.packageName),91 ).isNotEmpty()92 }93 }94 } ?: false95 val username = entry?.username ?: entry?.email96 val password = entry?.password97 if (entry == null || entry.deletedAt != null || !callerMatches ||98 username.isNullOrBlank() || password.isNullOrEmpty()99 ) {100 PendingIntentHandler.setGetCredentialException(101 result,102 GetCredentialUnsupportedException("Peach cannot fill this login."),103 )104 } else {105 runCatching { vault.updateEntry(entry.copy(lastUsedAt = System.currentTimeMillis())) }106 PendingIntentHandler.setGetCredentialResponse(107 result,108 GetCredentialResponse(PasswordCredential(username, password)),109 )110 }111 setResult(RESULT_OK, result)112 finish()113 }114115 companion object {116 private const val EXTRA_ENTRY_ID = "com.peachpasswords.credentials.EXTRA_ENTRY_ID"117118 fun pendingIntent(context: Context, entryId: String): PendingIntent {119 val intent = Intent(context, PasswordCredentialSelectionActivity::class.java)120 .putExtra(EXTRA_ENTRY_ID, entryId)121 val mutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0122 return PendingIntent.getActivity(123 context,124 "cm-password:$entryId".hashCode(),125 intent,126 PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag,127 )128 }129 }130}