key storage spotless

This commit is contained in:
nrobi144
2026-01-06 06:50:31 +02:00
parent 4339247815
commit df9d648988
5 changed files with 48 additions and 20 deletions
@@ -63,8 +63,9 @@ class AccountManager(
// and use SecureKeyStorage to retrieve the private key // and use SecureKeyStorage to retrieve the private key
val lastNpub = getLastNpub() ?: return Result.failure(Exception("No saved account")) val lastNpub = getLastNpub() ?: return Result.failure(Exception("No saved account"))
val privKeyHex = secureStorage.getPrivateKey(lastNpub) val privKeyHex =
?: return Result.failure(Exception("Private key not found for $lastNpub")) secureStorage.getPrivateKey(lastNpub)
?: return Result.failure(Exception("Private key not found for $lastNpub"))
val keyPair = KeyPair(privKey = privKeyHex.hexToByteArray()) val keyPair = KeyPair(privKey = privKeyHex.hexToByteArray())
val signer = NostrSignerInternal(keyPair) val signer = NostrSignerInternal(keyPair)
@@ -94,8 +95,9 @@ class AccountManager(
} }
return try { return try {
val privKeyHex = decodePrivateKeyAsHexOrNull(current.nsec) val privKeyHex =
?: return Result.failure(Exception("Invalid nsec format")) decodePrivateKeyAsHexOrNull(current.nsec)
?: return Result.failure(Exception("Invalid nsec format"))
secureStorage.savePrivateKey(current.npub, privKeyHex) secureStorage.savePrivateKey(current.npub, privKeyHex)
saveLastNpub(current.npub) saveLastNpub(current.npub)
@@ -390,7 +390,7 @@ fun ProfileScreen(
accountManager: AccountManager, accountManager: AccountManager,
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
Column { Column {
Text( Text(
"Profile", "Profile",
@@ -30,14 +30,17 @@ import kotlinx.coroutines.withContext
* Android implementation of SecureKeyStorage using EncryptedSharedPreferences * Android implementation of SecureKeyStorage using EncryptedSharedPreferences
* backed by Android Keystore (AES-256-GCM, hardware-backed when available). * backed by Android Keystore (AES-256-GCM, hardware-backed when available).
*/ */
actual class SecureKeyStorage(private val context: Context) { actual class SecureKeyStorage(
private val context: Context,
) {
companion object { companion object {
private const val PREFS_NAME = "amethyst_secure_keys" private const val PREFS_NAME = "amethyst_secure_keys"
private const val KEY_PREFIX = "privkey_" private const val KEY_PREFIX = "privkey_"
} }
private val masterKey: MasterKey by lazy { private val masterKey: MasterKey by lazy {
MasterKey.Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS) MasterKey
.Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build() .build()
} }
@@ -52,7 +55,10 @@ actual class SecureKeyStorage(private val context: Context) {
) )
} }
actual suspend fun savePrivateKey(npub: String, privKeyHex: String) { actual suspend fun savePrivateKey(
npub: String,
privKeyHex: String,
) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
encryptedPrefs.edit().putString(KEY_PREFIX + npub, privKeyHex).apply() encryptedPrefs.edit().putString(KEY_PREFIX + npub, privKeyHex).apply()
@@ -34,7 +34,10 @@ expect class SecureKeyStorage {
* @param privKeyHex The private key in hexadecimal format * @param privKeyHex The private key in hexadecimal format
* @throws SecureStorageException if storage operation fails * @throws SecureStorageException if storage operation fails
*/ */
suspend fun savePrivateKey(npub: String, privKeyHex: String) suspend fun savePrivateKey(
npub: String,
privKeyHex: String,
)
/** /**
* Retrieves a private key for the given npub. * Retrieves a private key for the given npub.
@@ -66,4 +69,7 @@ expect class SecureKeyStorage {
/** /**
* Exception thrown when secure storage operations fail. * Exception thrown when secure storage operations fail.
*/ */
class SecureStorageException(message: String, cause: Throwable? = null) : Exception(message, cause) class SecureStorageException(
message: String,
cause: Throwable? = null,
) : Exception(message, cause)
@@ -58,7 +58,10 @@ actual class SecureKeyStorage {
private var keyringAvailable: Boolean = true private var keyringAvailable: Boolean = true
private var fallbackPassword: String? = null private var fallbackPassword: String? = null
actual suspend fun savePrivateKey(npub: String, privKeyHex: String) { actual suspend fun savePrivateKey(
npub: String,
privKeyHex: String,
) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
if (keyringAvailable) { if (keyringAvailable) {
@@ -111,11 +114,13 @@ actual class SecureKeyStorage {
} }
} }
actual suspend fun hasPrivateKey(npub: String): Boolean = actual suspend fun hasPrivateKey(npub: String): Boolean = getPrivateKey(npub) != null
getPrivateKey(npub) != null
// Keyring-based storage // Keyring-based storage
private fun saveToKeyring(npub: String, privKeyHex: String) { private fun saveToKeyring(
npub: String,
privKeyHex: String,
) {
val keyring = Keyring.create() val keyring = Keyring.create()
keyring.setPassword(SERVICE_NAME, npub, privKeyHex) keyring.setPassword(SERVICE_NAME, npub, privKeyHex)
} }
@@ -138,7 +143,10 @@ actual class SecureKeyStorage {
} }
// Fallback encrypted file storage // Fallback encrypted file storage
private fun saveToFallback(npub: String, privKeyHex: String) { private fun saveToFallback(
npub: String,
privKeyHex: String,
) {
val password = getFallbackPassword() val password = getFallbackPassword()
val encrypted = encryptData(privKeyHex, password) val encrypted = encryptData(privKeyHex, password)
@@ -184,12 +192,12 @@ actual class SecureKeyStorage {
val fallbackFile = getFallbackFile() val fallbackFile = getFallbackFile()
if (!fallbackFile.exists()) return emptyMap() if (!fallbackFile.exists()) return emptyMap()
return fallbackFile.readLines() return fallbackFile
.readLines()
.mapNotNull { line -> .mapNotNull { line ->
val parts = line.split(":", limit = 2) val parts = line.split(":", limit = 2)
if (parts.size == 2) parts[0] to parts[1] else null if (parts.size == 2) parts[0] to parts[1] else null
} }.toMap()
.toMap()
} }
private fun getFallbackFile(): File { private fun getFallbackFile(): File {
@@ -206,7 +214,10 @@ actual class SecureKeyStorage {
return fallbackPassword!! return fallbackPassword!!
} }
private fun encryptData(plaintext: String, password: String): String { private fun encryptData(
plaintext: String,
password: String,
): String {
val salt = ByteArray(16).apply { SecureRandom().nextBytes(this) } val salt = ByteArray(16).apply { SecureRandom().nextBytes(this) }
val iv = ByteArray(IV_LENGTH).apply { SecureRandom().nextBytes(this) } val iv = ByteArray(IV_LENGTH).apply { SecureRandom().nextBytes(this) }
@@ -222,7 +233,10 @@ actual class SecureKeyStorage {
return Base64.getEncoder().encodeToString(combined) return Base64.getEncoder().encodeToString(combined)
} }
private fun decryptData(ciphertext: String, password: String): String { private fun decryptData(
ciphertext: String,
password: String,
): String {
val combined = Base64.getDecoder().decode(ciphertext) val combined = Base64.getDecoder().decode(ciphertext)
val salt = combined.copyOfRange(0, 16) val salt = combined.copyOfRange(0, 16)