55414a0ad11b
fix: harden PIN storage against copied-app offline brute force (v2.14.68)
- time
- 2026-08-30 11:59 UTC · 1 month ago
- machine
- koh2
- parent
- 715c2bf666ce
↓ peach-android-55414a0a.tar.gzatlas at this save
files changed2 files
VERSION+1 −1app/src/main/java/com/peachpasswords/vault/PinUnlockStore.kt+29 −11app/src/main/java/com/peachpasswords/vault/PinUnlockStore.kt+29 −11
@@ -52,6 +52,7 @@5252 // Remove it as soon as the security-remediated app opens the store; users5353 // can still unlock with the master password and configure the new format.5454 clearLegacyConfiguration()55+ if (prefs.getInt(KEY_FORMAT, 0) != FORMAT_VERSION) clearKnownKeystoreAliases()5556 }5657 5758 /** New PIN setup is safe only where Android reports hardware limited-use keys. */@@ -64,7 +65,9 @@6465 prefs.contains(KEY_SALT) &&6566 prefs.contains(KEY_VERIFIER) &&6667 prefs.contains(KEY_WRAPPED_KEY) &&67- prefs.contains(KEY_WRAPPED_IV)68+ prefs.contains(KEY_WRAPPED_IV) &&69+ existingSecretKey(VAULT_ALIAS) != null &&70+ existingSecretKey(VERIFIER_ALIAS) != null6871 6972 fun isPinPreferred(): Boolean = prefs.getBoolean(KEY_PREFERRED, false)7073 @@ -125,21 +128,25 @@125128 */126129 fun unlockVaultKey(pin: String): ByteArray? {127130 if (pinValidationError(pin) != null || !isConfigured() || !consumeAttemptGate()) return null128- val salt = prefs.getString(KEY_SALT, null)?.let(::decode) ?: return null129- val expected = prefs.getString(KEY_VERIFIER, null)?.let(::decode) ?: return null131+ var salt: ByteArray? = null132+ var expected: ByteArray? = null130133 var pinKey: ByteArray? = null131134 var actual: ByteArray? = null135+ var wrapped: ByteArray? = null136+ var iv: ByteArray? = null132137 return try {138+ salt = prefs.getString(KEY_SALT, null)?.let(::decode) ?: return null139+ expected = prefs.getString(KEY_VERIFIER, null)?.let(::decode) ?: return null133140 val verifierKey = existingSecretKey(VERIFIER_ALIAS) ?: return null134- pinKey = VaultCrypto.deriveKey(pin, salt)141+ pinKey = VaultCrypto.deriveKey(pin, salt!!)135142 actual = hmac(verifierKey, pinKey)136143 if (!MessageDigest.isEqual(expected, actual)) return null137144 138- val wrapped = prefs.getString(KEY_WRAPPED_KEY, null)?.let(::decode) ?: return null139- val iv = prefs.getString(KEY_WRAPPED_IV, null)?.let(::decode) ?: return null145+ wrapped = prefs.getString(KEY_WRAPPED_KEY, null)?.let(::decode) ?: return null146+ iv = prefs.getString(KEY_WRAPPED_IV, null)?.let(::decode) ?: return null140147 val vaultKey = Cipher.getInstance(TRANSFORM).run {141- init(Cipher.DECRYPT_MODE, existingSecretKey(VAULT_ALIAS) ?: return null, GCMParameterSpec(TAG_BITS, iv))142- doFinal(wrapped)148+ init(Cipher.DECRYPT_MODE, existingSecretKey(VAULT_ALIAS) ?: return null, GCMParameterSpec(TAG_BITS, iv!!))149+ doFinal(wrapped!!)143150 }144151 // A successful PIN proves possession of the PIN; replenish the145152 // hardware attempt budget for the next lock/unlock cycle.@@ -148,14 +155,17 @@148155 } catch (_: Exception) {149156 null150157 } finally {151- VaultCrypto.zero(salt)152- VaultCrypto.zero(expected)158+ salt?.let(VaultCrypto::zero)159+ expected?.let(VaultCrypto::zero)153160 pinKey?.let(VaultCrypto::zero)154161 actual?.let(VaultCrypto::zero)162+ wrapped?.let(VaultCrypto::zero)163+ iv?.let(VaultCrypto::zero)155164 }156165 }157166 158- fun areAttemptsExhausted(): Boolean = isConfigured() && loadAttemptAliases().isEmpty()167+ fun areAttemptsExhausted(): Boolean = isConfigured() &&168+ loadAttemptAliases().none { existingSecretKey(it) != null }159169 160170 /** Regenerate the hardware attempt budget after a master-password unlock. */161171 fun replenishAttemptGates() {@@ -314,6 +324,14 @@314324 315325 private fun deleteKey(alias: String) {316326 runCatching { KeyStore.getInstance(KEYSTORE).apply { load(null) }.deleteEntry(alias) }327+ }328+329+ private fun clearKnownKeystoreAliases() {330+ runCatching {331+ KeyStore.getInstance(KEYSTORE).apply { load(null) }.aliases().toList()332+ .filter { it == VAULT_ALIAS || it == VERIFIER_ALIAS || it.startsWith(ATTEMPT_ALIAS_PREFIX) }333+ .forEach(::deleteKey)334+ }317335 }318336 319337 private fun clearLegacyConfiguration() {