{"repo": "alexeyw/personalandroidaiagent", "files": {"app/src/main/java/app/knotwork/android/data/local/ApiKeyManager.kt": {"c310fb213ad94d67b77251b811a1fbdcba384ed8": {"t": 1785745735.1033282, "n": 7620, "h": "167d52ddb3bc3cd9", "c": "package app.knotwork.android.data.local\n\nimport android.content.Context\nimport app.knotwork.android.data.local.crypto.AeadCipher\nimport app.knotwork.android.data.local.crypto.KeystoreBackedPrefsStore\nimport app.knotwork.android.data.local.crypto.SecureValueUnreadableException\nimport app.knotwork.android.domain.constants.SettingsDefaults\nimport app.knotwork.android.domain.repositories.ApiKeyRepository\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport timber.log.Timber\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\n/**\n * Concrete implementation of [ApiKeyRepository] that securely stores API keys\n * in a [KeystoreBackedPrefsStore] — values encrypted with AES-GCM under a\n * dedicated, non-exportable Android Keystore key.\n *\n * **Recovery semantics — deliberately different from [EncryptedDbPassphraseProvider].**\n * A stored value that can no longer be decrypted (e.g. the Keystore key was lost during a\n * backup/restore) is treated as absent: the corrupt entry is removed and the getter returns\n * `null`. That is safe here because API keys are user-re-enterable — the worst outcome is the\n * user pasting their keys again. The database passphrase provider must never do this: its\n * secret cannot be re-derived, and destroying it would render the encrypted database\n * permanently unreadable.\n *\n * Earlier releases kept the keys in `EncryptedSharedPreferences` (deprecated upstream and\n * removed from this project without a data migration, as permitted by the pre-release storage\n * policy); a leftover legacy file is ignored — previously stored keys simply have to be\n * re-entered once.\n *\n * @property context The application context used to create the shared preferences.\n * @param cipher The AEAD boundary used to protect the stored values.\n */\n@Singleton\nclass ApiKeyManager @Inject constructor(@ApplicationContext private val context: Context, cipher: AeadCipher) :\n ApiKeyRepository {\n\n private val store = KeystoreBackedPrefsStore(\n context = context,\n prefsName = PREFS_NAME,\n keyAlias = KEY_ALIAS,\n cipher = cipher,\n )\n\n private object Keys {\n const val OPENAI_KEY = \"openai_api_key\"\n const val OPENAI_MODEL = \"openai_model\"\n const val ANTHROPIC_KEY = \"anthropic_api_key\"\n const val ANTHROPIC_MODEL = \"anthropic_model\"\n const val GOOGLE_KEY = \"google_api_key\"\n const val GOOGLE_MODEL = \"google_model\"\n const val DEEPSEEK_KEY = \"deepseek_api_key\"\n const val DEEPSEEK_MODEL = \"deepseek_model\"\n const val OLLAMA_URL = \"ollama_base_url\"\n const val OLLAMA_MODEL = \"ollama_model\"\n const val OLLAMA_CONTEXT = \"ollama_context\"\n }\n\n // Mutable state flows to allow reactive observing of key changes\n private val _openAIKeyFlow by lazy { MutableStateFlow(readOrNull(Keys.OPENAI_KEY)) }\n private val _openAIModelFlow by lazy { MutableStateFlow(readOrNull(Keys.OPENAI_MODEL)) }\n private val _anthropicKeyFlow by lazy { MutableStateFlow(readOrNull(Keys.ANTHROPIC_KEY)) }\n private val _anthropicModelFlow by lazy { MutableStateFlow(readOrNull(Keys.ANTHROPIC_MODEL)) }\n private val _googleKeyFlow by lazy { MutableStateFlow(readOrNull(Keys.GOOGLE_KEY)) }\n private val _googleModelFlow by lazy { MutableStateFlow(readOrNull(Keys.GOOGLE_MODEL)) }\n private val _deepSeekKeyFlow by lazy { MutableStateFlow(readOrNull(Keys.DEEPSEEK_KEY)) }\n private val _deepSeekModelFlow by lazy { MutableStateFlow(readOrNull(Keys.DEEPSEEK_MODEL)) }\n private val _ollamaUrlFlow by lazy { MutableStateFlow(readOrNull(Keys.OLLAMA_URL)) }\n private val _ollamaModelFlow by lazy { MutableStateFlow(readOrNull(Keys.OLLAMA_MODEL)) }\n private val _ollamaContextFlow by lazy {\n MutableStateFlow(\n readOrNull(Keys.OLLAMA_CONTEXT)?.toIntOrNull() ?: SettingsDefaults.OLLAMA_CONTEXT_WINDOW_DEFAULT,\n )\n }\n\n override fun getOpenAIKey(): Flow = _openAIKeyFlow.asStateFlow()\n\n override suspend fun setOpenAIKey(key: String?) {\n saveString(Keys.OPENAI_KEY, key)\n _openAIKeyFlow.value = key\n }\n\n override fun getOpenAIModel(): Flow = _openAIModelFlow.asStateFlow()\n\n override suspend fun setOpenAIModel(model: String?) {\n saveString(Keys.OPENAI_MODEL, model)\n _openAIModelFlow.value = model\n }\n\n override fun getAnthropicKey(): Flow = _anthropicKeyFlow.asStateFlow()\n\n override suspend fun setAnthropicKey(key: String?) {\n saveString(Keys.ANTHROPIC_KEY, key)\n _anthropicKeyFlow.value = key\n }\n\n override fun getAnthropicModel(): Flow = _anthropicModelFlow.asStateFlow()\n\n override suspend fun setAnthropicModel(model: String?) {\n saveString(Keys.ANTHROPIC_MODEL, model)\n _anthropicModelFlow.value = model\n }\n\n override fun getGoogleKey(): Flow = _googleKeyFlow.asStateFlow()\n\n override suspend fun setGoogleKey(key: String?) {\n saveString(Keys.GOOGLE_KEY, key)\n _googleKeyFlow.value = key\n }\n\n override fun getGoogleModel(): Flow = _googleModelFlow.asStateFlow()\n\n override suspend fun setGoogleModel(model: String?) {\n saveString(Keys.GOOGLE_MODEL, model)\n _googleModelFlow.value = model\n }\n\n override fun getDeepSeekKey(): Flow = _deepSeekKeyFlow.asStateFlow()\n\n override suspend fun setDeepSeekKey(key: String?) {\n saveString(Keys.DEEPSEEK_KEY, key)\n _deepSeekKeyFlow.value = key\n }\n\n override fun getDeepSeekModel(): Flow = _deepSeekModelFlow.asStateFlow()\n\n override suspend fun setDeepSeekModel(model: String?) {\n saveString(Keys.DEEPSEEK_MODEL, model)\n _deepSeekModelFlow.value = model\n }\n\n override fun getOllamaBaseUrl(): Flow = _ollamaUrlFlow.asStateFlow()\n\n override suspend fun setOllamaBaseUrl(url: String?) {\n saveString(Keys.OLLAMA_URL, url)\n _ollamaUrlFlow.value = url\n }\n\n override fun getOllamaModelName(): Flow = _ollamaModelFlow.asStateFlow()\n\n override suspend fun setOllamaModelName(model: String?) {\n saveString(Keys.OLLAMA_MODEL, model)\n _ollamaModelFlow.value = model\n }\n\n override fun getOllamaContextWindowSize(): Flow = _ollamaContextFlow.asStateFlow()\n\n override suspend fun setOllamaContextWindowSize(size: Int) {\n store.putString(Keys.OLLAMA_CONTEXT, size.toString(), synchronous = false)\n _ollamaContextFlow.value = size\n }\n\n /**\n * Reads a stored value, applying the re-enterable-secret recovery policy: an entry that\n * cannot be decrypted is dropped and reported as absent instead of propagating the error.\n */\n private fun readOrNull(key: String): String? = try {\n store.getString(key)\n } catch (e: SecureValueUnreadableException) {\n Timber.e(e, \"Stored API-key entry '%s' is unreadable; treating it as unset.\", key)\n store.remove(key)\n null\n }\n\n private fun saveString(key: String, value: String?) {\n if (value == null) {\n store.remove(key)\n } else {\n store.putString(key, value, synchronous = false)\n }\n }\n\n private companion object {\n /** Name of the [KeystoreBackedPrefsStore] preferences file holding the API keys. */\n const val PREFS_NAME = \"secure_api_keys_v2\"\n\n /** Android Keystore alias of the AEAD key dedicated to the API-key store. */\n const val KEY_ALIAS = \"knotwork.api_keys\"\n }\n}\n"}, "9eab68a849dd6f49d869d0b576243e556fd78fd5": {"t": 1785755981.912553, "n": 7620, "h": "167d52ddb3bc3cd9", "c": "package app.knotwork.android.data.local\n\nimport android.content.Context\nimport app.knotwork.android.data.local.crypto.AeadCipher\nimport app.knotwork.android.data.local.crypto.KeystoreBackedPrefsStore\nimport app.knotwork.android.data.local.crypto.SecureValueUnreadableException\nimport app.knotwork.android.domain.constants.SettingsDefaults\nimport app.knotwork.android.domain.repositories.ApiKeyRepository\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport timber.log.Timber\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\n/**\n * Concrete implementation of [ApiKeyRepository] that securely stores API keys\n * in a [KeystoreBackedPrefsStore] — values encrypted with AES-GCM under a\n * dedicated, non-exportable Android Keystore key.\n *\n * **Recovery semantics — deliberately different from [EncryptedDbPassphraseProvider].**\n * A stored value that can no longer be decrypted (e.g. the Keystore key was lost during a\n * backup/restore) is treated as absent: the corrupt entry is removed and the getter returns\n * `null`. That is safe here because API keys are user-re-enterable — the worst outcome is the\n * user pasting their keys again. The database passphrase provider must never do this: its\n * secret cannot be re-derived, and destroying it would render the encrypted database\n * permanently unreadable.\n *\n * Earlier releases kept the keys in `EncryptedSharedPreferences` (deprecated upstream and\n * removed from this project without a data migration, as permitted by the pre-release storage\n * policy); a leftover legacy file is ignored — previously stored keys simply have to be\n * re-entered once.\n *\n * @property context The application context used to create the shared preferences.\n * @param cipher The AEAD boundary used to protect the stored values.\n */\n@Singleton\nclass ApiKeyManager @Inject constructor(@ApplicationContext private val context: Context, cipher: AeadCipher) :\n ApiKeyRepository {\n\n private val store = KeystoreBackedPrefsStore(\n context = context,\n prefsName = PREFS_NAME,\n keyAlias = KEY_ALIAS,\n cipher = cipher,\n )\n\n private object Keys {\n const val OPENAI_KEY = \"openai_api_key\"\n const val OPENAI_MODEL = \"openai_model\"\n const val ANTHROPIC_KEY = \"anthropic_api_key\"\n const val ANTHROPIC_MODEL = \"anthropic_model\"\n const val GOOGLE_KEY = \"google_api_key\"\n const val GOOGLE_MODEL = \"google_model\"\n const val DEEPSEEK_KEY = \"deepseek_api_key\"\n const val DEEPSEEK_MODEL = \"deepseek_model\"\n const val OLLAMA_URL = \"ollama_base_url\"\n const val OLLAMA_MODEL = \"ollama_model\"\n const val OLLAMA_CONTEXT = \"ollama_context\"\n }\n\n // Mutable state flows to allow reactive observing of key changes\n private val _openAIKeyFlow by lazy { MutableStateFlow(readOrNull(Keys.OPENAI_KEY)) }\n private val _openAIModelFlow by lazy { MutableStateFlow(readOrNull(Keys.OPENAI_MODEL)) }\n private val _anthropicKeyFlow by lazy { MutableStateFlow(readOrNull(Keys.ANTHROPIC_KEY)) }\n private val _anthropicModelFlow by lazy { MutableStateFlow(readOrNull(Keys.ANTHROPIC_MODEL)) }\n private val _googleKeyFlow by lazy { MutableStateFlow(readOrNull(Keys.GOOGLE_KEY)) }\n private val _googleModelFlow by lazy { MutableStateFlow(readOrNull(Keys.GOOGLE_MODEL)) }\n private val _deepSeekKeyFlow by lazy { MutableStateFlow(readOrNull(Keys.DEEPSEEK_KEY)) }\n private val _deepSeekModelFlow by lazy { MutableStateFlow(readOrNull(Keys.DEEPSEEK_MODEL)) }\n private val _ollamaUrlFlow by lazy { MutableStateFlow(readOrNull(Keys.OLLAMA_URL)) }\n private val _ollamaModelFlow by lazy { MutableStateFlow(readOrNull(Keys.OLLAMA_MODEL)) }\n private val _ollamaContextFlow by lazy {\n MutableStateFlow(\n readOrNull(Keys.OLLAMA_CONTEXT)?.toIntOrNull() ?: SettingsDefaults.OLLAMA_CONTEXT_WINDOW_DEFAULT,\n )\n }\n\n override fun getOpenAIKey(): Flow = _openAIKeyFlow.asStateFlow()\n\n override suspend fun setOpenAIKey(key: String?) {\n saveString(Keys.OPENAI_KEY, key)\n _openAIKeyFlow.value = key\n }\n\n override fun getOpenAIModel(): Flow = _openAIModelFlow.asStateFlow()\n\n override suspend fun setOpenAIModel(model: String?) {\n saveString(Keys.OPENAI_MODEL, model)\n _openAIModelFlow.value = model\n }\n\n override fun getAnthropicKey(): Flow = _anthropicKeyFlow.asStateFlow()\n\n override suspend fun setAnthropicKey(key: String?) {\n saveString(Keys.ANTHROPIC_KEY, key)\n _anthropicKeyFlow.value = key\n }\n\n override fun getAnthropicModel(): Flow = _anthropicModelFlow.asStateFlow()\n\n override suspend fun setAnthropicModel(model: String?) {\n saveString(Keys.ANTHROPIC_MODEL, model)\n _anthropicModelFlow.value = model\n }\n\n override fun getGoogleKey(): Flow = _googleKeyFlow.asStateFlow()\n\n override suspend fun setGoogleKey(key: String?) {\n saveString(Keys.GOOGLE_KEY, key)\n _googleKeyFlow.value = key\n }\n\n override fun getGoogleModel(): Flow = _googleModelFlow.asStateFlow()\n\n override suspend fun setGoogleModel(model: String?) {\n saveString(Keys.GOOGLE_MODEL, model)\n _googleModelFlow.value = model\n }\n\n override fun getDeepSeekKey(): Flow = _deepSeekKeyFlow.asStateFlow()\n\n override suspend fun setDeepSeekKey(key: String?) {\n saveString(Keys.DEEPSEEK_KEY, key)\n _deepSeekKeyFlow.value = key\n }\n\n override fun getDeepSeekModel(): Flow = _deepSeekModelFlow.asStateFlow()\n\n override suspend fun setDeepSeekModel(model: String?) {\n saveString(Keys.DEEPSEEK_MODEL, model)\n _deepSeekModelFlow.value = model\n }\n\n override fun getOllamaBaseUrl(): Flow = _ollamaUrlFlow.asStateFlow()\n\n override suspend fun setOllamaBaseUrl(url: String?) {\n saveString(Keys.OLLAMA_URL, url)\n _ollamaUrlFlow.value = url\n }\n\n override fun getOllamaModelName(): Flow = _ollamaModelFlow.asStateFlow()\n\n override suspend fun setOllamaModelName(model: String?) {\n saveString(Keys.OLLAMA_MODEL, model)\n _ollamaModelFlow.value = model\n }\n\n override fun getOllamaContextWindowSize(): Flow = _ollamaContextFlow.asStateFlow()\n\n override suspend fun setOllamaContextWindowSize(size: Int) {\n store.putString(Keys.OLLAMA_CONTEXT, size.toString(), synchronous = false)\n _ollamaContextFlow.value = size\n }\n\n /**\n * Reads a stored value, applying the re-enterable-secret recovery policy: an entry that\n * cannot be decrypted is dropped and reported as absent instead of propagating the error.\n */\n private fun readOrNull(key: String): String? = try {\n store.getString(key)\n } catch (e: SecureValueUnreadableException) {\n Timber.e(e, \"Stored API-key entry '%s' is unreadable; treating it as unset.\", key)\n store.remove(key)\n null\n }\n\n private fun saveString(key: String, value: String?) {\n if (value == null) {\n store.remove(key)\n } else {\n store.putString(key, value, synchronous = false)\n }\n }\n\n private companion object {\n /** Name of the [KeystoreBackedPrefsStore] preferences file holding the API keys. */\n const val PREFS_NAME = \"secure_api_keys_v2\"\n\n /** Android Keystore alias of the AEAD key dedicated to the API-key store. */\n const val KEY_ALIAS = \"knotwork.api_keys\"\n }\n}\n"}, "6d416b05fe135e83ca2dec8daad27a5a5d8c278b": {"t": 1785755983.472331, "n": 7578, "h": "9db58b5a4083be02", "c": "package app.knotwork.android.data.local\n\nimport android.content.Context\nimport app.knotwork.android.data.local.crypto.AeadCipher\nimport app.knotwork.android.data.local.crypto.KeystoreBackedPrefsStore\nimport app.knotwork.android.data.local.crypto.SecureValueUnreadableException\nimport app.knotwork.android.domain.constants.SettingsDefaults\nimport app.knotwork.android.domain.repositories.ApiKeyRepository\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport timber.log.Timber\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\n/**\n * Concrete implementation of [ApiKeyRepository] that securely stores API keys\n * in a [KeystoreBackedPrefsStore] — values encrypted with AES-GCM under a\n * dedicated, non-exportable Android Keystore key.\n *\n * **Recovery semantics — deliberately different from [EncryptedDbPassphraseProvider].**\n * A stored value that can no longer be decrypted (e.g. the Keystore key was lost during a\n * backup/restore) is treated as absent: the corrupt entry is removed and the getter returns\n * `null`. That is safe here because API keys are user-re-enterable — the worst outcome is the\n * user pasting their keys again. The database passphrase provider must never do this: its\n * secret cannot be re-derived, and destroying it would render the encrypted database\n * permanently unreadable.\n *\n * Earlier releases kept the keys in `EncryptedSharedPreferences` (deprecated upstream and\n * removed from this project without a data migration, as permitted by the pre-release storage\n * policy); a leftover legacy file is ignored — previously stored keys simply have to be\n * re-entered once.\n *\n * @property context The application context used to create the shared preferences.\n * @param cipher The AEAD boundary used to protect the stored values.\n */\n@Singleton\nclass ApiKeyManager @Inject constructor(@ApplicationContext private val context: Context, cipher: AeadCipher) :\n ApiKeyRepository {\n\n private val store = KeystoreBackedPrefsStore(\n context = context,\n prefsName = PREFS_NAME,\n keyAlias = KEY_ALIAS,\n cipher = cipher,\n )\n\n private object Keys {\n const val OPENAI_KEY = \"openai_api_key\"\n const val OPENAI_MODEL = \"openai_model\"\n const val ANTHROPIC_KEY = \"anthropic_api_key\"\n const val ANTHROPIC_MODEL = \"anthropic_model\"\n const val GOOGLE_KEY = \"google_api_key\"\n const val GOOGLE_MODEL = \"google_model\"\n const val DEEPSEEK_KEY = \"deepseek_api_key\"\n const val DEEPSEEK_MODEL = \"deepseek_model\"\n const val OLLAMA_URL = \"ollama_base_url\"\n const val OLLAMA_MODEL = \"ollama_model\"\n const val OLLAMA_CONTEXT = \"ollama_context\"\n }\n\n // Mutable state flows to allow reactive observing of key changes\n private val _openAIKeyFlow by lazy { MutableStateFlow(readOrNull(Keys.OPENAI_KEY)) }\n private val _openAIModelFlow by lazy { MutableStateFlow(readOrNull(Keys.OPENAI_MODEL)) }\n private val _anthropicKeyFlow by lazy { MutableStateFlow(readOrNull(Keys.ANTHROPIC_KEY)) }\n private val _anthropicModelFlow by lazy { MutableStateFlow(readOrNull(Keys.ANTHROPIC_MODEL)) }\n private val _googleKeyFlow by lazy { MutableStateFlow(readOrNull(Keys.GOOGLE_KEY)) }\n private val _googleModelFlow by lazy { MutableStateFlow(readOrNull(Keys.GOOGLE_MODEL)) }\n private val _deepSeekKeyFlow by lazy { MutableStateFlow(readOrNull(Keys.DEEPSEEK_KEY)) }\n private val _deepSeekModelFlow by lazy { MutableStateFlow(readOrNull(Keys.DEEPSEEK_MODEL)) }\n private val _ollamaUrlFlow by lazy { MutableStateFlow(readOrNull(Keys.OLLAMA_URL)) }\n private val _ollamaModelFlow by lazy { MutableStateFlow(readOrNull(Keys.OLLAMA_MODEL)) }\n private val _ollamaContextFlow by lazy {\n MutableStateFlow(\n readOrNull(Keys.OLLAMA_CONTEXT)?.toIntOrNull() ?: SettingsDefaults.OLLAMA_CONTEXT_WINDOW_DEFAULT,\n )\n }\n\n override fun getOpenAIKey(): Flow = _openAIKeyFlow.asStateFlow()\n\n override suspend fun setOpenAIKey(key: String?) {\n saveString(Keys.OPENAI_KEY, key)\n _openAIKeyFlow.value = key\n }\n\n override fun getOpenAIModel(): Flow = _openAIModelFlow.asStateFlow()\n\n override suspend fun setOpenAIModel(model: String?) {\n saveString(Keys.OPENAI_MODEL, model)\n _openAIModelFlow.value = model\n }\n\n override fun getAnthropicKey(): Flow = _anthropicKeyFlow.asStateFlow()\n\n override suspend fun setAnthropicKey(key: String?) {\n saveString(Keys.ANTHROPIC_KEY, key)\n _anthropicKeyFlow.value = key\n }\n\n override fun getAnthropicModel(): Flow = _anthropicModelFlow.asStateFlow()\n\n override suspend fun setAnthropicModel(model: String?) {\n saveString(Keys.ANTHROPIC_MODEL, model)\n _anthropicModelFlow.value = model\n }\n\n override fun getGoogleKey(): Flow = _googleKeyFlow.asStateFlow()\n\n override suspend fun setGoogleKey(key: String?) {\n saveString(Keys.GOOGLE_KEY, key)\n _googleKeyFlow.value = key\n }\n\n override fun getGoogleModel(): Flow = _googleModelFlow.asStateFlow()\n\n override suspend fun setGoogleModel(model: String?) {\n saveString(Keys.GOOGLE_MODEL, model)\n _googleModelFlow.value = model\n }\n\n override fun getDeepSeekKey(): Flow = _deepSeekKeyFlow.asStateFlow()\n\n override suspend fun setDeepSeekKey(key: String?) {\n saveString(Keys.DEEPSEEK_KEY, key)\n _deepSeekKeyFlow.value = key\n }\n\n override fun getDeepSeekModel(): Flow = _deepSeekModelFlow.asStateFlow()\n\n override suspend fun setDeepSeekModel(model: String?) {\n saveString(Keys.DEEPSEEK_MODEL, model)\n _deepSeekModelFlow.value = model\n }\n\n override fun getOllamaBaseUrl(): Flow = _ollamaUrlFlow.asStateFlow()\n\n override suspend fun setOllamaBaseUrl(url: String?) {\n saveString(Keys.OLLAMA_URL, url)\n _ollamaUrlFlow.value = url\n }\n\n override fun getOllamaModelName(): Flow = _ollamaModelFlow.asStateFlow()\n\n override suspend fun setOllamaModelName(model: String?) {\n saveString(Keys.OLLAMA_MODEL, model)\n _ollamaModelFlow.value = model\n }\n\n override fun getOllamaContextWindowSize(): Flow = _ollamaContextFlow.asStateFlow()\n\n override suspend fun setOllamaContextWindowSize(size: Int) {\n store.putString(Keys.OLLAMA_CONTEXT, size.toString())\n _ollamaContextFlow.value = size\n }\n\n /**\n * Reads a stored value, applying the re-enterable-secret recovery policy: an entry that\n * cannot be decrypted is dropped and reported as absent instead of propagating the error.\n */\n private fun readOrNull(key: String): String? = try {\n store.getString(key)\n } catch (e: SecureValueUnreadableException) {\n Timber.e(e, \"Stored API-key entry '%s' is unreadable; treating it as unset.\", key)\n store.remove(key)\n null\n }\n\n private fun saveString(key: String, value: String?) {\n if (value == null) {\n store.remove(key)\n } else {\n store.putString(key, value)\n }\n }\n\n private companion object {\n /** Name of the [KeystoreBackedPrefsStore] preferences file holding the API keys. */\n const val PREFS_NAME = \"secure_api_keys_v2\"\n\n /** Android Keystore alias of the AEAD key dedicated to the API-key store. */\n const val KEY_ALIAS = \"knotwork.api_keys\"\n }\n}\n"}, "e762d90f3ff215111ee61e623dad05ad2d782266": {"t": 1785755984.962798, "n": 8002, "h": "8c8c95dc8651750c", "c": "package app.knotwork.android.data.local\n\nimport android.content.Context\nimport android.content.SharedPreferences\nimport androidx.core.content.edit\nimport androidx.security.crypto.EncryptedSharedPreferences\nimport androidx.security.crypto.MasterKey\nimport app.knotwork.android.domain.constants.SettingsDefaults\nimport app.knotwork.android.domain.repositories.ApiKeyRepository\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport timber.log.Timber\nimport java.io.File\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\n/**\n * Concrete implementation of [ApiKeyRepository] that securely stores API keys\n * using Android's EncryptedSharedPreferences.\n *\n * **Recovery semantics — deliberately different from [EncryptedDbPassphraseProvider].**\n * When the encrypted preferences file cannot be opened (e.g. the Keystore master key was lost\n * during a backup/restore), this class deletes the corrupt store and recreates it empty. That\n * is safe here because API keys are user-re-enterable: the worst outcome is the user pasting\n * their keys again. The database passphrase provider must never do this — its secret cannot be\n * re-derived, and destroying it would render the encrypted database permanently unreadable.\n *\n * @property context The application context used to create the shared preferences.\n */\n@Singleton\nclass ApiKeyManager @Inject constructor(@ApplicationContext private val context: Context) : ApiKeyRepository {\n\n private val prefsName = \"secure_api_keys\"\n\n private val masterKey by lazy {\n MasterKey.Builder(context)\n .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)\n .build()\n }\n\n private val sharedPreferences: SharedPreferences by lazy {\n try {\n createEncryptedSharedPreferences()\n } catch (e: Exception) {\n Timber.e(\n e,\n \"Failed to initialize EncryptedSharedPreferences. Attempting recovery by clearing corrupt data.\",\n )\n // Recovery path: if data is corrupted (e.g. key lost during backup/restore), delete the file and recreate\n deleteSharedPreferences(prefsName)\n createEncryptedSharedPreferences()\n }\n }\n\n private fun createEncryptedSharedPreferences(): SharedPreferences = EncryptedSharedPreferences.create(\n context,\n prefsName,\n masterKey,\n EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,\n EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,\n )\n\n private fun deleteSharedPreferences(name: String) {\n try {\n // Context.deleteSharedPreferences is available in API 24+\n context.deleteSharedPreferences(name)\n } catch (e: Exception) {\n // Fallback for older APIs or if deleteSharedPreferences fails\n val dir = File(context.applicationInfo.dataDir, \"shared_prefs\")\n val file = File(dir, \"$name.xml\")\n if (file.exists()) {\n file.delete()\n }\n }\n }\n\n private object Keys {\n const val OPENAI_KEY = \"openai_api_key\"\n const val OPENAI_MODEL = \"openai_model\"\n const val ANTHROPIC_KEY = \"anthropic_api_key\"\n const val ANTHROPIC_MODEL = \"anthropic_model\"\n const val GOOGLE_KEY = \"google_api_key\"\n const val GOOGLE_MODEL = \"google_model\"\n const val DEEPSEEK_KEY = \"deepseek_api_key\"\n const val DEEPSEEK_MODEL = \"deepseek_model\"\n const val OLLAMA_URL = \"ollama_base_url\"\n const val OLLAMA_MODEL = \"ollama_model\"\n const val OLLAMA_CONTEXT = \"ollama_context\"\n }\n\n // Mutable state flows to allow reactive observing of key changes\n private val _openAIKeyFlow by lazy { MutableStateFlow(sharedPreferences.getString(Keys.OPENAI_KEY, null)) }\n private val _openAIModelFlow by lazy { MutableStateFlow(sharedPreferences.getString(Keys.OPENAI_MODEL, null)) }\n private val _anthropicKeyFlow by lazy { MutableStateFlow(sharedPreferences.getString(Keys.ANTHROPIC_KEY, null)) }\n private val _anthropicModelFlow by lazy {\n MutableStateFlow(sharedPreferences.getString(Keys.ANTHROPIC_MODEL, null))\n }\n private val _googleKeyFlow by lazy { MutableStateFlow(sharedPreferences.getString(Keys.GOOGLE_KEY, null)) }\n private val _googleModelFlow by lazy { MutableStateFlow(sharedPreferences.getString(Keys.GOOGLE_MODEL, null)) }\n private val _deepSeekKeyFlow by lazy { MutableStateFlow(sharedPreferences.getString(Keys.DEEPSEEK_KEY, null)) }\n private val _deepSeekModelFlow by lazy { MutableStateFlow(sharedPreferences.getString(Keys.DEEPSEEK_MODEL, null)) }\n private val _ollamaUrlFlow by lazy { MutableStateFlow(sharedPreferences.getString(Keys.OLLAMA_URL, null)) }\n private val _ollamaModelFlow by lazy { MutableStateFlow(sharedPreferences.getString(Keys.OLLAMA_MODEL, null)) }\n private val _ollamaContextFlow by lazy {\n MutableStateFlow(sharedPreferences.getInt(Keys.OLLAMA_CONTEXT, SettingsDefaults.OLLAMA_CONTEXT_WINDOW_DEFAULT))\n }\n\n override fun getOpenAIKey(): Flow = _openAIKeyFlow.asStateFlow()\n\n override suspend fun setOpenAIKey(key: String?) {\n saveString(Keys.OPENAI_KEY, key)\n _openAIKeyFlow.value = key\n }\n\n override fun getOpenAIModel(): Flow = _openAIModelFlow.asStateFlow()\n\n override suspend fun setOpenAIModel(model: String?) {\n saveString(Keys.OPENAI_MODEL, model)\n _openAIModelFlow.value = model\n }\n\n override fun getAnthropicKey(): Flow = _anthropicKeyFlow.asStateFlow()\n\n override suspend fun setAnthropicKey(key: String?) {\n saveString(Keys.ANTHROPIC_KEY, key)\n _anthropicKeyFlow.value = key\n }\n\n override fun getAnthropicModel(): Flow = _anthropicModelFlow.asStateFlow()\n\n override suspend fun setAnthropicModel(model: String?) {\n saveString(Keys.ANTHROPIC_MODEL, model)\n _anthropicModelFlow.value = model\n }\n\n override fun getGoogleKey(): Flow = _googleKeyFlow.asStateFlow()\n\n override suspend fun setGoogleKey(key: String?) {\n saveString(Keys.GOOGLE_KEY, key)\n _googleKeyFlow.value = key\n }\n\n override fun getGoogleModel(): Flow = _googleModelFlow.asStateFlow()\n\n override suspend fun setGoogleModel(model: String?) {\n saveString(Keys.GOOGLE_MODEL, model)\n _googleModelFlow.value = model\n }\n\n override fun getDeepSeekKey(): Flow = _deepSeekKeyFlow.asStateFlow()\n\n override suspend fun setDeepSeekKey(key: String?) {\n saveString(Keys.DEEPSEEK_KEY, key)\n _deepSeekKeyFlow.value = key\n }\n\n override fun getDeepSeekModel(): Flow = _deepSeekModelFlow.asStateFlow()\n\n override suspend fun setDeepSeekModel(model: String?) {\n saveString(Keys.DEEPSEEK_MODEL, model)\n _deepSeekModelFlow.value = model\n }\n\n override fun getOllamaBaseUrl(): Flow = _ollamaUrlFlow.asStateFlow()\n\n override suspend fun setOllamaBaseUrl(url: String?) {\n saveString(Keys.OLLAMA_URL, url)\n _ollamaUrlFlow.value = url\n }\n\n override fun getOllamaModelName(): Flow = _ollamaModelFlow.asStateFlow()\n\n override suspend fun setOllamaModelName(model: String?) {\n saveString(Keys.OLLAMA_MODEL, model)\n _ollamaModelFlow.value = model\n }\n\n override fun getOllamaContextWindowSize(): Flow = _ollamaContextFlow.asStateFlow()\n\n override suspend fun setOllamaContextWindowSize(size: Int) {\n sharedPreferences.edit {\n putInt(Keys.OLLAMA_CONTEXT, size)\n }\n _ollamaContextFlow.value = size\n }\n\n private fun saveString(key: String, value: String?) {\n sharedPreferences.edit {\n if (value == null) {\n remove(key)\n } else {\n putString(key, value)\n }\n }\n }\n}\n"}, "ea514488abe4bfd2995e1a3c994c4aefc6bae63e": {"t": 1785755986.7984853, "n": 7437, "h": "d7298368cafa23e8", "c": "package app.knotwork.android.data.local\n\nimport android.content.Context\nimport android.content.SharedPreferences\nimport androidx.core.content.edit\nimport androidx.security.crypto.EncryptedSharedPreferences\nimport androidx.security.crypto.MasterKey\nimport app.knotwork.android.domain.constants.SettingsDefaults\nimport app.knotwork.android.domain.repositories.ApiKeyRepository\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport timber.log.Timber\nimport java.io.File\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\n/**\n * Concrete implementation of [ApiKeyRepository] that securely stores API keys\n * using Android's EncryptedSharedPreferences.\n *\n * @property context The application context used to create the shared preferences.\n */\n@Singleton\nclass ApiKeyManager @Inject constructor(@ApplicationContext private val context: Context) : ApiKeyRepository {\n\n private val prefsName = \"secure_api_keys\"\n\n private val masterKey by lazy {\n MasterKey.Builder(context)\n .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)\n .build()\n }\n\n private val sharedPreferences: SharedPreferences by lazy {\n try {\n createEncryptedSharedPreferences()\n } catch (e: Exception) {\n Timber.e(\n e,\n \"Failed to initialize EncryptedSharedPreferences. Attempting recovery by clearing corrupt data.\",\n )\n // Recovery path: if data is corrupted (e.g. key lost during backup/restore), delete the file and recreate\n deleteSharedPreferences(prefsName)\n createEncryptedSharedPreferences()\n }\n }\n\n private fun createEncryptedSharedPreferences(): SharedPreferences = EncryptedSharedPreferences.create(\n context,\n prefsName,\n masterKey,\n EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,\n EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,\n )\n\n private fun deleteSharedPreferences(name: String) {\n try {\n // Context.deleteSharedPreferences is available in API 24+\n context.deleteSharedPreferences(name)\n } catch (e: Exception) {\n // Fallback for older APIs or if deleteSharedPreferences fails\n val dir = File(context.applicationInfo.dataDir, \"shared_prefs\")\n val file = File(dir, \"$name.xml\")\n if (file.exists()) {\n file.delete()\n }\n }\n }\n\n private object Keys {\n const val OPENAI_KEY = \"openai_api_key\"\n const val OPENAI_MODEL = \"openai_model\"\n const val ANTHROPIC_KEY = \"anthropic_api_key\"\n const val ANTHROPIC_MODEL = \"anthropic_model\"\n const val GOOGLE_KEY = \"google_api_key\"\n const val GOOGLE_MODEL = \"google_model\"\n const val DEEPSEEK_KEY = \"deepseek_api_key\"\n const val DEEPSEEK_MODEL = \"deepseek_model\"\n const val OLLAMA_URL = \"ollama_base_url\"\n const val OLLAMA_MODEL = \"ollama_model\"\n const val OLLAMA_CONTEXT = \"ollama_context\"\n }\n\n // Mutable state flows to allow reactive observing of key changes\n private val _openAIKeyFlow by lazy { MutableStateFlow(sharedPreferences.getString(Keys.OPENAI_KEY, null)) }\n private val _openAIModelFlow by lazy { MutableStateFlow(sharedPreferences.getString(Keys.OPENAI_MODEL, null)) }\n private val _anthropicKeyFlow by lazy { MutableStateFlow(sharedPreferences.getString(Keys.ANTHROPIC_KEY, null)) }\n private val _anthropicModelFlow by lazy {\n MutableStateFlow(sharedPreferences.getString(Keys.ANTHROPIC_MODEL, null))\n }\n private val _googleKeyFlow by lazy { MutableStateFlow(sharedPreferences.getString(Keys.GOOGLE_KEY, null)) }\n private val _googleModelFlow by lazy { MutableStateFlow(sharedPreferences.getString(Keys.GOOGLE_MODEL, null)) }\n private val _deepSeekKeyFlow by lazy { MutableStateFlow(sharedPreferences.getString(Keys.DEEPSEEK_KEY, null)) }\n private val _deepSeekModelFlow by lazy { MutableStateFlow(sharedPreferences.getString(Keys.DEEPSEEK_MODEL, null)) }\n private val _ollamaUrlFlow by lazy { MutableStateFlow(sharedPreferences.getString(Keys.OLLAMA_URL, null)) }\n private val _ollamaModelFlow by lazy { MutableStateFlow(sharedPreferences.getString(Keys.OLLAMA_MODEL, null)) }\n private val _ollamaContextFlow by lazy {\n MutableStateFlow(sharedPreferences.getInt(Keys.OLLAMA_CONTEXT, SettingsDefaults.OLLAMA_CONTEXT_WINDOW_DEFAULT))\n }\n\n override fun getOpenAIKey(): Flow = _openAIKeyFlow.asStateFlow()\n\n override suspend fun setOpenAIKey(key: String?) {\n saveString(Keys.OPENAI_KEY, key)\n _openAIKeyFlow.value = key\n }\n\n override fun getOpenAIModel(): Flow = _openAIModelFlow.asStateFlow()\n\n override suspend fun setOpenAIModel(model: String?) {\n saveString(Keys.OPENAI_MODEL, model)\n _openAIModelFlow.value = model\n }\n\n override fun getAnthropicKey(): Flow = _anthropicKeyFlow.asStateFlow()\n\n override suspend fun setAnthropicKey(key: String?) {\n saveString(Keys.ANTHROPIC_KEY, key)\n _anthropicKeyFlow.value = key\n }\n\n override fun getAnthropicModel(): Flow = _anthropicModelFlow.asStateFlow()\n\n override suspend fun setAnthropicModel(model: String?) {\n saveString(Keys.ANTHROPIC_MODEL, model)\n _anthropicModelFlow.value = model\n }\n\n override fun getGoogleKey(): Flow = _googleKeyFlow.asStateFlow()\n\n override suspend fun setGoogleKey(key: String?) {\n saveString(Keys.GOOGLE_KEY, key)\n _googleKeyFlow.value = key\n }\n\n override fun getGoogleModel(): Flow = _googleModelFlow.asStateFlow()\n\n override suspend fun setGoogleModel(model: String?) {\n saveString(Keys.GOOGLE_MODEL, model)\n _googleModelFlow.value = model\n }\n\n override fun getDeepSeekKey(): Flow = _deepSeekKeyFlow.asStateFlow()\n\n override suspend fun setDeepSeekKey(key: String?) {\n saveString(Keys.DEEPSEEK_KEY, key)\n _deepSeekKeyFlow.value = key\n }\n\n override fun getDeepSeekModel(): Flow = _deepSeekModelFlow.asStateFlow()\n\n override suspend fun setDeepSeekModel(model: String?) {\n saveString(Keys.DEEPSEEK_MODEL, model)\n _deepSeekModelFlow.value = model\n }\n\n override fun getOllamaBaseUrl(): Flow = _ollamaUrlFlow.asStateFlow()\n\n override suspend fun setOllamaBaseUrl(url: String?) {\n saveString(Keys.OLLAMA_URL, url)\n _ollamaUrlFlow.value = url\n }\n\n override fun getOllamaModelName(): Flow = _ollamaModelFlow.asStateFlow()\n\n override suspend fun setOllamaModelName(model: String?) {\n saveString(Keys.OLLAMA_MODEL, model)\n _ollamaModelFlow.value = model\n }\n\n override fun getOllamaContextWindowSize(): Flow = _ollamaContextFlow.asStateFlow()\n\n override suspend fun setOllamaContextWindowSize(size: Int) {\n sharedPreferences.edit {\n putInt(Keys.OLLAMA_CONTEXT, size)\n }\n _ollamaContextFlow.value = size\n }\n\n private fun saveString(key: String, value: String?) {\n sharedPreferences.edit {\n if (value == null) {\n remove(key)\n } else {\n putString(key, value)\n }\n }\n }\n}\n"}}}}