3

I'm trying to use javax.crypto.Cipher on Android to encrypt a stream of data in chunks using AES-GCM. As I understand, one can use Cipher.update multiple times for a multi-part encryption operation, and finalize with Cipher.doFinal. However when using the AES/GCM/NoPadding transformation, Cipher.update refuses to output data to the provided buffer, and returns 0 bytes written. The buffer builds up inside the Cipher until I call .doFinal. This also appears to happen with CCM (and I assume other authenticated modes), but works for other modes like CBC.

I figured GCM can compute the authentication tag while encrypting, so I'm not sure why I'm not allowed to consume the buffer in the Cipher.

I've made an example with just one call to .update: (kotlin)

val secretKey = KeyGenerator.getInstance("AES").run {
    init(256)
    generateKey()
}

val iv = ByteArray(12)
SecureRandom().nextBytes(iv)

val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iv))

// Pretend this is some file I want to read and encrypt
val inputBuffer = Random.nextBytes(1024000)

val outputBuffer = ByteArray(cipher.getOutputSize(512))

val read = cipher.update(inputBuffer, 0, 512, outputBuffer, 0)
//   ^  at this point, read = 0 and outputBuffer is [0, 0, 0, ...]
// Future calls to cipher.update and cipher.getOutputSize indicate that
// the internal buffer is growing. But I would like to consume it through
// outputBuffer

// ...

cipher.doFinal(outputBuffer, 0)
// Now outputBuffer is populated

What I would like to do is stream a large file from disk, encrypt it and send it over the network chunk by chunk, without having to load the entire file data into memory. I've tried to use CipherInputStream but it suffers from the same problem.

Is this possible with AES/GCM?

Will
  • 135
  • 2
  • 9
  • I can't reproduce. Printing the value of `read` immediately after the call to cipher.update() prints 512. Writing in chunks to a ByteArrayOutputStream and printing the length of baos.toByteArray() also shows an increasing size at each iteration. I had to remove the IvParameterSpec though, because it causes a java.security.InvalidAlgorithmParameterException. – JB Nizet Aug 06 '19 at 19:04
  • Interesting... maybe I should have clarified this is on Android if that might make a difference to how it's implemented. Will update my question to reflect that. – Will Aug 06 '19 at 19:08
  • This is a "feature" of GCM mode. On decryption the plaintext is "embargoed" until `doFinal` is called and the tag can be verified. The reason for this is simple: if `update()` handed you back plaintext as soon as it was produced and at the end the tag failed to verify, the plaintext you were handed would be invalid. `doFinal()` would have no way to recall the defective plaintext, and you would have no way to recall the nuclear missile you just launched based on that defective plaintext. – President James K. Polk Aug 06 '19 at 19:30
  • Unfortunately, this is the choice of library designers. See the comment of Maarten under this [answer](https://stackoverflow.com/a/54056229/1820553). You can divide and chain your data if you want to. – kelalaka Aug 06 '19 at 19:31
  • @JamesKPolk I understand that when decrypting, but in this case I am looking to encrypt the file, in which case there is no need to verify the tag (since I am generating it!) – Will Aug 06 '19 at 19:35
  • @kelalaka I was worried this might be the case, thanks this is a useful comment chain – Will Aug 06 '19 at 19:38
  • For encrypt mode no data need be cached at all. – President James K. Polk Aug 06 '19 at 19:47
  • Just confirmed this on an Android emulator. Seems like a bug to me. I'll try to trace this at little farther. – President James K. Polk Aug 06 '19 at 19:58
  • I believe [this](https://github.com/google/conscrypt/blob/master/common/src/main/java/org/conscrypt/OpenSSLAeadCipher.java) is the relevant source code, in particular the `updateInternal()` method. You can see it always returns 0. I'm not sure why, but I have a guess, namely that they are trying to shoehorn both GCM and CCM modes through one interface. – President James K. Polk Aug 06 '19 at 20:17
  • 1
    I'm going to go ahead and open an issue at [conscrypt issues](https://github.com/google/conscrypt/issues). Almost certainly it will be ignored though. In the meantime, I suspect that a different provider such as bouncycastle will produce the expected results. However, total throughput will probably still be faster for the default Conscrypt provider because it uses native code. It's a tradeoff you have to decide on. – President James K. Polk Aug 06 '19 at 20:40

2 Answers2

6

This is caused by a limitation in the Conscrypt provider that Android now uses by default. Here is an example of code that I'm running not an Android but rather on my Mac that explicitly uses the Conscrypt provider, and next uses the Bouncycastle (BC) provider to show the difference. Therefore a work around is to add the BC provider to your Android project and specify it explicitly when calling Cipher.getInstance(). There is a tradeoff, of course. While the BC provider will return ciphertext to you for every call to update() the overall throughput will probably be substantially less since Conscrypt uses native libraries and BC is pure Java.

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.conscrypt.Conscrypt;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import java.security.GeneralSecurityException;
import java.security.Provider;
import java.security.SecureRandom;
import java.security.Security;

public class ConscryptIssue1 {

    private final static Provider CONSCRYPT = Conscrypt.newProvider();
    private final static Provider BC = new BouncyCastleProvider();

    public static void main(String[] args) throws GeneralSecurityException {
        Security.addProvider(CONSCRYPT);
        doExample();
    }

    private static void doExample() throws GeneralSecurityException {
        final SecureRandom secureRandom = new SecureRandom();
        {
            // first, try with Conscrypt
            KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
            keyGenerator.init(256, secureRandom);
            SecretKey aesKey = keyGenerator.generateKey();
            byte[] plaintext = new byte[10000]; // plaintext is all zeros
            byte[] nonce = new byte[12];
            secureRandom.nextBytes(nonce);
            Cipher c = Cipher.getInstance("AES/GCM/NoPadding", CONSCRYPT);// specify the provider explicitly
            GCMParameterSpec spec = new GCMParameterSpec(128, nonce);// tag length is specified in bits.
            c.init(Cipher.ENCRYPT_MODE, aesKey, spec);
            byte[] outBuf = new byte[c.getOutputSize(512)];
            int numProduced = c.update(plaintext, 0, 512, outBuf, 0);
            System.out.println(numProduced);
            final int finalProduced = c.doFinal(outBuf, numProduced);
            System.out.println(finalProduced);
        }

        {
            // Next, try with Bouncycastle
            KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
            keyGenerator.init(256, secureRandom);
            SecretKey aesKey = keyGenerator.generateKey();
            byte[] plaintext = new byte[10000]; // plaintext is all zeros
            byte[] nonce = new byte[12];
            secureRandom.nextBytes(nonce);
            Cipher c = Cipher.getInstance("AES/GCM/NoPadding", BC);// specify the provider explicitly
            GCMParameterSpec spec = new GCMParameterSpec(128, nonce);// tag length is specified in bits.
            c.init(Cipher.ENCRYPT_MODE, aesKey, spec);
            byte[] outBuf = new byte[c.getOutputSize(512)];
            int numProduced = c.update(plaintext, 0, 512, outBuf, 0);
            System.out.println(numProduced);
            final int finalProduced = c.doFinal(outBuf, numProduced);
            System.out.println(finalProduced);
        }

    }
}
President James K. Polk
  • 40,516
  • 21
  • 95
  • 125
  • Thanks for looking into this in so much detail. Regarding CCM, that is my mistake, it turns out Cipher chooses the built in BC provider on Android when specifying CCM, and I can't seem to replicate this behaviour in that case now. I will consider using GCM with BouncyCastle, or possibly CTR+HMAC with Conscrypt which does provide output from `update()` – Will Aug 06 '19 at 22:59
  • 'Limitation'? Bug? – user207421 Aug 06 '19 at 23:02
  • Interestingly, Android complains that the built in BC no longer supports AES/GCM if I explicitly call `Cipher.getInstance("AES/GCM/NoPadding", "BC")`, but I can coerce it into using BC if I don't specify the provider in `getInstance` but change the nonce size to something other than 12 (not that I want to do that). – Will Aug 06 '19 at 23:06
  • @user207421: It returns the correct result and complies with Javadocs, it just does so in a most unsatisfying way. – President James K. Polk Aug 06 '19 at 23:14
  • 1
    @Will: IIRC the BC provider is no longer supported by Android except for some backward compatibility, but you can add the BC jars to your Android project dependencies. Make sure you do `Security.removeProvider("BC"); Security.addProvider(new BouncyCastleProvider());` before trying to use it. – President James K. Polk Aug 06 '19 at 23:17
-2

If anyone is looking for the opposite of this. (No output, just a tag) cipher.updateAAD(src) works... took me two days to find it, but it works

Ole Pannier
  • 3,208
  • 9
  • 22
  • 33
Kingsley
  • 9
  • 3