Securely store app data in Android - Android KeyStore to the rescue!

Securely store app data in Android - Android KeyStore to the rescue!

Storing app data

The go to option for Android developers for storing application data is the SharedPreferences and it serves its purpose very well. SharedPreferences stores all the data as plain text in a xml file. A capable user can get access to that file quite easily, hence it's discouraged to store sensitive data in SharedPreferences. But there are some use cases when we need to store data that we'd like to keep secret and unaccessible to user. How do we achieve that?

Obfuscation

One way to protect sensitive data is to obfuscate it before storing. For example when we need to store user's password or a secret passcode we often get a hash of the secret text and then store that instead of the actual data. And then when we need to check if the user has entered the correct passcode, we hash the entered text with the same salt and compare it with the data we have stored. A simple hashing function looks like this:

String getHashed(String text, String randomSalt) throws NoSuchAlgorithmException, UnsupportedEncodingException {  
        final MessageDigest digest = MessageDigest.getInstance("SHA-256");
        digest.update(randomSalt.getBytes(DEFAULT_CHARSET));
        byte[] result = digest.digest(text.getBytes(DEFAULT_CHARSET));

        return toHex(result);
    }


As hashing is irreversible, it'll be hard for an intruder to find out the actual password even if he gets access to the hashed text. The salt randomSalt in the function makes it harder for intruders to find out the content using brute-force. But then we have the problem of storing this salt securely. And what in cases where we need the actual data back?

Encryption

A more secured way is to use encryption to protect the data. And then we can decrypt the stored data to get the actual data whenever we need it. AES-256 can be a moderately good choice of an encryption algorithm. But how do we we keep the encryption key secured? where do we store that?

Android KeyStore

With Android 4.3 (API level 18) Google introduced public APIs for the AndroidKeyStore provider. So on API level 18+ we can access the AndroidKeyStore Provider and store our encryption keys or certificates in Android's KeyStore. But on API level less than 23 AndroidKeyStore provider can store only asymmetric encryption keys i.e RSA keys. So we need an workaround for Android versions prior to Android Marshmallow. We can generate and store a RSA key pair in the KeyStore and then generate a secured AES key, encrypt that using RSA and store that encrypted data in a private file.

Don't forget to load the KeyStore before accessing it.

final String KEYSTORE_PROVIDER = "AndroidKeyStore";

void loadKeyStore() throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {  
        mStore = KeyStore.getInstance(KEYSTORE_PROVIDER);
        mStore.load(null);
    }

We can generate and store a RSA key pair like this:

void generateRSAKeys(Context context) throws NoSuchProviderException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, KeyStoreException {  
        if (!mStore.containsAlias(RSA_KEY_ALIAS)) {
            Calendar start = Calendar.getInstance();
            Calendar end = Calendar.getInstance();
            end.add(Calendar.YEAR, 25);

            KeyPairGenerator keyGen = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, KEYSTORE_PROVIDER);

            KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(context)
                        .setAlias(RSA_KEY_ALIAS)
                        .setKeySize(RSA_BIT_LENGTH)
                        .setKeyType(KeyProperties.KEY_ALGORITHM_RSA)
                        .setEndDate(end.getTime())
                        .setStartDate(start.getTime())
                        .setSerialNumber(BigInteger.ONE)
                        .setSubject(new X500Principal("CN = Secured Preference Store, O = Devliving Online"))
                        .build();

            keyGen.initialize(spec);
            keyGen.generateKeyPair();
        }
    }

Our RSA key pair is generated and stored securely. You can make the access to these keys more secured by protecting this PrivateKeyEntry with a user provided password. You can also setup the KeyStore to be locked down when the device screen is locked but in that case your keys will be deleted when the user changes the screen lock password/pin/pattern (Actually prior to Android 5 keys are almost always deleted even if you set the keys to be non-encrypted at rest). We can retrieve the keys when we need to like this:

void loadRSAKeys() throws KeyStoreException, UnrecoverableEntryException, NoSuchAlgorithmException {  
        if (mStore.containsAlias(RSA_KEY_ALIAS) && mStore.entryInstanceOf(RSA_KEY_ALIAS, KeyStore.PrivateKeyEntry.class)) {
            KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry) mStore.getEntry(RSA_KEY_ALIAS, null);
            publicKey = (RSAPublicKey) entry.getCertificate().getPublicKey();
            privateKey = (RSAPrivateKey) entry.getPrivateKey();
        }
    }

Then we can generate our AES key

byte[] generateAESKey() throws NoSuchAlgorithmException {  
        KeyGenerator keyGen = KeyGenerator.getInstance("AES");

        keyGen.init(AES_BIT_LENGTH);
        SecretKey sKey = keyGen.generateKey();
        return sKey.getEncoded();
    }

Now this key can be encrypted using RSA and stored for later usage. A simple RSA encryption can be done like this:

final String RSA_CIPHER = KeyProperties.KEY_ALGORITHM_RSA + "/" +  
            KeyProperties.BLOCK_MODE_ECB + "/" +
            KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1;
final String SSL_PROVIDER = "AndroidOpenSSL";

byte[] RSAEncrypt(byte[] bytes) throws KeyStoreException, UnrecoverableEntryException, NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException, InvalidKeyException, IOException {  
        Cipher cipher = Cipher.getInstance(RSA_CIPHER, SSL_PROVIDER);
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);

        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, cipher);
        cipherOutputStream.write(bytes);
        cipherOutputStream.close();

        return outputStream.toByteArray();
    }

And a simple decryption method to retrieve the key data later can be:

byte[] RSADecrypt(byte[] bytes) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException, IOException {  
        Cipher cipher = Cipher.getInstance(RSA_CIPHER, SSL_PROVIDER);
        cipher.init(Cipher.DECRYPT_MODE, privateKey);

        CipherInputStream cipherInputStream = new CipherInputStream(new ByteArrayInputStream(bytes), cipher);

        ArrayList<Byte> values = new ArrayList<>();
        int nextByte;
        while ((nextByte = cipherInputStream.read()) != -1) {
            values.add((byte) nextByte);
        }

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

        cipherInputStream.close();
        return dbytes;
    }

We're done with securing the encryption key, now we can use the encryption key to securely store data. A simple AES encryption method looks like this:

final String AES_CIPHER_COMPAT = KeyProperties.KEY_ALGORITHM_AES + "/" +  
            KeyProperties.BLOCK_MODE_CBC + "/" +
            KeyProperties.ENCRYPTION_PADDING_PKCS7;

byte[] encryptAESCompat(byte[] bytes, byte[] IV) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, UnsupportedEncodingException, InvalidAlgorithmParameterException {  
        Cipher c = Cipher.getInstance(AES_CIPHER_COMPAT, BOUNCY_CASTLE_PROVIDER);
        c.init(Cipher.ENCRYPT_MODE, aesKey, new IvParameterSpec(IV));
        return c.doFinal(bytes);
    }

The IV(initialization vector) needs to be randomly generated and not predictable. You can also let the Cipher generate one for you. But don't forget to store it along with the encrypted data as you'll need the same IV for decryption. You should also consider using a MAC to check the integrity/authenticity of the data.

A simple decryption method follows:

byte[] decryptAESCompat(byte[] bytes, byte[] IV) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, UnsupportedEncodingException, InvalidAlgorithmParameterException {  
        Cipher c = Cipher.getInstance(AES_CIPHER_COMPAT, BOUNCY_CASTLE_PROVIDER);
        c.init(Cipher.DECRYPT_MODE, aesKey, new IvParameterSpec(IV));
        return c.doFinal(bytes);
    }

On API level 23+, all these get a lot easier. We just generate our AES key and store it in KeyStore like this:

KeyGenerator keyGen = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_PROVIDER);

                KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(AES_KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                        .setCertificateSubject(new X500Principal("CN = Secured Preference Store, O = Devliving Online"))
                        .setCertificateSerialNumber(BigInteger.ONE)
                        .setKeySize(AES_BIT_LENGTH)
                        .setKeyValidityEnd(end.getTime())
                        .setKeyValidityStart(start.getTime())
                        .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                        .setRandomizedEncryptionRequired(false)
                        .build();
                keyGen.init(spec);

                keyGen.generateKey();

It is recommended that you pass true to setRandomizedEncryptionRequired so that while encrypting using this key the Cipher will generate a secured IV for you and use that.

We can retrieve this key like this:

if (mStore.containsAlias(AES_KEY_ALIAS) && mStore.entryInstanceOf(AES_KEY_ALIAS, KeyStore.SecretKeyEntry.class)) {  
                KeyStore.SecretKeyEntry entry = (KeyStore.SecretKeyEntry) mStore.getEntry(AES_KEY_ALIAS, null);
                aesKey = entry.getSecretKey();
            }

We can then encrypt using this key like below:

final String AES_CIPHER = KeyProperties.KEY_ALGORITHM_AES + "/" +  
            KeyProperties.BLOCK_MODE_GCM + "/" +
            KeyProperties.ENCRYPTION_PADDING_NONE;

byte[] encryptAES(byte[] bytes, byte[] IV) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, UnsupportedEncodingException {  
        Cipher cipher = Cipher.getInstance(AES_CIPHER);
        cipher.init(Cipher.ENCRYPT_MODE, aesKey, new GCMParameterSpec(GCM_TAG_LENGTH, IV));
        return cipher.doFinal(bytes);
    }

When using the GCM block mode we don't need to worry about the integrity/authenticity ourselves. GCM mode is available from API level 19.

The decryption can be done like below:

byte[] decryptAES(byte[] bytes, byte[] IV) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, UnsupportedEncodingException {  
        Cipher cipher = Cipher.getInstance(AES_CIPHER);
        cipher.init(Cipher.DECRYPT_MODE, aesKey, new GCMParameterSpec(GCM_TAG_LENGTH, IV));
        return cipher.doFinal(bytes);
    }

I've written a SharedPreferences wrapper library which keeps the content secured using encryption and stores the encryption keys in the KeyStore. If you need something like that you can find the library here.

Mehedi Hasan Khan

Mehedi Hasan Khan

Programmer, Entrepreneur, Tech enthusiast.

 

Related Post

Comments powered by Disqus