package com.peachpasswords.vault import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import java.util.Base64 import javax.inject.Inject import javax.inject.Singleton internal const val PIN_MIN_LENGTH = 4 internal const val PIN_MAX_LENGTH = 12 internal fun pinValidationError(pin: String): String? = when { pin.length !in PIN_MIN_LENGTH..PIN_MAX_LENGTH -> "PIN must be 4–12 digits" pin.any { it !in '0'..'9' } -> "PIN must contain digits only" else -> null } /** Device-local Argon2id PIN key and AES-GCM-wrapped copy of the live vault key. */ @Singleton class PinUnlockStore @Inject constructor( @ApplicationContext context: Context, ) { private val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) fun isConfigured(): Boolean = prefs.contains(KEY_SALT) && prefs.contains(KEY_WRAPPED_KEY) fun isPinPreferred(): Boolean = prefs.getBoolean(KEY_PREFERRED, false) fun markPinUsed() { prefs.edit().putBoolean(KEY_PREFERRED, true).apply() } fun markPasswordUsed() { prefs.edit().putBoolean(KEY_PREFERRED, false).apply() } fun configure(pin: String, vaultKey: ByteArray): Boolean { if (pinValidationError(pin) != null) return false val salt = VaultCrypto.generateSalt() val pinKey = VaultCrypto.deriveKey(pin, salt) return try { val wrapped = wrap(vaultKey, pinKey) ?: return false prefs.edit() .putString(KEY_SALT, Base64.getEncoder().withoutPadding().encodeToString(salt)) .putString(KEY_WRAPPED_KEY, wrapped) .commit() } finally { VaultCrypto.zero(salt) VaultCrypto.zero(pinKey) } } fun unlockVaultKey(pin: String): ByteArray? { if (pinValidationError(pin) != null) return null val salt = prefs.getString(KEY_SALT, null)?.let(::decode) ?: return null val blob = prefs.getString(KEY_WRAPPED_KEY, null)?.let(::decode) ?: return null val pinKey = VaultCrypto.deriveKey(pin, salt) return try { unwrap(blob, pinKey) } finally { VaultCrypto.zero(salt) VaultCrypto.zero(blob) VaultCrypto.zero(pinKey) } } fun clear() { prefs.edit().remove(KEY_SALT).remove(KEY_WRAPPED_KEY).remove(KEY_PREFERRED).commit() } private fun wrap(vaultKey: ByteArray, pinKey: ByteArray): String? = runCatching { val blob = VaultCrypto.encrypt(vaultKey, pinKey, AAD) try { Base64.getEncoder().withoutPadding().encodeToString(blob) } finally { VaultCrypto.zero(blob) } }.getOrNull() private fun unwrap(blob: ByteArray, pinKey: ByteArray): ByteArray? = runCatching { VaultCrypto.decrypt(blob, pinKey, AAD) }.getOrNull() private fun decode(value: String): ByteArray = Base64.getDecoder().decode(value) private companion object { const val PREFS = "peach_vault_store" const val KEY_SALT = "peach_pin_salt" const val KEY_WRAPPED_KEY = "peach_pin_wrapped_key" const val KEY_PREFERRED = "peach_pin_preferred" const val AAD = "pin_unlock_v1" } }