1package com.peachpasswords.vault23import android.content.Context4import dagger.hilt.android.qualifiers.ApplicationContext5import java.util.Base646import javax.inject.Inject7import javax.inject.Singleton89internal const val PIN_MIN_LENGTH = 410internal const val PIN_MAX_LENGTH = 121112internal fun pinValidationError(pin: String): String? = when {13 pin.length !in PIN_MIN_LENGTH..PIN_MAX_LENGTH -> "PIN must be 4–12 digits"14 pin.any { it !in '0'..'9' } -> "PIN must contain digits only"15 else -> null16}171819@Singleton20class PinUnlockStore @Inject constructor(21 @ApplicationContext context: Context,22) {23 private val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)2425 fun isConfigured(): Boolean =26 prefs.contains(KEY_SALT) && prefs.contains(KEY_WRAPPED_KEY)2728 fun isPinPreferred(): Boolean = prefs.getBoolean(KEY_PREFERRED, false)2930 fun markPinUsed() {31 prefs.edit().putBoolean(KEY_PREFERRED, true).apply()32 }3334 fun markPasswordUsed() {35 prefs.edit().putBoolean(KEY_PREFERRED, false).apply()36 }3738 fun configure(pin: String, vaultKey: ByteArray): Boolean {39 if (pinValidationError(pin) != null) return false40 val salt = VaultCrypto.generateSalt()41 val pinKey = VaultCrypto.deriveKey(pin, salt)42 return try {43 val wrapped = wrap(vaultKey, pinKey) ?: return false44 prefs.edit()45 .putString(KEY_SALT, Base64.getEncoder().withoutPadding().encodeToString(salt))46 .putString(KEY_WRAPPED_KEY, wrapped)47 .commit()48 } finally {49 VaultCrypto.zero(salt)50 VaultCrypto.zero(pinKey)51 }52 }5354 fun unlockVaultKey(pin: String): ByteArray? {55 if (pinValidationError(pin) != null) return null56 val salt = prefs.getString(KEY_SALT, null)?.let(::decode) ?: return null57 val blob = prefs.getString(KEY_WRAPPED_KEY, null)?.let(::decode) ?: return null58 val pinKey = VaultCrypto.deriveKey(pin, salt)59 return try {60 unwrap(blob, pinKey)61 } finally {62 VaultCrypto.zero(salt)63 VaultCrypto.zero(blob)64 VaultCrypto.zero(pinKey)65 }66 }6768 fun clear() {69 prefs.edit().remove(KEY_SALT).remove(KEY_WRAPPED_KEY).remove(KEY_PREFERRED).commit()70 }7172 private fun wrap(vaultKey: ByteArray, pinKey: ByteArray): String? = runCatching {73 val blob = VaultCrypto.encrypt(vaultKey, pinKey, AAD)74 try {75 Base64.getEncoder().withoutPadding().encodeToString(blob)76 } finally {77 VaultCrypto.zero(blob)78 }79 }.getOrNull()8081 private fun unwrap(blob: ByteArray, pinKey: ByteArray): ByteArray? =82 runCatching { VaultCrypto.decrypt(blob, pinKey, AAD) }.getOrNull()8384 private fun decode(value: String): ByteArray = Base64.getDecoder().decode(value)8586 private companion object {87 const val PREFS = "peach_vault_store"88 const val KEY_SALT = "peach_pin_salt"89 const val KEY_WRAPPED_KEY = "peach_pin_wrapped_key"90 const val KEY_PREFERRED = "peach_pin_preferred"91 const val AAD = "pin_unlock_v1"92 }93}