PinUnlockStore.kt

3.2 KB · raw

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}1718/** Device-local Argon2id PIN key and AES-GCM-wrapped copy of the live vault key. */19@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}