Search code examples
javaandroidencryptionrealm

Calling .getEncoded() on SecretKey returns null


I use the following code to generate an AES key:

KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder("db_enc_key", KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT);

        KeyGenParameterSpec keySpec = builder
                .setKeySize(256)
                .setBlockModes("CBC")
                .setEncryptionPaddings("PKCS7Padding")
                .setRandomizedEncryptionRequired(true)
                .setUserAuthenticationRequired(true)
                .setUserAuthenticationValidityDurationSeconds(5 * 60)
                .build();

        KeyGenerator keyGen = KeyGenerator.getInstance("AES", "AndroidKeyStore");
        keyGen.init(keySpec);

        SecretKey sk = keyGen.generateKey();

but everytime I try to get the byte[] version of the key via sk.getEncoded(), the method returns null. The documentation says that it should return the encoded key, or null if the key does not support encoding, but I don't think that the key doesn't support encoding.

I need the byte[] because I want to encrypt a realm database (for which I need to combine 2 AES-256 keys as byte-arrays) [https://realm.io/docs/java/latest/#encryption]

The official documentation uses SecureRandom, but also states that this is a silly way of doing this and that the key is never stored. Therefore, I wanted to use the KeyStore to securely store the two separate AES-256 keys.

P.S.: The code is only a test code and not the final product, so any comment on coding style is useless. I'm currently just trying to get a working version going.

edit: So I tried the following code, which successfully generates an AES key (though only 16 bytes of length):

SecretKey sk1 = KeyGenerator.getInstance("AES").generateKey();

When I use the getEncoded() method on it, I'll even get the byte array, so naturally I went on and saved it to the KeyStore with the following code:

KeyStore.SecretKeyEntry entry = new KeyStore.SecretKeyEntry(sk1);
KeyStore.ProtectionParameter pp = new KeyProtection.Builder(KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT).build();
keyStore.setEntry("db_enc_key_test", entry, pp);

Which also works. So I tried to read the key from the keystore via KeyStore.Entry entry2 = keyStore.getEntry("db_enc_key_test", null); which worked as well. But when I call entry2.getEncoded() the method returns null again. Is this a keystore problem?

edit2: So I just found out, that symmetric keys generated in (and apparently saved to) the keystore are unexportable in Android M, which seems to be intended, which puts me in a bit of a problem, as I need the key itself to encrypt the realm database.

Some realm-developer here to recommend a best-practice?


Solution

  • The fact that you cannot retrieve the encoded key is by design as the Keystore should be the only one knowing it. However you can use a double layered key:

    1. Generate a random key and store it in the Keystore.

    2. Generate the "real" key used by Realm and encrypt it using the key from the Keystore.

    3. Now you have some completely random text that can be stored in e.g SharedPreferences or in a file on disk.

    4. Whenever people wants to open the Realm, read the encrypted key on disk, decrypt it using the Keystore and now you can use it to open the Realm.

    This repo here uses the same technique to save User data in a secure way: https://github.com/realm/realm-android-user-store

    This is probably the class you are after: https://github.com/realm/realm-android-user-store/blob/master/app/src/main/java/io/realm/android/CipherClient.java It also handle fallback through the various Android versions (the Keystore has quite a few quirks).