From e3e060f6db6e6e3a4f46c8b109cf6f7046ccc673 Mon Sep 17 00:00:00 2001 From: Mark Atwood Date: Thu, 1 Oct 2026 12:22:42 -0700 Subject: [PATCH] feat: streaming AES-GCM encrypt in wolfJCE Use wc_AesGcmEncryptInit/Update/Final for AES/GCM/NoPadding encryption when wolfSSL is built with WOLFSSL_AESGCM_STREAM, instead of buffering the whole plaintext until doFinal(). Falls back to the buffered path otherwise, and when wolfCrypt generated the IV (gcmInternalIv), since streaming init takes an external IV. Enforces the NIST SP 800-38D plaintext limit. Decrypt is not streamed. --- jni/include/com_wolfssl_wolfcrypt_AesGcm.h | 27 ++ jni/jni_aesgcm.c | 240 ++++++++++++++++++ .../wolfssl/provider/jce/WolfCryptCipher.java | 181 ++++++++++--- .../java/com/wolfssl/wolfcrypt/AesGcm.java | 104 ++++++++ .../jce/test/WolfCryptCipherTest.java | 121 +++++++-- 5 files changed, 621 insertions(+), 52 deletions(-) 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);