Securely Storing Secrets in an Android Application

Sometimes an app developer needs to store secrets inside an app. For example, I am writing a healthcare app. To make it more convenient for the end user, I store the username and password in the app after a user logs in and ask them to enter a 4 digit passcode. When a user re-launches the app, or brings the app to foreground, I present a screen for her to enter the passcode and logs her in using the saved login name and password.

Storing secrets on iOS is easy: just use the KeyChain service. Keychain is a piece of Apple’s overall security framework for iOS. Secrets such as usernames and passwords are encrypted and backed up. For most apps, this is all is needed.

What about Android? You can store secrets in Preferences after encrypting them.

But what about the keys used to encrypt the data? A general rule is you should not use any hardcoded keys because a hacker can easily decompile your code and obtain the key, thereby rendering the encryption useless. You need a key management framework, and that’s what the Android KeyStore API is designed for.

KeyStore provides two functions:

  • Randomly generates keys; and
  • Securely stores the keys

With these, storing secrets becomes easy. All you have to do is:

  • Generate a random key when the app runs the first time;
  • When you want to store a secrete, retrieve the key from KeyStore, encrypt the data with it, and then store the encrypted data in Preferences.
  • When you retrieve the secret, read the encrypted data from Preferences, get the key from KeyStore and then use the key to decrypt the data.

Because your key is randomly generated and securely managed by KeyStore and nothing but your code can read it, the secrets are secured.

You also need a block cipher such as AES for the encryption.

That’s all it is in theory. In practice, an API change in Android M makes it a little tricky to implement. You essentially have to handle two cases: Android versions after M (API level 23) and Android version before that.

Android M and Higher

For API level 23 and higher, the solution is relatively easy because the API generates random AES keys for you. An example can be found in the KeyGenParameterSpec API. Here’s the code.

Generating the key

private static final String AndroidKeyStore = "AndroidKeyStore";
private static final String AES_MODE = "AES/GCM/NoPadding";
keyStore = KeyStore.getInstance(AndroidKeyStore);
keyStore.load(null);

if (!keyStore.containsAlias(KEY_ALIAS)) {
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, AndroidKeyStore);
keyGenerator.init(
new KeyGenParameterSpec.Builder(KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setRandomizedEncryptionRequired(false)
.build());
keyGenerator.generateKey();
}

The part worth pointing out is the call to setRandomizedEncryptionRequired(). More on that later.

Encrypting the data

Cipher c = Cipher.getInstance(AES_MODE);
c.init(Cipher.ENCRYPT_MODE, getSecretKey(context), new GCMParameterSpec(128, FIXED_IV.getBytes()));
byte[] encodedBytes = c.doFinal(input);
String encryptedBase64Encoded = Base64.encodeToString(encodedBytes, Base64.DEFAULT);
return encryptedBase64Encoded;

Decrypting the data

Cipher c = Cipher.getInstance(AES_MODE);
c.init(Cipher.DECRYPT_MODE, getSecretKey(context), new GCMParameterSpec(128, FIXED_IV.getBytes()));
byte[] decodedBytes = c.doFinal(encrypted);
return decodedBytes;

The IV

IV stands for Initialization Vector. Put it simply, it’s a cryptographic feature that injects randomness to make it more secure. The important part is that the IV you use in the encryption must be the same one you use in the decryption. By default, Android forces you to use a random IV every time, but you can turn it off by calling setRandomizedEncryptionRequired() when the key is generated.

A random IV is useful if you have ongoing communication between two systems because it randomizes the data for each message, thereby making it even harder to crack. Because of the security provided by Android KeyStore, a random IV is an overkill here is so I use a fixed IV instead. If you wish to use random IVs, you can just call getIV() on the cipher when you encrypt the data and use the same IV when you decrypt it.

Pre Android M

For Android API versions lower than 23, a little more work is involved. The KeyGenParameterSpec is only available in API 23 so you can’t have KeyStore generate random AES keys for you. Instead, you will need to use the KeyPairGeneratorSpec API.

As the name implies, KeyPairGeneratorSpec generates public key and private key pairs such RSA. Public key encryption is mainly for signing and authentication and is not suitable for encrypting large blocks of data, but it can be paired with a block cipher such as AES.

This is how we will use it to encrypt our data.

Key Generation

  • Generate a pair of RSA keys;
  • Generate a random AES key;
  • Encrypt the AES key using the RSA public key;
  • Store the encrypted AES key in Preferences.

Encrypting and Storing the data

  • Retrieve the encrypted AES key from Preferences;
  • Decrypt the above to obtain the AES key using the private RSA key;
  • Encrypt the data using the AES key;

Retrieving and decrypting the data

  • Retrieve the encrypted AES key from Preferences;
  • Decrypt the above to obtain the AES key using the private RSA key;
  • Decrypt the data using the AES key

The code is shown below.

Generate the RSA key pairs

private static final String     AndroidKeyStore = "AndroidKeyStore";
keyStore = KeyStore.getInstance(AndroidKeyStore);
keyStore.load(null);
// Generate the RSA key pairs
if (!keyStore.containsAlias(KEY_ALIAS)) {
// Generate a key pair for encryption
Calendar start = Calendar.getInstance();
Calendar end = Calendar.getInstance();
end.add(Calendar.YEAR, 30);
    KeyPairGeneratorSpec spec = new      KeyPairGeneratorSpec.Builder(context)
.setAlias(KEY_ALIAS)
.setSubject(new X500Principal("CN=" + KEY_ALIAS))
.setSerialNumber(BigInteger.TEN)
.setStartDate(start.getTime())
.setEndDate(end.getTime())
.build();
KeyPairGenerator kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, AndroidKeyStore);
kpg.initialize(spec);
kpg.generateKeyPair();
}

RSA Encryption and Decryption Routines

private static final String RSA_MODE =  "RSA/ECB/PKCS1Padding";
private byte[] rsaEncrypt(byte[] secret) throws Exception{
KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(KEY_ALIAS, null);
// Encrypt the text
Cipher inputCipher = Cipher.getInstance(RSA_MODE, "AndroidOpenSSL");
inputCipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry.getCertificate().getPublicKey());

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, inputCipher);
cipherOutputStream.write(secret);
cipherOutputStream.close();

byte[] vals = outputStream.toByteArray();
return vals;
}

private byte[] rsaDecrypt(byte[] encrypted) throws Exception {
KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(KEY_ALIAS, null);
Cipher output = Cipher.getInstance(RSA_MODE, "AndroidOpenSSL");
output.init(Cipher.DECRYPT_MODE, privateKeyEntry.getPrivateKey());
CipherInputStream cipherInputStream = new CipherInputStream(
new ByteArrayInputStream(encrypted), output);
ArrayList<Byte> values = new ArrayList<>();
int nextByte;
while ((nextByte = cipherInputStream.read()) != -1) {
values.add((byte)nextByte);
}

byte[] bytes = new byte[values.size()];
for(int i = 0; i < bytes.length; i++) {
bytes[i] = values.get(i).byteValue();
}
return bytes;
}

Generate and Store the AES Key

SharedPreferences pref = context.getSharedPreferences(SHARED_PREFENCE_NAME, Context.MODE_PRIVATE);
String enryptedKeyB64 = pref.getString(ENCRYPTED_KEY, null);
if (enryptedKeyB64 == null) {
byte[] key = new byte[16];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(key);
byte[] encryptedKey = rsaEncrypt(key);
enryptedKeyB64 = Base64.encodeToString(encryptedKey, Base64.DEFAULT);
SharedPreferences.Editor edit = pref.edit();
edit.putString(ENCRYPTED_KEY, enryptedKeyB64);
edit.commit();
}

And finally, our calls to encrypt and decrypt the data.

Encrypting the Data

private static final String AES_MODE = "AES/ECB/PKCS7Padding";
private
Key getSecretKey(Context context) throws Exception{
SharedPreferences pref = context.getSharedPreferences(SHARED_PREFENCE_NAME, Context.MODE_PRIVATE);
String enryptedKeyB64 = pref.getString(ENCRYPTED_KEY, null);
    // need to check null, omitted here
byte[] encryptedKey = Base64.decode(enryptedKeyB64, Base64.DEFAULT);
byte[] key = rsaDecrypt(encryptedKey);
return new SecretKeySpec(key, "AES");
}

public String encrypt(Context context, byte[] input) {
Cipher c = Cipher.getInstance(AES_MODE, "BC");
c.init(Cipher.ENCRYPT_MODE, getSecretKey(context));
byte[] encodedBytes = c.doFinal(input);
String encryptedBase64Encoded = Base64.encodeToString(encodedBytes, Base64.DEFAULT);
return encryptedBase64Encoded;
}


public byte[] decrypt(Context context, byte[] encrypted) {
Cipher c = Cipher.getInstance(AES_MODE, "BC");
c.init(Cipher.DECRYPT_MODE, getSecretKey(context));
byte[] decodedBytes = c.doFinal(encrypted);
return decodedBytes;
}
Show your support

Clapping shows how much you appreciated minun’s story.