Android 4.2 broke my encrypt/decrypt code and the provided solutions don't work

pandre picture pandre · Nov 17, 2012 · Viewed 29k times · Source

First of all, I've already seen Android 4.2 broke my AES encrypt/decrypt code and Encryption error on Android 4.2 and the provided solution:

SecureRandom sr = null;
if (android.os.Build.VERSION.SDK_INT >= JELLY_BEAN_4_2) {
    sr = SecureRandom.getInstance("SHA1PRNG", "Crypto");
} else {
    sr = SecureRandom.getInstance("SHA1PRNG");
}

doesn't work for me, because, when decoding data encrypted in Android<4.2 in Android 4.2, I get:

javax.crypto.BadPaddingException: pad block corrupted
at com.android.org.bouncycastle.jcajce.provider.symmetric.util.BaseBlockCipher.engineDoFinal(BaseBlockCipher.java:709)

My code is quite simple, and was working until Android 4.2:

public static byte[] encrypt(byte[] data, String seed) throws Exception {

    KeyGenerator keygen = KeyGenerator.getInstance("AES");
    SecureRandom secrand = SecureRandom.getInstance("SHA1PRNG");
    secrand.setSeed(seed.getBytes());
    keygen.init(128, secrand);

    SecretKey seckey = keygen.generateKey();
    byte[] rawKey = seckey.getEncoded();

    SecretKeySpec skeySpec = new SecretKeySpec(rawKey, "AES");
    Cipher cipher = Cipher.getInstance("AES");
    cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
    return cipher.doFinal(data);
}

public static byte[] decrypt(byte[] data, String seed) throws Exception {

    KeyGenerator keygen = KeyGenerator.getInstance("AES");
    SecureRandom secrand = SecureRandom.getInstance("SHA1PRNG");
    secrand.setSeed(seed.getBytes());
    keygen.init(128, secrand);

    SecretKey seckey = keygen.generateKey();
    byte[] rawKey = seckey.getEncoded();

    SecretKeySpec skeySpec = new SecretKeySpec(rawKey, "AES");
    Cipher cipher = Cipher.getInstance("AES");
    cipher.init(Cipher.DECRYPT_MODE, skeySpec);
    return cipher.doFinal(data);
}

My guess is that the default provider wasn't the only thing that changed in Android 4.2, otherwise my code would work with the proposed solution.

My code was based on some post I found here at StackOverflow a long time ago; I see that it differs from the mentioned posts as it just crypts and decrypts byte arrays, whereas the others solutions crypt and decrypt Strings (HEX Strings, I think).

Does it have to do with the seed? Does it have a min/max length, restriction of chars, etc?

Any idea / solution?

EDIT: After a lot of tests, I see that there are 2 problems:

  1. The provider changed in Android 4.2 (API 17) -> This one is easy to fix, just apply the solution I mentioned at top of the post

  2. BouncyCastle changed from 1.34 to 1.45 in Android 2.2 (API 8)->Android2.3 (API 9), so the decryption problem I previously told is the same as described here: BouncyCastle AES error when upgrading to 1.45

So now the question is: is there any way to recover data crypted in BouncyCastle 1.34 in BouncyCastle 1.45+?

Answer

kroot picture kroot · Nov 18, 2012

First a disclaimer:

DO NOT ever use SecureRandom to derive a key! This is broken and doesn't make sense!

The following block of code from the question tries to deterministically derive a key from a password, called the "seed" as the password is used to "seed" the random number generator.

KeyGenerator keygen = KeyGenerator.getInstance("AES");
SecureRandom secrand = SecureRandom.getInstance("SHA1PRNG");
secrand.setSeed(seed.getBytes());
keygen.init(128, secrand);
SecretKey seckey = keygen.generateKey();

However, the "SHA1PRNG" algorithm is not well defined and implementations of "SHA1PRNG" may return different or even fully random keys as a result.


If you're reading an AES key from disk, just store the actual key and don't go through this weird dance. You can get a SecretKey for AES usage from the bytes by doing:

    SecretKey key = new SecretKeySpec(keyBytes, "AES");

If you're using a password to derive a key, follow Nelenkov's excellent tutorial with the caveat that a good rule of thumb is the salt size should be the same size as the key output.

The iterationCount (work factor) is of course subject to change and should be changed as CPU power progresses - generally it is recommended not to go lower than 40 to 100K as of 2018. Beware that PBKDF2 only adds a constant time delay to guessing passwords; it is not a replacement for really weak passwords.

It looks like this:

    /* User types in their password: */
    String password = "password";

    /* Store these things on disk used to derive key later: */
    int iterationCount = 1000;
    int saltLength = 32; // bytes; should be the same size as the output (256 / 8 = 32)
    int keyLength = 256; // 256-bits for AES-256, 128-bits for AES-128, etc
    byte[] salt; // Should be of saltLength

    /* When first creating the key, obtain a salt with this: */
    SecureRandom random = new SecureRandom();
    byte[] salt = new byte[saltLength];
    random.nextBytes(salt);

    /* Use this to derive the key from the password: */
    KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt,
                iterationCount, keyLength);
    SecretKeyFactory keyFactory = SecretKeyFactory
                .getInstance("PBKDF2WithHmacSHA1");
    byte[] keyBytes = keyFactory.generateSecret(keySpec).getEncoded();
    SecretKey key = new SecretKeySpec(keyBytes, "AES");

That's it. Anything else you should not use.