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.
-
SecurityStoring Data Securely on Android
-
AndroidHow to Secure an Android App
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 UI that asks the user to log in, 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 3rd 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 having to ever 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 3rd 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
and
X509Certificate
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 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 login 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 working with your
own app-specific asymmetric keys, and in Android M (API 23) it can store an AES
symmetric 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
So 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 lock the key 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 finger print 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 along side 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 exact 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 the
SecretKey
. 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 HashMapencrypt(final byte[] decryptedBytes) { final HashMap map = new HashMap (); 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 HashMapmap) { 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 HashMapmap = 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 the
Cipher
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 ArrayListarrayList = 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 the
previous tutorial for the rest of your data. You can generate a new AES key and convert it to a
byte[]
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.
Powered by WPeMatico