In the previous post on Android user data security, we looked at encrypting data via a user-supplied passcode. This tutorial will shift the focus to credential and key storage. I'll begin by introducing account credentials and end with an example of protecting data using the KeyStore.
Often, when working with a third-party service, there will be some form of
authentication required. This may be as simple as a /login
endpoint
that accepts a username and password.
It would seem at first that a simple
solution is to build a UI that asks the user to log in, and then capture and
store their login credentials. However, this isn't the best practice because
our app shouldn't need to know the credentials for a third-party account. Instead, we can use the
Account Manager, which delegates handling that sensitive information
for us.
Account Manager
The Account Manager is a centralized helper for user account credentials so that your app does not have to deal with passwords directly. It often provides a token in place of the real username and password that can be used to make authenticated requests to a service. An example is when requesting an OAuth2 token.
Sometimes, all the required information is already stored on the device, and other times the Account Manager will need to call a server for a refreshed token. You may have seen the Accounts section in your device's Settings for various apps. We can get that list of available accounts like this:
AccountManager accountManager = AccountManager.get(this); Account[] accounts = accountManager.getAccounts();
The code will require the android.permission.GET_ACCOUNTS
permission. If you're looking for a specific account, you can find it like this:
AccountManager accountManager = AccountManager.get(this); Account[] accounts = accountManager.getAccountsByType("com.google");
Once
you have the account, a
token for
the account can
be retrieved
by calling the getAuthToken(Account, String, Bundle, Activity, AccountManagerCallback, Handler)
method. The token can then be used to make authenticated API requests to a service. This could be a RESTful API where you pass in a token parameter during an HTTPS request, without ever having to know the user's private account details.
Because each service will have a different way of authenticating and storing the private credentials, the Account Manager provides authenticator modules for a third-party service to implement. While Android has implementations for many popular services, it means you can write your own authenticator to handle your app's account authentication and credential storage. This allows you to make sure the credentials are encrypted. Keep in mind, this also means that credentials in the Account Manager that are used by other services may be stored in clear text, making them visible to anyone who has rooted their device.
Instead of simple credentials, there are times when you will need to deal with a key or a certificate for an individual or entity—for example, when a third party sends you a certificate file which you need to keep. The most common scenario is when an app needs to authenticate to a private organization's server.
In the next tutorial, we will be looking at using certificates for authentication and secure communications, but I still want to address how to store these items in the meantime. The Keychain API was originally built for that very specific use—installing a private key or certificate pair from a PKCS#12 file.
The Keychain
Introduced
in Android 4.0 (API Level 14), the Keychain API deals with key
management. Specifically, it works with PrivateKey
andX509Certificate
objects and provides a more secure container than using your app's data storage. That's because permissions for private keys only allow for your own app
to access the keys, and only after user authorization. This means
that a lock screen must be set up on the device before you can make
use of the credential storage. Also, the objects in the keychain may be
bound to secure hardware, if available.
The code to install a certificate is as follows:
Intent intent = KeyChain.createInstallIntent(); byte[] p12Bytes = //... read from file, such as example.pfx or example.p12... intent.putExtra(KeyChain.EXTRA_PKCS12, p12Bytes); startActivity(intent);
The
user will be prompted for a password to access the private key and an
option to name the certificate. To retrieve the key, the following code presents a UI that lets the user choose from the list of installed keys.
KeyChain.choosePrivateKeyAlias(this, this, new String[]{"RSA"}, null, null, -1, null);
Once the choice is made, a string alias name is returned in the alias(final String alias)
callback where you can access the private key or
certificate chain directly.
public class KeychainTest extends Activity implements ..., KeyChainAliasCallback { //... @Override public void alias(final String alias) { Log.e("MyApp", "Alias is " + alias); try { PrivateKey privateKey = KeyChain.getPrivateKey(this, alias); X509Certificate[] certificateChain = KeyChain.getCertificateChain(this, alias); } catch ... } //... }
Armed with that knowledge, let's now see how we can use the credential storage to save your own sensitive data.
The KeyStore
In the previous tutorial, we looked at protecting data via a user-supplied passcode. This kind of setup is good, but app requirements often steer away from having users log in each time and remember an additional passcode.
That's where the KeyStore API can be used. Since API 1, the KeyStore has been used by the system to store WiFi and VPN credentials. As of 4.3 (API 18), it allows you to work with your own app-specific asymmetric keys, and in Android M (API 23) it can store an AESsymmetric key. So while the API doesn't allow storing sensitive strings directly, these keys can be stored and then used to encrypt strings.
The benefit to storing a key in the KeyStore is that it allows keys to be operated on without exposing the secret content of that key; key data does not enter the app space. Remember that keys are protected by permissions so that only your app can access them, and they may additionally be secure hardware-backed if the device is capable. This creates a container that makes it more difficult to extract keys from a device.
Generate a New Random Key
For this example, instead of generating an AES key from a user-supplied
passcode, we can auto-generate a random key that will be protected in
the KeyStore. We can do this by creating a KeyGenerator
instance, set
to the "AndroidKeyStore"
provider.
//Generate a key and store it in the KeyStore final KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); final KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder("MyKeyAlias", KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) //.setUserAuthenticationRequired(true) //requires lock screen, invalidated if lock screen is disabled //.setUserAuthenticationValidityDurationSeconds(120) //only available x seconds from password authentication. -1 requires finger print - every time .setRandomizedEncryptionRequired(true) //different ciphertext for same plaintext on each call .build(); keyGenerator.init(keyGenParameterSpec); keyGenerator.generateKey();
Important parts to look at here are the .setUserAuthenticationRequired(true)
and .setUserAuthenticationValidityDurationSeconds(120)
specifications. These require a lock screen to be set up and the key to be locked until the user has authenticated.
Looking at the documentation for .setUserAuthenticationValidityDurationSeconds()
, you will see that it means the key is only available a certain number of seconds from password authentication, and that passing in -1
requires fingerprint authentication every time you want to access the key. Enabling the requirement for authentication also has the effect of revoking the key when the user removes or changes the lock screen.
Because storing an unprotected key alongside the encrypted data is like putting a house key under the doormat, these options attempt to protect the key at rest in the event a device is compromised. An example might be an offline data dump of the device. Without the password being known for the device, that data is rendered useless.
The .setRandomizedEncryptionRequired(true)
option enables the requirement that there is enough randomization (a new random IV each time) so that if the same data is encrypted a second time around, that encrypted output will still be different. This prevents an attacker from gaining clues about the ciphertext based on feeding in the same data.
Another option to note is setUserAuthenticationValidWhileOnBody(boolean remainsValid)
, which locks the key once the device has detected it is no longer on the person.
Encrypting Data
Now
that the key is stored in the KeyStore, we can create a method that
encrypts data using the Cipher
object, given theSecretKey
. It will return a HashMap
containing the encrypted data and a randomized IV that will be needed to decrypt the data. The encrypted data, along with the IV, can then be saved to a file or into the shared preferences.
private HashMap<String, byte[]> encrypt(final byte[] decryptedBytes) { final HashMap<String, byte[]> map = new HashMap<String, byte[]>(); try { //Get the key final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); final KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry)keyStore.getEntry("MyKeyAlias", null); final SecretKey secretKey = secretKeyEntry.getSecretKey(); //Encrypt data final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); cipher.init(Cipher.ENCRYPT_MODE, secretKey); final byte[] ivBytes = cipher.getIV(); final byte[] encryptedBytes = cipher.doFinal(decryptedBytes); map.put("iv", ivBytes); map.put("encrypted", encryptedBytes); } catch (Throwable e) { e.printStackTrace(); } return map; }
Decrypting to a Byte Array
For
decryption, the reverse is applied. The Cipher
object is initialized
using the DECRYPT_MODE
constant, and a decrypted byte[]
array is
returned.
private byte[] decrypt(final HashMap<String, byte[]> map) { byte[] decryptedBytes = null; try { //Get the key final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); final KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry)keyStore.getEntry("MyKeyAlias", null); final SecretKey secretKey = secretKeyEntry.getSecretKey(); //Extract info from map final byte[] encryptedBytes = map.get("encrypted"); final byte[] ivBytes = map.get("iv"); //Decrypt data final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); final GCMParameterSpec spec = new GCMParameterSpec(128, ivBytes); cipher.init(Cipher.DECRYPT_MODE, secretKey, spec); decryptedBytes = cipher.doFinal(encryptedBytes); } catch (Throwable e) { e.printStackTrace(); } return decryptedBytes; }
Testing the Example
We can now test our example!
@TargetApi(Build.VERSION_CODES.M) private void testEncryption() { try { //Generate a key and store it in the KeyStore final KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); final KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder("MyKeyAlias", KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) //.setUserAuthenticationRequired(true) //requires lock screen, invalidated if lock screen is disabled //.setUserAuthenticationValidityDurationSeconds(120) //only available x seconds from password authentication. -1 requires finger print - every time .setRandomizedEncryptionRequired(true) //different ciphertext for same plaintext on each call .build(); keyGenerator.init(keyGenParameterSpec); keyGenerator.generateKey(); //Test final HashMap<String, byte[]> map = encrypt("My very sensitive string!".getBytes("UTF-8")); final byte[] decryptedBytes = decrypt(map); final String decryptedString = new String(decryptedBytes, "UTF-8"); Log.e("MyApp", "The decrypted string is " + decryptedString); } catch (Throwable e) { e.printStackTrace(); } }
Using RSA Asymmetric Keys for Older Devices
This is a good solution to store data for versions M and higher, but what if your app supports earlier versions? While AES symmetric keys are not supported under M, RSA asymmetric keys are. That means we can use RSA keys and encryption to accomplish the same thing.
The
main difference here is that an asymmetric keypair contains two keys,
a private and a public key, where the public key encrypts the data
and the private key decrypts it. A KeyPairGeneratorSpec
is passed
into the KeyPairGenerator
that is initialized with KEY_ALGORITHM_RSA
and the "AndroidKeyStore"
provider.
private void testPreMEncryption() { try { //Generate a keypair and store it in the KeyStore KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); Calendar start = Calendar.getInstance(); Calendar end = Calendar.getInstance(); end.add(Calendar.YEAR, 10); KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(this) .setAlias("MyKeyAlias") .setSubject(new X500Principal("CN=MyKeyName, O=Android Authority")) .setSerialNumber(new BigInteger(1024, new Random())) .setStartDate(start.getTime()) .setEndDate(end.getTime()) .setEncryptionRequired() //on API level 18, encrypted at rest, requires lock screen to be set up, changing lock screen removes key .build(); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore"); keyPairGenerator.initialize(spec); keyPairGenerator.generateKeyPair(); //Encryption test final byte[] encryptedBytes = rsaEncrypt("My secret string!".getBytes("UTF-8")); final byte[] decryptedBytes = rsaDecrypt(encryptedBytes); final String decryptedString = new String(decryptedBytes, "UTF-8"); Log.e("MyApp", "Decrypted string is " + decryptedString); } catch (Throwable e) { e.printStackTrace(); } }
To
encrypt, we get the RSAPublicKey
from the keypair and use it with theCipher
object.
public byte[] rsaEncrypt(final byte[] decryptedBytes) { byte[] encryptedBytes = null; try { final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); final KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry("MyKeyAlias", null); final RSAPublicKey publicKey = (RSAPublicKey)privateKeyEntry.getCertificate().getPublicKey(); final Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL"); cipher.init(Cipher.ENCRYPT_MODE, publicKey); final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); final CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, cipher); cipherOutputStream.write(decryptedBytes); cipherOutputStream.close(); encryptedBytes = outputStream.toByteArray(); } catch (Throwable e) { e.printStackTrace(); } return encryptedBytes; }
Decryption is done using the RSAPrivateKey
object.
public byte[] rsaDecrypt(final byte[] encryptedBytes) { byte[] decryptedBytes = null; try { final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); final KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry("MyKeyAlias", null); final RSAPrivateKey privateKey = (RSAPrivateKey)privateKeyEntry.getPrivateKey(); final Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL"); cipher.init(Cipher.DECRYPT_MODE, privateKey); final CipherInputStream cipherInputStream = new CipherInputStream(new ByteArrayInputStream(encryptedBytes), cipher); final ArrayList<Byte> arrayList = new ArrayList<>(); int nextByte; while ( (nextByte = cipherInputStream.read()) != -1 ) { arrayList.add((byte)nextByte); } decryptedBytes = new byte[arrayList.size()]; for(int i = 0; i < decryptedBytes.length; i++) { decryptedBytes[i] = arrayList.get(i); } } catch (Throwable e) { e.printStackTrace(); } return decryptedBytes; }
One
thing about RSA is that encryption is slower than it is in AES. This is
usually fine for small amounts of information, such as when you're securing
shared preference strings. If you find there is a performance problem
encrypting large amounts of data, however, you can instead use this example to
encrypt and store just an AES key. Then, use that faster AES
encryption that was discussed in theprevious tutorial for the rest of your data. You can generate a new AES key and convert it to abyte[]
array that is compatible with this example.
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); keyGenerator.init(256); //AES-256 SecretKey secretKey = keyGenerator.generateKey(); byte[] keyBytes = secretKey.getEncoded();
To get the key back from the bytes, do this:
SecretKey key = new SecretKeySpec(keyBytes, 0, keyBytes.length, "AES");
That
was a lot of code! To keep all of the examples simple, I have
omitted thorough exception handling. But remember that for your production
code, it's not recommended to simply catch all Throwable
cases
in one catch statement.
Conclusion
This completes the tutorial on working with credentials and keys. Much of the confusion around keys and storage has to do with the evolution of the Android OS, but you can choose which solution to use given the API level your app supports.
Now that we have covered the best practices for securing data at rest, the next tutorial will focus on securing data in transit.