106d9ea293aa
measure single-use Keystore support instead of trusting the feature flags (v2.14.76)
- time
- 2026-08-31 13:53 UTC · 1 month ago
- machine
- koh2
- agent
- hy4-preview
- session
- pin store device verification
- parent
- c0543fb5cc99
↓ peach-android-106d9ea2.tar.gzatlas at this save
files changed3 files
VERSION+1 −1app/src/main/java/com/peachpasswords/ui/settings/SettingsViewModel.kt+1 −1app/src/main/java/com/peachpasswords/vault/PinUnlockStore.kt+18 −9app/src/main/java/com/peachpasswords/ui/settings/SettingsViewModel.kt+1 −1
@@ -344,7 +344,7 @@344344 }345345 346346 suspend fun savePin(masterPassword: String, pin: String): String? {347- if (!pinUnlock.isSupported()) return "PIN unlock requires Android hardware limited-use key support"347+ if (!pinUnlock.isSupported()) return "PIN unlock needs hardware that limits PIN attempts"348348 if (masterPassword.isBlank()) return "enter your master password"349349 pinValidationError(pin)?.let { return it }350350 val masterMatches = withContext(Dispatchers.Default) {app/src/main/java/com/peachpasswords/vault/PinUnlockStore.kt+18 −9
@@ -46,6 +46,7 @@4646 ) {4747 private val appContext = context.applicationContext4848 private val prefs = appContext.getSharedPreferences(PREFS, Context.MODE_PRIVATE)49+ private var supported: Boolean? = null4950 5051 init {5152 // The v1 wrapper was directly brute-forceable from a copied prefs file.@@ -59,10 +60,19 @@5960 }6061 }6162 62- /** New PIN setup is safe only where Android reports hardware limited-use keys. */63- fun isSupported(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&64- (appContext.packageManager.hasSystemFeature(PackageManager.FEATURE_KEYSTORE_LIMITED_USE_KEY) ||65- appContext.packageManager.hasSystemFeature(PackageManager.FEATURE_KEYSTORE_SINGLE_USE_KEY))63+ /**64+ * Whether this device can host a PIN at all. Measured by probing the Keystore65+ * rather than by reading FEATURE_KEYSTORE_LIMITED_USE_KEY / SINGLE_USE_KEY:66+ * a Galaxy Z Fold 6 on SDK 36 enforces max-usage-1 HMAC keys in both67+ * StrongBox and TEE while declaring neither feature, so the flags would68+ * disable PIN unlock on hardware that supports it outright.69+ */70+ fun isSupported(): Boolean {71+ supported?.let { return it }72+ val measured = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && probeLimitedUseEnforcement()73+ supported = measured74+ return measured75+ }6676 6777 fun isConfigured(): Boolean =6878 prefs.getInt(KEY_FORMAT, 0) == FORMAT_VERSION &&@@ -85,7 +95,6 @@8595 8696 fun configure(pin: String, vaultKey: ByteArray): Boolean {8797 if (!isSupported() || pinValidationError(pin) != null || vaultKey.size != VaultCrypto.KEY_SIZE) return false88- if (!probeLimitedUseEnforcement()) return false8998 clear()9099 91100 var salt: ByteArray? = null@@ -173,12 +182,12 @@173182 loadAttemptAliases().none { existingSecretKey(it) != null }174183 175184 /**176- * Prove this device actually enforces a one-use hardware HMAC key instead177- * of trusting its feature flag. PIN setup proceeds only when the first MAC178- * succeeds and the same key is then unavailable or rejects a second MAC.185+ * Prove this device actually enforces a one-use hardware HMAC key instead of186+ * trusting its feature flag: the first MAC must succeed and the same key must187+ * then be unavailable or reject a second MAC. Generation also requires a188+ * hardware-backed key, so a software-only provider fails the probe.179189 */180190 internal fun probeLimitedUseEnforcement(): Boolean {181- if (!isSupported()) return false182191 val alias = "$PROBE_ALIAS_PREFIX${UUID.randomUUID()}"183192 val createdAliases = mutableListOf<String>()184193 return try {