Biometric Authentication Without Cryptographic Binding
Authentification biométrique sans liaison cryptographique
Description
L'authentification biométrique sans liaison cryptographique se produit lorsqu'une application implémente l'authentification biométrique (empreinte digitale, reconnaissance faciale, etc.) sans la lier correctement à des clés cryptographiques ou à des identifiants sécurisés. Cette vulnérabilité permet aux attaquants de contourner l'authentification biométrique en accédant directement aux ressources protégées ou en interceptant et rejouant les jetons d'authentification. La vérification biométrique devient simplement une vérification au niveau de l'interface utilisateur qui peut être contournée au niveau de l'API ou du stockage, car les données biométriques ne sont pas cryptographiquement liées aux données ou à la session sécurisées.
Exemples
Dart
import 'package:flutter/material.dart';
import 'package:local_auth/local_auth.dart';
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Biometric Auth Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: LoginScreen(),
);
}
}
class LoginScreen extends StatefulWidget {
@override
_LoginScreenState createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final LocalAuthentication auth = LocalAuthentication();
bool _isAuthenticated = false;
final String apiKey = "secret_api_key_12345";
Future<void> _authenticate() async {
bool authenticated = false;
try {
authenticated = await auth.authenticate(
localizedReason: 'Authenticate to access the app',
options: const AuthenticationOptions(
stickyAuth: true,
biometricOnly: true,
),
);
} catch (e) {
print(e);
}
if (authenticated) {
setState(() {
_isAuthenticated = true;
});
// VULNÉRABLE : Stockage de données sensibles sans liaison cryptographique
final prefs = await SharedPreferences.getInstance();
prefs.setString('auth_status', 'authenticated');
prefs.setString('api_key', apiKey);
}
}
void _accessSecureData() async {
final prefs = await SharedPreferences.getInstance();
// VULNÉRABLE : Vérification uniquement d'un drapeau (flag) local sans vérification cryptographique
if (prefs.getString('auth_status') == 'authenticated') {
final retrievedApiKey = prefs.getString('api_key');
print('Accessed secure data: $retrievedApiKey');
} else {
print('Authentication required');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Biometric Auth Demo')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
if (!_isAuthenticated)
ElevatedButton(
onPressed: _authenticate,
child: Text('Authenticate with Biometrics'),
)
else
Column(
children: [
Text('Authentication Successful!'),
SizedBox(height: 20),
ElevatedButton(
onPressed: _accessSecureData,
child: Text('Access Secure Data'),
),
],
),
],
),
),
);
}
}
Kotlin
import android.hardware.biometrics.BiometricPrompt
import android.os.Build
import androidx.annotation.RequiresApi
@RequiresApi(Build.VERSION_CODES.P)
class BiometricAuthVulnerable {
private var cryptoObject: BiometricPrompt.CryptoObject? = null
init {
// Dans une implémentation vulnérable, le CryptoObject peut être nul ou mal lié
cryptoObject = null
}
fun authenticate(biometricPrompt: BiometricPrompt, callback: BiometricPrompt.AuthenticationCallback) {
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Biometric Authentication")
.setSubtitle("Authenticate using your fingerprint")
.setNegativeButtonText("Cancel")
.build()
// The cryptoObject is passed but is null or not properly initialized,
// meaning the biometric data isn't cryptographically bound.
biometricPrompt.authenticate(promptInfo, cryptoObject, callback)
println("Authentication initiated without proper cryptographic binding.")
}
// In a real scenario, you would have a callback to handle success/failure
class AuthenticationCallback : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
println("Authentication succeeded, but without cryptographic binding, the result is vulnerable.")
// Access granted without proper security measures.
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
println("Authentication failed.")
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
System.err.println("Authentication error: $errString")
}
}
}
fun main() {
// This is a simplified example and would require an Activity context
// and proper BiometricPrompt initialization in a real Android application.
println("This is a demonstration of vulnerable biometric authentication.")
println("In a real app, the CryptoObject would be missing or improperly used.")
}
Java
import android.hardware.biometrics.BiometricPrompt;
import android.os.Build;
import androidx.annotation.RequiresApi;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import java.security.KeyStore;
@RequiresApi(api = Build.VERSION_CODES.P)
public class SecureBiometricAuthCrypto {
private static final String KEY_NAME = "biometric_key";
private KeyStore keyStore;
public SecureBiometricAuthCrypto() {
try {
keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
if (!keyStore.containsAlias(KEY_NAME)) {
generateKey();
}
} catch (Exception e) {
throw new RuntimeException("Failed to initialize Keystore", e);
}
}
private void generateKey() {
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES", "AndroidKeyStore");
keyGenerator.init(null); // No specific parameters needed for AndroidKeyStore
keyGenerator.generateKey();
} catch (Exception e) {
throw new RuntimeException("Failed to generate key", e);
}
}
private BiometricPrompt.CryptoObject getCryptoObject() {
try {
SecretKey key = (SecretKey) keyStore.getKey(KEY_NAME, null);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
cipher.init(Cipher.ENCRYPT_MODE, key);
return new BiometricPrompt.CryptoObject(cipher);
} catch (Exception e) {
throw new RuntimeException("Failed to get CryptoObject", e);
}
}
public void authenticate(BiometricPrompt biometricPrompt, BiometricPrompt.AuthenticationCallback callback) {
BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
.setTitle("Biometric Authentication")
.setSubtitle("Authenticate using your fingerprint")
.setNegativeButtonText("Cancel")
.build();
// Properly obtain and pass the CryptoObject to bind the biometric data
BiometricPrompt.CryptoObject cryptoObject = getCryptoObject();
if (cryptoObject != null) {
biometricPrompt.authenticate(promptInfo, cryptoObject, callback);
System.out.println("Authentication initiated with cryptographic binding.");
} else {
System.err.println("Failed to obtain CryptoObject. Authentication cannot be securely performed.");
}
}
public static class AuthenticationCallback extends BiometricPrompt.AuthenticationCallback {
@Override
public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
if (result.getCryptoObject() != null) {
// The getCryptoObject() will return the CryptoObject used during authentication.
// You can now use the Cipher object within it to perform cryptographic operations
// on the data that the user intended to protect with their biometric.
System.out.println("Authentication succeeded with cryptographic binding. Cipher: " + result.getCryptoObject().getCipher().getAlgorithm());
// Proceed with secure operations using the cipher.
} else {
System.err.println("Authentication succeeded, but CryptoObject is null. This should not happen in a secure implementation.");
// Handle this error appropriately.
}
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
System.out.println("Authentication failed.");
}
@Override
public void onAuthenticationError(int errorCode, CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
System.err.println("Authentication error: " + errString);
}
}
public static void main(String[] args) {
// This is a simplified example and would require an Activity context
// and proper BiometricPrompt initialization in a real Android application.
System.out.println("This demonstrates secure biometric authentication using CryptoObject.");
}
}
Recommandation
Pour implémenter l'authentification biométrique de manière sécurisée, liez cryptographiquement la vérification biométrique aux données sensibles ou au processus d'authentification. Utilisez le matériel sécurisé de l'appareil (tel que Keystore/Keychain) pour générer et stocker des clés cryptographiques qui ne peuvent être consultées qu'après une authentification biométrique réussie. Cela garantit que même si un attaquant contourne la couche d'interface utilisateur ou obtient l'accès aux préférences stockées, il ne peut pas accéder aux données protégées sans passer la vérification biométrique, car les clés de déchiffrement restent protégées par le matériel et conditionnées à la biométrie.
Exemples de code :
Dart
import 'package:flutter/material.dart';
import 'package:local_auth/local_auth.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:encrypt/encrypt.dart' as encrypt;
class SecureBiometricAuth {
final LocalAuthentication auth = LocalAuthentication();
final FlutterSecureStorage secureStorage = FlutterSecureStorage();
// Pour stocker la clé de chiffrement de manière sécurisée
final String keyId = 'biometric_protected_key';
Future<bool> authenticateUser() async {
return await auth.authenticate(
localizedReason: 'Authenticate to access the app',
options: const AuthenticationOptions(
stickyAuth: true,
biometricOnly: true,
),
);
}
Future<void> secureData(String sensitiveData) async {
if (await authenticateUser()) {
// Générer la clé cryptographique après l'authentification biométrique
final key = encrypt.Key.fromSecureRandom(32);
final iv = encrypt.IV.fromSecureRandom(16);
// Store encryption key in secure storage
await secureStorage.write(key: keyId, value: key.base64);
await secureStorage.write(key: '${keyId}_iv', value: iv.base64);
// Chiffrer les données sensibles
final encrypter = encrypt.Encrypter(encrypt.AES(key));
final encrypted = encrypter.encrypt(sensitiveData, iv: iv);
// Store encrypted data - only useful with the key
final prefs = await SharedPreferences.getInstance();
prefs.setString('encrypted_sensitive_data', encrypted.base64);
return true;
}
return false;
}
Future<String?> retrieveSecureData() async {
if (await authenticateUser()) {
// Get encryption key (requires biometric auth to access secure storage)
final keyString = await secureStorage.read(key: keyId);
final ivString = await secureStorage.read(key: '${keyId}_iv');
if (keyString == null || ivString == null) return null;
// Get encrypted data
final prefs = await SharedPreferences.getInstance();
final encryptedData = prefs.getString('encrypted_sensitive_data');
if (encryptedData == null) return null;
// Decrypt data using key from secure storage
final key = encrypt.Key.fromBase64(keyString);
final iv = encrypt.IV.fromBase64(ivString);
final encrypter = encrypt.Encrypter(encrypt.AES(key));
return encrypter.decrypt64(encryptedData, iv: iv);
}
return null;
}
}
Kotlin
// Recommended Kotlin code for Recommendation (Secure Data Access with CryptoObject)
import android.hardware.biometrics.BiometricPrompt
import android.os.Build
import androidx.annotation.RequiresApi
import java.nio.charset.StandardCharsets
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.NoSuchPaddingException
import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import java.util.Base64
@RequiresApi(Build.VERSION_CODES.P)
class SecureBiometricDataAccess {
private val KEY_NAME = "biometric_data_key"
private val TRANSFORMATION = "AES/CBC/PKCS7Padding"
private val keyStore: KeyStore
init {
try {
keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
if (!keyStore.containsAlias(KEY_NAME)) {
generateKey()
}
} catch (e: Exception) {
throw RuntimeException("Failed to initialize Keystore", e)
}
}
private fun generateKey() {
try {
val keyGenerator = KeyGenerator.getInstance("AES", "AndroidKeyStore")
keyGenerator.init(null)
keyGenerator.generateKey()
} catch (e: Exception) {
throw RuntimeException("Failed to generate key", e)
}
}
private fun getCryptoObject(): BiometricPrompt.CryptoObject? {
return try {
val key = keyStore.getKey(KEY_NAME, null) as SecretKey
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.DECRYPT_MODE, key, IvParameterSpec(ByteArray(cipher.blockSize)))
BiometricPrompt.CryptoObject(cipher)
} catch (e: NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException) {
RuntimeException("Failed to get CryptoObject for decryption", e)
null
}
}
// Simuler le stockage de données chiffrées
private val encryptedData = "some_encrypted_data" // In a real app, this would be fetched
fun authenticateAndDecrypt(biometricPrompt: BiometricPrompt, callback: BiometricPrompt.AuthenticationCallback) {
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Biometric Authentication for Data Access")
.setSubtitle("Authenticate to decrypt sensitive data")
.setNegativeButtonText("Cancel")
.build()
val cryptoObject = getCryptoObject()
if (cryptoObject != null) {
biometricPrompt.authenticate(promptInfo, cryptoObject, callback)
println("Authentication initiated for data decryption.")
} else {
System.err.println("Failed to obtain CryptoObject for decryption.")
}
}
class AuthenticationCallback : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
val cryptoObject = result.cryptoObject
if (cryptoObject != null) {
val cipher = cryptoObject.cipher
try {
// Simulate fetching encrypted data
val encryptedBytes = Base64.getDecoder().decode("YWFhYWFhYWFhYWFhYWFhYQ==") // Replace with actual encrypted data
val decryptedBytes = cipher?.doFinal(encryptedBytes)
val decryptedData = decryptedBytes?.toString(StandardCharsets.UTF_8)
println("Biometric authentication successful. Decrypted data: $decryptedData")
// Now you can securely use the decrypted data.
} catch (e: Exception) {
System.err.println("Error during decryption: ${e.message}")
}
} else {
System.err.println("Authentication succeeded, but CryptoObject is null.")
}
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
println("Biometric authentication failed.")
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
System.err.println("Authentication error: $errString")
}
}
}
fun main() {
// Requires an Android environment for BiometricPrompt
println("This demonstrates secure data access after biometric authentication.")
}
Java
// Recommended Java code for Recommendation (Secure Data Access with CryptoObject)
import android.hardware.biometrics.BiometricPrompt;
import android.os.Build;
import androidx.annotation.RequiresApi;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
@RequiresApi(api = Build.VERSION_CODES.P)
public class SecureBiometricDataAccess {
private static final String KEY_NAME = "biometric_data_key";
private static final String TRANSFORMATION = "AES/CBC/PKCS7Padding";
private KeyStore keyStore;
public SecureBiometricDataAccess() {
try {
keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
if (!keyStore.containsAlias(KEY_NAME)) {
generateKey();
}
} catch (Exception e) {
throw new RuntimeException("Failed to initialize Keystore", e);
}
}
private void generateKey() {
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES", "AndroidKeyStore");
keyGenerator.init(null);
keyGenerator.generateKey();
} catch (Exception e) {
throw new RuntimeException("Failed to generate key", e);
}
}
private BiometricPrompt.CryptoObject getCryptoObject() {
try {
SecretKey key = (SecretKey) keyStore.getKey(KEY_NAME, null);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(new byte[cipher.getBlockSize()]));
return new BiometricPrompt.CryptoObject(cipher);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new RuntimeException("Failed to get CryptoObject for decryption", e);
}
}
// Simuler le stockage de données chiffrées
private String encryptedData = "some_encrypted_data"; // In a real app, this would be fetched
public void authenticateAndDecrypt(BiometricPrompt biometricPrompt, BiometricPrompt.AuthenticationCallback callback) {
BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
.setTitle("Biometric Authentication for Data Access")
.setSubtitle("Authenticate to decrypt sensitive data")
.setNegativeButtonText("Cancel")
.build();
BiometricPrompt.CryptoObject cryptoObject = getCryptoObject();
if (cryptoObject != null) {
biometricPrompt.authenticate(promptInfo, cryptoObject, callback);
System.out.println("Authentication initiated for data decryption.");
} else {
System.err.println("Failed to obtain CryptoObject for decryption.");
}
}
public static class AuthenticationCallback extends BiometricPrompt.AuthenticationCallback {
@Override
public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
if (result.getCryptoObject() != null) {
Cipher cipher = result.getCryptoObject().getCipher();
try {
// Simulate fetching encrypted data
byte[] encryptedBytes = Base64.getDecoder().decode("YWFhYWFhYWFhYWFhYWFhYQ=="); // Replace with actual encrypted data
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
String decryptedData = new String(decryptedBytes, StandardCharsets.UTF_8);
System.out.println("Biometric authentication successful. Decrypted data: " + decryptedData);
// Now you can securely use the decrypted data.
} catch (Exception e) {
System.err.println("Error during decryption: " + e.getMessage());
}
} else {
System.err.println("Authentication succeeded, but CryptoObject is null.");
}
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
System.out.println("Biometric authentication failed.");
}
@Override
public void onAuthenticationError(int errorCode, CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
System.err.println("Authentication error: " + errString);
}
}
public static void main(String[] args) {
// Requires an Android environment for BiometricPrompt
System.out.println("This demonstrates secure data access after biometric authentication.");
}
}
Liens
Normes
- OWASP_MASVS_L1:
- MSTG_STORAGE_1
- MSTG_CRYPTO_1
- OWASP_MASVS_L2:
- MSTG_AUTH_8
- MSTG_AUTH_10
- HIPAA_CONTROLS:
- SECURITY221
- SECURITY212
- SECURITY213
- SECURITY251