Mobile platforms support the use of biometrics to unlock access to a secure storage area. MIRACL Trust can integrate with these systems by storing the user’s PIN within the platform’s secure storage, enabling the app to retrieve it using biometric authentication instead of requiring manual PIN entry.
When using biometrics for authentication, users may have the option to set a fallback PIN, though this is not required and depends on the implementation.
# Secure Storage Options
Mobile platforms provide secure native options for storing the PIN:
- iOS: Store the PIN as encrypted binary data in the iOS Keychain.
- Android: Encrypt the PIN using a key managed by the Android Keystore system.
# How It Works
-
During registration, the end user selects a PIN.
-
The end user is then prompted to enable biometric authentication.
-
If biometric authentication is enabled, the PIN is securely stored using the iOS Keychain or Android Keystore system.
-
When the end user attempts to authenticate, a biometric scan is performed.
-
Upon successful biometric verification, the stored PIN is retrieved and used for authentication.
# Android: Encrypting and Retrieving the PIN
To encrypt the end user’s PIN, you can incorporate cryptography into the biometric authentication workflow using an instance of CryptoObject.
The following examples use a Cipher object and a SecretKey object to encrypt and decrypt the end user’s PIN:
# Encrypting the PIN
-
Generate a key that uses the following KeyGenParameterSpec configuration:
val keyGenerator = KeyGenerator.getInstance( KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore") val keyGenParameterSpec = KeyGenParameterSpec.Builder( KEY_NAME, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT ) .setBlockModes(KeyProperties.BLOCK_MODE_CBC) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) .setUserAuthenticationRequired(true) .build() keyGenerator.init(keyGenParameterSpec) keyGenerator.generateKey()
-
Get the
SecretKey
:val keyStore = KeyStore.getInstance("AndroidKeyStore") keyStore.load(null) val secretKey = keyStore.getKey(KEY_NAME, null) as SecretKey
-
Initialize a cipher for encryption:
val cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7) cipher.init(Cipher.ENCRYPT_MODE, secretKey)
-
Start a biometric authentication workflow that incorporates the cipher:
val biometricPrompt = BiometricPrompt(this, executor, callback) val promptInfo = BiometricPrompt.PromptInfo.Builder() .setTitle("Biometric authentication") .setNegativeButtonText("Cancel") .build() biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
-
Within the biometric authentication callbacks, use the secret key to encrypt the end user’s PIN:
override fun onAuthenticationSucceeded( result: BiometricPrompt.AuthenticationResult) { val encryptedPinCode: ByteArray = result.cryptoObject.cipher?.doFinal( pinCode.toByteArray(Charset.defaultCharset()) ) val iv = cipher.iv // Store the encrypted PIN and the initialization vector (IV) to // decrypt the PIN the next time the end user attempts to authenticate. }
# Retrieving the PIN
-
Get the
SecretKey
:val keyStore = KeyStore.getInstance("AndroidKeyStore") keyStore.load(null) val secretKey = keyStore.getKey(KEY_NAME, null) as SecretKey
-
Initialize a cipher for decryption:
val cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7) cipher.init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(iv))
-
Start a biometric authentication workflow that incorporates the cipher:
val biometricPrompt = BiometricPrompt(this, executor, callback) val promptInfo = BiometricPrompt.PromptInfo.Builder() .setTitle("Biometric authentication") .setNegativeButtonText("Cancel") .build() biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
-
Within the biometric authentication callbacks, use the secret key to decrypt the PIN:
override fun onAuthenticationSucceeded( result: BiometricPrompt.AuthenticationResult) { val decriptedPinCode: ByteArray? = result.cryptoObject.cipher?.doFinal( encryptedPinCode ) decryptedPinCode?.let { val pinCode = String(decryptedPinCode) // Authenticate the user with the PIN. } }
For more information, see the Android biometric authentication documentation.
# iOS: Storing and Retrieving the PIN in Keychain
The examples below demonstrate how to use the Keychain Services API to store and retrieve the PIN.
# Storing the PIN
func storePinInKeychain(pinCode: String, userId: String, projectId: String, server: String) {
// Create an access control object with `biometryCurrentSet`,
// which enables keychain item access with additional FaceID or TouchID authentication.
let accessControl = SecAccessControlCreateWithFlags(
nil,
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
.biometryCurrentSet,
nil
)
// Create a query dictionary for saving items in the Keychain.
let query: [String: Any] = [
// The item can be configured as a password.
kSecClass as String: kSecClassInternetPassword,
// This account must be unique, since the same User ID may be reused across multiple projects.
kSecAttrAccount as String: "\(userId):\(projectId)",
// The server indicates which service the PIN is associated with.
kSecAttrServer as String: server,
// Set the previously created control object.
kSecAttrAccessControl as String: accessControl as Any,
// Provide an authentication context. It is responsible for requesting permission from the user.
kSecUseAuthenticationContext as String: LAContext(),
// Pass the PIN as binary data.
kSecValueData as String: pinCode.data(using: String.Encoding.utf8)!
]
// Add the PIN code to the Keychain by executing the query.
let status = SecItemAdd(query as CFDictionary, nil)
if status == errSecSuccess {
// The PIN is stored successfully.
} else {
// For error messages, call SecCopyErrorMessageString(status, nil).
}
}
# Retrieving the PIN
func getPinFromKeychain(userId: String, projectId: String, server: String) -> String? {
// Create a query dictionary for retrieving the PIN from the Keychain.
let query: [String: Any] = [
// The item is stored as a password.
kSecClass as String: kSecClassInternetPassword,
// This account must be unique, since the same User ID may be reused across multiple projects.
kSecAttrAccount as String: "\(userId):\(projectId)",
// The server indicates which service the PIN is associated with.
kSecAttrServer as String: server,
// Return a single object.
kSecMatchLimit as String: kSecMatchLimitOne,
// Provide an authentication context. It is responsible for requesting permission from the user.
kSecUseAuthenticationContext as String: LAContext(),
// Indicate that the stored data should be returned.
kSecReturnData as String: true
]
// Execute the query to retrieve the PIN as binary data.
var queryResult: AnyObject?
SecItemCopyMatching(query as CFDictionary, &queryResult)
// Unwrap the PIN as a string.
if let data = queryResult as? Data, let pinCode = String(data: data, encoding: .utf8) {
return pinCode
}
return nil
}