diff --git a/tangem-sdk-android-demo/src/main/java/com/tangem/demo/DemoActivity.kt b/tangem-sdk-android-demo/src/main/java/com/tangem/demo/DemoActivity.kt index d813bd27..843cf28c 100644 --- a/tangem-sdk-android-demo/src/main/java/com/tangem/demo/DemoActivity.kt +++ b/tangem-sdk-android-demo/src/main/java/com/tangem/demo/DemoActivity.kt @@ -23,7 +23,7 @@ import com.tangem.demo.ui.viewDelegate.ViewDelegateFragment import com.tangem.sdk.DefaultSessionViewDelegate import com.tangem.sdk.extensions.createLogger import com.tangem.sdk.extensions.getWordlist -import com.tangem.sdk.extensions.initBiometricManager +import com.tangem.sdk.extensions.initAuthenticationManager import com.tangem.sdk.extensions.initKeystoreManager import com.tangem.sdk.extensions.initNfcManager import com.tangem.sdk.storage.create @@ -87,7 +87,7 @@ class DemoActivity : AppCompatActivity() { } val secureStorage = SecureStorage.create(this) val nfcManager = TangemSdk.initNfcManager(this) - val authenticationManager = TangemSdk.initBiometricManager(this) + val authenticationManager = TangemSdk.initAuthenticationManager(this) val viewDelegate = DefaultSessionViewDelegate(nfcManager, this) viewDelegate.sdkConfig = config diff --git a/tangem-sdk-android/src/main/java/com/tangem/sdk/authentication/AndroidAuthenticationManager.kt b/tangem-sdk-android/src/main/java/com/tangem/sdk/authentication/AndroidAuthenticationManager.kt index c593d558..ea4f4bbe 100644 --- a/tangem-sdk-android/src/main/java/com/tangem/sdk/authentication/AndroidAuthenticationManager.kt +++ b/tangem-sdk-android/src/main/java/com/tangem/sdk/authentication/AndroidAuthenticationManager.kt @@ -5,6 +5,7 @@ import androidx.annotation.RequiresApi import androidx.biometric.BiometricPrompt import androidx.fragment.app.FragmentActivity import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import com.tangem.Log @@ -29,7 +30,8 @@ import androidx.biometric.BiometricManager as SystemBiometricManager internal class AndroidAuthenticationManager( private val activity: FragmentActivity, ) : AuthenticationManager, - DefaultLifecycleObserver { + DefaultLifecycleObserver, + LifecycleOwner by activity { private val biometricPromptInfo by lazy { BiometricPrompt.PromptInfo.Builder() @@ -62,9 +64,12 @@ internal class AndroidAuthenticationManager( } override suspend fun authenticate() { + if (lifecycle.currentState != Lifecycle.State.RESUMED) return + if (authenticationMutex.isLocked) { Log.warning { "$TAG - A user authentication has already been launched" } Log.biometric { "A user authentication has already been launched" } + return } authenticationMutex.withLock { @@ -204,6 +209,8 @@ internal class AndroidAuthenticationManager( Log.warning { """ $TAG - Biometric authentication error + |- Code: $errorCode + |- Message: $errString |- Cause: $error """.trimIndent() } diff --git a/tangem-sdk-android/src/main/java/com/tangem/sdk/authentication/AndroidKeystoreManager.kt b/tangem-sdk-android/src/main/java/com/tangem/sdk/authentication/AndroidKeystoreManager.kt index 88654213..d034284c 100644 --- a/tangem-sdk-android/src/main/java/com/tangem/sdk/authentication/AndroidKeystoreManager.kt +++ b/tangem-sdk-android/src/main/java/com/tangem/sdk/authentication/AndroidKeystoreManager.kt @@ -40,7 +40,7 @@ internal class AndroidKeystoreManager( cause = IllegalStateException("The master key is not stored in the keystore"), ) - override suspend fun authenticateAndGetKey(keyAlias: String): SecretKey? = withContext(Dispatchers.IO) { + override suspend fun get(keyAlias: String): SecretKey? = withContext(Dispatchers.IO) { val wrappedKeyBytes = secureStorage.get(getStorageKeyForWrappedSecretKey(keyAlias)) ?.takeIf { it.isNotEmpty() } @@ -55,7 +55,7 @@ internal class AndroidKeystoreManager( return@withContext null } - val cipher = initUnwrapCipher() + val cipher = authenticateAndInitUnwrapCipher() val unwrappedKey = RSACipherOperations.unwrapKey( cipher = cipher, wrappedKeyBytes = wrappedKeyBytes, @@ -72,7 +72,49 @@ internal class AndroidKeystoreManager( unwrappedKey } - override suspend fun storeKey(keyAlias: String, key: SecretKey) = withContext(Dispatchers.IO) { + override suspend fun get(keyAliases: Collection): Map = withContext(Dispatchers.IO) { + val wrappedKeysBytes = keyAliases + .mapNotNull { keyAlias -> + val wrappedKeyBytes = secureStorage.get(getStorageKeyForWrappedSecretKey(keyAlias)) + ?.takeIf { it.isNotEmpty() } + ?: return@mapNotNull null + + keyAlias to wrappedKeyBytes + } + .toMap() + + if (wrappedKeysBytes.isEmpty()) { + Log.warning { + """ + $TAG - The secret keys are not stored + |- Key aliases: $keyAliases + """.trimIndent() + } + + return@withContext emptyMap() + } + + val cipher = authenticateAndInitUnwrapCipher() + val unwrappedKeys = wrappedKeysBytes + .mapValues { (_, wrappedKeyBytes) -> + RSACipherOperations.unwrapKey( + cipher = cipher, + wrappedKeyBytes = wrappedKeyBytes, + wrappedKeyAlgorithm = AESCipherOperations.KEY_ALGORITHM, + ) + } + + Log.debug { + """ + $TAG - The secret keys were retrieved + |- Key aliases: $keyAliases + """.trimIndent() + } + + unwrappedKeys + } + + override suspend fun store(keyAlias: String, key: SecretKey) = withContext(Dispatchers.IO) { val masterCipher = RSACipherOperations.initWrapKeyCipher(masterPublicKey) val wrappedKey = RSACipherOperations.wrapKey(masterCipher, key) @@ -86,20 +128,14 @@ internal class AndroidKeystoreManager( } } - private suspend fun initUnwrapCipher(): Cipher { - Log.debug { "$TAG - Initializing the unwrap cipher" } - - return try { - RSACipherOperations.initUnwrapKeyCipher(masterPrivateKey) - } catch (e: UserNotAuthenticatedException) { - authenticateAndInitUnwrapCipher() - } catch (e: InvalidKeyException) { - handleInvalidKeyException(e) - } - } - + /** + * If the master key has been invalidated due to new biometric enrollment, the [UserNotAuthenticatedException] + * will be thrown anyway because the master key has the positive timeout. + * + * @see KeyGenParameterSpec.Builder.setInvalidatedByBiometricEnrollment + * */ private suspend fun authenticateAndInitUnwrapCipher(): Cipher { - Log.warning { "$TAG - Unable to initialize the cipher because the user is not authenticated" } + Log.debug { "$TAG - Initializing the unwrap cipher" } return try { authenticationManager.authenticate() @@ -110,16 +146,11 @@ internal class AndroidKeystoreManager( } } - /** - * If the master key has been invalidated due to new biometric enrollment, the [UserNotAuthenticatedException] - * will be thrown anyway because the master key has the positive timeout. - * - * @see KeyGenParameterSpec.Builder.setInvalidatedByBiometricEnrollment - * */ private fun handleInvalidKeyException(e: InvalidKeyException): Nothing { Log.error { """ - $TAG - Unable to initialize the unwrap cipher because the master key is invalid + $TAG - Unable to initialize the unwrap cipher because the master key is invalidated, + master key will be deleted |- Cause: $e """.trimIndent() } @@ -137,6 +168,7 @@ internal class AndroidKeystoreManager( ) } + /** Key regeneration is required to edit these parameters */ private fun buildMasterKeyGenSpec(): KeyGenParameterSpec { return KeyGenParameterSpec.Builder( MASTER_KEY_ALIAS, diff --git a/tangem-sdk-android/src/main/java/com/tangem/sdk/extensions/TangemSdk.kt b/tangem-sdk-android/src/main/java/com/tangem/sdk/extensions/TangemSdk.kt index 4b6fec20..40dcd8c4 100644 --- a/tangem-sdk-android/src/main/java/com/tangem/sdk/extensions/TangemSdk.kt +++ b/tangem-sdk-android/src/main/java/com/tangem/sdk/extensions/TangemSdk.kt @@ -47,7 +47,7 @@ fun TangemSdk.Companion.init(activity: ComponentActivity, config: Config = Confi fun TangemSdk.Companion.initWithBiometrics(activity: FragmentActivity, config: Config = Config()): TangemSdk { val secureStorage = SecureStorage.create(activity) val nfcManager = TangemSdk.initNfcManager(activity) - val authenticationManager = initBiometricManager(activity) + val authenticationManager = initAuthenticationManager(activity) val viewDelegate = DefaultSessionViewDelegate(nfcManager, activity) viewDelegate.sdkConfig = config @@ -113,7 +113,7 @@ fun TangemSdk.Companion.createLogger(formatter: LogFormat? = null): TangemSdkLog } } -fun TangemSdk.Companion.initBiometricManager(activity: FragmentActivity): AuthenticationManager { +fun TangemSdk.Companion.initAuthenticationManager(activity: FragmentActivity): AuthenticationManager { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { AndroidAuthenticationManager(activity) .also { activity.lifecycle.addObserver(it) } diff --git a/tangem-sdk-core/src/main/java/com/tangem/common/authentication/AuthenticatedStorage.kt b/tangem-sdk-core/src/main/java/com/tangem/common/authentication/AuthenticatedStorage.kt index 8e5fc0dc..cbb08b6e 100644 --- a/tangem-sdk-core/src/main/java/com/tangem/common/authentication/AuthenticatedStorage.kt +++ b/tangem-sdk-core/src/main/java/com/tangem/common/authentication/AuthenticatedStorage.kt @@ -21,38 +21,70 @@ class AuthenticatedStorage( /** * Retrieves and decrypts data from the storage after necessary user authentication. * - * @param key The unique identifier for the stored encrypted data. + * @param keyAlias The unique identifier for the stored encrypted data. * * @return The decrypted data as [ByteArray] or `null` if data is not found. */ - suspend fun get(key: String): ByteArray? = withContext(Dispatchers.IO) { - val encryptedData = secureStorage.get(key) + suspend fun get(keyAlias: String): ByteArray? = withContext(Dispatchers.IO) { + val encryptedData = secureStorage.get(keyAlias) ?.takeIf(ByteArray::isNotEmpty) if (encryptedData == null) { Log.warning { """ $TAG - Data not found in storage - |- Key: $key + |- Key: $keyAlias """.trimIndent() } return@withContext null } - decrypt(key, encryptedData) + decrypt(keyAlias, encryptedData) + } + + /** + * Retrieves and decrypts data from the storage after necessary user authentication. + * + * @param keysAliases The unique identifiers for the stored encrypted data. + * + * @return The decrypted data as a map of key-alias to [ByteArray] or an empty map if data is not found. + */ + suspend fun get(keysAliases: Collection): Map = withContext(Dispatchers.IO) { + val encryptedData = keysAliases + .mapNotNull { keyAlias -> + val data = secureStorage.get(keyAlias) + ?.takeIf(ByteArray::isNotEmpty) + ?: return@mapNotNull null + + keyAlias to data + } + .toMap() + + if (encryptedData.isEmpty()) { + Log.warning { + """ + $TAG - Data not found in storage + |- Keys: $keysAliases + """.trimIndent() + } + + return@withContext emptyMap() + } + + decrypt(encryptedData) } /** * Encrypts and stores data securely in the storage. * - * @param key The unique identifier which will be associated with the encrypted data. + * @param keyAlias The unique identifier which will be associated with the encrypted data. * @param data The plain data to be encrypted and stored. */ - suspend fun store(key: String, data: ByteArray) = withContext(Dispatchers.IO) { - val encryptedData = encrypt(key, data) + suspend fun store(keyAlias: String, data: ByteArray) = withContext(Dispatchers.IO) { + val encryptedData = encrypt(keyAlias, data) - secureStorage.store(encryptedData, key) + secureStorage.store(encryptedData, keyAlias) } /** @@ -75,7 +107,7 @@ class AuthenticatedStorage( } private suspend fun decrypt(keyAlias: String, encryptedData: ByteArray): ByteArray? { - val key = keystoreManager.authenticateAndGetKey(keyAlias) + val key = keystoreManager.get(keyAlias) if (key == null) { Log.warning { @@ -94,10 +126,36 @@ class AuthenticatedStorage( return AESCipherOperations.decrypt(decryptionCipher, encryptedData) } + private suspend fun decrypt(keyAliasToEncryptedData: Map): Map { + val keys = keystoreManager.get(keyAliasToEncryptedData.keys) + + if (keys.isEmpty()) { + Log.warning { + """ + $TAG - The data keys are not stored + |- Key aliases: ${keyAliasToEncryptedData.keys} + """.trimIndent() + } + + return emptyMap() + } + + return keyAliasToEncryptedData + .mapNotNull { (keyAlias, encryptedData) -> + val key = keys[keyAlias] ?: return@mapNotNull null + val iv = getDataIv(keyAlias) + val decryptionCipher = AESCipherOperations.initDecryptionCipher(key, iv) + val decryptedData = AESCipherOperations.decrypt(decryptionCipher, encryptedData) + + keyAlias to decryptedData + } + .toMap() + } + private suspend fun generateAndStoreDataKey(keyAlias: String): SecretKey { val dataKey = AESCipherOperations.generateKey() - keystoreManager.storeKey(keyAlias, dataKey) + keystoreManager.store(keyAlias, dataKey) return dataKey } diff --git a/tangem-sdk-core/src/main/java/com/tangem/common/authentication/AuthenticationManager.kt b/tangem-sdk-core/src/main/java/com/tangem/common/authentication/AuthenticationManager.kt index b6c4f412..b45b9eac 100644 --- a/tangem-sdk-core/src/main/java/com/tangem/common/authentication/AuthenticationManager.kt +++ b/tangem-sdk-core/src/main/java/com/tangem/common/authentication/AuthenticationManager.kt @@ -24,6 +24,8 @@ interface AuthenticationManager { * this might trigger biometric prompts or other authentication mechanisms. * * @throws TangemSdkError.AuthenticationUnavailable if authentication is unavailable. + * @throws TangemSdkError.UserCanceledAuthentication if the user cancels the authentication process. + * @throws TangemSdkError.AuthenticationFailed if authentication fails for any other reason. */ suspend fun authenticate() } diff --git a/tangem-sdk-core/src/main/java/com/tangem/common/authentication/DummyKeystoreManager.kt b/tangem-sdk-core/src/main/java/com/tangem/common/authentication/DummyKeystoreManager.kt index e4061329..d81ab24b 100644 --- a/tangem-sdk-core/src/main/java/com/tangem/common/authentication/DummyKeystoreManager.kt +++ b/tangem-sdk-core/src/main/java/com/tangem/common/authentication/DummyKeystoreManager.kt @@ -4,7 +4,9 @@ import javax.crypto.SecretKey class DummyKeystoreManager : KeystoreManager { - override suspend fun authenticateAndGetKey(keyAlias: String): SecretKey? = null + override suspend fun get(keyAlias: String): SecretKey? = null - override suspend fun storeKey(keyAlias: String, key: SecretKey) = Unit + override suspend fun get(keyAliases: Collection): Map = emptyMap() + + override suspend fun store(keyAlias: String, key: SecretKey) = Unit } diff --git a/tangem-sdk-core/src/main/java/com/tangem/common/authentication/KeystoreManager.kt b/tangem-sdk-core/src/main/java/com/tangem/common/authentication/KeystoreManager.kt index 087aedd3..a9310df7 100644 --- a/tangem-sdk-core/src/main/java/com/tangem/common/authentication/KeystoreManager.kt +++ b/tangem-sdk-core/src/main/java/com/tangem/common/authentication/KeystoreManager.kt @@ -1,5 +1,6 @@ package com.tangem.common.authentication +import com.tangem.common.core.TangemSdkError import javax.crypto.SecretKey /** @@ -10,13 +11,27 @@ interface KeystoreManager { /** * Retrieves the [SecretKey] for a given [keyAlias]. * - * This requires user authentication (e.g. biometric authentication). + * This operation requires user authentication. * * @param keyAlias The alias of the key to be retrieved. - * @return The [SecretKey] if found. If the keystore is locked or the key cannot be found, - * then `null` will be returned. + * @return The [SecretKey] if found. If the key cannot be found, then `null` will be returned. + * + * @throws TangemSdkError.KeystoreInvalidated if the keystore is invalidated. + */ + suspend fun get(keyAlias: String): SecretKey? + + /** + * Retrieves the map of key alias to [SecretKey] for a given [keyAliases]. + * + * This operation requires user authentication. + * + * @param keyAliases The aliases of the keys to be retrieved. + * @return The map of key alias to [SecretKey] if found. If the key cannot be found, then the key will not be + * included in the map. + * + * @throws TangemSdkError.KeystoreInvalidated if the keystore is invalidated. */ - suspend fun authenticateAndGetKey(keyAlias: String): SecretKey? + suspend fun get(keyAliases: Collection): Map /** * Stores the given [SecretKey] with a specified [keyAlias] in the keystore. @@ -24,5 +39,5 @@ interface KeystoreManager { * @param keyAlias The alias under which the key should be stored. * @param key The [SecretKey] to be stored. */ - suspend fun storeKey(keyAlias: String, key: SecretKey) + suspend fun store(keyAlias: String, key: SecretKey) } diff --git a/tangem-sdk-core/src/main/java/com/tangem/common/core/TangemError.kt b/tangem-sdk-core/src/main/java/com/tangem/common/core/TangemError.kt index 1cd0317c..e3559ba2 100644 --- a/tangem-sdk-core/src/main/java/com/tangem/common/core/TangemError.kt +++ b/tangem-sdk-core/src/main/java/com/tangem/common/core/TangemError.kt @@ -321,6 +321,7 @@ sealed class TangemSdkError(code: Int) : TangemError(code) { class KeyGenerationException(override var customMessage: String) : TangemSdkError(code = 50022) class MnemonicException(val mnemonicResult: MnemonicErrorResult) : TangemSdkError(code = 50023) + /** * Get error according to the pin type * @param userCodeType: Specific user code type diff --git a/tangem-sdk-core/src/main/java/com/tangem/common/usersCode/UserCodeRepository.kt b/tangem-sdk-core/src/main/java/com/tangem/common/usersCode/UserCodeRepository.kt index 674295b6..f7d8107c 100644 --- a/tangem-sdk-core/src/main/java/com/tangem/common/usersCode/UserCodeRepository.kt +++ b/tangem-sdk-core/src/main/java/com/tangem/common/usersCode/UserCodeRepository.kt @@ -9,6 +9,7 @@ import com.tangem.common.authentication.AuthenticatedStorage import com.tangem.common.authentication.KeystoreManager import com.tangem.common.catching import com.tangem.common.extensions.calculateSha256 +import com.tangem.common.extensions.mapNotNullValues import com.tangem.common.flatMap import com.tangem.common.json.MoshiJsonConverter import com.tangem.common.map @@ -37,24 +38,24 @@ class UserCodeRepository( private val cardIdToUserCode: HashMap = hashMapOf() suspend fun unlock(): CompletionResult = withContext(Dispatchers.IO) { - val cardIdToUserCodeInternal = getSavedCardsIds() - .associateWith { cardId -> - when (val result = getSavedUserCode(cardId)) { - is CompletionResult.Success -> result.data - is CompletionResult.Failure -> return@withContext CompletionResult.Failure(result.error) - } - } - catching { + val cardsIds = getSavedCardsIds() + val userCodes = getSavedUserCodes(cardsIds) + cardIdToUserCode.clear() - cardIdToUserCodeInternal.forEach { (cardId, userCode) -> - if (userCode != null) { - cardIdToUserCode[cardId] = userCode - } - } + cardIdToUserCode.putAll(userCodes) } } + private suspend fun getSavedUserCodes(cardsIds: Set): Map { + val keys = cardsIds.map { StorageKey.UserCode(it).name } + val encodedData = authenticatedStorage.get(keys) + + return encodedData + .mapKeys { (key, _) -> key.removePrefix(StorageKey.UserCode.PREFIX) } + .mapNotNullValues { (_, value) -> value.decodeToUserCode() } + } + fun lock() { cardIdToUserCode.clear() } @@ -126,13 +127,6 @@ class UserCodeRepository( return hasChanges } - private suspend fun getSavedUserCode(cardId: String): CompletionResult { - return catching { - authenticatedStorage.get(StorageKey.UserCode(cardId).name) - .decodeToUserCode() - } - } - private suspend fun saveUserCode(cardsIds: Set, userCode: UserCode): CompletionResult { return catching { cardsIds.forEach { cardId -> @@ -173,44 +167,40 @@ class UserCodeRepository( } } - private suspend fun UserCode.encode(): ByteArray { - return withContext(Dispatchers.Default) { - this@encode - .let(userCodeAdapter::toJson) - .encodeToByteArray(throwOnInvalidSequence = true) - } + private fun UserCode.encode(): ByteArray { + return this@encode + .let(userCodeAdapter::toJson) + .encodeToByteArray(throwOnInvalidSequence = true) } - private suspend fun Set.encode(): ByteArray { - return withContext(Dispatchers.Default) { - this@encode - .let(cardsIdsAdapter::toJson) - .encodeToByteArray(throwOnInvalidSequence = true) - } + private fun Set.encode(): ByteArray { + return this@encode + .let(cardsIdsAdapter::toJson) + .encodeToByteArray(throwOnInvalidSequence = true) } - private suspend fun ByteArray?.decodeToUserCode(): UserCode? { - return withContext(Dispatchers.Default) { - this@decodeToUserCode - ?.decodeToString(throwOnInvalidSequence = true) - ?.let(userCodeAdapter::fromJson) - } + private fun ByteArray?.decodeToUserCode(): UserCode? { + return this@decodeToUserCode + ?.decodeToString(throwOnInvalidSequence = true) + ?.let(userCodeAdapter::fromJson) } - private suspend fun ByteArray?.decodeToCardsIds(): Set { - return withContext(Dispatchers.Default) { - this@decodeToCardsIds - ?.decodeToString(throwOnInvalidSequence = true) - ?.let(cardsIdsAdapter::fromJson) - .orEmpty() - } + private fun ByteArray?.decodeToCardsIds(): Set { + return this@decodeToCardsIds + ?.decodeToString(throwOnInvalidSequence = true) + ?.let(cardsIdsAdapter::fromJson) + .orEmpty() } private sealed interface StorageKey { val name: String class UserCode(cardId: String) : StorageKey { - override val name: String = "user_code_$cardId" + override val name: String = PREFIX + cardId + + companion object { + const val PREFIX = "user_code_" + } } object CardsWithSavedUserCode : StorageKey {