Most of the authentication and session management requirements of the MASVS refer to architectural and server-side issues that can be verified independent of the specific implementation on iOS or Android. In the MSTG, we therefore discuss these test cases in a platform-independent way (see the appendix "Testing Authentication and Session Management on the Endpoint"). There's however also cases where local authentication mechanisms are used - e.g. to locally "unlock" the app and/or provide an easy means for users to resume an existing session. These cases are discussed here.
Android 6.0 introduced public APIs for authenticating users via fingerprint. Access to the fingerprint hardware is provided through the FingerprintManager
class [1]. An app can request fingerprint authentication by instantiating a FingerprintManager
object and calling its authenticate()
method. The caller registers callback methods to handle possible outcomes of the authentication process (success, failure or error).
By using the fingerprint API in conjunction with the Android KeyGenerator class, apps can create a cryptographic key that must be "unlocked" with the user's fingerprint. This can be used to implement more convenient forms of user login. For example, to allow users access to a remote service, a symmetric key can be created and used to encrypt the user PIN or authentication token. By calling setUserAuthenticationRequired(true)
when creating the key, it is ensured that the user must re-authenticate using their fingerprint to retrieve it. The encrypted authentication data itself can then be saved using regular storage (e.g. SharedPreferences).
Apart from this relatively reasonable method, fingerprint authentication can also be implemented in unsafe ways. For instance, developers might opt to assume successful authentication based solely on whether the onAuthenticationSucceeded
callback [3] is called or when the Samsung Pass SDK is used for instance. This event however isn't proof that the user has performed biometric authentication - such a check can be easily patched or bypassed using instrumentation. Leveraging the Keystore is the only way to be reasonably sure that the user has actually entered their fingerprint.
Search for calls of FingerprintManager.authenticate()
. The first parameter passed to this method should be a CryptoObject
instance. CryptoObject
is a wrapper class for the crypto objects supported by FingerprintManager [2]. If this parameter is set to null
, the fingerprint auth is purely event-bound, which likely causes a security issue.
Trace back the creation of the key used to initialize the cipher wrapped in the CryptoObject. Verify that the key was created using the KeyGenerator
class, and that setUserAuthenticationRequired(true)
was called when creating the KeyGenParameterSpec
object (see also the code samples below).
Verify the authentication logic. For the authentication to be successful, the remote endpoint must require the client to present the secret retrieved from the Keystore, or some value derived from the secret.
Patch the app or use runtime instrumentation to bypass fingerprint authentication on the client. For example, you could use Frida call the onAuthenticationSucceeded
callback directly. Refer to the chapter "Tampering and Reverse Engineering on Android" for more information.
Fingerprint authentication should be implemented along the following lines:
Check whether fingerprint authentication is possible. The device must run Android 6.0 or higher (SDK 23+) and feature a fingerprint sensor. There are a two pre-requisites that you need to check:
- The user must have protected their lockscreen
KeyguardManager keyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
keyguardManager.isKeyguardSecure();
- Fingerprinthardware must be available:
FingerprintManager fingerprintManager = (FingerprintManager)
context.getSystemService(Context.FINGERPRINT_SERVICE);
fingerprintManager.isHardwareDetected();
- At least one finger should be registered:
fingerprintManager.hasEnrolledFingerprints();
- The application should have permission to ask for the users fingerprint:
context.checkSelfPermission(Manifest.permission.USE_FINGERPRINT) == PermissionResult.PERMISSION_GRANTED;
If any of those checks failed, the option for fingerprint authentication should not be offered.
When setting up fingerprint authentication, create a new AES key using the KeyGenerator
class. Add setUserAuthenticationRequired(true)
in KeyGenParameterSpec.Builder
.
generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE);
generator.init(new KeyGenParameterSpec.Builder (KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setUserAuthenticationRequired(true)
.build()
);
generator.generateKey();
Please note, that since Android 7 you can use the setInvalidatedByBiometricEnrollment(boolean value)
as a method of the builder. If you set this to true, then the fingerprint will not be invalidated when new fingerprints are enrolled. Even though this might provide user-convenience, it opens op a problem area when possible attackers are somehow able to social-engineer their fingerprint in.
To perform encryption or decryption, create a Cipher
object and initialize it with the AES key.
SecretKey keyspec = (SecretKey)keyStore.getKey(KEY_ALIAS, null);
if (mode == Cipher.ENCRYPT_MODE) {
cipher.init(mode, keyspec);
Note that the key cannot be used right away - it has to be authenticated through the FingerprintManager
first. This involves wrapping Cipher
into a FingerprintManager.CryptoObject
which is passed to FingerprintManager.authenticate()
.
cryptoObject = new FingerprintManager.CryptoObject(cipher);
fingerprintManager.authenticate(cryptoObject, new CancellationSignal(), 0, this, null);
If authentication succeeds, the callback method onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result)
is called, and the authenticated CryptoObject can be retrieved from the authentication result.
public void authenticationSucceeded(FingerprintManager.AuthenticationResult result) {
cipher = result.getCryptoObject().getCipher();
(... do something with the authenticated cipher object ...)
}
Please bare in mind that the keys might not be always in secure hardware, for that you can do the following to validate the posture of the key:
SecretKeyFactory factory = SecretKeyFactory.getInstance(getEncryptionKey().getAlgorithm(), ANDROID_KEYSTORE);
KeyInfo secetkeyInfo = (KeyInfo) factory.getKeySpec(yourencryptionkeyhere, KeyInfo.class);
secetkeyInfo.isInsideSecureHardware()
Please note that, on some systems, you can make sure that the biometric authentication policy itself is hardware enforced as well. This is checked by:
keyInfo.isUserAuthenticationRequirementEnforcedBySecureHardware();
For a full example, see the blog article by Deivi Taka [4].
- M4 - Insecure Authentication - https://www.owasp.org/index.php/Mobile_Top_10_2016-M4-Insecure_Authentication
- 4.6: "Biometric authentication, if any, is not event-bound (i.e. using an API that simply returns "true" or "false"). Instead, it is based on unlocking the keychain/keystore."
- CWE-287 - Improper Authentication
- CWE-604 - Use of Client-Side Authentication
- [1] FingerprintManager - https://developer.android.com/reference/android/hardware/fingerprint/FingerprintManager.html
- [2] FingerprintManager.CryptoObject - https://developer.android.com/reference/android/hardware/fingerprint/FingerprintManager.CryptoObject.html
- [3] https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder.html#setUserAuthenticationRequired(boolean)
- [4] Securing Your Android Apps with the Fingerprint API - https://www.sitepoint.com/securing-your-android-apps-with-the-fingerprint-api/#savingcredentials
- [5] Android Security Bulletins - https://source.android.com/security/bulletin/
- [6] Extracting Qualcomm's KeyMaster Keys - Breaking Android Full Disk Encryption - http://bits-please.blogspot.co.uk/2016/06/extracting-qualcomms-keymaster-keys.html