Skip to content

Commit

Permalink
Merge pull request #326 from tangem/feature/AND-4167_refactor_biometrics
Browse files Browse the repository at this point in the history
AND-4167 Improved logic for working with encrypted user data
  • Loading branch information
kozarezvlad authored Sep 27, 2023
2 parents cc5c68a + 4096699 commit 9957c72
Show file tree
Hide file tree
Showing 20 changed files with 776 additions and 497 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ 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.initCryptographyManager
import com.tangem.sdk.extensions.initNfcManager
import com.tangem.sdk.storage.create
import com.tangem.tangem_demo.R
Expand Down Expand Up @@ -86,7 +87,7 @@ class DemoActivity : AppCompatActivity() {
}
val secureStorage = SecureStorage.create(this)
val nfcManager = TangemSdk.initNfcManager(this)
val authManager = TangemSdk.initBiometricManager(this, secureStorage)
val authenticationManager = TangemSdk.initBiometricManager(this)

val viewDelegate = DefaultSessionViewDelegate(nfcManager, nfcManager.reader, this)
viewDelegate.sdkConfig = config
Expand All @@ -98,7 +99,8 @@ class DemoActivity : AppCompatActivity() {
secureStorage = secureStorage,
wordlist = Wordlist.getWordlist(this),
config = config,
biometricManager = authManager,
authenticationManager = authenticationManager,
keystoreManager = TangemSdk.initCryptographyManager(authenticationManager, secureStorage),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ abstract class BaseFragment : Fragment() {

private val userCodeRepository: UserCodeRepository by lazy {
UserCodeRepository(
biometricManager = sdk.biometricManager,
keystoreManager = sdk.keystoreManager,
secureStorage = sdk.secureStorage,
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,46 +1,32 @@
package com.tangem.sdk.biometrics
package com.tangem.sdk.authentication

import android.os.Build
import android.security.keystore.KeyPermanentlyInvalidatedException
import android.security.keystore.UserNotAuthenticatedException
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricPrompt
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.tangem.Log
import com.tangem.common.CompletionResult
import com.tangem.common.biometric.BiometricManager
import com.tangem.common.catching
import com.tangem.common.authentication.AuthenticationManager
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.flatMapOnFailure
import com.tangem.common.map
import com.tangem.common.mapFailure
import com.tangem.common.services.secure.SecureStorage
import com.tangem.sdk.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.security.InvalidKeyException
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlin.coroutines.resumeWithException
import androidx.biometric.BiometricManager as SystemBiometricManager

// TODO: Add docs
// TODO: Refactoring needed
@RequiresApi(Build.VERSION_CODES.M)
internal class AndroidBiometricManager(
secureStorage: SecureStorage,
internal class AndroidAuthenticationManager(
private val activity: FragmentActivity,
) : BiometricManager,
DefaultLifecycleObserver,
LifecycleOwner by activity {

private val cryptographyManager by lazy {
HybridCryptographyManager(secureStorage)
}
) : AuthenticationManager,
DefaultLifecycleObserver {

private val biometricPromptInfo by lazy {
BiometricPrompt.PromptInfo.Builder()
Expand All @@ -50,101 +36,80 @@ internal class AndroidBiometricManager(
.build()
}

private val authenticationMutex = Mutex()

override var canAuthenticate: Boolean = false
private set
override var canEnrollBiometrics: Boolean = false
private set

override fun onResume(owner: LifecycleOwner) {
lifecycleScope.launch {
owner.lifecycleScope.launch(Dispatchers.Main) {
val (available, canBeEnrolled) = checkBiometricsAvailabilityStatus()

canAuthenticate = available
canEnrollBiometrics = canBeEnrolled
}
}

override suspend fun authenticate(mode: BiometricManager.AuthenticationMode): CompletionResult<ByteArray> {
return if (canAuthenticate) {
initCrypto(mode)
.map { cryptographyManager.invoke(mode.keyName, mode.data) }
.mapFailure { error ->
when (error) {
is TangemSdkError.ExceptionError -> when (val cause = error.cause) {
is KeyPermanentlyInvalidatedException,
is UserNotAuthenticatedException,
-> {
cryptographyManager.deleteMasterKey()
TangemSdkError.BiometricCryptographyKeyInvalidated()
}
is InvalidKeyException -> {
TangemSdkError.InvalidBiometricCryptographyKey(cause.localizedMessage.orEmpty(), cause)
}
else -> {
TangemSdkError.BiometricCryptographyOperationFailed(error.customMessage, error)
}
}
else -> error
}
override suspend fun authenticate() {
if (authenticationMutex.isLocked) {
Log.warning { "$TAG - A user authentication has already been launched" }
}

authenticationMutex.withLock {
Log.debug { "$TAG - Trying to authenticate a user" }

if (canAuthenticate) {
withContext(Dispatchers.Main) {
authenticateInternal()
}
} else {
CompletionResult.Failure(TangemSdkError.BiometricsUnavailable())
} else {
Log.warning { "$TAG - Unable to authenticate the user as the biometrics feature is unavailable" }

throw TangemSdkError.AuthenticationUnavailable()
}
}
}

private suspend fun authenticateInternal(): CompletionResult<Unit> {
return suspendCoroutine { continuation ->
private suspend fun authenticateInternal() {
return suspendCancellableCoroutine { continuation ->
val biometricPrompt = BiometricPrompt(
activity,
createAuthenticationCallback { result ->
continuation.resume(
value = when (result) {
is AuthenticationResult.Failure -> CompletionResult.Failure(result.error)
is AuthenticationResult.Success -> CompletionResult.Success(Unit)
},
)
createAuthenticationCallback { biometricResult ->
when (biometricResult) {
is BiometricAuthenticationResult.Failure -> {
continuation.resumeWithException(biometricResult.error)
}
is BiometricAuthenticationResult.Success -> {
continuation.resume(Unit)
}
}
},
)

biometricPrompt.authenticate(biometricPromptInfo)
}
}

private suspend fun initCrypto(mode: BiometricManager.AuthenticationMode): CompletionResult<Unit> {
return when (mode) {
is BiometricManager.AuthenticationMode.Decryption -> {
catching(cryptographyManager::initDecryption).flatMapOnFailure { error ->
if ((error as? TangemSdkError.ExceptionError)?.cause is UserNotAuthenticatedException) {
withContext(Dispatchers.Main) { authenticateInternal() }
.map { cryptographyManager.initDecryption() }
} else {
CompletionResult.Failure(error)
}
}
}
is BiometricManager.AuthenticationMode.Encryption -> {
catching { cryptographyManager.initEncryption() }
}
}
}

@Suppress("LongMethod")
private suspend fun checkBiometricsAvailabilityStatus(): BiometricsAvailability {
val biometricManager = SystemBiometricManager.from(activity)
val authenticators = SystemBiometricManager.Authenticators.BIOMETRIC_STRONG

return suspendCoroutine { continuation ->
when (biometricManager.canAuthenticate(authenticators)) {
return suspendCancellableCoroutine { continuation ->
when (biometricManager.canAuthenticate(AUTHENTICATORS)) {
SystemBiometricManager.BIOMETRIC_SUCCESS -> {
Log.debug { "Biometric features are available" }
Log.debug { "$TAG - Biometric features are available" }
continuation.resume(
BiometricsAvailability(
available = true,
canBeEnrolled = false,
),
)
}

SystemBiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
Log.debug { "No biometric features enrolled" }
Log.debug { "$TAG - No biometric features enrolled" }
continuation.resume(
BiometricsAvailability(
available = false,
Expand All @@ -153,7 +118,7 @@ internal class AndroidBiometricManager(
)
}
SystemBiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
Log.debug { "No biometric features available on this device" }
Log.debug { "$TAG - No biometric features available on this device" }
continuation.resume(
BiometricsAvailability(
available = false,
Expand All @@ -162,7 +127,7 @@ internal class AndroidBiometricManager(
)
}
SystemBiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
Log.debug { "Biometric features are currently unavailable" }
Log.debug { "$TAG - Biometric features are currently unavailable" }
continuation.resume(
BiometricsAvailability(
available = false,
Expand All @@ -171,7 +136,7 @@ internal class AndroidBiometricManager(
)
}
SystemBiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
Log.debug { "Biometric features are currently unavailable, security update required" }
Log.debug { "$TAG - Biometric features are currently unavailable, security update required" }
continuation.resume(
BiometricsAvailability(
available = false,
Expand All @@ -180,7 +145,7 @@ internal class AndroidBiometricManager(
)
}
SystemBiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
Log.debug { "Biometric features are in unknown status" }
Log.debug { "$TAG - Biometric features are in unknown status" }
continuation.resume(
BiometricsAvailability(
available = false,
Expand All @@ -189,7 +154,7 @@ internal class AndroidBiometricManager(
)
}
SystemBiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
Log.debug { "Biometric features are unsupported" }
Log.debug { "$TAG - Biometric features are unsupported" }
continuation.resume(
BiometricsAvailability(
available = false,
Expand All @@ -202,51 +167,59 @@ internal class AndroidBiometricManager(
}

private fun createAuthenticationCallback(
result: (AuthenticationResult) -> Unit,
result: (BiometricAuthenticationResult) -> Unit,
): BiometricPrompt.AuthenticationCallback {
return object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
Log.error {
"""
Biometric authentication error
|- Code: $errorCode
|- Cause: $errString
""".trimIndent()
}
val error = when (errorCode) {
BiometricPrompt.ERROR_LOCKOUT -> TangemSdkError.BiometricsAuthenticationLockout()
BiometricPrompt.ERROR_LOCKOUT_PERMANENT -> TangemSdkError.BiometricsAuthenticationPermanentLockout()
BiometricPrompt.ERROR_LOCKOUT -> TangemSdkError.AuthenticationLockout()
BiometricPrompt.ERROR_LOCKOUT_PERMANENT -> TangemSdkError.AuthenticationPermanentLockout()
BiometricPrompt.ERROR_USER_CANCELED,
BiometricPrompt.ERROR_NEGATIVE_BUTTON,
-> TangemSdkError.UserCanceledBiometricsAuthentication()
else -> TangemSdkError.BiometricsAuthenticationFailed(errorCode, errString.toString())
-> TangemSdkError.UserCanceledAuthentication()

else -> TangemSdkError.AuthenticationFailed(errorCode, errString.toString())
}

Log.warning {
"""
$TAG - Biometric authentication error
|- Cause: $error
""".trimIndent()
}
result(AuthenticationResult.Failure(error))

result(BiometricAuthenticationResult.Failure(error))
}

override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
Log.debug { "Biometric authentication succeed" }
result(AuthenticationResult.Success(result))
Log.debug { "$TAG - Biometric authentication succeed" }
result(BiometricAuthenticationResult.Success(result))
}

override fun onAuthenticationFailed() {
Log.error { "Biometric authentication failed" }
Log.warning { "$TAG - Biometric authentication failed" }
}
}
}

data class BiometricsAvailability(
private data class BiometricsAvailability(
val available: Boolean,
val canBeEnrolled: Boolean,
)

sealed interface AuthenticationResult {
private sealed interface BiometricAuthenticationResult {
data class Failure(
val error: TangemError,
) : AuthenticationResult
) : BiometricAuthenticationResult

data class Success(
val result: BiometricPrompt.AuthenticationResult,
) : AuthenticationResult
) : BiometricAuthenticationResult
}

private companion object {
const val AUTHENTICATORS = SystemBiometricManager.Authenticators.BIOMETRIC_STRONG

const val TAG = "Android Authentication Manager"
}
}
Loading

0 comments on commit 9957c72

Please sign in to comment.