mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-11-10 17:46:53 +01:00
Beginnings for the migration to DataStore
This commit is contained in:
@@ -237,6 +237,7 @@ dependencies {
|
|||||||
|
|
||||||
// Encrypted Key Storage
|
// Encrypted Key Storage
|
||||||
implementation libs.androidx.security.crypto.ktx
|
implementation libs.androidx.security.crypto.ktx
|
||||||
|
implementation libs.androidx.datastore.preferences
|
||||||
|
|
||||||
// view videos
|
// view videos
|
||||||
implementation libs.androidx.media3.exoplayer
|
implementation libs.androidx.media3.exoplayer
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.model.preferences
|
||||||
|
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
|
import com.vitorpamplona.quartz.utils.LargeCache
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class AccountPreferenceStores(
|
||||||
|
val rootFilesDir: () -> File,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
val defaultHomeFollowList = stringPreferencesKey("defaultHomeFollowList")
|
||||||
|
val defaultStoriesFollowList = stringPreferencesKey("defaultStoriesFollowList")
|
||||||
|
val defaultNotificationFollowList = stringPreferencesKey("defaultNotificationFollowList")
|
||||||
|
val defaultDiscoveryFollowList = stringPreferencesKey("defaultDiscoveryFollowList")
|
||||||
|
|
||||||
|
val localRelayServers = stringPreferencesKey("localRelayServers")
|
||||||
|
val defaultFileServer = stringPreferencesKey("defaultFileServer")
|
||||||
|
|
||||||
|
val latestUserMetadata = stringPreferencesKey("latestUserMetadata")
|
||||||
|
val latestContactList = stringPreferencesKey("latestContactList")
|
||||||
|
val latestDMRelayList = stringPreferencesKey("latestDMRelayList")
|
||||||
|
val latestNIP65RelayList = stringPreferencesKey("latestNIP65RelayList")
|
||||||
|
val latestSearchRelayList = stringPreferencesKey("latestSearchRelayList")
|
||||||
|
val latestBlockedRelayList = stringPreferencesKey("latestBlockedRelayList")
|
||||||
|
val latestTrustedRelayList = stringPreferencesKey("latestTrustedRelayList")
|
||||||
|
val latestMuteList = stringPreferencesKey("latestMuteList")
|
||||||
|
val latestPrivateHomeRelayList = stringPreferencesKey("latestPrivateHomeRelayList")
|
||||||
|
val latestAppSpecificData = stringPreferencesKey("latestAppSpecificData")
|
||||||
|
val latestChannelList = stringPreferencesKey("latestChannelList")
|
||||||
|
val latestCommunityList = stringPreferencesKey("latestCommunityList")
|
||||||
|
val latestHashtagList = stringPreferencesKey("latestHashtagList")
|
||||||
|
val latestGeohashList = stringPreferencesKey("latestGeohashList")
|
||||||
|
val latestEphemeralChatList = stringPreferencesKey("latestEphemeralChatList")
|
||||||
|
|
||||||
|
val hideDeleteRequestDialog = stringPreferencesKey("hideDeleteRequestDialog")
|
||||||
|
val hideBlockAlertDialog = stringPreferencesKey("hideBlockAlertDialog")
|
||||||
|
val hideNip17WarningDialog = stringPreferencesKey("hideNip17WarningDialog")
|
||||||
|
|
||||||
|
val torSettings = stringPreferencesKey("tor_settings")
|
||||||
|
|
||||||
|
val hasDonatedInVersion = stringPreferencesKey("hasDonatedInVersion")
|
||||||
|
}
|
||||||
|
|
||||||
|
private val storeCache = LargeCache<String, DataStore<Preferences>>()
|
||||||
|
|
||||||
|
fun file(npub: String) = File(rootFilesDir(), "datastore/$npub.preferences")
|
||||||
|
|
||||||
|
private fun getDataStore(npub: String): DataStore<Preferences> =
|
||||||
|
storeCache.getOrCreate(npub) {
|
||||||
|
PreferenceDataStoreFactory.create(
|
||||||
|
produceFile = { file(npub) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeAccount(npub: String) {
|
||||||
|
file(npub).delete()
|
||||||
|
storeCache.remove(npub)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.model.preferences
|
||||||
|
|
||||||
|
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||||
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
|
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||||
|
import com.vitorpamplona.quartz.utils.LargeCache
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class AccountSecretsEncryptedStores(
|
||||||
|
val rootFilesDir: () -> File,
|
||||||
|
) {
|
||||||
|
companion object Companion {
|
||||||
|
val encryption = KeyStoreEncryption()
|
||||||
|
val key = stringPreferencesKey("privKey")
|
||||||
|
val nwc = stringPreferencesKey("nwc")
|
||||||
|
}
|
||||||
|
|
||||||
|
private val storeCache = LargeCache<String, EncryptedDataStore>()
|
||||||
|
|
||||||
|
fun file(npub: String) = File(rootFilesDir(), "datastore/$npub.secrets")
|
||||||
|
|
||||||
|
private fun getDataStore(npub: String): EncryptedDataStore =
|
||||||
|
storeCache.getOrCreate(npub) {
|
||||||
|
EncryptedDataStore(
|
||||||
|
PreferenceDataStoreFactory.create(
|
||||||
|
produceFile = { file(npub) },
|
||||||
|
),
|
||||||
|
encryption,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getPrivateKey(npub: String): String? = getDataStore(npub).get(key)
|
||||||
|
|
||||||
|
suspend fun savePrivateKey(
|
||||||
|
npub: String,
|
||||||
|
value: HexKey,
|
||||||
|
) {
|
||||||
|
getDataStore(npub).save(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun nwc(npub: String): UpdatablePropertyFlow<Nip47WalletConnect.Nip47URI> =
|
||||||
|
getDataStore(npub).getProperty(
|
||||||
|
key = nwc,
|
||||||
|
parser = Nip47WalletConnect.Nip47URI::parser,
|
||||||
|
serializer = Nip47WalletConnect.Nip47URI::serializer,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun removeAccount(npub: String) {
|
||||||
|
file(npub).delete()
|
||||||
|
storeCache.remove(npub)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.model.preferences
|
||||||
|
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.core.edit
|
||||||
|
import androidx.datastore.preferences.core.emptyPreferences
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav.scope
|
||||||
|
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect.Nip47URI.Companion.parser
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
suspend fun <T> DataStore<Preferences>.getProperty(
|
||||||
|
key: Preferences.Key<String>,
|
||||||
|
parser: (String) -> T,
|
||||||
|
serializer: (T) -> String,
|
||||||
|
): UpdatablePropertyFlow<T> =
|
||||||
|
UpdatablePropertyFlow<T>(
|
||||||
|
flow =
|
||||||
|
data
|
||||||
|
.catch { e ->
|
||||||
|
if (e is IOException) emit(emptyPreferences()) else throw e
|
||||||
|
}.map { prefs ->
|
||||||
|
val value = prefs[key]
|
||||||
|
if (value != null) {
|
||||||
|
parser(value)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update = { newValue ->
|
||||||
|
if (newValue != null) {
|
||||||
|
val serialized = serializer(newValue)
|
||||||
|
if (serialized.isNotBlank()) {
|
||||||
|
edit { prefs ->
|
||||||
|
prefs[key] = serialized
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
edit { prefs ->
|
||||||
|
prefs.remove(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
edit { prefs ->
|
||||||
|
prefs.remove(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scope = scope,
|
||||||
|
)
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.model.preferences
|
||||||
|
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.core.edit
|
||||||
|
import androidx.datastore.preferences.core.emptyPreferences
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav.scope
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.Base64
|
||||||
|
|
||||||
|
class EncryptedDataStore(
|
||||||
|
private val store: DataStore<Preferences>,
|
||||||
|
private val encryption: KeyStoreEncryption = KeyStoreEncryption(),
|
||||||
|
) {
|
||||||
|
private fun decode(str: String): ByteArray = Base64.getDecoder().decode(str)
|
||||||
|
|
||||||
|
private fun encode(bytes: ByteArray): String = Base64.getEncoder().encodeToString(bytes)
|
||||||
|
|
||||||
|
private fun encrypt(value: String): String = encode(encryption.encrypt(value.toByteArray()))
|
||||||
|
|
||||||
|
private fun decrypt(value: String): String = encryption.decrypt(decode(value)).contentToString()
|
||||||
|
|
||||||
|
suspend fun remove(key: Preferences.Key<String>) {
|
||||||
|
store.edit { prefs ->
|
||||||
|
prefs.remove(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun save(
|
||||||
|
key: Preferences.Key<String>,
|
||||||
|
value: String,
|
||||||
|
) {
|
||||||
|
store.edit { prefs ->
|
||||||
|
prefs[key] = encrypt(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun get(key: Preferences.Key<String>): String? =
|
||||||
|
store.data
|
||||||
|
.catch { e ->
|
||||||
|
if (e is IOException) emit(emptyPreferences()) else throw e
|
||||||
|
}.firstOrNull()
|
||||||
|
?.get(key)
|
||||||
|
?.let { decrypt(it) }
|
||||||
|
|
||||||
|
suspend fun <T> getProperty(
|
||||||
|
key: Preferences.Key<String>,
|
||||||
|
parser: (String) -> T,
|
||||||
|
serializer: (T) -> String,
|
||||||
|
): UpdatablePropertyFlow<T> =
|
||||||
|
UpdatablePropertyFlow<T>(
|
||||||
|
flow =
|
||||||
|
store.data
|
||||||
|
.catch { e ->
|
||||||
|
if (e is IOException) emit(emptyPreferences()) else throw e
|
||||||
|
}.map { prefs ->
|
||||||
|
val value = prefs[key]
|
||||||
|
if (value != null) {
|
||||||
|
val decrypted = decrypt(value)
|
||||||
|
if (decrypted.isNotBlank()) {
|
||||||
|
parser(decrypted)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update = { newValue ->
|
||||||
|
if (newValue != null) {
|
||||||
|
val serialized = serializer(newValue)
|
||||||
|
if (serialized.isNotBlank()) {
|
||||||
|
save(key, serialized)
|
||||||
|
} else {
|
||||||
|
remove(key)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
remove(key)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scope = scope,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.model.preferences
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.security.keystore.KeyGenParameterSpec
|
||||||
|
import android.security.keystore.KeyProperties
|
||||||
|
import android.security.keystore.StrongBoxUnavailableException
|
||||||
|
import java.security.KeyStore
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.KeyGenerator
|
||||||
|
import javax.crypto.SecretKey
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
|
||||||
|
class KeyStoreEncryption {
|
||||||
|
companion object {
|
||||||
|
private const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
|
||||||
|
private const val BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM
|
||||||
|
private const val PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
|
||||||
|
private const val TRANSFORMATION = "$ALGORITHM/$BLOCK_MODE/$PADDING"
|
||||||
|
private const val PURPOSE = KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
|
||||||
|
private const val KEY_ALIAS = "AMETHYST_AES_KEY"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||||
|
private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
|
||||||
|
|
||||||
|
private fun getKey(): SecretKey {
|
||||||
|
val existingKey = keyStore.getEntry(KEY_ALIAS, null) as? KeyStore.SecretKeyEntry
|
||||||
|
return existingKey?.secretKey ?: createKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createKeyStrongBoxIfAvailable(): SecretKey? =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
try {
|
||||||
|
val keyParams =
|
||||||
|
KeyGenParameterSpec
|
||||||
|
.Builder(KEY_ALIAS, PURPOSE)
|
||||||
|
.setBlockModes(BLOCK_MODE)
|
||||||
|
.setEncryptionPaddings(PADDING)
|
||||||
|
.setIsStrongBoxBacked(true)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val generator = KeyGenerator.getInstance(ALGORITHM)
|
||||||
|
generator.init(keyParams)
|
||||||
|
generator.generateKey()
|
||||||
|
} catch (_: StrongBoxUnavailableException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createKeyRegular(): SecretKey {
|
||||||
|
val keyParams =
|
||||||
|
KeyGenParameterSpec
|
||||||
|
.Builder(KEY_ALIAS, PURPOSE)
|
||||||
|
.setBlockModes(BLOCK_MODE)
|
||||||
|
.setEncryptionPaddings(PADDING)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val generator = KeyGenerator.getInstance(ALGORITHM)
|
||||||
|
generator.init(keyParams)
|
||||||
|
return generator.generateKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createKey(): SecretKey = createKeyStrongBoxIfAvailable() ?: createKeyRegular()
|
||||||
|
|
||||||
|
fun encrypt(bytes: ByteArray): ByteArray {
|
||||||
|
// Initializes the cipher in encrypt mode and encrypts data
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, getKey())
|
||||||
|
val iv = cipher.iv
|
||||||
|
val encrypted = cipher.doFinal(bytes)
|
||||||
|
return iv + encrypted
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decrypt(bytes: ByteArray): ByteArray? {
|
||||||
|
// Extracts IV and decrypts the data
|
||||||
|
val iv = bytes.copyOfRange(0, cipher.blockSize)
|
||||||
|
val data = bytes.copyOfRange(cipher.blockSize, bytes.size)
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, getKey(), IvParameterSpec(iv))
|
||||||
|
return cipher.doFinal(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.model.preferences
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
|
||||||
|
class UpdatablePropertyFlow<T>(
|
||||||
|
flow: Flow<T?>,
|
||||||
|
val update: suspend (T?) -> Unit,
|
||||||
|
val scope: CoroutineScope,
|
||||||
|
) {
|
||||||
|
val stateFlow =
|
||||||
|
flow
|
||||||
|
.flowOn(Dispatchers.Default)
|
||||||
|
.stateIn(
|
||||||
|
scope = scope,
|
||||||
|
started = SharingStarted.Eagerly,
|
||||||
|
initialValue = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ biometricKtx = "1.2.0-alpha05"
|
|||||||
coil = "3.3.0"
|
coil = "3.3.0"
|
||||||
composeBom = "2025.07.00"
|
composeBom = "2025.07.00"
|
||||||
coreKtx = "1.16.0"
|
coreKtx = "1.16.0"
|
||||||
|
datastore = "1.1.7"
|
||||||
espressoCore = "3.6.1"
|
espressoCore = "3.6.1"
|
||||||
firebaseBom = "34.0.0"
|
firebaseBom = "34.0.0"
|
||||||
fragmentKtx = "1.8.8"
|
fragmentKtx = "1.8.8"
|
||||||
@@ -69,6 +70,7 @@ androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "
|
|||||||
androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidxCamera" }
|
androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidxCamera" }
|
||||||
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
|
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" }
|
||||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||||
androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" }
|
androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" }
|
||||||
androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" }
|
androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" }
|
||||||
|
|||||||
Reference in New Issue
Block a user