This article describes how to talk to the system
keystore
daemon directly and store app-specific secrets in the system credential storage. It will introduce private API's, not available via the Android SDK and some OS services implementation details. Those may change at any time, and are not guaranteed to work. While the techniques described have been tested on a few different devices and OS versions (2.1 to 4.0), there are no guarantees. Use caution if you decide to implement them in a production app.Android's credential storage is implemented as a native Linux service (daemon), with a few extra layers on top of it that make it available to the framework. Let's quickly review what we know about the
keystore
daemon (described in more detail here):- it's a native daemon, started at boot
- it provides a local control socket to allow apps and system services to talk to it
- it encrypts keys using an AES 128 bit master key
- encrypted keys are stored in
/data/misc/keystore
, one file per key - the master key is derived from the device unlock password or PIN
- it authorizes administration commands execution and key access based on caller UID
Command | Description | Allowed UIDs | Parameters |
---|---|---|---|
test | Check that the key store is in a usable state | anyone but root, vpn and wifi | none |
get | Get unencrypted key | anyone (*1) | key name |
insert | Add or overwrite key | anyone but root, vpn and wifi | key name and value |
del | Delete a key | anyone but root, vpn and wifi (*1) | key name |
exist | Check if a key exists | anyone but root, vpn and wifi (*1) | key name |
saw | List keys with the specified prefix | anyone but root, vpn and wifi (*1) | key prefix |
reset | Reset the key store | system | none |
password | Change the key store password | system | new password |
lock | Lock the key store | system | none |
unlock | Unlock the key store | system | none |
zero | Check if the key store is empty | system | none |
*1 Only keys created with the same UID are visible/accessible |
As you can see from the table above, once the credential storage is initialized and unlocked, any app can add, delete, list and get keys. Each key is bound to the UID of the process that created it, so that apps cannot access each other's keys or the system ones. Additionally, even system apps cannot see app keys, and root is explicitly prohibited from creating or listing keys. Thus, if the API were public user apps could use the credential storage to securely store their secrets, as long as it is unlocked. Unlocking, however, requires a system permission. On ICS, the credential storage is unlocked when you enter your device unlock pattern, PIN or password, so in practice the
keystore
daemon will be already in an unlocked state by the time your app starts. On pre-ICS devices the device unlock password and the credential storage protection password are separate, so unlocking the device has no effect on credential storage state. Fortunately, Android provides a system activity that can unlock the key store. All we have to do is send an intent with the proper action to start the unlock activity. The action is however, slightly different on pre-Honeycomb and Honeycomb/ICS devices, so we need to check the Android version, before sending it:try {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
startActivity(new Intent("android.credentials.UNLOCK"));
} else {
startActivity(new Intent("com.android.credentials.UNLOCK"));
}
} catch (ActivityNotFoundException e) {
Log.e(TAG, "No UNLOCK activity: " + e.getMessage(), e);
}
Note that the unlock activity is using the transparent theme, so it will look like a dialog originating from your own activity. It is, however, managed by the system, so your app will be paused and resumed only after the unlock activity finishes. You need to handle this in your activity's code (you can't use
startActivityForResult()
though, since the unlock activity doesn't call setResult()
). Additionally, if you don't have a device (or credential storage on pre-ICS devices) password set up, you will be prompted to set one. Control will be returned to your app only after you have set and confirmed an unlock password and initialized the credential storage.Now that the keystore is unlocked, we can try to actually use it. As briefly mentioned above, it uses a local control socket for IPC, and the protocol is rather simple: a single letter command, followed by the length and value of any parameters (up to two). The protocol is already implemented in the
android.security.KeyStore
class, which is however hidden from non-system applications. The reason for not exposing this API given in the JavaDoc comment is that 'it assumes that private and secret key bytes are available and would preclude the use of hardware crypto'. This is a very valid comment: in the current implementation keys are exported and imported as unencrypted blobs. If the keys were protected by a hardware device, the API would have to return some sort of an opaque key handle, since the actual key material would not be available, or would only be exportable if wrapped with another key. If the next Android version introduces hardware cryptography support, the API would have to change dramatically. Having said that, we want to use the keystore now, so we will ignore the warning and go ahead. Since the KeyStore
is hidden we cannot import it directly, but we can call it using reflection. This is easy enough to do, but somewhat cumbersome. As the class doesn't really have any dependencies it is easier to copy it in our project, adding a few minor modifications to get it to compile (see sample code). Once this is done, we can list, add and get keys: KeyStore ks = KeyStore.getInstance();
// get the names of all keys created by our app
String[] keyNames = ks.saw("");
// store a symmetric key in the keystore
SecretKey key = Crypto.generateKey();
boolean success = ks.put("secretKey1", key.getEncoded());
// check if operation succeeded and get error code if not
if (!success) {
int errorCode = ks.getLastError();
throw new RuntimeException("Keystore error: " + errorCode);
}
// get a key from the keystore
byte[] keyBytes = ks.get("secretKey1");
SecretKey key = new SecretKeySpec(keyBytes, "AES");
// delete a key
boolean success = ks.delete("secretKey1");
As you can see from the code above, using the credential storage is pretty straightforward. You save keys by giving them a name (used as part of the file name the encrypted blobs are saved into), and then use that name to retrieve or delete them. The UID of the process that created the key is also a part of the file name, and thus key names only need to be unique within your application. One thing to note is that
KeyStore
methods that don't return a value (key name(s) or bytes), return a success flag, so you need to make sure you check it. In case of an error a more detailed error code can be obtained by calling getLastError()
. All error codes are defined in the KeyStore
class, but you are most likely to encounter PERMISSION_DENIED
(if you try to call one of the methods reserved for the system
user) or KEY_NOT_FOUND
(if you try to access a non-existing key). Check the sample project for a full app that generates an AES key, encrypts some data, then stores the key in the system credential storage and later retrieves it in order to decrypt the data. It generates and saves a new key each time you press 'Encrypt' and you can see the stored keys in the list view. Press the 'Reset' button to delete all keys created by the app. Note that the
KeyStore
class used is not compatible with the original Donut (Android 1.6) credential storage implementation, but it should work with all (public) subsequent versions. Here's how the app's screen looks like. Full code is, as usual, on github.Besides keys you can store any sensitive information your app needs such as login passwords or tokens. Since decrypting the files on disk requires a key derived from the unlock password (or a dedicated password on pre-ICS devices), your secrets cannot be extracted even by apps with root access, or someone with physical access to the device (unless they know the password, of course). The master encryption key, however, is not tied to the device (like in iOS), so it is possible to copy the encrypted key files and perform a brute force attack on a different, more powerful machine(s).
You can experiment with other
KeyStore
API's, but most of those will result in a PERMISSION_DENIED
when called from a non-system app. On ICS, there is also a public intent (action: com.android.credentials.RESET
) that resets the credential storage, so you could prompt the user to clear it from your app, if necessary. Note that this will delete all stored data (keys, certificates, etc.), not just the ones your app created, so use with caution. As a final warning, the code presented in this post does rely on private API's and OS implementation details, so it might break with the next Android version, or even not work on all current devices. Keep this in mind if you decide to use it in a production app.
Post a Comment