How Secure is your Android Keystore Authentication?
-
Mateusz Fruba
- 21 Aug 2019
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.
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.
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.
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");
The table below lists keystore types supported by the stock Android (up utill Android 9):
Algorithm | Supported 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.
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:
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:
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:
KeyGenParameterSpec tracer
SecretKeyFactory tracer
Cipher tracer
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:
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"}
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:
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:
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:
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 authentication, specifically fingerprint authentication, was introduced in Android 6.0 (API 23). To use fingerprint authentication, the following conditions should be met:
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:
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:
The above scripts are mostly hooks which are used to reimplement the authenticate method to use onAuthenticationSucceeded callback instead of the onAuthenticationFailed one.
Yes, it is. Use AndroidKeystore. Just follow steps listed below:
Was that so hard? :)
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.
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 :)
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!
Credits: Mateusz Fruba for writting the Frida scripts Kamil Breński for Keystore and biometrics research Krzysztof Pranczk for research and putting things together