diff --git a/jni/include/com_wolfssl_wolfcrypt_AesGcm.h b/jni/include/com_wolfssl_wolfcrypt_AesGcm.h index 9eae7b3e..86f7977b 100644 --- a/jni/include/com_wolfssl_wolfcrypt_AesGcm.h +++ b/jni/include/com_wolfssl_wolfcrypt_AesGcm.h @@ -83,6 +83,33 @@ JNIEXPORT jbyteArray JNICALL Java_com_wolfssl_wolfcrypt_AesGcm_wc_1AesGcmEncrypt JNIEXPORT jboolean JNICALL Java_com_wolfssl_wolfcrypt_AesGcm_wc_1AesGcmShortIvAllowed (JNIEnv *, jclass); +/* + * Class: com_wolfssl_wolfcrypt_AesGcm + * Method: wc_AesGcmEncryptInitStreaming + * Signature: ([B)V + */ +JNIEXPORT void JNICALL +Java_com_wolfssl_wolfcrypt_AesGcm_wc_1AesGcmEncryptInitStreaming( + JNIEnv *, jobject, jbyteArray); + +/* + * Class: com_wolfssl_wolfcrypt_AesGcm + * Method: wc_AesGcmEncryptUpdateStreaming + * Signature: ([B[B)[B + */ +JNIEXPORT jbyteArray JNICALL +Java_com_wolfssl_wolfcrypt_AesGcm_wc_1AesGcmEncryptUpdateStreaming( + JNIEnv *, jobject, jbyteArray, jbyteArray); + +/* + * Class: com_wolfssl_wolfcrypt_AesGcm + * Method: wc_AesGcmEncryptFinalStreaming + * Signature: (I)[B + */ +JNIEXPORT jbyteArray JNICALL +Java_com_wolfssl_wolfcrypt_AesGcm_wc_1AesGcmEncryptFinalStreaming( + JNIEnv *, jobject, jint); + #ifdef __cplusplus } #endif diff --git a/jni/jni_aesgcm.c b/jni/jni_aesgcm.c index 828b38cf..f601db95 100644 --- a/jni/jni_aesgcm.c +++ b/jni/jni_aesgcm.c @@ -764,3 +764,243 @@ JNIEXPORT jbyteArray JNICALL Java_com_wolfssl_wolfcrypt_AesGcm_wc_1AesGcmEncrypt return NULL; #endif } + +/* + * Initialize AES-GCM streaming encryption. + * Key must already be loaded via wc_AesGcmSetKey. This call sets the IV + * and prepares internal streaming state (WOLFSSL_AESGCM_STREAM). + */ +JNIEXPORT void JNICALL +Java_com_wolfssl_wolfcrypt_AesGcm_wc_1AesGcmEncryptInitStreaming( + JNIEnv* env, jobject this, jbyteArray ivArr) +{ +#if !defined(NO_AES) && defined(HAVE_AESGCM) && defined(WOLFSSL_AESGCM_STREAM) + int ret = 0; + Aes* aes = NULL; + const byte* iv = NULL; + word32 ivSz = 0; + + aes = (Aes*) getNativeStruct(env, this); + if ((*env)->ExceptionOccurred(env)) { + return; + } + + if (ivArr != NULL) { + ivSz = (*env)->GetArrayLength(env, ivArr); + iv = (const byte*)(*env)->GetByteArrayElements(env, ivArr, NULL); + if ((*env)->ExceptionOccurred(env)) { + ret = MEMORY_E; + } + } + + if ((ret == 0) && (iv == NULL || ivSz == 0)) { + ret = BAD_FUNC_ARG; + } + + /* + * Pass NULL key (key already loaded via wc_AesGcmSetKey). + * wc_AesGcmEncryptInit only sets key when key != NULL. + */ + if (ret == 0) { + ret = wc_AesGcmEncryptInit(aes, NULL, 0, iv, ivSz); + } + + if (iv != NULL) { + (*env)->ReleaseByteArrayElements(env, ivArr, (jbyte*)iv, JNI_ABORT); + } + + if (ret != 0) { + throwWolfCryptExceptionFromError(env, ret); + } + + LogStr("wc_AesGcmEncryptInit(aes = %p, ivSz = %d)\n", aes, ivSz); +#else + (void)this; + (void)ivArr; + throwNotCompiledInException(env); +#endif +} + +/* + * Streaming AES-GCM encrypt update: encrypt plaintext and/or process AAD. + * inputArr may be NULL or empty (AAD-only call). + * authInArr may be NULL (no AAD for this call). + * Returns a jbyteArray of length inputArr.length containing ciphertext. + */ +JNIEXPORT jbyteArray JNICALL +Java_com_wolfssl_wolfcrypt_AesGcm_wc_1AesGcmEncryptUpdateStreaming( + JNIEnv* env, jobject this, jbyteArray inputArr, + jbyteArray authInArr) +{ +#if !defined(NO_AES) && defined(HAVE_AESGCM) && defined(WOLFSSL_AESGCM_STREAM) + int ret = 0; + Aes* aes = NULL; + const byte* in = NULL; + const byte* authIn = NULL; + word32 inLen = 0; + word32 authInSz = 0; + jboolean inIsCopy = JNI_FALSE; + byte* out = NULL; + jbyteArray outArr = NULL; + + aes = (Aes*) getNativeStruct(env, this); + if ((*env)->ExceptionOccurred(env)) { + return NULL; + } + + if (inputArr != NULL) { + inLen = (*env)->GetArrayLength(env, inputArr); + in = (const byte*)(*env)->GetByteArrayElements(env, inputArr, + &inIsCopy); + if ((*env)->ExceptionOccurred(env)) { + ret = MEMORY_E; + } + if ((ret == 0) && (inLen > 0) && (in == NULL)) { + ret = BAD_FUNC_ARG; + } + } + if ((ret == 0) && (authInArr != NULL)) { + authInSz = (*env)->GetArrayLength(env, authInArr); + authIn = (const byte*)(*env)->GetByteArrayElements(env, + authInArr, NULL); + if ((*env)->ExceptionOccurred(env)) { + ret = MEMORY_E; + } + if ((ret == 0) && (authInSz > 0) && (authIn == NULL)) { + ret = BAD_FUNC_ARG; + } + } + + if ((ret == 0) && (inLen > 0)) { + out = (byte*)XMALLOC(inLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (out == NULL) { + ret = MEMORY_E; + } + else { + XMEMSET(out, 0, inLen); + } + } + + if (ret == 0) { + ret = wc_AesGcmEncryptUpdate(aes, out, in, inLen, authIn, authInSz); + } + + if (ret == 0) { + outArr = (*env)->NewByteArray(env, (jsize)inLen); + if (outArr == NULL) { + ret = MEMORY_E; + } + else if (inLen > 0) { + (*env)->SetByteArrayRegion(env, outArr, 0, (jsize)inLen, + (jbyte*)out); + if ((*env)->ExceptionOccurred(env)) { + (*env)->ExceptionDescribe(env); + (*env)->ExceptionClear(env); + (*env)->DeleteLocalRef(env, outArr); + outArr = NULL; + ret = -1; + } + } + } + + /* Wipe any plaintext copy before release */ + if (in != NULL) { + zeroizeByteArrayCopy((byte*)in, inLen, inIsCopy); + (*env)->ReleaseByteArrayElements(env, inputArr, (jbyte*)in, JNI_ABORT); + } + if (authIn != NULL) { + (*env)->ReleaseByteArrayElements(env, authInArr, (jbyte*)authIn, + JNI_ABORT); + } + if (out != NULL) { + XFREE(out, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } + + LogStr("wc_AesGcmEncryptUpdate(aes = %p, inLen = %d, authInSz = %d)\n", + aes, inLen, authInSz); + + if (ret != 0) { + throwWolfCryptExceptionFromError(env, ret); + return NULL; + } + + return outArr; + +#else + (void)this; + (void)inputArr; + (void)authInArr; + throwNotCompiledInException(env); + return NULL; +#endif +} + +/* + * Finalize AES-GCM streaming encryption and generate authentication tag. + * Returns a jbyteArray of length tagLen containing the authentication tag. + */ +JNIEXPORT jbyteArray JNICALL +Java_com_wolfssl_wolfcrypt_AesGcm_wc_1AesGcmEncryptFinalStreaming( + JNIEnv* env, jobject this, jint tagLen) +{ +#if !defined(NO_AES) && defined(HAVE_AESGCM) && defined(WOLFSSL_AESGCM_STREAM) + int ret = 0; + Aes* aes = NULL; + byte* tag = NULL; + jbyteArray tagArr = NULL; + + aes = (Aes*) getNativeStruct(env, this); + if ((*env)->ExceptionOccurred(env)) { + return NULL; + } + + if (tagLen <= 0 || tagLen > AES_BLOCK_SIZE) { + throwWolfCryptExceptionFromError(env, BAD_FUNC_ARG); + return NULL; + } + + tag = (byte*)XMALLOC((word32)tagLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (tag == NULL) { + throwWolfCryptExceptionFromError(env, MEMORY_E); + return NULL; + } + XMEMSET(tag, 0, (word32)tagLen); + + ret = wc_AesGcmEncryptFinal(aes, tag, (word32)tagLen); + + if (ret == 0) { + tagArr = (*env)->NewByteArray(env, tagLen); + if (tagArr == NULL) { + ret = MEMORY_E; + } + else { + (*env)->SetByteArrayRegion(env, tagArr, 0, tagLen, (jbyte*)tag); + if ((*env)->ExceptionOccurred(env)) { + (*env)->ExceptionDescribe(env); + (*env)->ExceptionClear(env); + (*env)->DeleteLocalRef(env, tagArr); + tagArr = NULL; + ret = -1; + } + } + } + + XFREE(tag, NULL, DYNAMIC_TYPE_TMP_BUFFER); + + LogStr("wc_AesGcmEncryptFinal(aes = %p, tagLen = %d)\n", aes, tagLen); + + if (ret != 0) { + throwWolfCryptExceptionFromError(env, ret); + return NULL; + } + + return tagArr; + +#else + (void)this; + (void)tagLen; + throwNotCompiledInException(env); + return NULL; +#endif +} + diff --git a/src/main/java/com/wolfssl/provider/jce/WolfCryptCipher.java b/src/main/java/com/wolfssl/provider/jce/WolfCryptCipher.java index 6c80cf3b..a1a141d7 100644 --- a/src/main/java/com/wolfssl/provider/jce/WolfCryptCipher.java +++ b/src/main/java/com/wolfssl/provider/jce/WolfCryptCipher.java @@ -240,9 +240,16 @@ private static final class DefaultIvRandom { /* Has update/final been called yet, gates setting of AAD for GCM */ private boolean operationStarted = false; - /* Has this Cipher been inintialized? */ + /* Has this Cipher been initialized? */ private boolean cipherInitialized = false; + /* Streaming GCM encrypt state (WOLFSSL_AESGCM_STREAM) */ + private boolean gcmStreamingActive = false; + private boolean gcmAadPassed = false; + private long gcmBytesEncrypted = 0L; + /* NIST SP 800-38D: max plaintext = (2^32 - 2) * 16 bytes per session */ + private static final long GCM_MAX_PLAINTEXT_BYTES = 0xFFFFFFFEL * 16L; + /* Buffered data from update calls, only the first bufferedLen bytes * are valid. Capacity grows to max(2 * capacity, needed). */ private byte[] buffered = new byte[0]; @@ -1717,6 +1724,23 @@ private void wolfCryptCipherInit(int opmode, Key key, wolfCryptSetIV(spec, random); checkGcmKeyIvReuse(key); wolfCryptSetKey(key); + + /* Initialize streaming GCM encrypt if available. Not used when + * wolfCrypt generated the IV (gcmInternalIv): streaming init takes + * an external IV, which would bypass the internal IV path. Done + * before recording the key/IV and marking the Cipher initialized, + * so a failure here leaves neither behind. */ + this.gcmStreamingActive = false; + this.gcmAadPassed = false; + this.gcmBytesEncrypted = 0L; + if (cipherMode == CipherMode.WC_GCM && + direction == OpMode.WC_ENCRYPT && + !this.gcmInternalIv && + FeatureDetect.AesGcmStreamEnabled()) { + this.aesGcm.encryptInitStreaming(this.iv); + this.gcmStreamingActive = true; + } + recordGcmKeyIv(key); this.operationStarted = false; this.cipherInitialized = true; @@ -1819,13 +1843,16 @@ private boolean isNoOpUpdate(int inputSz) { } /* AES-GCM, AES-CCM, AES-CTS, and AES Key Wrap keep all data buffered - * until final(). wolfJCE does not support streaming GCM/CCM yet, CTS - * and Key Wrap need the entire message. */ + * until final(). Streaming GCM encrypt bypasses this. CCM requires the + * total length upfront, CTS and Key Wrap need the entire message. */ if (cipherType == CipherType.WC_AES && (cipherMode == CipherMode.WC_GCM || cipherMode == CipherMode.WC_CCM || cipherMode == CipherMode.WC_CTS || cipherMode == CipherMode.WC_KW)) { + if (cipherMode == CipherMode.WC_GCM && gcmStreamingActive) { + return false; + } return true; } @@ -1878,6 +1905,35 @@ private byte[] wolfCryptUpdate(byte[] input, int inputOffset, int len) this.operationStarted = true; + /* Streaming GCM encrypt: process data immediately, skip buffering */ + if (gcmStreamingActive) { + if (len > 0 && gcmBytesEncrypted + len > GCM_MAX_PLAINTEXT_BYTES) { + throw new IllegalStateException( + "AES-GCM plaintext exceeds NIST SP 800-38D limit of " + + GCM_MAX_PLAINTEXT_BYTES + " bytes"); + } + byte[] aadForUpdate = null; + if (!gcmAadPassed && this.aadStream != null) { + aadForUpdate = this.aadStream.toByteArray(); + } + gcmAadPassed = true; + byte[] inputSlice = (len > 0) ? + Arrays.copyOfRange( + input, inputOffset, inputOffset + len) : + null; + byte[] ct; + try { + ct = this.aesGcm.encryptUpdateStreaming(inputSlice, + aadForUpdate); + } finally { + zeroArray(inputSlice); + } + if (len > 0) { + gcmBytesEncrypted += len; + } + return (ct != null) ? ct : new byte[0]; + } + if ((bufferedLen + len) == 0) { /* no data to process */ return null; @@ -2217,6 +2273,26 @@ private void resetAfterFinal() { this.operationStarted = false; this.cipherInitialized = true; + /* Streaming GCM may have already released ciphertext under this + * key/IV via update(), even if final() then failed. Require + * re-init so the same key/IV cannot encrypt again. */ + if (this.gcmStreamingActive && this.gcmAadPassed) { + this.gcmEncryptNeedsReinit = true; + } + + /* Re-initialize streaming GCM encrypt if available, caller + * supplied IV only (see engineInit) */ + this.gcmStreamingActive = false; + this.gcmAadPassed = false; + this.gcmBytesEncrypted = 0L; + if (cipherMode == CipherMode.WC_GCM && + direction == OpMode.WC_ENCRYPT && + !this.gcmInternalIv && + FeatureDetect.AesGcmStreamEnabled()) { + this.aesGcm.encryptInitStreaming(this.iv); + this.gcmStreamingActive = true; + } + } catch (InvalidKeyException e) { throw new RuntimeException( "Failed to reset cipher after final(): " + e.getMessage(), e); @@ -2402,35 +2478,75 @@ else if (cipherMode == CipherMode.WC_GCM) { "GCM encryption"); } - byte[] tag = new byte[this.gcmTagLen]; - byte[] ivOut = null; - if (this.gcmInternalIv) { - ivOut = new byte[this.iv.length]; - tmpOut = this.aesGcm.encryptEx(tmpIn, ivOut, - tag, aad); - } - else { - tmpOut = this.aesGcm.encrypt(tmpIn, this.iv, - tag, aad); - } + if (gcmStreamingActive) { + /* Streaming path: prior data was encrypted in + * update calls. Handle remaining plaintext + * and generate the authentication tag. */ + if (!gcmAadPassed && aad != null) { + this.aesGcm.encryptUpdateStreaming(null, + aad); + } + gcmAadPassed = true; + byte[] finalCt = null; + if (tmpIn.length > 0) { + if (gcmBytesEncrypted + tmpIn.length > + GCM_MAX_PLAINTEXT_BYTES) { + throw new IllegalBlockSizeException( + "AES-GCM plaintext exceeds NIST " + + "SP 800-38D limit of " + + GCM_MAX_PLAINTEXT_BYTES + + " bytes"); + } + finalCt = + this.aesGcm.encryptUpdateStreaming( + tmpIn, null); + gcmBytesEncrypted += tmpIn.length; + } + byte[] tag = this.aesGcm.encryptFinalStreaming( + this.gcmTagLen); - /* Block future encrypts with this key and IV */ - this.gcmEncryptNeedsReinit = true; + this.gcmEncryptNeedsReinit = true; - if (ivOut != null && - !MessageDigest.isEqual(ivOut, this.iv)) { - throw new ProviderException( - "GCM IV used does not match getIV()"); - } + int ctLen = + (finalCt != null) ? finalCt.length : 0; + tmpOut = new byte[ctLen + tag.length]; + if (ctLen > 0) { + System.arraycopy(finalCt, 0, tmpOut, 0, + ctLen); + } + System.arraycopy(tag, 0, tmpOut, ctLen, + tag.length); + } else { + byte[] tag = new byte[this.gcmTagLen]; + byte[] ivOut = null; + if (this.gcmInternalIv) { + ivOut = new byte[this.iv.length]; + tmpOut = this.aesGcm.encryptEx(tmpIn, ivOut, + tag, aad); + } + else { + tmpOut = this.aesGcm.encrypt(tmpIn, this.iv, + tag, aad); + } - /* Concatenate auth tag to end of ciphertext */ - byte[] totalOut = - new byte[tmpOut.length + tag.length]; - System.arraycopy(tmpOut, 0, totalOut, 0, - tmpOut.length); - System.arraycopy(tag, 0, totalOut, tmpOut.length, - tag.length); - tmpOut = totalOut; + /* Block future encrypts with this key and IV */ + this.gcmEncryptNeedsReinit = true; + + if (ivOut != null && + !MessageDigest.isEqual(ivOut, this.iv)) { + throw new ProviderException( + "GCM IV used does not match getIV()"); + } + + /* Concatenate auth tag to end of ciphertext */ + byte[] totalOut = + new byte[tmpOut.length + tag.length]; + System.arraycopy(tmpOut, 0, totalOut, 0, + tmpOut.length); + System.arraycopy(tag, 0, totalOut, + tmpOut.length, tag.length); + tmpOut = totalOut; + } } else { /* Case where input is only the authentication tag, @@ -2727,9 +2843,10 @@ private void checkUpdateOutputBufferSize(int inputSz, int outputSz) return; } - /* Stream modes output everything, block modes whole blocks, XTS and - * PKCS#5 decryption hold one block back */ - if (isStreamMode()) { + /* Stream modes (and streaming GCM encrypt) output everything, block + * modes whole blocks, XTS and PKCS#5 decryption hold one block back */ + if (isStreamMode() || + (cipherMode == CipherMode.WC_GCM && gcmStreamingActive)) { outSize = totalSz; } else { diff --git a/src/main/java/com/wolfssl/wolfcrypt/AesGcm.java b/src/main/java/com/wolfssl/wolfcrypt/AesGcm.java index 3f3d1dc8..1a3e2ac5 100644 --- a/src/main/java/com/wolfssl/wolfcrypt/AesGcm.java +++ b/src/main/java/com/wolfssl/wolfcrypt/AesGcm.java @@ -33,6 +33,14 @@ public class AesGcm extends NativeStruct { /* Size of IV generated by setIV(), 0 until setIV() is called */ private int internalIvSz = 0; + /** Streaming operation state */ + private enum StreamingState { + /** No streaming operation in progress */ + IDLE, + /** After encryptInitStreaming(), before encryptFinalStreaming() */ + STREAMING + } + private StreamingState streamingState = StreamingState.IDLE; /** Lock around object state */ protected final Object stateLock = new Object(); @@ -53,6 +61,10 @@ private native byte[] wc_AesGcmDecrypt(byte[] input, byte[] iv, private native byte[] wc_AesGcmEncrypt_ex(byte[] input, byte[] ivOut, byte[] authTagOut, byte[] authIn); private static native boolean wc_AesGcmShortIvAllowed(); + private native void wc_AesGcmEncryptInitStreaming(byte[] iv); + private native byte[] wc_AesGcmEncryptUpdateStreaming(byte[] input, + byte[] authIn); + private native byte[] wc_AesGcmEncryptFinalStreaming(int tagLen); /** * Checks if short AES-GCM and GMAC IVs (under 96 bits) are allowed. @@ -406,5 +418,97 @@ public synchronized byte[] decrypt(byte[] input, byte[] iv, byte[] authTag, return output; } + + /** + * Initialize streaming AES-GCM encryption. Key must already be loaded + * via setKey(). Only available when wolfSSL is compiled with + * WOLFSSL_AESGCM_STREAM. + * + * @param iv IV for AES-GCM operation + * @throws WolfCryptException if native operation fails or + * feature not compiled in + * @throws IllegalStateException if key not loaded or object released + */ + public synchronized void encryptInitStreaming(byte[] iv) + throws IllegalStateException, WolfCryptException { + + checkStateAndInitialize(); + throwIfKeyNotLoaded(); + + /* Allowed from IDLE or STREAMING (re-init resets a prior stream). + * Only enter STREAMING once native init succeeds. */ + streamingState = StreamingState.IDLE; + + synchronized (pointerLock) { + wc_AesGcmEncryptInitStreaming(iv); + } + + streamingState = StreamingState.STREAMING; + } + + /** + * Streaming AES-GCM encrypt update. Encrypts plaintext and/or processes + * AAD. May be called multiple times between encryptInitStreaming() and + * encryptFinalStreaming(). + * + * @param input plaintext to encrypt, may be null or empty for AAD-only + * @param authIn additional authenticated data, may be null + * @return ciphertext bytes (same length as input) + * @throws WolfCryptException if native operation fails + * @throws IllegalStateException if key not loaded or object released + */ + public synchronized byte[] encryptUpdateStreaming(byte[] input, + byte[] authIn) throws IllegalStateException, WolfCryptException { + + byte[] output = null; + + checkStateAndInitialize(); + throwIfKeyNotLoaded(); + + if (streamingState != StreamingState.STREAMING) { + throw new IllegalStateException( + "encryptInitStreaming must be called before " + + "encryptUpdateStreaming (current state: IDLE)"); + } + + synchronized (pointerLock) { + output = wc_AesGcmEncryptUpdateStreaming(input, authIn); + } + + return output; + } + + /** + * Finalize streaming AES-GCM encryption and generate authentication tag. + * + * @param tagLen desired auth tag length in bytes (up to AES block size 16) + * @return authentication tag bytes of length tagLen + * @throws WolfCryptException if native operation fails + * @throws IllegalStateException if key not loaded or object released + */ + public synchronized byte[] encryptFinalStreaming(int tagLen) + throws IllegalStateException, WolfCryptException { + + byte[] tag = null; + + checkStateAndInitialize(); + throwIfKeyNotLoaded(); + + if (streamingState != StreamingState.STREAMING) { + throw new IllegalStateException( + "encryptInitStreaming must be called before " + + "encryptFinalStreaming (current state: IDLE)"); + } + + try { + synchronized (pointerLock) { + tag = wc_AesGcmEncryptFinalStreaming(tagLen); + } + } finally { + streamingState = StreamingState.IDLE; + } + + return tag; + } } diff --git a/src/test/java/com/wolfssl/provider/jce/test/WolfCryptCipherTest.java b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptCipherTest.java index 625169d7..31e79491 100644 --- a/src/test/java/com/wolfssl/provider/jce/test/WolfCryptCipherTest.java +++ b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptCipherTest.java @@ -2281,6 +2281,13 @@ public void testAesGcmNoPaddingWithUpdate() enc.init(Cipher.ENCRYPT_MODE, key, spec); enc.updateAAD(aesGcmVectors[i].getAAD()); + /* With AES-GCM streaming (native WOLFSSL_AESGCM_STREAM), encrypt + * update() emits ciphertext incrementally and doFinal() returns + * only the tag; without it all output is buffered until doFinal(). + * Both are valid per the JCE Cipher.update() contract. Decrypt is + * never streamed (plaintext withheld until the tag is verified). */ + boolean gcmStreaming = FeatureDetect.AesGcmStreamEnabled(); + /* Expected output is size vOut + vTag.length */ output = new byte[vOut.length + vTag.length]; @@ -2291,8 +2298,7 @@ public void testAesGcmNoPaddingWithUpdate() for (int j = 0; j < fourByteBlocks; j++) { tmp = enc.update(Arrays.copyOfRange(vIn, inIdx, inIdx + 4)); assertNotNull(tmp); - /* AES-GCM stream API not supported in JCE yet */ - assertEquals(0, tmp.length); + assertEquals(gcmStreaming ? 4 : 0, tmp.length); System.arraycopy(tmp, 0, output, outIdx, tmp.length); inIdx += 4; outIdx += tmp.length; @@ -2304,19 +2310,18 @@ public void testAesGcmNoPaddingWithUpdate() tmp = enc.update(Arrays.copyOfRange(vIn, inIdx, inIdx + remainingBytes)); assertNotNull(tmp); - /* AES-GCM stream API not supported in JCE yet */ - assertEquals(0, tmp.length); + assertEquals(gcmStreaming ? remainingBytes : 0, tmp.length); System.arraycopy(tmp, 0, output, outIdx, tmp.length); inIdx += remainingBytes; outIdx += tmp.length; } - /* doFinal() should get tag (or whole ciphertext if AES-GCM - * streaming is not enabled at native wolfSSL level) */ + /* doFinal() returns only the tag when streaming, or the whole + * ciphertext + tag when buffered. */ tmp = enc.doFinal(); assertNotNull(tmp); - /* AES-GCM stream API not supported in JCE yet */ - assertEquals(tmpOut.length, tmp.length); + assertEquals(gcmStreaming ? vTag.length : tmpOut.length, + tmp.length); System.arraycopy(tmp, 0, output, outIdx, tmp.length); outIdx += tmp.length; @@ -2392,8 +2397,9 @@ public void testAesGcmNoPaddingWithUpdate() } /* - * Test that many small update() calls match a single call. GCM buffers - * everything until doFinal(), so this drives buffer growth only. + * Test that many small update() calls match a single call. update() may + * return ciphertext (streaming GCM) or buffer it until doFinal(), so + * compare the concatenation of all output. */ @Test public void testAesGcmChunkedUpdateMatchesSingle() throws Exception { @@ -2428,12 +2434,66 @@ public void testAesGcmChunkedUpdateMatchesSingle() throws Exception { /* Same plaintext, sent one byte per update() call */ Cipher chunked = Cipher.getInstance("AES/GCM/NoPadding", jceProvider); chunked.init(Cipher.ENCRYPT_MODE, key, spec); + ByteArrayOutputStream chunkedOut = new ByteArrayOutputStream(); for (int i = 0; i < plaintext.length; i++) { - chunked.update(plaintext, i, 1); + byte[] out = chunked.update(plaintext, i, 1); + if (out != null) { + chunkedOut.write(out); + } } + chunkedOut.write(chunked.doFinal()); assertArrayEquals("Chunked update should match single doFinal", - expected, chunked.doFinal()); + expected, chunkedOut.toByteArray()); + } + + /* + * Test that a streaming GCM update() into a too-short output buffer + * throws ShortBufferException before consuming input, so a retry with a + * larger buffer still produces the known-answer ciphertext and tag. + */ + @Test + public void testAesGcmUpdateShortBufferRetry() throws Exception { + + if (!enabledJCEAlgos.contains("AES/GCM/NoPadding") || + !FeatureDetect.Aes256Enabled()) { + /* skip if AES-256-GCM is not enabled */ + return; + } + + /* AES-256 vector, 12-byte IV */ + CipherVector v = aesGcmVectors[2]; + byte[] vIn = v.getInput(); + byte[] vOut = v.getOutput(); + byte[] vTag = v.getTag(); + byte[] expected = new byte[vOut.length + vTag.length]; + System.arraycopy(vOut, 0, expected, 0, vOut.length); + System.arraycopy(vTag, 0, expected, vOut.length, vTag.length); + + Cipher c = Cipher.getInstance("AES/GCM/NoPadding", jceProvider); + c.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(v.getKey(), "AES"), + new GCMParameterSpec(vTag.length * 8, v.getIV())); + c.updateAAD(v.getAAD()); + + /* Streaming emits 1 byte for a 1-byte update, buffered emits none */ + if (FeatureDetect.AesGcmStreamEnabled()) { + try { + c.update(vIn, 0, 1, new byte[0], 0); + fail("Expected ShortBufferException for 1-byte update " + + "into empty output buffer"); + } catch (ShortBufferException e) { + /* expected */ + } + } + + byte[] out = new byte[expected.length]; + int outLen = c.update(vIn, 0, 1, out, 0); + outLen += c.update(vIn, 1, vIn.length - 1, out, outLen); + outLen += c.doFinal(out, outLen); + + assertEquals(expected.length, outLen); + assertArrayEquals("Retry after ShortBufferException should match " + + "known-answer ciphertext and tag", expected, out); } /* @@ -2465,10 +2525,15 @@ public void testAesGcmLargeThenSmallReusesCipher() throws Exception { /* Large message fed in chunks, so capacity grows well past the * retention limit */ c.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv)); + ByteArrayOutputStream largeOut = new ByteArrayOutputStream(); for (int i = 0; i < large.length; i += 4096) { - c.update(large, i, Math.min(4096, large.length - i)); + byte[] out = c.update(large, i, Math.min(4096, large.length - i)); + if (out != null) { + largeOut.write(out); + } } - byte[] largeCipher = c.doFinal(); + largeOut.write(c.doFinal()); + byte[] largeCipher = largeOut.toByteArray(); /* Same instance, smaller message with a different IV */ iv[0] = (byte)0x01; @@ -3005,14 +3070,22 @@ public void testAesGcmGetOutputSize() throws Exception { "minus tag length", 10, cipher.getOutputSize(10 + TAG_LENGTH_BYTES)); - /* Test ENCRYPT after partial update */ + /* Test ENCRYPT after partial update. With AES-GCM streaming the 5 + * bytes are emitted by update() immediately, so getOutputSize() only + * covers the remaining input + tag; without streaming they stay + * buffered and are included here. Both are the correct size for the + * respective native build. */ byte[] partialInput = new byte[5]; iv[0] = (byte) 0x05; spec = new GCMParameterSpec(TAG_LENGTH_BYTES * 8, iv); cipher.init(Cipher.ENCRYPT_MODE, key, spec); cipher.update(partialInput); /* Process some data */ + int expectedSizeAfterUpdate = FeatureDetect.AesGcmStreamEnabled() + ? 11 + TAG_LENGTH_BYTES /* 5 already emitted */ + : 16 + TAG_LENGTH_BYTES; /* 5 buffered + 11 input */ assertEquals("Output size after update should account for remaining " + - "input plus tag", 16 + TAG_LENGTH_BYTES, cipher.getOutputSize(11)); + "input plus tag", expectedSizeAfterUpdate, + cipher.getOutputSize(11)); /* Test getOutputSize() before initialization, expect exception */ Cipher uninitializedCipher = Cipher.getInstance("AES/GCM/NoPadding"); @@ -11392,14 +11465,22 @@ public void testNoOpUpdateBehavior() byte[] input = "Test GCM".getBytes(); - /* AES-GCM update() should return empty array (no-op) */ + /* AES-GCM update() behavior depends on the native build: with + * streaming it emits ciphertext incrementally (legal per the JCE + * Cipher.update() contract); without it, output is buffered until + * doFinal() and update() returns an empty array. */ byte[] updateResult = cipher.update(input); assertNotNull("AES-GCM update should return non-null array", updateResult); - assertEquals("AES-GCM update should return empty array", 0, - updateResult.length); + if (FeatureDetect.AesGcmStreamEnabled()) { + assertEquals("AES-GCM streaming update returns ciphertext", + input.length, updateResult.length); + } else { + assertEquals("AES-GCM buffered update returns empty array", + 0, updateResult.length); + } - /* All processing should happen in doFinal() */ + /* doFinal() returns the tag (streaming) or ciphertext + tag */ byte[] finalResult = cipher.doFinal(); assertTrue("AES-GCM doFinal should return encrypted data", finalResult.length > 0);