4

I'd like to use the Android Keystore to do symmetric AES encryption of large (multi MB) data files.

I've written demo code that will encrypt/decrypt multi-KB files with the Keystore, but when the file size gets too large it starts to fall down. This max size varies by device and can range anywhere from ~80KB to ~1MB. On every Android-M device I've tested on (including the emulator) there seems to be a maximum size after which encryption will fail.

When it fails, it fails silently-- however the ciphertext size is typically quite a bit smaller than it should be (which of course can't then be decrypted).

Since it's so pervasive across multiple devices, either I'm doing something wrong (likely!), or there's some kind of undocumented limit to what can be encrypted in the Keystore.

I've written up a demo app on Github that shows the problem (here, specifically this file). You can run the app gui to make the problem happen manually, or run the instrumentation tests to make it happen.

Any help or pointers to documentation about this problem would be greatly appreciated!

For reference, I'm generating the symmetric key like this:

KeyGenerator keyGenerator  = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
keyGenerator.init(
        new KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT|KeyProperties.PURPOSE_DECRYPT)
                .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                .build()
);
SecretKey key = keyGenerator.generateKey();

SecretKeyFactory factory = SecretKeyFactory.getInstance(key.getAlgorithm(), "AndroidKeyStore");
KeyInfo keyInfo= (KeyInfo)factory.getKeySpec(key, KeyInfo.class);
logger.debug("isInsideSecureHardware: {}", keyInfo.isInsideSecureHardware());

and I'm encrypting like this:

KeyStore keyStore= KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
KeyStore.SecretKeyEntry keyEntry= (KeyStore.SecretKeyEntry)keyStore.getEntry(KEY_ALIAS, null);

Cipher cipher= getCipher();
cipher.init(Cipher.ENCRYPT_MODE, keyEntry.getSecretKey());
GCMParameterSpec params= cipher.getParameters().getParameterSpec(GCMParameterSpec.class);

ByteArrayOutputStream byteStream= new ByteArrayOutputStream();
DataOutputStream dataStream= new DataOutputStream(byteStream);

dataStream.writeInt(params.getTLen());
byte[] iv= params.getIV();
dataStream.writeInt(iv.length);
dataStream.write(iv);

dataStream.write(cipher.doFinal(plaintext));

Update:

Per the suggestions of user2481360 and Artjom B. I changed to chunking the plaintext as it goes into the cypher like this:

ByteArrayInputStream plaintextStream= new ByteArrayInputStream(plaintext);
final int chunkSize= 4*1024;
byte[] buffer= new byte[chunkSize];
while (plaintextStream.available() > chunkSize) {
    int readBytes= plaintextStream.read(buffer);
    byte[] ciphertextChunk= cipher.update(buffer, 0, readBytes);
    dataStream.write(ciphertextChunk);
}
int readBytes= plaintextStream.read(buffer);
byte[] ciphertextChunk= cipher.doFinal(buffer, 0, readBytes);
dataStream.write(ciphertextChunk);

This appears to solve the problem with the ciphertext being completely wrong. I can now use very large plaintext sizes.

However, depending on the size sometimes the data doesn't roundtrip. For example, if I use 1MB the roundtripped plaintext is missing a few bytes at the end. But if I use 1MB+1B, it will work. My understanding of AES/GCM was that the input plaintext doesn't have to have a special size (aligned to the block length, etc).

paleozogt
  • 6,393
  • 11
  • 51
  • 94

1 Answers1

1

Chop the data into small pieces and then call update, when it reach the last piece call doFinal

user2481360
  • 176
  • 3