Search code examples
javaandroidencryptionandroid-keystore

Is it possible to generate a 64-byte (256-bit) key and store/retrieve it with AndroidKeyStore?


In my Android app, I need a way to encrypt the data I store in a local DB. I chose Realm DB because the offer a seamless integration with encryption. I just need to pass a key when initializing the Realm instance. This key must be of 64 byte size.

For security reason, I found out that the best way to store this key is in AndroidKeyStore. I'm struggling to find a way to generate a key (using any algorithm) with that size, and getting it into a 64-byte array. I'm trying to keep a minSdk of API 19, but I believe I can bump it up to 23 if needed (many changes to AndroidKeyStore between these two versions).

Does anyone have an idea? Here is my code:

Class Encryption.java

private static KeyStore ks = null;
private static String ALIAS = "com.oi.pap";

public static byte[] loadkey(Context context) {

    byte[] content = new byte[64];
    try {
        if (ks == null) {
            createNewKeys(context);
        }

        ks = KeyStore.getInstance("AndroidKeyStore");
        ks.load(null);

        content= ks.getCertificate(ALIAS).getEncoded(); //<----- HERE, I GET SIZE GREATER THAN 64
        Log.e(TAG, "original key :" + Arrays.toString(content));
    } catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException e) {
        e.printStackTrace();
    }
    content = Arrays.copyOfRange(content, 0, 64); //<---- I would like to remove this part.
    return content;
}

private static void createNewKeys(Context context) throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {

    ks = KeyStore.getInstance("AndroidKeyStore");
    ks.load(null);
    try {
        // Create new key if needed
        if (!ks.containsAlias(ALIAS)) {
            Calendar start = Calendar.getInstance();
            Calendar end = Calendar.getInstance();
            end.add(Calendar.YEAR, 1);
            KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(context)
                    .setAlias(ALIAS)
                    .setSubject(new X500Principal("CN=PapRealmKey, O=oipap"))
                    .setSerialNumber(BigInteger.ONE)
                    .setStartDate(start.getTime())
                    .setEndDate(end.getTime())
                    .setKeySize(256)
                    .setKeyType(KeyProperties.KEY_ALGORITHM_EC)
                    .build();
            KeyPairGenerator generator = KeyPairGenerator
                    .getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore");
            generator.initialize(spec);

            KeyPair keyPair = generator.generateKeyPair();
            Log.e(TAG, "generated key :" + Arrays.toString(keyPair.getPrivate().getEncoded()));

        }
    } catch (Exception e) {
        Log.e(TAG, Log.getStackTraceString(e));
    }
}

Solution

  • The point of AndroidKeyStore is to move sensitive key material out of your app, out of the operating system and into secure hardware where it can never leak or be compromised. So, by design, if you create a key in AndroidKeyStore, you can never get the key material out.

    In this case, Realm DB wants the secret key material, so you can't give it an AndroidKeyStore key. Also, what Realm wants is two AES keys, not an EC key, as you were trying to generate.

    The right way to generate the key material you need is:

    byte[] dbKey = new byte[64];
    Random random = new SecureRandom();
    random.nextBytes(dbKey);
    // Pass dbKey to Realm DB...
    Arrays.fill(dbKey, 0); // Wipe key after use.
    

    Just 64 random bytes. However, you're going to need to store those bytes somewhere. You could create an AES key with AndroidKeyStore and use it to encrypt dbKey. Something like:

    KeyGenerator keyGenerator = KeyGenerator.getInstance(
            KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
    keyGenerator.init(
            new KeyGenParameterSpec.Builder("dbKeyWrappingKey",
                    KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                    .setBlockModes(KeyProperties.BLOCK_MODE_GCM)      
                    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                    .build());
    SecretKey key = keyGenerator.generateKey();
    
    Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
    cipher.init(Cipher.ENCRYPT_MODE, key);
    byte[] iv = cipher.getIV();
    byte[] encryptedDbKey = cipher.doFinal(dbKey);
    

    You'll need to save both iv and encryptedDbKey somewhere (not in the database!) so that you can recover dbKey. Then you can decrypt it with:

    KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
    keyStore.load(null);
    key = (SecretKey) keyStore.getKey("dbKeyWrappingKey", null);
    Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
    cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv));
    byte[] dbKey = cipher.doFinal(encryptedDbKey);
    // Pass dbKey to Realm DB and then wipe it.
    

    However, with all of that said... I don't think you should do any of it. I don't think this actually gives you any security that Android doesn't give you by default anyway. If an attacker tries to dump the device storage, which contains your database, he'll get nothing because Android encrypts all of the storage anyway. If an attacker can root the device, he can run code as your app and use it to decrypt dbKey the same way your app does.

    Where AndroidKeyStore may really add value is if you add some additional protections on dbKeyWrappingKey. For example, if you set it to require user authentication within, say five minutes, it will only be possible to use dbWrappingKey to decrypt dbKey when the user is around to enter their PIN/pattern/password or touch the fingerprint scanner. Note that this only works if the user has a PIN/pattern/password, but if they don't, well, your database is wide open to anyone who picks up the phone anyway.

    See KeyGenParameterSpec for all of the things you can do to restrict the ways dbKeyWrappingKey can be used.