Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-12 13:04:19 +03:00
parent e5408a2ac7
commit 89fbfa5971
20 changed files with 828 additions and 39 deletions

View file

@ -11,14 +11,18 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.models.scan.serialization.*
import com.tangem.domain.visa.model.VisaActivationRemoteState
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.sdk.storage.AndroidSecureStorage
import com.tangem.sdk.storage.AndroidSecureStorageV2
import com.tangem.sdk.storage.createEncryptedSharedPreferences
import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager
import com.tangem.tap.domain.userWalletList.implementation.GeneralUserWalletsListManager
import com.tangem.tap.domain.userWalletList.implementation.RuntimeUserWalletsListManager
import com.tangem.tap.domain.userWalletList.repository.DefaultUserWalletsListRepository
import com.tangem.tap.domain.userWalletList.repository.DelegatedKeystoreManager
import com.tangem.tap.domain.userWalletList.repository.UserWalletEncryptionKeysRepository
import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysStoreDecorator
import com.tangem.tap.domain.userWalletList.repository.implementation.BiometricUserWalletsKeysRepository
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultSelectedUserWalletRepository
@ -26,6 +30,7 @@ import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUse
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsSensitiveInformationRepository
import com.tangem.tap.tangemSdkManager
import com.tangem.utils.Provider
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -40,6 +45,7 @@ internal object UserWalletsListManagerModule {
@Provides
@Singleton
@Deprecated("Use UserWalletsListRepository instead")
fun provideGeneralUserWalletsListManager(
@ApplicationContext applicationContext: Context,
appPreferencesStore: AppPreferencesStore,
@ -58,42 +64,14 @@ internal object UserWalletsListManagerModule {
)
}
@Deprecated("Use UserWalletsListRepository instead")
private fun createBiometricUserWalletsListManager(
applicationContext: Context,
analyticsEventHandler: AnalyticsEventHandler,
dispatchers: CoroutineDispatcherProvider,
): UserWalletsListManager {
val moshi = Moshi.Builder()
.add(WalletDerivedKeysMapAdapter())
.add(ScanResponseDerivedKeysMapAdapter())
.add(ByteArrayKeyAdapter())
.add(ExtendedPublicKeysMapAdapter())
.add(CardBackupStatusAdapter())
.add(DerivationPathAdapterWithMigration())
.add(TangemSdkAdapter.DateAdapter())
.add(TangemSdkAdapter.DerivationNodeAdapter())
.add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model
.add(VisaActivationRemoteState.jsonAdapter)
.add(VisaCardActivationStatus.jsonAdapter)
.addLast(KotlinJsonAdapterFactory())
.build()
val secureStorage = AndroidSecureStorage(
preferences = SecureStorage.createEncryptedSharedPreferences(
context = applicationContext,
storageName = "user_wallets_storage",
),
androidSecureStorageV2 = AndroidSecureStorageV2(
appContext = applicationContext,
useStrongBox = true,
name = "user_wallets_storage2",
),
androidSecureStorageV3 = AndroidSecureStorageV2(
appContext = applicationContext,
useStrongBox = false,
name = "user_wallets_storage3",
),
)
val moshi = buildMoshi()
val secureStorage = buildSecureStorage(applicationContext = applicationContext)
val authenticatedStorage = AuthenticatedStorage(
secureStorage = UserWalletsKeysStoreDecorator(
@ -134,4 +112,94 @@ internal object UserWalletsListManagerModule {
selectedUserWalletRepository = selectedUserWalletRepository,
)
}
@Provides
@Singleton
fun provideUserWalletsListRepository(
@ApplicationContext applicationContext: Context,
dispatchers: CoroutineDispatcherProvider,
passwordRequester: HotWalletPasswordRequester,
): UserWalletsListRepository {
val moshi = buildMoshi()
val secureStorage = buildSecureStorage(applicationContext = applicationContext)
val authenticatedStorage = AuthenticatedStorage(
secureStorage = UserWalletsKeysStoreDecorator(
featureStorage = secureStorage,
cardSdkStorageProvider = Provider { tangemSdkManager.secureStorage },
),
keystoreManager = DelegatedKeystoreManager(
keystoreManagerProvider = Provider { tangemSdkManager.keystoreManager },
),
)
val publicInformationRepository = DefaultUserWalletsPublicInformationRepository(
moshi = moshi,
secureStorage = secureStorage,
)
val sensitiveInformationRepository = DefaultUserWalletsSensitiveInformationRepository(
moshi = moshi,
secureStorage = secureStorage,
)
val selectedUserWalletRepository = DefaultSelectedUserWalletRepository(
secureStorage = secureStorage,
dispatchers = dispatchers,
)
val userWalletEncryptionKeysRepository = UserWalletEncryptionKeysRepository(
moshi = moshi,
authenticatedStorage = authenticatedStorage,
dispatchers = dispatchers,
secureStorage = secureStorage,
)
return DefaultUserWalletsListRepository(
publicInformationRepository = publicInformationRepository,
sensitiveInformationRepository = sensitiveInformationRepository,
selectedUserWalletRepository = selectedUserWalletRepository,
passwordRequester = passwordRequester,
userWalletEncryptionKeysRepository = userWalletEncryptionKeysRepository,
tangemSdkManagerProvider = Provider { tangemSdkManager },
savePersistentInformation = ProviderSuspend { true }, // Always save persistent information for now
// TODO add a settings toggle to disable saving persistent information
)
}
fun buildMoshi(): Moshi {
return Moshi.Builder()
.add(WalletDerivedKeysMapAdapter())
.add(ScanResponseDerivedKeysMapAdapter())
.add(ByteArrayKeyAdapter())
.add(ExtendedPublicKeysMapAdapter())
.add(CardBackupStatusAdapter())
.add(DerivationPathAdapterWithMigration())
.add(TangemSdkAdapter.DateAdapter())
.add(TangemSdkAdapter.DerivationNodeAdapter())
.add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model
.add(VisaActivationRemoteState.jsonAdapter)
.add(VisaCardActivationStatus.jsonAdapter)
.addLast(KotlinJsonAdapterFactory())
.build()
}
fun buildSecureStorage(@ApplicationContext applicationContext: Context): SecureStorage {
return AndroidSecureStorage(
preferences = SecureStorage.createEncryptedSharedPreferences(
context = applicationContext,
storageName = "user_wallets_storage",
),
androidSecureStorageV2 = AndroidSecureStorageV2(
appContext = applicationContext,
useStrongBox = true,
name = "user_wallets_storage2",
),
androidSecureStorageV3 = AndroidSecureStorageV2(
appContext = applicationContext,
useStrongBox = false,
name = "user_wallets_storage3",
),
)
}
}

View file

@ -0,0 +1,357 @@
package com.tangem.tap.domain.userWalletList.repository
import arrow.core.Either
import arrow.core.left
import arrow.core.raise.either
import arrow.core.right
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.flatMap
import com.tangem.common.map
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.wallets.R
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.domain.core.wallets.error.DeleteWalletError
import com.tangem.domain.core.wallets.error.LockWalletsError
import com.tangem.domain.core.wallets.error.SaveWalletError
import com.tangem.domain.core.wallets.error.SelectWalletError
import com.tangem.domain.core.wallets.error.SetLockError
import com.tangem.domain.core.wallets.error.UnlockWalletError
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.core.wallets.UserWalletsListRepository.LockMethod
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
import com.tangem.tap.domain.userWalletList.utils.encryptionKey
import com.tangem.tap.domain.userWalletList.utils.lock
import com.tangem.tap.domain.userWalletList.utils.toUserWallets
import com.tangem.tap.domain.userWalletList.utils.updateWith
import com.tangem.utils.Provider
import com.tangem.utils.ProviderSuspend
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
@Suppress("LongParameterList")
internal class DefaultUserWalletsListRepository(
private val publicInformationRepository: UserWalletsPublicInformationRepository,
private val sensitiveInformationRepository: UserWalletsSensitiveInformationRepository,
private val selectedUserWalletRepository: SelectedUserWalletRepository,
private val passwordRequester: HotWalletPasswordRequester,
private val userWalletEncryptionKeysRepository: UserWalletEncryptionKeysRepository,
private val tangemSdkManagerProvider: Provider<TangemSdkManager>,
private val savePersistentInformation: ProviderSuspend<Boolean>,
) : UserWalletsListRepository {
override val userWallets = MutableStateFlow<List<UserWallet>?>(null)
override val selectedUserWallet = MutableStateFlow<UserWallet?>(null)
override suspend fun load() {
if (userWallets.value != null) return
if (savePersistentInformation().not()) {
// If we don't save persistent information, we don't need to load user wallets
// and we should clear any existing data
clearPersistentData()
userWallets.value = emptyList()
return
}
val unsecuredEncryptionKeys = userWalletEncryptionKeysRepository.getAllUnsecured()
publicInformationRepository.getAll()
.map { it.toUserWallets() }
.flatMap { wallets ->
sensitiveInformationRepository.getAll(unsecuredEncryptionKeys)
.map { wallets.updateWith(it) }
}.doOnSuccess {
userWallets.value = it
}
val selectedUserWalletId = selectedUserWalletRepository.get()
selectedUserWallet.value = userWallets.value?.firstOrNull { it.walletId == selectedUserWalletId }
?: userWallets.value?.firstOrNull()
}
override suspend fun userWalletsSync(): List<UserWallet> {
load()
return userWallets.value!!
}
override suspend fun selectedUserWalletSync(): UserWallet? {
load()
return selectedUserWallet.value
}
override suspend fun select(userWalletId: UserWalletId): Either<SelectWalletError, UserWallet> = either {
val userWallet = userWallets.value?.find { it.walletId == userWalletId }
?: raise(SelectWalletError.UnableToSelectUserWallet)
selectedUserWalletRepository.set(userWalletId)
selectedUserWallet.value = userWallet
userWallet
}
override suspend fun saveWithoutLock(
userWallet: UserWallet,
canOverride: Boolean,
): Either<SaveWalletError, UserWallet> = either {
if (canOverride.not() && userWallets.value?.any { it.walletId == userWallet.walletId } == true) {
raise(SaveWalletError.WalletAlreadySaved(messageId = R.string.user_wallet_list_error_wallet_already_saved))
}
if (savePersistentInformation()) {
publicInformationRepository.save(userWallet, canOverride)
if (userWallet.isLocked.not()) {
sensitiveInformationRepository.save(userWallet, userWallet.encryptionKey)
}
}
// update the userWallets state and add if it doesn't exist
userWallets.update { currentWallets ->
val wallets = currentWallets ?: emptyList()
if (wallets.any { it.walletId == userWallet.walletId }) {
wallets.map { if (it.walletId == userWallet.walletId) userWallet else it }
} else {
wallets + userWallet
}
}
// update the selectedUserWallet state if it is the only wallet
if (userWallets.value?.size == 1) {
selectedUserWalletRepository.set(userWallet.walletId)
selectedUserWallet.value = userWallet
}
userWallet
}
override suspend fun setLock(userWalletId: UserWalletId, lockMethod: LockMethod): Either<SetLockError, Unit> =
either {
val userWallet = userWallets.value?.find { it.walletId == userWalletId }
?: raise(SetLockError.UserWalletNotFound)
val encryptionKey = userWallet.encryptionKey
?: raise(SetLockError.UserWalletLocked)
runCatching {
userWalletEncryptionKeysRepository.save(
encryptionKey = UserWalletEncryptionKey(
walletId = userWalletId,
encryptionKey = encryptionKey,
),
method = when (lockMethod) {
is LockMethod.AccessCode -> {
UserWalletEncryptionKeysRepository.EncryptionMethod.Password(lockMethod.accessCode)
}
LockMethod.Biometric -> {
UserWalletEncryptionKeysRepository.EncryptionMethod.Biometric
}
LockMethod.NoLock -> {
if (userWallet is UserWallet.Cold) {
raise(SetLockError.UserWalletNotFound)
}
UserWalletEncryptionKeysRepository.EncryptionMethod.Unsecured
}
},
)
}.onFailure { raise(SetLockError.UnableToSetLock(it)) }
}
override suspend fun delete(userWalletIds: List<UserWalletId>): Either<DeleteWalletError, Unit> = either {
if (userWalletIds.isEmpty()) return Unit.right()
publicInformationRepository.delete(userWalletIds)
.doOnFailure {
raise(DeleteWalletError.UnableToDelete)
}
sensitiveInformationRepository.delete(userWalletIds)
.doOnFailure {
raise(DeleteWalletError.UnableToDelete)
}
userWalletEncryptionKeysRepository.delete(userWalletIds)
userWallets.update { currentWallets ->
currentWallets?.filterNot { it.walletId in userWalletIds }
}
selectedUserWallet.update { currentSelected ->
if (currentSelected == null) return@update null
userWallets.value?.findAvailableUserWallet(
userWallets.value?.indexOfFirst { it.walletId == currentSelected.walletId } ?: 0,
)
}
}
override suspend fun unlock(
userWalletId: UserWalletId,
unlockMethod: UserWalletsListRepository.UnlockMethod,
): Either<UnlockWalletError, Unit> = either {
val userWallet = userWallets.value?.find { it.walletId == userWalletId }
?: raise(UnlockWalletError.UserWalletNotFound)
if (userWallet.isLocked.not()) {
raise(UnlockWalletError.AlreadyUnlocked)
}
when (unlockMethod) {
UserWalletsListRepository.UnlockMethod.Biometric -> {
unlockAllWallets()
select(userWalletId)
}
UserWalletsListRepository.UnlockMethod.AccessCode -> {
if (userWallet !is UserWallet.Hot) {
raise(UnlockWalletError.UnableToUnlock)
}
val encryptionKey = requestPasswordRecursive(
block = { password ->
runCatching {
userWalletEncryptionKeysRepository.getEncryptedWithPassword(userWalletId, password)
}.onFailure {
raise(UnlockWalletError.UnableToUnlock)
}.getOrNull()
},
biometryFallback = {
unlock(userWalletId, UserWalletsListRepository.UnlockMethod.Biometric)
},
).bind()
if (encryptionKey == null) {
return@either
}
sensitiveInformationRepository.getAll(listOf(encryptionKey))
.doOnSuccess { userWallets.value?.updateWith(it) }
.doOnFailure { error ->
raise(UnlockWalletError.UnableToUnlock)
}
}
UserWalletsListRepository.UnlockMethod.Scan -> {
if (userWallet !is UserWallet.Cold) {
raise(UnlockWalletError.UnableToUnlock)
}
tangemSdkManagerProvider().scanProduct()
.doOnSuccess { scanResponse ->
val expectedId = UserWalletIdBuilder.scanResponse(scanResponse).build()
if (expectedId != userWallet.walletId) {
raise(UnlockWalletError.ScannedCardWalletNotMatched)
}
saveWithoutLock(userWallet.copy(scanResponse = scanResponse), canOverride = true)
.mapLeft { UnlockWalletError.UnableToUnlock }
.bind()
}
.doOnFailure {
raise(UnlockWalletError.UserCancelled)
}
}
}
}
override suspend fun unlockAllWallets(): Either<UnlockWalletError, Unit> = either {
val biometricKeys = runCatching {
userWalletEncryptionKeysRepository.getAllBiometric()
}.getOrElse {
// TODO handle error properly [REDACTED_TASK_KEY]
raise(UnlockWalletError.UserCancelled)
}
val unsecuredKeys = userWalletEncryptionKeysRepository.getAllUnsecured()
val allKeys = biometricKeys + unsecuredKeys
sensitiveInformationRepository.getAll(allKeys)
.doOnSuccess { userWallets.value?.updateWith(it) }
}
override suspend fun lockAllWallets(): Either<LockWalletsError, Unit> = either {
val unsecuredWalletIds = userWalletEncryptionKeysRepository.getAllUnsecured().map { it.walletId }.toSet()
if (unsecuredWalletIds.size == userWallets.value?.size) {
raise(LockWalletsError.NothingToLock)
}
userWallets.update {
it?.map {
if (it.walletId !in unsecuredWalletIds) {
it.lock()
} else {
it
}
}
}
}
override suspend fun clearPersistentData() {
publicInformationRepository.clear()
sensitiveInformationRepository.clear()
userWalletEncryptionKeysRepository.clear()
}
private suspend fun requestPasswordRecursive(
block: suspend (CharArray) -> UserWalletEncryptionKey?,
biometryFallback: suspend () -> Either<UnlockWalletError, Unit>,
): Either<UnlockWalletError, UserWalletEncryptionKey?> {
val result = passwordRequester.requestPassword(
hasBiometry = tangemSdkManagerProvider.invoke().needEnrollBiometrics,
)
return when (result) {
HotWalletPasswordRequester.Result.Dismiss -> {
passwordRequester.dismiss()
UnlockWalletError.UserCancelled.left()
}
is HotWalletPasswordRequester.Result.EnteredPassword -> {
val decrypted = block(result.password.value)
if (decrypted == null) {
passwordRequester.wrongPassword()
requestPasswordRecursive(block, biometryFallback)
} else {
passwordRequester.successfulAuthentication()
decrypted.right()
}
}
HotWalletPasswordRequester.Result.UseBiometry -> {
biometryFallback()
.onRight {
passwordRequester.successfulAuthentication()
}
passwordRequester.dismiss()
null.right()
}
}
}
/**
* Find the nearest available wallet that can be selected
*
* Example:
* Number with *n* is previous selected wallet with index [prevSelectedIndex].
*
* 1. [*1*, 2, 3, 4] => delete 1 => [2, 3, 4] => find and select => [*2*, 3, 4]
* 2. [1, *2*, 3, 4] => delete 2 => [1, 3, 4] => find and select => [1, *3*, 4]
* 3. [1, 2, *3*, 4] => delete 3 => [1, 2, 4] => find and select => [1, 2, *4*]
* 4. [1, 2, 3, *4*] => delete 4 => [1, 2, 3] => find and select => [1, 2, *3*]
*
* @receiver list of user wallets without deleted wallet
*/
private fun List<UserWallet>.findAvailableUserWallet(prevSelectedIndex: Int): UserWallet? {
if (prevSelectedIndex == 0) return firstOrNull { !it.isLocked } ?: firstOrNull()
if (prevSelectedIndex in indices && !this[prevSelectedIndex].isLocked) return this[prevSelectedIndex]
for (offset in 1..size) {
val rightIndex = prevSelectedIndex + offset
if (rightIndex in indices && !this[rightIndex].isLocked) return this[rightIndex]
val leftIndex = prevSelectedIndex - offset
if (leftIndex in indices && !this[leftIndex].isLocked) return this[leftIndex]
}
return lastOrNull()
}
}

View file

@ -0,0 +1,185 @@
package com.tangem.tap.domain.userWalletList.repository
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.tangem.common.authentication.storage.AuthenticatedStorage
import com.tangem.common.services.secure.SecureStorage
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.hot.sdk.android.crypto.AESEncryptionProtocol
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
internal class UserWalletEncryptionKeysRepository(
moshi: Moshi,
private val authenticatedStorage: AuthenticatedStorage,
private val dispatchers: CoroutineDispatcherProvider,
private val secureStorage: SecureStorage,
) {
private val encryptionKeyAdapter: JsonAdapter<UserWalletEncryptionKey> = moshi.adapter(
UserWalletEncryptionKey::class.java,
)
private val userWalletsIdsListAdapter: JsonAdapter<List<UserWalletId>> = moshi.adapter(
Types.newParameterizedType(List::class.java, UserWalletId::class.java),
)
suspend fun save(encryptionKey: UserWalletEncryptionKey, method: EncryptionMethod) = withContext(dispatchers.io) {
secureStorage.delete(StorageKey.UserWalletEncryptionKeyUnsecured(encryptionKey.walletId).name)
when (method) {
EncryptionMethod.Unsecured -> {
secureStorage.store(
account = StorageKey.UserWalletEncryptionKeyUnsecured(encryptionKey.walletId).name,
data = encryptionKey.encode(),
)
}
EncryptionMethod.Biometric -> {
authenticatedStorage.store(
keyAlias = StorageKey.UserWalletEncryptionKey(encryptionKey.walletId).name,
data = encryptionKey.encode(),
)
}
is EncryptionMethod.Password -> {
val encodedWithPass = AESEncryptionProtocol.encryptWithPassword(
password = method.password,
content = encryptionKey.encode(),
)
secureStorage.store(
account = StorageKey.UserWalletEncryptionKeyEncrypted(encryptionKey.walletId).name,
data = encodedWithPass,
)
}
}
storeUserWalletId(userWalletId = encryptionKey.walletId)
}
suspend fun getAllUnsecured(): List<UserWalletEncryptionKey> = withContext(dispatchers.io) {
getUserWalletsIds().mapNotNull { userWalletId ->
secureStorage.get(account = StorageKey.UserWalletEncryptionKeyUnsecured(userWalletId).name).decodeToKey()
}
}
suspend fun getEncryptedWithPassword(userWalletId: UserWalletId, password: CharArray): UserWalletEncryptionKey? {
val encrypted = secureStorage.get(
account = StorageKey.UserWalletEncryptionKeyEncrypted(userWalletId).name,
) ?: return null
val decrypted = AESEncryptionProtocol.decryptWithPassword(password, encrypted)
return decrypted.decodeToKey()
}
suspend fun getAllBiometric(): List<UserWalletEncryptionKey> = withContext(dispatchers.io) {
val keys = getUserWalletsIds().map { userWalletId ->
StorageKey.UserWalletEncryptionKey(userWalletId).name
}
authenticatedStorage.get(keys).mapNotNull {
it.value.decodeToKey()
}
}
suspend fun delete(userWalletIds: List<UserWalletId>) {
if (userWalletIds.isEmpty()) return
withContext(dispatchers.io) {
userWalletIds.forEach { userWalletId ->
secureStorage.delete(StorageKey.UserWalletEncryptionKeyUnsecured(userWalletId).name)
secureStorage.delete(StorageKey.UserWalletEncryptionKeyEncrypted(userWalletId).name)
authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name)
}
val userWalletsIds = getUserWalletsIds().filterNot { it in userWalletIds }
secureStorage.store(userWalletsIds.encode(), StorageKey.UserWalletIds.name)
}
}
suspend fun clear() {
withContext(dispatchers.io) {
val userWalletsIds = getUserWalletsIds()
userWalletsIds.forEach { userWalletId ->
secureStorage.delete(StorageKey.UserWalletEncryptionKeyUnsecured(userWalletId).name)
secureStorage.delete(StorageKey.UserWalletEncryptionKeyEncrypted(userWalletId).name)
authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name)
}
secureStorage.delete(StorageKey.UserWalletIds.name)
}
}
private suspend fun getUserWalletsIds(): List<UserWalletId> {
return withContext(dispatchers.io) {
secureStorage.get(StorageKey.UserWalletIds.name)
.decodeToUserWalletsIds()
}
}
private suspend fun storeUserWalletId(userWalletId: UserWalletId) {
val userWalletIds = (getUserWalletsIds() + userWalletId).distinct()
withContext(dispatchers.io) {
secureStorage.store(userWalletIds.encode(), StorageKey.UserWalletIds.name)
}
}
private suspend fun UserWalletEncryptionKey.encode(): ByteArray {
return withContext(dispatchers.default) {
this@encode
.let(encryptionKeyAdapter::toJson)
.encodeToByteArray(throwOnInvalidSequence = true)
}
}
private suspend fun ByteArray?.decodeToKey(): UserWalletEncryptionKey? {
return withContext(dispatchers.default) {
this@decodeToKey
?.decodeToString(throwOnInvalidSequence = true)
?.let(encryptionKeyAdapter::fromJson)
}
}
private suspend fun List<UserWalletId>.encode(): ByteArray {
return withContext(dispatchers.default) {
this@encode
.let(userWalletsIdsListAdapter::toJson)
.encodeToByteArray(throwOnInvalidSequence = true)
}
}
private suspend fun ByteArray?.decodeToUserWalletsIds(): List<UserWalletId> {
return withContext(dispatchers.default) {
this@decodeToUserWalletsIds
?.decodeToString(throwOnInvalidSequence = true)
?.let(userWalletsIdsListAdapter::fromJson)
.orEmpty()
}
}
sealed class EncryptionMethod {
data object Unsecured : EncryptionMethod()
data object Biometric : EncryptionMethod()
class Password(val password: CharArray) : EncryptionMethod()
}
private sealed interface StorageKey {
val name: String
class UserWalletEncryptionKeyUnsecured(userWalletId: UserWalletId) : StorageKey {
override val name: String = "user_wallet_encryption_key_unsecured_${userWalletId.stringValue}"
}
class UserWalletEncryptionKey(userWalletId: UserWalletId) : StorageKey {
override val name: String = "user_wallet_encryption_key_${userWalletId.stringValue}"
}
class UserWalletEncryptionKeyEncrypted(userWalletId: UserWalletId) : StorageKey {
override val name: String = "user_wallet_encryption_key_encrypted_${userWalletId.stringValue}"
}
object UserWalletIds : StorageKey {
override val name: String = "user_wallets_ids_with_saved_keys"
}
}
}