SecureRandom provider "Crypto" unavailable in Android N for deterministially generating a key

NSouth picture NSouth · Apr 23, 2016 · Viewed 7.7k times · Source

Users can purchase a "Pro" version of my app. When they do, I store and verify their purchase as follows.

  • Combine the user's UUID and another unique string.
  • The resulting string is then encrypted using a static seed. I do this using SecureRandom.getInstance("SHA1PRNG", "Crypto")- This is the problem!
  • The resulting encrypted string is then the "unlock code".
  • Therefore, I always know the expected unique unlock code value for the user.
  • When the user purchases "Pro", I store the "unlock code" in the database.
  • I check to see whether the user has "Pro" by seeing if the stored "unlock code" in the database matches the expected code based on their unique info.

So, not the best system, but everything is obfuscated enough for my humble app.

The problem is that SecureRandom.getInstance("SHA1PRNG", "Crypto") fails on N because "Crypto" is not supported. I have learned that relying on specific providers is bad practice and Crypto is not supported on N. Oops.

So I have a problem: I rely on the encryption of a value-seed pair to always have the same output. Android N does not support the encryption provider I use, so I don't know how to ensure that the encryption output will be the same on N as it is on other devices.

My questions:

  1. Is it possible to include "Crypto" in my APK so that it is always available?
  2. Can I otherwise ensure the same output when encrypting a value-seed pair on Android N?

My code:

public static String encrypt(String seed, String cleartext) throws Exception {
    byte[] rawKey = getRawKey(seed.getBytes(), seed);
    byte[] result = encrypt(rawKey, cleartext.getBytes());
    return toHex(result); // "unlock code" which must always be the same for the same seed and clearText accross android versions
}

private static byte[] getRawKey(byte[] seed, String seedStr) throws Exception {
    SecureRandom sr;
    sr = SecureRandom.getInstance("SHA1PRNG", "Crypto");  // what used to work
    KeyGenerator kgen = KeyGenerator.getInstance("AES");
    sr.setSeed(seed);
    kgen.init(128, sr); 
    SecretKey skey = kgen.generateKey();
    byte[] raw = skey.getEncoded();
    return raw;
}

private static byte[] encrypt(byte[] raw, byte[] clear) throws Exception {
    SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
    Cipher cipher = Cipher.getInstance("AES");
    cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
    byte[] encrypted = cipher.doFinal(clear);
    return encrypted;
}

public static String toHex(byte[] buf) {
    if (buf == null)
        return "";
    StringBuffer result = new StringBuffer(2 * buf.length);
    for (int i = 0; i < buf.length; i++) {
        appendHex(result, buf[i]);
    }
    return result.toString();
}

Answer

Trevor Johns picture Trevor Johns · May 18, 2016

I had a discussion with the Android Security team about this recently.

In Android N, SHA1PRNG was removed because we don't have a secure implementation of it. Specifically, calling .setSeed(long) before requesting output from the PRNG replaces all of the entropy in the SecureRandom instance.

This behavior has long been pointed to as a security failure (read: frequently causes subtle bugs in apps), so we chose not to replicate it when the SecureRandom provider was replaced.

If you need a PRNG, then just use new SecureRandom().

That said... SecureRandom() is not designed to be used as a key derivation function, as you've done in your example. Please don't do this! Instead, use an algorithm such as PBKDF2, available via SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1").

We've been warning developers about this for a while. Please see these posts:

IF YOU REALLY NEED SHA1PRNG, EVEN AFTER ALL OF THAT... then the workaround is to copy the implementation out of the Android source, like @artjom-b mentioned in his answer.

But please, only do this if you need compatibility while migrating to PBKDF2 or similar.