How Secure is your Android Keystore Authentication ?

Introduction

Privileged malware or an attacker with physical access to an Android device is a difficult attack vector to protect against. How would your application maintain security in such a scenario?

This blog post will discuss the Android keystore mechanisms and the difficulties encountered when attempting to implement secure local authentication. By providing an introduction to the AndroidKeystore, it's API and usage you will be able to understand the common vulnerabilities associated with the keystore as they are discussed. The core of this article will highlight the developed tools which can be used to audit an application's local authentication. This will conclude with general guidance on secure implementations and an application which can be used as a reference is presented.

KeyStore Introduction

The Android Keystore is a system that lets developers create and store cryptographic keys in a container making them more difficult to extract from the device. These keys are stored within specialized hardware, a so called trusted execution environment. Key material can be generated inside it and even the operating system itself should not have direct access to this secure memory. The Android Keystore provides APIs to perform cryptographic operations within this trusted environment and receive the result. It was introduced in API 18 (Android 4.3). A strongbox backed Android Keystore is currently the most secure and recommended type of keystore.

Android supports 7 different types of keystore mechanisms, each having their own advantages and disadvantages. For example the Android Keystore uses a hardware chip to store the keys in a secure way, while the Bouncy Castle Keystore (BKS) is a software keystore and uses an encrypted file placed on the file system. The Android documentation offers many code examples useful for developers but is somewhat confusing and convoluted when describing its keystore mechanisms. This results in a lot of developer headaches. In a lot of keystore related classes, the Android documentation is often taken directly from Java documentation. After not finding a concise explanation and a solution for developing secure local authentication, they resolve to StackOverflow, copying and pasting code which is not secure and can be easily bypassed with some Frida scripts. The fragmented Android ecosystem also makes working with the keystore an unpleasant experience as multiple compatibility checks need to be performed and various code paths need to be implemented in order to support a wide variety of devices.

Lets look at the following screenshot. The Android documentation is presented on the left and the Java documentation is presented on the right.

documentations comparison2

The AndroidKeystore implementation doesn't support passwords for unlocking the keystore or its specific entries. The code snippet shown in the documentation will throw an exception. The screenshot above shows that the Android documentation is not a 100% reliable source of knowledge about the AndroidKeystore implementation. Due to the fact that Android supports a new type of keystore system which is not included in classic Java, the example above will not work with AndroidKeystore. Having discovered more issues, we decided to take a deeper look at the keystore system available in Android. Further on in this post, you can find methods that can be used to test for insecure keystore usage.

Keystores, keystores everywhere!

An Android Keystore is just a Java class that Android developers can use. The Android Keystore is yet another implementation of the Java Keystore API, other types of keystores, like BSK, also implement this API.

We can just use the regular Java KeyStore API to access Android KeyStore:

KeyStore ks = KeyStore.getInstance("AndroidKeystore");

Keystore types

The table below lists keystore types supported by the stock Android (up utill Android 9):

AlgorithmSupported API Levels
AndroidCAStore 14+
AndroidKeyStore 18+
BCPKCS12 1-8
BKS 1+
BouncyCastle 1+
PKCS12 1+
PKCS12-DEF 1-8

A more up-to-date list can be found in this article:

https://developer.android.com/reference/java/security/KeyStore

There any other keystore types, for example Samsung supports its own keystore type named TIMA.

How to use the Android Keystore?

The Java keystore API contains a  java.security.KeyStore class with methods for inserting keys. However, most of the calls that are supposed to insert keys into keystore will throw an exception. This was done by Android to prevent developers from inserting hardcoded keys. The assumption of Android Keystore is that keys should never leave the trusted environment, therefore developers can only generate new keys using android.security.keystore.KeyGenParameterSpec.Builder class. An example reference application which implements local authentication can be found here.

Every key stored within the keystore can have the following parameters set:

  • alias - used to identify the key.
  • key size (API 23).
  • purpose – encrypt/decrypt (API 23).
  • encryption mode, algorithm and padding (API 23).
  • should the key be authenticated with the keystore before usage? (API 23).
  • the time duration for which the key can be used after a successful authentication (API 23).
  • should a key be invalidated on new fingerprint enrolment? (API 24)
  • should a keystore require the screen to be unlocked before performing cryptographic operations? (API 28)
  • should a key be protected by a StrongBox hardware security module? (API 28)

Supported Android API versions are included in brackets to show what security features were introduced in different Android releases. More settings and information about them can be found in the appropriate Android documentation.

Let's discuss common vulnerabilities associated with the keystore:

  • Insecure keystore type in use:
    For example, if a BKS keystore is in use, keys are stored within a file that can be accessed by a privileged user. AndroidKeystore supports hardware-backed containers and should be preferred.
  • Key not invalidated on new fingerprint enrollment:
    An attacker with physical access to a device can enroll their own fingerprint and use a key locked by biometrics.
  • Keystore accessible without screen unlock:
    Allowing the key to be used without unlocking the screen increases attack surface.
  • Weak cryptography algorithms in use:
    Weak cryptography algorithms used in KeyGenerator.getInstance method.
  • Weak/hardcoded password for keystore or keystore entry:
    This type of vulnerability can only occur when it is possible to set a password to access a keystore. The AndroidKeystore is the recommended keystore type but if the application design requires the usage of a software backed keystore then setting a strong user-derived password is advised.

Frida audit scripts

To speed up keystore auditing and make assessments more robust, we prepared some useful Frida scripts. These script are available here. The list below describes their functionalities.

Keystore tracer

Generic keystore debugging script:

  • lists keystore types used by the application.
  • lists keystore entry aliases used by the specific keystore (or by any keystore within the application).
  • list all information about a key in AndroidKeystore. Useful when determining if biometrics can be bypassed.
  • other utilities useful in reverse engineering, when looking for interesting keys.

KeyGenParameterSpec tracer

  • lists information about a key which is generated at runtime using the KeyGenParameterSpec API. Information includes key size, encryption mode, algorithm and more.
  • useful in reviewing cryptography used within the application.

SecretKeyFactory tracer

  • hooks into PBEKeySpec constructor and getInstance() method to extract a password, number of iterations, salt and key size used to create a PBKDF key.
  • useful in reviewing cryptography used within the application - not only keystore

Cipher tracer

  • hooks into the Cipher API, lists information about a specific key such as key size, encryption mode, algorithm and more.
  • useful in reviewing cryptography used within the application - not only keystore

These scripts can be started with the following command:

$ frida -U -f com.example.keystorecrypto --no-pause -l SCRIPT-PATH.js

The following snippet shows the output of the Keystore tracer script used on an example application:

$ frida -U -f com.example.keystorecrypto --no-pause -l keystore-tracer.js
...
[Google Pixel::com.example.keystorecrypto]-> KeystoreListAndroidKeystoreAliases()
...
[
    "'SYMMETRIC_MASTER_KEY'",
    "'ASYMMETRIC_MASTER_KEY'"
]
[Google Pixel::com.example.keystorecrypto]-> DumpKeystoreKeyInfo('SYMMETRIC_MASTER_KEY')
...
{
    "blockModes": [
        "GCM"
    ],
    "digests": [],
    "encryptionPaddings": [
        "NoPadding"
    ],
    "isInsideSecureHardware": true,
    "isInvalidatedByBiometricEnrollment": false,
    "isUserAuthenticationRequired": false,
    "isUserAuthenticationRequirementEnforcedBySecureHardware": true,
    "isUserAuthenticationValidWhileOnBody": false,
    "keyAlgorithm": "AES",
    "keySize": 256,
    "keyValidityForConsumptionEnd": null,
    "keyValidityForOriginationEnd": null,
    "keyValidityStart": null,
    "keystoreAlias": "SYMMETRIC_MASTER_KEY",
    "origin": 1,
    "purposes": 3,
    "signaturePaddings": [],
    "userAuthenticationValidityDurationSeconds": -1
}

As shown above, we have found 2 weaknesses using a single funtion:

  • It will be possible to add new fingerprint which will unlock the application
  • The user authentication for key usage is not required

The Cipher tracer can be a very useful script in reviewing cryptography operations. The script dumps information about the encryption algorithms, modes and padding used by the application. Moreover, it hooks into the doFinal method and shows the operation input and output that can be understood as data before encryption/decryption and after that process.
The following fragment of the Cipher tracer output presents described functionalities in a readable format:

[Cipher.getInstance()]: type: AES/GCM/NoPadding
[Cipher.getInstance()]:  cipherObj: javax.crypto.Cipher@875ca09
[Cipher.init()]: mode: Decrypt mode, secretKey: android.security.keystore.AndroidKeyStoreSecretKey spec:[object Object] , cipherObj: javax.crypto.Cipher@875ca09
[Cipher.init()]: mode: Decrypt mode, secretKey: android.security.keystore.AndroidKeyStoreSecretKey spec:[object Object] secureRandom: java.security.SecureRandom@cc52b6a , cipherObj: javax.crypto.Cipher@875ca09
[Cipher.doFinal2()]:   cipherObj: javax.crypto.Cipher@875ca09
In buffer:

  Offset  00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000  B1 5A 1E 0F F3 19 AD 80 80 A7 8F 9A E5 F8 4A 1A  .Z............J.
00000010  5E DA C4 F0 D6 E0 0C 7D 56 14 6F 92 CA 4E B2 C0  ^......}V.o..N..
00000020  CD 42 A9 5F 06 05 BA 6B 9D 36 3A 73 61 87 34 C7  .B._...k.6:sa.4.
00000030  F8 BB 0C 2D 21 8A 80 2E FB 0B 41 EB 63 7B B4 12  ...-!.....A.c{..
00000040  BE A6 48 19 D2 C3 C7 97 9E 93 5E 6B 57 07 15 A0  ..H.......^kW...
00000050  A3 4F A6 07 C7 27 10 2B D0 81 3E 17 F6 C3 69 7D  .O...'.+..>...i}
00000060  25 F7 B2 0D 25 8D 72 6B 56 5B 95 4C FB CD 5F 69  %...%.rkV[.L.._i
00000070  74 A8 5E 91 29 0C 3D E5                          t.^.).=.        

Result:

  Offset  00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000  7B 22 63 6C 61 73 73 69 63 43 69 70 68 65 72 4B  {"classicCipherK
00000010  65 79 22 3A 6E 75 6C 6C 2C 22 63 6C 61 73 73 69  ey":null,"classi
00000020  63 4D 61 63 4B 65 79 22 3A 6E 75 6C 6C 2C 22 6D  cMacKey":null,"m
00000030  6F 64 65 72 6E 4B 65 79 22 3A 22 38 77 30 32 5A  odernKey":"8w02Z
00000040  61 47 4B 35 46 30 6D 64 46 38 66 76 61 5A 6C 4A  aGK5F0mdF8fvaZlJ
00000050  38 5A 50 74 54 37 74 42 74 72 37 66 45 58 4F 63  8ZPtT7tBtr7fEXOc
00000060  78 4E 6D 56 64 55 22 7D                          xNmVdU"}        

Android Keystore and application hooking

So we have the Android Keystore which is considered secure as we cannot access key material. However, an attacker might not actually need the key contents. The Keystore API could be used to retrieve key references, then they could be used to initialize the Cipher object and then they could be used to decrypt or encrypt application storage.

Yes, this is possible and most applications will be vulnerable to this class of attacks, as an attacker with physical access to the device or privileged malware can:

  • Start the victim application
  • Hook the victim application using Frida to execute code within context of the victim application which will do following:
  1. Retrieve reference to the AndroidKeystore key using Keystore API.
  2. Initialize the Cipher object with the retrieved key reference.
  3. Decrypt/Encrypt/Sign data within application storage.

Aaand gone! Android Keystore usage is not a binary security guarantee. In order to protect against this kind of attack developers have to mark the keystore keys as accessible only after:

  • The device has been unlocked.
  • Fingerprint or other biometrics have been validated.

For this configuration, the developer has to set setUserAuthenticationRequired() to true during key generation. The other important property is setUserAuthenticationValidityDurationSeconds(). If it is set to -1 then the key can only be unlocked using Fingerprint or Biometrics. If it is set to any other value, the key can be unlocked using a device screenlock too.

In the case of a device screenlock, accessing a key is first done by calling KeyguardManager.createConfirmDeviceCredentialIntent().

It's important to note that the KeyguardManager API does not give developers the ability to check what type of screen lock is configured or to verify a password/PIN/pattern policy. Therefore, the device can have an insecure screen lock like:

  • Simple pattern (on most devices 3x3, could be guessed by trying common patterns or by inspecting finger streaks on the screen).
  • Easy pin (usually 4-5 numbers, common patterns or trivial like 0000 or 1234).
  • Guessable passwords (name of your dog).

Therefore it is advised that for highly sensitive applications like banking apps, password managers or secure messengers setUserAuthenticationValidityDurationSeconds() should not have any value other than -1.

This script can be used to trigger "device unlock" state using KeyguardManager and unlock keys that have not set a validity duration to -1.

Biometric/Fingerprint authentication

Biometric authentication, specifically fingerprint authentication, was introduced in Android 6.0 (API 23). To use fingerprint authentication, the following conditions should be met:

  • API 23 or above supported by a device.
  • Available fingerprint sensor on a device.
  • At least one registered fingerprint.
  • The application needs to include the fingerprint permission in the Manifest.xml file.

Biometric authentication can be implemented using the FingerprintManager or BiometricPrompt class and its nested classes that manage authentication mechanisms and an application dialogue asking the user to authenticate. The FingerprintManager as its names suggests only supports fingerprint authentication. The FingerprintManager class was introduced in API 23 and is deprecated since API 28 when BiometricPrompt was released. Usage of BiometricPrompt is very similar to FingerprintManager. The most important part of the biometric authentication is the following method:

public void authenticate (BiometricPrompt.CryptoObject crypto, 
                CancellationSignal cancel, 
                Executor executor, 
                BiometricPrompt.AuthenticationCallback callback)

This method warms up the biometric hardware and starts scanning for a biometric authentication attempt. This method has 2 important parameters:

  • crypto - object which contains a reference to the keystore entry that should be unlocked. In order to implement biometric authentication in a secure manner, the keystore key which is inside of this crypto object has to be used for some application critical cryptographic operation.
  • callback - structure with callbacks which will be called by the OS when the user places their finger on the fingerprint sensor, or when the prompt is cancelled.

The BiometricPrompt.AuthenticationCallback parameter is used as a callback structure that implements methods such as:

The onAuthenticationSucceded method triggers when a user is successfully authenticated by the system. Most of the encountered biometric authentication implementations rely on this method being called, without caring about the CryptoObject. Application logic responsible for unlocking the application is usually included in this callback method. This approach is trivially exploited by hooking into the application process and directly calling onAuthenticationSucceded method, as a result the application should be unlocked without providing valid biometrics.

About 70% of the assessed applications that utilised fingerprint authentication were unlocked without even requiring a valid fingerprint. Furthermore, data stored by the application was successfully decrypted after unlocking the application in 50% of cases.

The vulnerable implementations usually included something similar to the code shown below:

public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
	Toast.makeText(getActivity(), "Access granted",Toast.LENGTH_LONG).show();
        accessGranted();
}

The code listed above does not use the  CryptoObject passed in the AuthenticationResult, instead it just assumes that authentication was successful since onAuthenticationSucceeded was called.

In order to verify this test case we have created 2 following Frida scripts which can be used to test insecure biometric authentication implementation and bypass them:

  • Fingerprint bypass - This script will bypass authentication when the crypto object is not used. The authentication implementation relies on the callback onAuthenticationSucceded being called.
  • Fingerprint bypass via exception handling - This script will attempt to bypass authentication when CryptoObject is used but used in an incorrect way. The longer description of this issue can be found in 'Crypto Object Exception Handling section.

The above scripts are mostly hooks which are used to reimplement the authenticate method to use onAuthenticationSucceeded callback instead of the onAuthenticationFailed one.

But is it possible to implement local authentication in a secure way?

Yes, it is. Use AndroidKeystore. Just follow steps listed below:

  1. Create the Android keystore key with setUserAuthenticationRequired set to true and setInvalidatedByBiometricEnrollment set to -1.
  2. Initialize cipher object with keystore key created above.
  3. Create BiometricPrompt.CryptoObject using cipher object from previous step.
  4. Implement BiometricPrompt.AuthenticationCallback.onAuthenticationSucceeded callback which will retrieve cipher object from the parameter and USE this cipher object to decrypt some other crucial data such as session key, or a secondary symmetric key which will be used to decrypt application data.
  5. Call BiometricPrompt.authenticate function with crypto object and callbacks created in steps 3 and 4.

Was that so hard? :)

Crypto Object Exception Handling

Some developers use CryptoObject but they do not encrypt/decrypt data that is crucial for the application to function correctly. Therefore, we could totally skip the authentication step and proceed to use the application.

A different kind of bypass was developed for this scenario. All the script needs to do is manually call the onAuthenticationSucceded with a non-authorised (not unlocked by fingerprint) CryptoObject. However, if the application will attempt to use a locked cipher object then a javax.crypto.IllegalBlockSizeException exception will be thrown. However, nothing stops us from just handling that exception in a Frida script.

This script will attempt to call onAuthenticationSucceded and catch javax.crypto.IllegalBlockSizeException exceptions in Cipher class. Therefore, if the application does not use this key to decrypt crucial data then you will probably get into an application without authentication ;)

So, again how should this be solved? There is no single answer, it depends what the purpose of the local authentication is. For the data storage the best solution will be to use a keystore key protected by a fingerprint which will be used to... decrypt a secondary symmetric key (so a user is not prompted every time a cryptographic operation needs to take place). This symmetric key should be used to decrypt application storage. However, if you just need to call authenticate, to for example authorise a transaction, you can use an asymmetric private key to sign the data which will later be sent to the server which should verify the signature server side.

Authentication Timeout

Sometime the first authentication call might not be spoofable, as the implementation will decrypt "secondary" decryption keys (as mentioned earlier). But, all calls after that (e.g. application timeout), can sometimes be spoofable. This is because many mobile applications keep the cryptographic keys in the memory until the process is killed. Therefore, if you try to bypass authentication after the application was unlocked once there is good chance that the application will use the keys already stored in memory :)

Example Reference Application

Having knowledge about IT security and some development skills, we decided to create a project that implements biometric authentication the proper way. The following project aims to create an application that can be used as a reference for secure local authentication.
https://github.com/mwrlabs/android-keystore-audit/tree/master/keystorecrypto-app

Yes, it is available on our public github account, you can also support the project!

Resources and References:

  • Android Keystore
  • BiometricPrompt
  • FingerprintManager
  • Biometric-Auth-Sample (vulnerable library which does not use a CipherObject, authentication can be bypassed)
  • android-FingerprintDialog (Google's example application which uses a CipherObject to encrypt a static string, the base64 encoded result is displayed on the UI but is not necessary for the application to function properly. For production use, the result should be verified server-side or it's correctness should be critical to the application. The example can be bypassed to display a "Purchase successful" message.)

Credits:
Mateusz Fruba for writting the Frida scripts
Kamil Breński for Keystore and biometrics research
Krzysztof Pranczk for research and putting things together