Skip to content

Commit

Permalink
Merge pull request #5 from RomanTsisyk/code-scanning-aler-1
Browse files Browse the repository at this point in the history
Fix Insecure local authentication Security Alert
  • Loading branch information
RomanTsisyk authored Nov 30, 2024
2 parents dc70bdd + c36c064 commit a5b91b4
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 43 deletions.
Original file line number Diff line number Diff line change
@@ -1,61 +1,162 @@
package io.github.romantsisyk.cryptolib.biometrics

import android.app.Activity
import androidx.biometric.BiometricManager
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import java.util.concurrent.Executor

object BiometricHelper {

/**
* Enhanced authentication method with fallback to device credentials.
*/
fun authenticateWithFallback(
activity: Activity,
title: String = "Authentication Required",
subtitle: String = "",
description: String = "",
onSuccess: () -> Unit,
onFailure: (Exception?) -> Unit
) {
val biometricManager = BiometricManager.from(activity)
val canAuthenticate = biometricManager.canAuthenticate(
BiometricManager.Authenticators.BIOMETRIC_STRONG or
BiometricManager.Authenticators.DEVICE_CREDENTIAL
import io.github.romantsisyk.cryptolib.crypto.keymanagement.KeyHelper
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey

/**
* A helper class for managing biometric authentication with secure cryptographic operations.
*/
class BiometricHelper(
private val activity: FragmentActivity,
private val executor: java.util.concurrent.Executor,
private val onSuccess: (decryptedData: ByteArray?) -> Unit,
private val onFailure: (error: String) -> Unit
) {

private val keyAlias = "MySecretKey"

init {
generateSecretKey()
}

private fun generateSecretKey() {
try {
val keyGenParameterSpec = KeyGenParameterSpec.Builder(
keyAlias,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setUserAuthenticationRequired(true)
.build()

val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
"AndroidKeyStore"
)
keyGenerator.init(keyGenParameterSpec)
keyGenerator.generateKey()
} catch (e: Exception) {
onFailure("Failed to generate secure key: ${e.message}")
}
}

private fun getSecretKey(): SecretKey {
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
return keyStore.getKey(keyAlias, null) as SecretKey
}

private fun getCipher(): Cipher {
return Cipher.getInstance(
"${KeyProperties.KEY_ALGORITHM_AES}/${KeyProperties.BLOCK_MODE_CBC}/${KeyProperties.ENCRYPTION_PADDING_PKCS7}"
)
}

if (canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) {
val executor: Executor = ContextCompat.getMainExecutor(activity)
val biometricPrompt = BiometricPrompt(activity as FragmentActivity, executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
onFailure(Exception(errString.toString()))
}
fun authenticateWithBiometrics(encryptedData: ByteArray) {
try {
val cipher = getCipher()
val secretKey = getSecretKey()
cipher.init(Cipher.DECRYPT_MODE, secretKey)

val biometricPrompt = BiometricPrompt(
activity,
executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
onSuccess()
val decryptedData = result.cryptoObject?.cipher?.doFinal(encryptedData)
onSuccess(decryptedData)
}

override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
onFailure("Authentication error: $errString")
}

override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
onFailure(Exception("Authentication Failed"))
onFailure("Authentication failed")
}
})
}
)

val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(title)
.setSubtitle(subtitle)
.setDescription(description)
.setDeviceCredentialAllowed(true) // Allows PIN/password fallback
.setTitle("Biometric Authentication")
.setSubtitle("Authenticate to access secure data")
.setNegativeButtonText("Cancel")
.build()

biometricPrompt.authenticate(promptInfo)
} else {
onFailure(Exception("Biometric authentication not available."))
biometricPrompt.authenticate(
promptInfo,
BiometricPrompt.CryptoObject(cipher)
)
} catch (e: Exception) {
onFailure("Biometric authentication failed: ${e.message}")
}
}

companion object {

/**
* Handles biometric authentication with simplified usage.
*/
fun authenticate(
activity: FragmentActivity,
title: String,
description: String,
onSuccess: (Cipher) -> Unit,
onFailure: (Exception?) -> Unit
) {
try {
val executor = activity.mainExecutor

val cipher = Cipher.getInstance(
"${KeyProperties.KEY_ALGORITHM_AES}/${KeyProperties.BLOCK_MODE_GCM}/${KeyProperties.ENCRYPTION_PADDING_NONE}"
)
val secretKey = KeyHelper.getOrCreateSecretKey()
cipher.init(Cipher.ENCRYPT_MODE, secretKey)

val biometricPrompt = BiometricPrompt(
activity,
executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
onSuccess(cipher)
}

override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
onFailure(Exception(errString.toString()))
}

override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
onFailure(Exception("Authentication failed"))
}
}
)

val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(title)
.setDescription(description)
.setNegativeButtonText("Cancel")
.build()

biometricPrompt.authenticate(
promptInfo,
BiometricPrompt.CryptoObject(cipher)
)
} catch (e: Exception) {
onFailure(e)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import javax.crypto.SecretKeyFactory
object KeyHelper {

private const val ANDROID_KEYSTORE = "AndroidKeyStore"
private const val KEY_ALIAS = "MySecureKeyAlias"

/**
* Generates an AES symmetric key and stores it in the Keystore.
Expand Down Expand Up @@ -180,4 +181,30 @@ object KeyHelper {
return keyFactory.getKeySpec(key, KeyInfo::class.java) as? KeyInfo
?: throw CryptoLibException("Unable to retrieve KeyInfo for alias '$alias'.")
}

/**
* Retrieves the existing secret key from the Android Keystore or generates a new one if it doesn't exist.
*/
fun getOrCreateSecretKey(): SecretKey {
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }

// Check if the key already exists
if (keyStore.containsAlias(KEY_ALIAS)) {
return keyStore.getKey(KEY_ALIAS, null) as SecretKey
}

// Generate a new secret key
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE)
val keyGenParameterSpec = KeyGenParameterSpec.Builder(
KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true)
.build()

keyGenerator.init(keyGenParameterSpec)
return keyGenerator.generateKey()
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.github.romantsisyk.cryptolib.crypto.manager

import android.app.Activity
import androidx.fragment.app.FragmentActivity
import io.github.romantsisyk.cryptolib.biometrics.BiometricHelper
import io.github.romantsisyk.cryptolib.crypto.config.CryptoConfig
import io.github.romantsisyk.cryptolib.crypto.aes.AESEncryption
Expand Down Expand Up @@ -91,10 +92,9 @@ object CryptoManager {
val secretKey = KeyHelper.getAESKey(config.keyAlias)

if (config.requireUserAuthentication) {
BiometricHelper.authenticateWithFallback(
activity,
BiometricHelper.authenticate(
activity = activity as FragmentActivity,
title = title,
subtitle = "",
description = description,
onSuccess = {
onAuthenticated(secretKey)
Expand Down

0 comments on commit a5b91b4

Please sign in to comment.