Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Merge 5.8 dev #361

Merged
merged 11 commits into from
Mar 19, 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
2 changes: 2 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,17 @@ package com.tangem.sdk.authentication

import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyPermanentlyInvalidatedException
import android.security.keystore.KeyProperties
import android.security.keystore.UserNotAuthenticatedException
import androidx.annotation.RequiresApi
import com.tangem.Log
import com.tangem.common.authentication.AuthenticationManager
import com.tangem.common.authentication.KeystoreManager
import com.tangem.common.authentication.keystore.KeystoreManager
import com.tangem.common.core.TangemSdkError
import com.tangem.common.services.secure.SecureStorage
import com.tangem.crypto.operations.AESCipherOperations
import com.tangem.crypto.operations.RSACipherOperations
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.security.InvalidKeyException
import com.tangem.sdk.authentication.AndroidAuthenticationManager.AndroidAuthenticationParams
import java.security.KeyPair
import java.security.KeyStore
import java.security.PrivateKey
Expand All @@ -31,48 +29,43 @@ internal class AndroidKeystoreManager(
private val keyStore: KeyStore = KeyStore.getInstance(KEY_STORE_PROVIDER)
.apply { load(null) }

private val masterPublicKey: PublicKey
get() = keyStore.getCertificate(MASTER_KEY_ALIAS)?.publicKey ?: generateMasterKey().public

private val masterPrivateKey: PrivateKey
get() = keyStore.getKey(MASTER_KEY_ALIAS, null) as? PrivateKey
?: throw TangemSdkError.KeystoreInvalidated(
cause = IllegalStateException("The master key is not stored in the keystore"),
)

override suspend fun get(keyAlias: String): SecretKey? = withContext(Dispatchers.IO) {
override suspend fun get(masterKeyConfig: KeystoreManager.MasterKeyConfig, keyAlias: String): SecretKey? {
val wrappedKeyBytes = secureStorage.get(getStorageKeyForWrappedSecretKey(keyAlias))
?.takeIf { it.isNotEmpty() }

if (wrappedKeyBytes == null) {
Log.warning {
Log.biometric {
"""
$TAG - The secret key is not stored
The secret key is not stored
|- Key alias: $keyAlias
""".trimIndent()
}

return@withContext null
return null
}

val cipher = authenticateAndInitUnwrapCipher()
val privateKey = getPrivateMasterKey(masterKeyConfig) ?: return null
val cipher = authenticateAndInitUnwrapCipher(privateKey, masterKeyConfig)
val unwrappedKey = RSACipherOperations.unwrapKey(
cipher = cipher,
wrappedKeyBytes = wrappedKeyBytes,
wrappedKeyAlgorithm = AESCipherOperations.KEY_ALGORITHM,
)

Log.debug {
Log.biometric {
"""
$TAG - The secret key was retrieved
The secret key was retrieved
|- Key alias: $keyAlias
""".trimIndent()
}

unwrappedKey
return unwrappedKey
}

override suspend fun get(keyAliases: Collection<String>): Map<String, SecretKey> = withContext(Dispatchers.IO) {
override suspend fun get(
masterKeyConfig: KeystoreManager.MasterKeyConfig,
keyAliases: Set<String>,
): Map<String, SecretKey> {
val wrappedKeysBytes = keyAliases
.mapNotNull { keyAlias ->
val wrappedKeyBytes = secureStorage.get(getStorageKeyForWrappedSecretKey(keyAlias))
Expand All @@ -84,17 +77,18 @@ internal class AndroidKeystoreManager(
.toMap()

if (wrappedKeysBytes.isEmpty()) {
Log.warning {
Log.biometric {
"""
$TAG - The secret keys are not stored
The secret keys are not stored
|- Key aliases: $keyAliases
""".trimIndent()
}

return@withContext emptyMap()
return emptyMap()
}

val cipher = authenticateAndInitUnwrapCipher()
val privateKey = getPrivateMasterKey(masterKeyConfig) ?: return emptyMap()
val cipher = authenticateAndInitUnwrapCipher(privateKey, masterKeyConfig)
val unwrappedKeys = wrappedKeysBytes
.mapValues { (_, wrappedKeyBytes) ->
RSACipherOperations.unwrapKey(
Expand All @@ -104,74 +98,165 @@ internal class AndroidKeystoreManager(
)
}

Log.debug {
Log.biometric {
"""
$TAG - The secret keys were retrieved
The secret keys were retrieved
|- Key aliases: $keyAliases
""".trimIndent()
}

unwrappedKeys
return unwrappedKeys
}

private fun getPrivateMasterKey(masterKeyConfig: KeystoreManager.MasterKeyConfig): PrivateKey? {
val key = keyStore.getKey(masterKeyConfig.alias, null) as? PrivateKey

if (key == null) {
Log.biometric {
"""
The private master key is not generated
|- Alias: ${masterKeyConfig.alias}
""".trimIndent()
}
}

return key
}

override suspend fun store(keyAlias: String, key: SecretKey) = withContext(Dispatchers.IO) {
val masterCipher = RSACipherOperations.initWrapKeyCipher(masterPublicKey)
val wrappedKey = RSACipherOperations.wrapKey(masterCipher, key)
override suspend fun store(masterKeyConfig: KeystoreManager.MasterKeyConfig, keyAlias: String, key: SecretKey) {
val publicKey = getPublicMasterKey(masterKeyConfig)
val cipher = RSACipherOperations.initWrapKeyCipher(publicKey)
val wrappedKey = RSACipherOperations.wrapKey(cipher, key)

secureStorage.store(wrappedKey, getStorageKeyForWrappedSecretKey(keyAlias))

Log.debug {
Log.biometric {
"""
$TAG - The secret key was stored
The secret key was stored
|- Key alias: $keyAlias
""".trimIndent()
}
}

/**
* 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.debug { "$TAG - Initializing the unwrap cipher" }
private fun getPublicMasterKey(masterKeyConfig: KeystoreManager.MasterKeyConfig): PublicKey {
var key = keyStore.getCertificate(masterKeyConfig.alias)?.publicKey

if (key == null) {
Log.biometric { "The public master key is not generated" }

generateInvalidationCheckKey()
key = generateMasterKey(masterKeyConfig).public!!
}

return key
}

private suspend fun authenticateAndInitUnwrapCipher(
privateKey: PrivateKey,
masterKeyConfig: KeystoreManager.MasterKeyConfig,
): Cipher {
Log.biometric { "Initializing the unwrap cipher" }

authenticateUser(masterKeyConfig)

return try {
authenticationManager.authenticate()
RSACipherOperations.initUnwrapKeyCipher(privateKey)
} catch (e: Throwable) {
Log.biometric {
"""
Unable to initialize the unwrap cipher
|- Cause: $e
""".trimIndent()
}

RSACipherOperations.initUnwrapKeyCipher(masterPrivateKey)
} catch (e: InvalidKeyException) {
handleInvalidKeyException(e)
throw e
}
}

private fun handleInvalidKeyException(e: InvalidKeyException): Nothing {
Log.error {
private suspend fun authenticateUser(masterKeyConfig: KeystoreManager.MasterKeyConfig) {
try {
/**
* Authentication timeout is reduced by [AUTHENTICATION_TIMEOUT_MULTIPLIER] of the master key timeout
* to avoid the situation when the master key is invalidated due to the timeout while the user is authenticating.
* */
authenticationManager.authenticate(
params = AndroidAuthenticationParams(
cipher = getInvalidationCheckCipher(),
timeout = masterKeyConfig.securityDelay * AUTHENTICATION_TIMEOUT_MULTIPLIER,
),
)
} catch (e: KeyPermanentlyInvalidatedException) {
handleKeyInvalidationException(masterKeyConfig.alias, e)
} catch (e: Throwable) {
Log.biometric {
"""
Unable to authenticate the user
|- Cause: $e
""".trimIndent()
}

throw e
}
}

private fun getInvalidationCheckCipher(): Cipher {
var key = keyStore.getKey(INVALIDATION_CHECK_KEY_ALIAS, null) as? SecretKey

if (key == null) {
Log.biometric { "The invalidation check key is not generated" }

key = generateInvalidationCheckKey()
}

return AESCipherOperations.initEncryptionCipher(key)
}

private fun handleKeyInvalidationException(
privateKeyAlias: String,
e: KeyPermanentlyInvalidatedException,
): Nothing {
Log.biometric {
"""
$TAG - Unable to initialize the unwrap cipher because the master key is invalidated,
master key will be deleted
Unable to initialize the unwrap cipher because the key is permanently invalidated
|- Cause: $e
""".trimIndent()
}

keyStore.deleteEntry(MASTER_KEY_ALIAS)
keyStore.deleteEntry(privateKeyAlias)
keyStore.load(null)

throw TangemSdkError.KeystoreInvalidated(e)
}

private fun generateMasterKey(): KeyPair {
private fun generateMasterKey(masterKeyConfig: KeystoreManager.MasterKeyConfig): KeyPair {
Log.biometric {
"""
Generating the master key
|- Alias: ${masterKeyConfig.alias}
""".trimIndent()
}

return RSACipherOperations.generateKeyPair(
keyStoreProvider = keyStore.provider.name,
keyGenSpec = buildMasterKeyGenSpec(),
keyGenSpec = buildMasterKeySpec(masterKeyConfig),
)
}

private fun generateInvalidationCheckKey(): SecretKey {
Log.biometric { "Generating the key for invalidation check" }

return AESCipherOperations.generateKey(
keyStoreProvider = keyStore.provider.name,
keyGetSpec = buildInvalidationCheckKeySpec(),
)
}

/** Key regeneration is required to edit these parameters */
private fun buildMasterKeyGenSpec(): KeyGenParameterSpec {
private fun buildMasterKeySpec(masterKeyConfig: KeystoreManager.MasterKeyConfig): KeyGenParameterSpec {
val securityDelaySeconds = masterKeyConfig.securityDelay.inWholeSeconds.toInt()

return KeyGenParameterSpec.Builder(
MASTER_KEY_ALIAS,
masterKeyConfig.alias,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
)
.setKeySize(MASTER_KEY_SIZE)
Expand All @@ -189,12 +274,45 @@ internal class AndroidKeystoreManager(
.let { builder ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
builder
.setUserConfirmationRequired(masterKeyConfig.userConfirmationRequired)
.setUserAuthenticationParameters(
securityDelaySeconds,
KeyProperties.AUTH_BIOMETRIC_STRONG,
)
} else {
builder.setUserAuthenticationValidityDurationSeconds(securityDelaySeconds)
}
}
.build()
}

/** Key regeneration is required to edit these parameters */
private fun buildInvalidationCheckKeySpec(): KeyGenParameterSpec {
return KeyGenParameterSpec.Builder(
INVALIDATION_CHECK_KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT,
)
.setKeySize(AUTHENTICATION_CHECK_KEY_SIZE)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setUserAuthenticationRequired(true)
.let { builder ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
builder.setInvalidatedByBiometricEnrollment(true)
} else {
builder
}
}
.let { builder ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
builder
.setUserConfirmationRequired(false)
.setUserAuthenticationParameters(
MASTER_KEY_TIMEOUT_SECONDS,
0,
KeyProperties.AUTH_BIOMETRIC_STRONG,
)
} else {
builder.setUserAuthenticationValidityDurationSeconds(MASTER_KEY_TIMEOUT_SECONDS)
builder.setUserAuthenticationValidityDurationSeconds(-1)
}
}
.build()
Expand All @@ -206,11 +324,10 @@ internal class AndroidKeystoreManager(

private companion object {
const val KEY_STORE_PROVIDER = "AndroidKeyStore"

const val MASTER_KEY_ALIAS = "master_key"
const val MASTER_KEY_SIZE = 1024
const val MASTER_KEY_TIMEOUT_SECONDS = 5
const val AUTHENTICATION_TIMEOUT_MULTIPLIER = 0.9

const val TAG = "Keystore Manager"
const val INVALIDATION_CHECK_KEY_ALIAS = "invalidation_check_key"
const val AUTHENTICATION_CHECK_KEY_SIZE = 128
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import com.tangem.TangemSdk
import com.tangem.TangemSdkLogger
import com.tangem.common.authentication.AuthenticationManager
import com.tangem.common.authentication.DummyAuthenticationManager
import com.tangem.common.authentication.DummyKeystoreManager
import com.tangem.common.authentication.KeystoreManager
import com.tangem.common.authentication.keystore.DummyKeystoreManager
import com.tangem.common.authentication.keystore.KeystoreManager
import com.tangem.common.core.Config
import com.tangem.common.services.secure.SecureStorage
import com.tangem.crypto.bip39.Wordlist
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ fun TangemSdkError.localizedDescriptionRes(): TangemSdkErrorDescription {
is TangemSdkError.AuthenticationUnavailable,
is TangemSdkError.AuthenticationLockout,
is TangemSdkError.AuthenticationPermanentLockout,
is TangemSdkError.UserCanceledAuthentication,
is TangemSdkError.AuthenticationCanceled,
is TangemSdkError.AuthenticationAlreadyInProgress,
is TangemSdkError.KeyGenerationException,
is TangemSdkError.MnemonicException,
is TangemSdkError.KeysImportDisabled,
Expand Down
Loading
Loading