package com.peachpasswords.autofill import android.app.PendingIntent import android.content.Context import android.content.Intent import android.content.IntentSender import android.os.Build /** * PendingIntents the autofill surfaces hand to the framework: a no-op * attribution target the inline chip requires, the authentication * IntentSender that opens the branded [AutofillEntryPickerActivity], and the * "open new entry" launcher that the picker fires when the user taps the * "+ Create new entry" affordance. */ object AutofillIntents { const val EXTRA_IDENTITY_ID = "peach.autofill.identityId" const val EXTRA_PASSWORD_ID = "peach.autofill.passwordId" const val EXTRA_OTP_ID = "peach.autofill.otpId" const val EXTRA_OTP_DIGIT_BOX_IDS = "peach.autofill.otpDigitBoxIds" const val EXTRA_ORIGIN = "peach.autofill.origin" const val EXTRA_IS_WEB_ORIGIN = "peach.autofill.isWebOrigin" /** Authenticated caller package so the picker can re-resolve web-origin domains in-process. */ const val EXTRA_CALLER_PACKAGE = "peach.autofill.callerPackage" const val EXTRA_CARD_NUMBER_ID = "peach.autofill.cardNumberId" const val EXTRA_CARD_EXP_MONTH_ID = "peach.autofill.cardExpMonthId" const val EXTRA_CARD_EXP_YEAR_ID = "peach.autofill.cardExpYearId" const val EXTRA_CARD_EXP_DATE_ID = "peach.autofill.cardExpDateId" const val EXTRA_CARD_CVC_ID = "peach.autofill.cardCvcId" const val EXTRA_CARD_NAME_ID = "peach.autofill.cardNameId" /** Host-typed username captured by the autofill framework — used to prefill the new-entry editor. */ const val EXTRA_TYPED_USERNAME = "peach.autofill.typedUsername" /** Host-typed email captured by the autofill framework — used to prefill the new-entry editor. */ const val EXTRA_TYPED_EMAIL = "peach.autofill.typedEmail" /** * Sent by [AutofillEntryPickerActivity] to launch MainActivity on the "+ Create * new entry" path. The picker cancels itself and starts MainActivity with the * origin + typed identity so the user can save a brand-new entry pre-filled with * what they had typed on the host page. Carries the same key/value shape as the * update notification's [com.peachpasswords.updates.EXTRA_OPEN_UPDATE]. */ const val EXTRA_OPEN_NEW_ENTRY = "peach.autofill.openNewEntry" const val EXTRA_NEW_ENTRY_URL = "peach.autofill.newEntryUrl" const val EXTRA_NEW_ENTRY_USERNAME = "peach.autofill.newEntryUsername" const val EXTRA_NEW_ENTRY_EMAIL = "peach.autofill.newEntryEmail" /** * F3: one-time token proving a new-entry launch really came from the * in-process picker. MainActivity is exported (launcher), so without this * any app could pre-fill the new-entry editor with attacker-chosen * url/username/email. */ const val EXTRA_NEW_ENTRY_TOKEN = "peach.autofill.newEntryToken" private const val REQ_CHIP = 0xC0DE private const val REQ_PICKER = 0xBEEF @Volatile private var pendingNewEntryToken: String? = null /** F3: the picker calls this once per launch; the token lives only in memory. */ fun issueNewEntryToken(): String { val token = java.util.UUID.randomUUID().toString() pendingNewEntryToken = token return token } /** * F3: single-use check. A wrong token does NOT burn the pending one, so a * racing spoofed intent can only fail, not DoS the real handoff. */ fun consumeNewEntryToken(token: String?): Boolean { val expected = pendingNewEntryToken ?: return false if (token != expected) return false pendingNewEntryToken = null return true } /** Required attribution for an inline suggestion; tapping it is a no-op. */ fun noopChipIntent(context: Context): PendingIntent { val intent = Intent(context, AutofillEntryPickerActivity::class.java) return PendingIntent.getActivity(context, REQ_CHIP, intent, immutableFlags()) } /** Authentication target for the "Search Peach…" dataset. */ fun pickerIntentSender(context: Context, parsed: ParsedStructure): IntentSender { val intent = Intent(context, AutofillEntryPickerActivity::class.java).apply { putExtra(EXTRA_IDENTITY_ID, parsed.identityId) putExtra(EXTRA_PASSWORD_ID, parsed.passwordId) putExtra(EXTRA_OTP_ID, parsed.otpId) putParcelableArrayListExtra(EXTRA_OTP_DIGIT_BOX_IDS, ArrayList(parsed.otpDigitBoxIds)) putExtra(EXTRA_ORIGIN, parsed.origin) putExtra(EXTRA_IS_WEB_ORIGIN, parsed.webDomain != null) // The picker re-resolves origin domains/app title in-process from // the authenticated caller package. putExtra(EXTRA_CALLER_PACKAGE, parsed.packageName) putExtra(EXTRA_CARD_NUMBER_ID, parsed.cardNumberId) putExtra(EXTRA_CARD_EXP_MONTH_ID, parsed.cardExpMonthId) putExtra(EXTRA_CARD_EXP_YEAR_ID, parsed.cardExpYearId) putExtra(EXTRA_CARD_EXP_DATE_ID, parsed.cardExpDateId) putExtra(EXTRA_CARD_CVC_ID, parsed.cardCvcId) putExtra(EXTRA_CARD_NAME_ID, parsed.cardNameId) // Mirror the browser's preUsername/preEmail capture so the "+ Create // new entry" path can hand the typed identity to the new-entry editor. // Empty/blank values are dropped — the picker reads the typed username // from the identity field the host anchored on. parsed.usernameValue?.trim()?.takeIf { it.isNotBlank() }?.let { putExtra(EXTRA_TYPED_USERNAME, it) } parsed.emailValue?.trim()?.takeIf { it.isNotBlank() }?.let { putExtra(EXTRA_TYPED_EMAIL, it) } } // Freezing the sender prevents the host app from rewriting origin // extras before the private picker receives them. The picker re-resolves // the origin from raw authenticated inputs in-process. return PendingIntent.getActivity(context, REQ_PICKER, intent, immutableFlags()).intentSender } private fun immutableFlags(): Int = PendingIntent.FLAG_UPDATE_CURRENT or if (Build.VERSION.SDK_INT >= 23) PendingIntent.FLAG_IMMUTABLE else 0 }