key storage implementation
This commit is contained in:
+14
-1
@@ -47,9 +47,22 @@ sealed class AccountState {
|
|||||||
) : AccountState()
|
) : AccountState()
|
||||||
}
|
}
|
||||||
|
|
||||||
class AccountManager(
|
class AccountManager private constructor(
|
||||||
private val secureStorage: SecureKeyStorage,
|
private val secureStorage: SecureKeyStorage,
|
||||||
) {
|
) {
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Creates an AccountManager instance.
|
||||||
|
*
|
||||||
|
* @param context Platform-specific context (required on Android, ignored on Desktop)
|
||||||
|
* @return AccountManager instance
|
||||||
|
*/
|
||||||
|
fun create(context: Any? = null): AccountManager {
|
||||||
|
val storage = SecureKeyStorage.create(context)
|
||||||
|
return AccountManager(storage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val _accountState = MutableStateFlow<AccountState>(AccountState.LoggedOut)
|
private val _accountState = MutableStateFlow<AccountState>(AccountState.LoggedOut)
|
||||||
val accountState: StateFlow<AccountState> = _accountState.asStateFlow()
|
val accountState: StateFlow<AccountState> = _accountState.asStateFlow()
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ import com.vitorpamplona.amethyst.desktop.ui.FeedScreen
|
|||||||
import com.vitorpamplona.amethyst.desktop.ui.LoginScreen
|
import com.vitorpamplona.amethyst.desktop.ui.LoginScreen
|
||||||
import com.vitorpamplona.amethyst.desktop.ui.NotificationsScreen
|
import com.vitorpamplona.amethyst.desktop.ui.NotificationsScreen
|
||||||
import com.vitorpamplona.amethyst.desktop.ui.UserProfileScreen
|
import com.vitorpamplona.amethyst.desktop.ui.UserProfileScreen
|
||||||
import com.vitorpamplona.quartz.nip01Core.crypto.SecureKeyStorage
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
@@ -213,14 +212,14 @@ fun App(
|
|||||||
) {
|
) {
|
||||||
var currentScreen by remember { mutableStateOf<DesktopScreen>(DesktopScreen.Feed) }
|
var currentScreen by remember { mutableStateOf<DesktopScreen>(DesktopScreen.Feed) }
|
||||||
val relayManager = remember { DesktopRelayConnectionManager() }
|
val relayManager = remember { DesktopRelayConnectionManager() }
|
||||||
val secureStorage = remember { SecureKeyStorage() }
|
val accountManager = remember { AccountManager.create() }
|
||||||
val accountManager = remember { AccountManager(secureStorage) }
|
|
||||||
val accountState by accountManager.accountState.collectAsState()
|
val accountState by accountManager.accountState.collectAsState()
|
||||||
val scope = remember { CoroutineScope(SupervisorJob() + Dispatchers.Main) }
|
val scope = remember { CoroutineScope(SupervisorJob() + Dispatchers.Main) }
|
||||||
|
|
||||||
// Try to load saved account on startup
|
// Try to load saved account on startup
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
scope.launch {
|
scope.launch(Dispatchers.IO) {
|
||||||
|
// Load account on IO dispatcher to avoid blocking UI with password prompt (readLine)
|
||||||
accountManager.loadSavedAccount()
|
accountManager.loadSavedAccount()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import com.vitorpamplona.amethyst.commons.account.AccountManager
|
|||||||
import com.vitorpamplona.amethyst.commons.account.AccountState
|
import com.vitorpamplona.amethyst.commons.account.AccountState
|
||||||
import com.vitorpamplona.amethyst.commons.ui.auth.LoginCard
|
import com.vitorpamplona.amethyst.commons.ui.auth.LoginCard
|
||||||
import com.vitorpamplona.amethyst.commons.ui.auth.NewKeyWarningCard
|
import com.vitorpamplona.amethyst.commons.ui.auth.NewKeyWarningCard
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -76,8 +77,8 @@ fun LoginScreen(
|
|||||||
LoginCard(
|
LoginCard(
|
||||||
onLogin = { keyInput ->
|
onLogin = { keyInput ->
|
||||||
accountManager.loginWithKey(keyInput).map {
|
accountManager.loginWithKey(keyInput).map {
|
||||||
// Save account to secure storage
|
// Save account to secure storage (use IO dispatcher to avoid blocking UI)
|
||||||
scope.launch {
|
scope.launch(Dispatchers.IO) {
|
||||||
accountManager.saveCurrentAccount()
|
accountManager.saveCurrentAccount()
|
||||||
onLoginSuccess()
|
onLoginSuccess()
|
||||||
}
|
}
|
||||||
@@ -96,8 +97,8 @@ fun LoginScreen(
|
|||||||
nsec = generatedAccount!!.nsec,
|
nsec = generatedAccount!!.nsec,
|
||||||
onContinue = {
|
onContinue = {
|
||||||
showNewKeyDialog = false
|
showNewKeyDialog = false
|
||||||
// Save generated account
|
// Save generated account (use IO dispatcher to avoid blocking UI)
|
||||||
scope.launch {
|
scope.launch(Dispatchers.IO) {
|
||||||
accountManager.saveCurrentAccount()
|
accountManager.saveCurrentAccount()
|
||||||
onLoginSuccess()
|
onLoginSuccess()
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-6
@@ -29,25 +29,50 @@ 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).
|
||||||
|
*
|
||||||
|
* ## Security Features
|
||||||
|
*
|
||||||
|
* - **Hardware Security:** Uses Android Keystore (hardware-backed on supported devices with StrongBox)
|
||||||
|
* - **Encryption:** AES-256-GCM for both keys and values
|
||||||
|
* - **Key Derivation:** AES-256-SIV for preference keys, AES-256-GCM for values
|
||||||
|
* - **Application Context:** Uses applicationContext to prevent memory leaks
|
||||||
|
* - **Auto-backup Disabled:** EncryptedSharedPreferences automatically excluded from cloud backups
|
||||||
|
*
|
||||||
|
* **Note:** While the encryption keys are protected by hardware security modules (when available),
|
||||||
|
* the decrypted private keys returned by [getPrivateKey] are still subject to the String memory
|
||||||
|
* limitation described in [SecureKeyStorage].
|
||||||
*/
|
*/
|
||||||
actual class SecureKeyStorage(
|
actual class SecureKeyStorage private actual constructor() {
|
||||||
private val context: Context,
|
actual 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 lateinit var appContext: Context
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a SecureKeyStorage instance for Android.
|
||||||
|
*
|
||||||
|
* @param context Android Context (will use applicationContext to avoid leaks)
|
||||||
|
* @return SecureKeyStorage instance
|
||||||
|
* @throws IllegalArgumentException if context is null or not a valid Context
|
||||||
|
*/
|
||||||
|
actual fun create(context: Any?): SecureKeyStorage {
|
||||||
|
require(context is Context) { "Android requires a valid Context" }
|
||||||
|
appContext = context.applicationContext
|
||||||
|
return SecureKeyStorage()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val masterKey: MasterKey by lazy {
|
private val masterKey: MasterKey by lazy {
|
||||||
MasterKey
|
MasterKey
|
||||||
.Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS)
|
.Builder(appContext, MasterKey.DEFAULT_MASTER_KEY_ALIAS)
|
||||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val encryptedPrefs by lazy {
|
private val encryptedPrefs by lazy {
|
||||||
EncryptedSharedPreferences.create(
|
EncryptedSharedPreferences.create(
|
||||||
context,
|
appContext,
|
||||||
PREFS_NAME,
|
PREFS_NAME,
|
||||||
masterKey,
|
masterKey,
|
||||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||||
|
|||||||
+37
-3
@@ -23,10 +23,41 @@ package com.vitorpamplona.quartz.nip01Core.crypto
|
|||||||
/**
|
/**
|
||||||
* Secure storage for private keys using platform-specific secure storage mechanisms.
|
* Secure storage for private keys using platform-specific secure storage mechanisms.
|
||||||
*
|
*
|
||||||
* Android: Android Keystore + EncryptedDataStore (AES-256-GCM, StrongBox when available)
|
* ## Platform Implementations
|
||||||
* Desktop: OS native credential managers (macOS Keychain, Windows Credential Manager, Linux Secret Service)
|
*
|
||||||
|
* **Android:** Android Keystore + EncryptedSharedPreferences (AES-256-GCM, StrongBox when available)
|
||||||
|
*
|
||||||
|
* **Desktop:** OS native credential managers (macOS Keychain, Windows Credential Manager, Linux Secret Service)
|
||||||
|
* with encrypted file fallback.
|
||||||
|
*
|
||||||
|
* ## Security Considerations
|
||||||
|
*
|
||||||
|
* **String Memory Limitation:** Private keys are returned as String (hexadecimal format). Unlike char arrays,
|
||||||
|
* Kotlin/JVM Strings are immutable and cannot be securely zeroed from memory after use. The private key
|
||||||
|
* will remain in memory until garbage collected, which may be delayed. This is an inherent limitation of
|
||||||
|
* the JVM platform.
|
||||||
|
*
|
||||||
|
* **Mitigation:** Private keys are only exposed through this API when needed for signing operations.
|
||||||
|
* They are encrypted at rest on all platforms. For maximum security, minimize the time private keys
|
||||||
|
* are held in memory and ensure they are dereferenced promptly after use.
|
||||||
|
*
|
||||||
|
* **Production Recommendation:** For high-security applications, consider using hardware security modules
|
||||||
|
* (HSM) or secure enclaves (Android StrongBox, iOS Secure Enclave) that never expose private keys to
|
||||||
|
* application memory.
|
||||||
|
*
|
||||||
|
* Use [SecureKeyStorage.create] factory method to create instances.
|
||||||
*/
|
*/
|
||||||
expect class SecureKeyStorage {
|
expect class SecureKeyStorage private constructor() {
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Creates a SecureKeyStorage instance.
|
||||||
|
*
|
||||||
|
* @param context Platform-specific context (required on Android, ignored on Desktop)
|
||||||
|
* @return SecureKeyStorage instance
|
||||||
|
*/
|
||||||
|
fun create(context: Any? = null): SecureKeyStorage
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves a private key securely for the given npub.
|
* Saves a private key securely for the given npub.
|
||||||
*
|
*
|
||||||
@@ -42,6 +73,9 @@ expect class SecureKeyStorage {
|
|||||||
/**
|
/**
|
||||||
* Retrieves a private key for the given npub.
|
* Retrieves a private key for the given npub.
|
||||||
*
|
*
|
||||||
|
* **Security Warning:** The returned String cannot be securely zeroed from memory (JVM limitation).
|
||||||
|
* Dereference the returned value immediately after use to minimize exposure time.
|
||||||
|
*
|
||||||
* @param npub The public key in npub (Bech32) format
|
* @param npub The public key in npub (Bech32) format
|
||||||
* @return The private key in hexadecimal format, or null if not found
|
* @return The private key in hexadecimal format, or null if not found
|
||||||
* @throws SecureStorageException if retrieval operation fails
|
* @throws SecureStorageException if retrieval operation fails
|
||||||
|
|||||||
+184
-40
@@ -24,8 +24,14 @@ import com.github.javakeyring.BackendNotSupportedException
|
|||||||
import com.github.javakeyring.Keyring
|
import com.github.javakeyring.Keyring
|
||||||
import com.github.javakeyring.PasswordAccessException
|
import com.github.javakeyring.PasswordAccessException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.RandomAccessFile
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.StandardCopyOption
|
||||||
|
import java.nio.file.attribute.PosixFilePermission
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.Base64
|
import java.util.Base64
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
@@ -40,9 +46,30 @@ import javax.crypto.spec.SecretKeySpec
|
|||||||
*
|
*
|
||||||
* Falls back to encrypted file storage with user-provided password if OS keyring
|
* Falls back to encrypted file storage with user-provided password if OS keyring
|
||||||
* is unavailable.
|
* is unavailable.
|
||||||
|
*
|
||||||
|
* ## Fallback Storage Security
|
||||||
|
*
|
||||||
|
* When OS keyring is unavailable, the implementation uses:
|
||||||
|
* - **Encryption:** AES-256-GCM with PBKDF2 (100k iterations)
|
||||||
|
* - **File Permissions:** Owner-only read/write (600 for files, 700 for directories) on Unix systems
|
||||||
|
* - **Atomic Writes:** Temp file + atomic move to prevent corruption
|
||||||
|
* - **File Locking:** Prevents concurrent access race conditions
|
||||||
|
*
|
||||||
|
* **Password Memory Limitation:** The fallback password is stored as a String and cannot be
|
||||||
|
* securely zeroed from memory. It remains cached for the application lifetime to avoid repeated
|
||||||
|
* password prompts. This is acceptable for desktop applications where the user's session is
|
||||||
|
* already trusted, but may not be suitable for shared/multi-user systems.
|
||||||
*/
|
*/
|
||||||
actual class SecureKeyStorage {
|
actual class SecureKeyStorage private actual constructor() {
|
||||||
companion object {
|
actual companion object {
|
||||||
|
/**
|
||||||
|
* Creates a SecureKeyStorage instance for Desktop.
|
||||||
|
*
|
||||||
|
* @param context Ignored on Desktop (no context needed)
|
||||||
|
* @return SecureKeyStorage instance
|
||||||
|
*/
|
||||||
|
actual fun create(context: Any?): SecureKeyStorage = SecureKeyStorage()
|
||||||
|
|
||||||
private const val SERVICE_NAME = "amethyst-desktop"
|
private const val SERVICE_NAME = "amethyst-desktop"
|
||||||
private const val FALLBACK_DIR = ".amethyst"
|
private const val FALLBACK_DIR = ".amethyst"
|
||||||
private const val FALLBACK_FILE = "keys.enc"
|
private const val FALLBACK_FILE = "keys.enc"
|
||||||
@@ -57,6 +84,7 @@ actual class SecureKeyStorage {
|
|||||||
|
|
||||||
private var keyringAvailable: Boolean = true
|
private var keyringAvailable: Boolean = true
|
||||||
private var fallbackPassword: String? = null
|
private var fallbackPassword: String? = null
|
||||||
|
private val fallbackMutex = Mutex() // Protects concurrent access to fallback file
|
||||||
|
|
||||||
actual suspend fun savePrivateKey(
|
actual suspend fun savePrivateKey(
|
||||||
npub: String,
|
npub: String,
|
||||||
@@ -143,52 +171,78 @@ actual class SecureKeyStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fallback encrypted file storage
|
// Fallback encrypted file storage
|
||||||
private fun saveToFallback(
|
private suspend fun saveToFallback(
|
||||||
npub: String,
|
npub: String,
|
||||||
privKeyHex: String,
|
privKeyHex: String,
|
||||||
) {
|
) {
|
||||||
val password = getFallbackPassword()
|
fallbackMutex.withLock {
|
||||||
val encrypted = encryptData(privKeyHex, password)
|
val password = getFallbackPassword()
|
||||||
|
val encrypted = encryptData(privKeyHex, password)
|
||||||
|
|
||||||
val fallbackFile = getFallbackFile()
|
val fallbackFile = getFallbackFile()
|
||||||
val data = loadFallbackData().toMutableMap()
|
|
||||||
data[npub] = encrypted
|
|
||||||
|
|
||||||
fallbackFile.parentFile?.mkdirs()
|
// Create directory with restrictive permissions
|
||||||
fallbackFile.writeText(data.entries.joinToString("\n") { "${it.key}:${it.value}" })
|
fallbackFile.parentFile?.let { dir ->
|
||||||
}
|
if (!dir.exists()) {
|
||||||
|
dir.mkdirs()
|
||||||
|
setRestrictivePermissions(dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getFromFallback(npub: String): String? {
|
withFileLock(fallbackFile) {
|
||||||
val password = fallbackPassword ?: return null // No password set yet
|
val data = loadFallbackDataUnsafe().toMutableMap()
|
||||||
val data = loadFallbackData()
|
data[npub] = encrypted
|
||||||
val encrypted = data[npub] ?: return null
|
atomicWriteFallbackData(fallbackFile, data)
|
||||||
|
|
||||||
return try {
|
|
||||||
decryptData(encrypted, password)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun deleteFromFallback(npub: String): Boolean {
|
|
||||||
val fallbackFile = getFallbackFile()
|
|
||||||
if (!fallbackFile.exists()) return false
|
|
||||||
|
|
||||||
val data = loadFallbackData().toMutableMap()
|
|
||||||
val existed = data.remove(npub) != null
|
|
||||||
|
|
||||||
if (existed) {
|
|
||||||
if (data.isEmpty()) {
|
|
||||||
fallbackFile.delete()
|
|
||||||
} else {
|
|
||||||
fallbackFile.writeText(data.entries.joinToString("\n") { "${it.key}:${it.value}" })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return existed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadFallbackData(): Map<String, String> {
|
private suspend fun getFromFallback(npub: String): String? {
|
||||||
|
val password = fallbackPassword ?: return null // No password set yet
|
||||||
|
|
||||||
|
return fallbackMutex.withLock {
|
||||||
|
val fallbackFile = getFallbackFile()
|
||||||
|
if (!fallbackFile.exists()) return@withLock null
|
||||||
|
|
||||||
|
withFileLock(fallbackFile) {
|
||||||
|
val data = loadFallbackDataUnsafe()
|
||||||
|
val encrypted = data[npub] ?: return@withFileLock null
|
||||||
|
|
||||||
|
try {
|
||||||
|
decryptData(encrypted, password)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun deleteFromFallback(npub: String): Boolean {
|
||||||
|
return fallbackMutex.withLock {
|
||||||
|
val fallbackFile = getFallbackFile()
|
||||||
|
if (!fallbackFile.exists()) return@withLock false
|
||||||
|
|
||||||
|
withFileLock(fallbackFile) {
|
||||||
|
val data = loadFallbackDataUnsafe().toMutableMap()
|
||||||
|
val existed = data.remove(npub) != null
|
||||||
|
|
||||||
|
if (existed) {
|
||||||
|
if (data.isEmpty()) {
|
||||||
|
fallbackFile.delete()
|
||||||
|
} else {
|
||||||
|
atomicWriteFallbackData(fallbackFile, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
existed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads fallback data without locking. Caller must hold mutex and file lock.
|
||||||
|
*/
|
||||||
|
private fun loadFallbackDataUnsafe(): Map<String, String> {
|
||||||
val fallbackFile = getFallbackFile()
|
val fallbackFile = getFallbackFile()
|
||||||
if (!fallbackFile.exists()) return emptyMap()
|
if (!fallbackFile.exists()) return emptyMap()
|
||||||
|
|
||||||
@@ -200,6 +254,56 @@ actual class SecureKeyStorage {
|
|||||||
}.toMap()
|
}.toMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atomically writes fallback data using temp file + rename.
|
||||||
|
*/
|
||||||
|
private fun atomicWriteFallbackData(
|
||||||
|
fallbackFile: File,
|
||||||
|
data: Map<String, String>,
|
||||||
|
) {
|
||||||
|
val tempFile = File(fallbackFile.parentFile, "${fallbackFile.name}.tmp")
|
||||||
|
try {
|
||||||
|
// Write to temp file
|
||||||
|
tempFile.writeText(data.entries.joinToString("\n") { "${it.key}:${it.value}" })
|
||||||
|
setRestrictivePermissions(tempFile)
|
||||||
|
|
||||||
|
// Atomic rename
|
||||||
|
Files.move(
|
||||||
|
tempFile.toPath(),
|
||||||
|
fallbackFile.toPath(),
|
||||||
|
StandardCopyOption.ATOMIC_MOVE,
|
||||||
|
StandardCopyOption.REPLACE_EXISTING,
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
// Clean up temp file if it still exists
|
||||||
|
if (tempFile.exists()) {
|
||||||
|
tempFile.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes block with file lock held.
|
||||||
|
*/
|
||||||
|
private fun <T> withFileLock(
|
||||||
|
file: File,
|
||||||
|
block: () -> T,
|
||||||
|
): T {
|
||||||
|
// Ensure lock file exists
|
||||||
|
val lockFile = File(file.parentFile, "${file.name}.lock")
|
||||||
|
lockFile.parentFile?.mkdirs()
|
||||||
|
if (!lockFile.exists()) {
|
||||||
|
lockFile.createNewFile()
|
||||||
|
setRestrictivePermissions(lockFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
return RandomAccessFile(lockFile, "rw").use { raf ->
|
||||||
|
raf.channel.lock().use { lock ->
|
||||||
|
block()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getFallbackFile(): File {
|
private fun getFallbackFile(): File {
|
||||||
val homeDir = System.getProperty("user.home")
|
val homeDir = System.getProperty("user.home")
|
||||||
return File(homeDir, "$FALLBACK_DIR/$FALLBACK_FILE")
|
return File(homeDir, "$FALLBACK_DIR/$FALLBACK_FILE")
|
||||||
@@ -208,12 +312,52 @@ actual class SecureKeyStorage {
|
|||||||
private fun getFallbackPassword(): String {
|
private fun getFallbackPassword(): String {
|
||||||
if (fallbackPassword == null) {
|
if (fallbackPassword == null) {
|
||||||
println("OS keyring not available. Fallback encrypted storage requires a password.")
|
println("OS keyring not available. Fallback encrypted storage requires a password.")
|
||||||
print("Enter master password: ")
|
val console = System.console()
|
||||||
fallbackPassword = readLine() ?: throw SecureStorageException("Password required for fallback storage")
|
fallbackPassword =
|
||||||
|
if (console != null) {
|
||||||
|
// Use Console.readPassword() for masked input
|
||||||
|
val password = console.readPassword("Enter master password: ")
|
||||||
|
password?.let {
|
||||||
|
val str = String(it)
|
||||||
|
it.fill('\u0000') // Clear the char array from memory
|
||||||
|
str
|
||||||
|
} ?: throw SecureStorageException("Password required for fallback storage")
|
||||||
|
} else {
|
||||||
|
// Fallback for non-interactive environments (testing, etc.)
|
||||||
|
print("Enter master password: ")
|
||||||
|
readLine() ?: throw SecureStorageException("Password required for fallback storage")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return fallbackPassword!!
|
return fallbackPassword!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setRestrictivePermissions(file: File) {
|
||||||
|
try {
|
||||||
|
val path = file.toPath()
|
||||||
|
// Set owner-only read/write permissions (600 for files, 700 for directories)
|
||||||
|
val permissions =
|
||||||
|
if (file.isDirectory) {
|
||||||
|
setOf(
|
||||||
|
PosixFilePermission.OWNER_READ,
|
||||||
|
PosixFilePermission.OWNER_WRITE,
|
||||||
|
PosixFilePermission.OWNER_EXECUTE,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setOf(
|
||||||
|
PosixFilePermission.OWNER_READ,
|
||||||
|
PosixFilePermission.OWNER_WRITE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Files.setPosixFilePermissions(path, permissions)
|
||||||
|
} catch (e: UnsupportedOperationException) {
|
||||||
|
// Windows doesn't support POSIX permissions - file system security handles this
|
||||||
|
// No action needed
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Log but don't fail - permissions are a security enhancement, not critical
|
||||||
|
System.err.println("Warning: Could not set restrictive file permissions: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun encryptData(
|
private fun encryptData(
|
||||||
plaintext: String,
|
plaintext: String,
|
||||||
password: String,
|
password: String,
|
||||||
|
|||||||
Reference in New Issue
Block a user