Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merge 5.7.1 dev #357

Merged
merged 4 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -204,6 +209,8 @@ internal class AndroidAuthenticationManager(
Log.warning {
"""
$TAG - Biometric authentication error
|- Code: $errorCode
|- Message: $errString
|- Cause: $error
""".trimIndent()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() }

Expand All @@ -55,7 +55,7 @@ internal class AndroidKeystoreManager(
return@withContext null
}

val cipher = initUnwrapCipher()
val cipher = authenticateAndInitUnwrapCipher()
val unwrappedKey = RSACipherOperations.unwrapKey(
cipher = cipher,
wrappedKeyBytes = wrappedKeyBytes,
Expand All @@ -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<String>): Map<String, SecretKey> = 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)

Expand All @@ -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()
Expand All @@ -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()
}
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>): Map<String, ByteArray> = 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)
}

/**
Expand All @@ -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 {
Expand All @@ -94,10 +126,36 @@ class AuthenticatedStorage(
return AESCipherOperations.decrypt(decryptionCipher, encryptedData)
}

private suspend fun decrypt(keyAliasToEncryptedData: Map<String, ByteArray>): Map<String, ByteArray> {
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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>): Map<String, SecretKey> = emptyMap()

override suspend fun store(keyAlias: String, key: SecretKey) = Unit
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.tangem.common.authentication

import com.tangem.common.core.TangemSdkError
import javax.crypto.SecretKey

/**
Expand All @@ -10,19 +11,33 @@ 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<String>): Map<String, SecretKey>

/**
* Stores the given [SecretKey] with a specified [keyAlias] in the keystore.
*
* @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)
}
Loading
Loading