diff --git a/jni/jni_aesgcm.c b/jni/jni_aesgcm.c index 87f56667..f5565887 100644 --- a/jni/jni_aesgcm.c +++ b/jni/jni_aesgcm.c @@ -192,9 +192,10 @@ JNIEXPORT jbyteArray JNICALL Java_com_wolfssl_wolfcrypt_AesGcm_wc_1AesGcmEncrypt authInSz = (*env)->GetArrayLength(env, authInArr); } - /* authIn can be null */ - if (in == NULL || inLen == 0 || iv == NULL || ivSz == 0 || - authTag == NULL || authTagSz == 0) { + /* in may be null, users might only pass in AAD to generate tag */ + if (authTagSz > WC_AES_BLOCK_SIZE || iv == NULL || ivSz == 0 || + ((authTagSz > 0) && (authTag == NULL)) || + ((authInSz > 0) && (authIn == NULL))) { ret = BAD_FUNC_ARG; } @@ -325,8 +326,10 @@ JNIEXPORT jbyteArray JNICALL Java_com_wolfssl_wolfcrypt_AesGcm_wc_1AesGcmDecrypt authInSz = (*env)->GetArrayLength(env, authInArr); } - if (in == NULL || inLen == 0 || iv == NULL || ivSz == 0 || - authTag == NULL || authTagSz == 0) { + /* If inLen is non-zero, both in and out must be set. If inLen is 0, + * in and out are don't cares, as this is the GMAC case */ + if (iv == NULL || ivSz == 0 || (inLen != 0 && in == NULL) || + authTag == NULL || (authTagSz > WC_AES_BLOCK_SIZE) || authTagSz == 0) { ret = BAD_FUNC_ARG; } diff --git a/src/main/java/com/wolfssl/provider/jce/WolfCryptCipher.java b/src/main/java/com/wolfssl/provider/jce/WolfCryptCipher.java index 6896322e..d110a11f 100644 --- a/src/main/java/com/wolfssl/provider/jce/WolfCryptCipher.java +++ b/src/main/java/com/wolfssl/provider/jce/WolfCryptCipher.java @@ -979,9 +979,12 @@ private byte[] wolfCryptFinal(byte[] input, int inputOffset, int len) /* do final encrypt over totalSz */ tmpIn = new byte[totalSz]; - System.arraycopy(buffered, 0, tmpIn, 0, buffered.length); - if (input != null && len > 0) { - System.arraycopy(input, inputOffset, tmpIn, buffered.length, len); + if (totalSz > 0) { + System.arraycopy(buffered, 0, tmpIn, 0, buffered.length); + if (input != null && len > 0) { + System.arraycopy(input, inputOffset, tmpIn, + buffered.length, len); + } } /* add padding if encrypting and PKCS5 padding is used. PKCS#5 padding 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 30a1bbbf..6cd0de71 100644 --- a/src/test/java/com/wolfssl/provider/jce/test/WolfCryptCipherTest.java +++ b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptCipherTest.java @@ -6483,5 +6483,113 @@ public void testGetOutputSizeZeroInputPKCS5Padding() throws Exception { "block (8 bytes)", 8, outputSize); } } + + /** + * Test AES-GCM with null plaintext input. This tests scenarios where + * users may only provide AAD to generate an authentication tag. + * Uses test vectors from OpenJDK SunJCE tests that have null plaintext. + */ + @Test + public void testAesGcmWithNullPlaintext() + throws NoSuchAlgorithmException, InvalidKeyException, + IllegalBlockSizeException, NoSuchProviderException, + InvalidAlgorithmParameterException, BadPaddingException, + NoSuchPaddingException { + + if (!enabledJCEAlgos.contains("AES/GCM/NoPadding")) { + /* skip if AES-GCM is not enabled */ + return; + } + + /* + * Test vector from OpenJDK TestKATForGCM.java - Test case 1 + * 96-bit iv with 128-bit tag, no plaintext, no AAD + */ + byte[] key = new byte[] { + (byte)0x11, (byte)0x75, (byte)0x4c, (byte)0xd7, + (byte)0x2a, (byte)0xec, (byte)0x30, (byte)0x9b, + (byte)0xf5, (byte)0x2f, (byte)0x76, (byte)0x87, + (byte)0x21, (byte)0x2e, (byte)0x89, (byte)0x57 + }; + byte[] iv = new byte[] { + (byte)0x3c, (byte)0x81, (byte)0x9d, (byte)0x9a, + (byte)0x9b, (byte)0xed, (byte)0x08, (byte)0x76, + (byte)0x15, (byte)0x03, (byte)0x0b, (byte)0x65 + }; + byte[] expectedTag = new byte[] { + (byte)0x25, (byte)0x03, (byte)0x27, (byte)0xc6, + (byte)0x74, (byte)0xaa, (byte)0xf4, (byte)0x77, + (byte)0xae, (byte)0xf2, (byte)0x67, (byte)0x57, + (byte)0x48, (byte)0xcf, (byte)0x69, (byte)0x71 + }; + + /* + * Test vector from OpenJDK TestKATForGCM.java - Test case 6 + * 96-bit iv with 128-bit tag, no plaintext, 16-byte AAD + */ + byte[] key2 = new byte[] { + (byte)0x77, (byte)0xbe, (byte)0x63, (byte)0x70, + (byte)0x89, (byte)0x71, (byte)0xc4, (byte)0xe2, + (byte)0x40, (byte)0xd1, (byte)0xcb, (byte)0x79, + (byte)0xe8, (byte)0xd7, (byte)0x7f, (byte)0xeb + }; + byte[] iv2 = new byte[] { + (byte)0xe0, (byte)0xe0, (byte)0x0f, (byte)0x19, + (byte)0xfe, (byte)0xd7, (byte)0xba, (byte)0x01, + (byte)0x36, (byte)0xa7, (byte)0x97, (byte)0xf3 + }; + byte[] aad2 = new byte[] { + (byte)0x7a, (byte)0x43, (byte)0xec, (byte)0x1d, + (byte)0x9c, (byte)0x0a, (byte)0x5a, (byte)0x78, + (byte)0xa0, (byte)0xb1, (byte)0x65, (byte)0x33, + (byte)0xa6, (byte)0x21, (byte)0x3c, (byte)0xab + }; + byte[] expectedTag2 = new byte[] { + (byte)0x20, (byte)0x9f, (byte)0xcc, (byte)0x8d, + (byte)0x36, (byte)0x75, (byte)0xed, (byte)0x93, + (byte)0x8e, (byte)0x9c, (byte)0x71, (byte)0x66, + (byte)0x70, (byte)0x9d, (byte)0xd9, (byte)0x46 + }; + + /* Using byte[0] instead of null because OpenJDK Cipher.java + * throws IllegalArgumentException when in is null. */ + byte[] nullInput = new byte[0]; + + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", jceProvider); + + /* Test case 1: No plaintext, no AAD */ + SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); + GCMParameterSpec gcmSpec = new GCMParameterSpec( + expectedTag.length * 8, iv); + + cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec); + byte[] output = cipher.doFinal(nullInput); + + /* Output should just be the tag since no ciphertext */ + assertArrayEquals("Tag should match expected for null plaintext", + expectedTag, output); + + /* Test case 2: No plaintext, with AAD */ + SecretKeySpec keySpec2 = new SecretKeySpec(key2, "AES"); + GCMParameterSpec gcmSpec2 = new GCMParameterSpec( + expectedTag2.length * 8, iv2); + + cipher.init(Cipher.ENCRYPT_MODE, keySpec2, gcmSpec2); + cipher.updateAAD(aad2); + byte[] output2 = cipher.doFinal(nullInput); + + /* Output should just be the tag since no ciphertext */ + assertArrayEquals("Tag should match expected for null plaintext " + + "with AAD", expectedTag2, output2); + + /* Verify decryption works too */ + cipher.init(Cipher.DECRYPT_MODE, keySpec2, gcmSpec2); + cipher.updateAAD(aad2); + byte[] decrypted = cipher.doFinal(output2); + + /* Decrypted should be empty/null since original plaintext was null */ + assertTrue("Decrypted plaintext should be empty when original " + + "plaintext was null", decrypted == null || decrypted.length == 0); + } } diff --git a/src/test/java/com/wolfssl/wolfcrypt/test/AesGcmTest.java b/src/test/java/com/wolfssl/wolfcrypt/test/AesGcmTest.java index 021dec0c..297403d4 100644 --- a/src/test/java/com/wolfssl/wolfcrypt/test/AesGcmTest.java +++ b/src/test/java/com/wolfssl/wolfcrypt/test/AesGcmTest.java @@ -341,12 +341,11 @@ public void testAesGcm128() throws WolfCryptException { plain = dec.decrypt(cipher, iv3, tag, a3); assertArrayEquals(p3, plain); - /* bad encrypt arguments: null input */ + /* encrypt with null input should pass */ try { enc.encrypt(null, iv3, tag, a3); - fail("encrypt() with null input should fail"); } catch (WolfCryptException e) { - /* expected */ + fail("encrypt() with null input should pass"); } /* bad encrypt arguments: null iv */ @@ -365,12 +364,11 @@ public void testAesGcm128() throws WolfCryptException { /* expected */ } - /* bad decrypt arguments: null input */ + /* decrypt with null input but valid tag and AAD should pass */ try { enc.decrypt(null, iv3, tag, a3); - fail("decrypt() with null input should fail"); } catch (WolfCryptException e) { - /* expected */ + fail("decrypt() with null input should pass"); } /* bad decrypt arguments: null iv */ @@ -422,12 +420,11 @@ public void testAesGcm192() throws WolfCryptException { assertNotNull(plain); assertArrayEquals(p, plain); - /* bad encrypt arguments: null input */ + /* encrypt with null input should pass */ try { enc.encrypt(null, iv2, tag, a); - fail("encrypt() with null input should fail"); } catch (WolfCryptException e) { - /* expected */ + fail("encrypt() with null input should pass"); } /* bad encrypt arguments: null iv */ @@ -446,12 +443,11 @@ public void testAesGcm192() throws WolfCryptException { /* expected */ } - /* bad decrypt arguments: null input */ + /* decrypt with null input but valid tag and AAD should pass */ try { enc.decrypt(null, iv2, tag, a); - fail("decrypt() with null input should fail"); } catch (WolfCryptException e) { - /* expected */ + fail("decrypt() with null input should pass"); } /* bad decrypt arguments: null iv */ @@ -503,12 +499,11 @@ public void testAesGcm256() throws WolfCryptException { assertNotNull(plain); assertArrayEquals(p, plain); - /* bad encrypt arguments: null input */ + /* encrypt with null input should pass */ try { enc.encrypt(null, iv1, tag, a); - fail("encrypt() with null input should fail"); } catch (WolfCryptException e) { - /* expected */ + fail("encrypt() with null input should pass"); } /* bad encrypt arguments: null iv */ @@ -527,12 +522,11 @@ public void testAesGcm256() throws WolfCryptException { /* expected */ } - /* bad decrypt arguments: null input */ + /* decrypt with null input but valid tag and AAD should pass */ try { enc.decrypt(null, iv1, tag, a); - fail("decrypt() with null input should fail"); } catch (WolfCryptException e) { - /* expected */ + fail("decrypt() with null input should pass"); } /* bad decrypt arguments: null iv */ @@ -1042,5 +1036,111 @@ public void testThreadedAes256() throws InterruptedException { } } } + + /** + * Test AES-GCM with null plaintext using test vectors from + * OpenJDK TestKATForGCM.java that have null plaintext input. + * This tests scenarios where users may only provide AAD to + * generate an authentication tag. + */ + @Test + public void testAesGcmWithNullPlaintext() throws WolfCryptException { + + /* + * Test vector 1 from OpenJDK: AES-128, 96-bit IV, + * no plaintext, no AAD, 128-bit tag + */ + byte[] key1 = new byte[] { + (byte)0x11, (byte)0x75, (byte)0x4c, (byte)0xd7, + (byte)0x2a, (byte)0xec, (byte)0x30, (byte)0x9b, + (byte)0xf5, (byte)0x2f, (byte)0x76, (byte)0x87, + (byte)0x21, (byte)0x2e, (byte)0x89, (byte)0x57 + }; + byte[] iv1 = new byte[] { + (byte)0x3c, (byte)0x81, (byte)0x9d, (byte)0x9a, + (byte)0x9b, (byte)0xed, (byte)0x08, (byte)0x76, + (byte)0x15, (byte)0x03, (byte)0x0b, (byte)0x65 + }; + byte[] expectedTag1 = new byte[] { + (byte)0x25, (byte)0x03, (byte)0x27, (byte)0xc6, + (byte)0x74, (byte)0xaa, (byte)0xf4, (byte)0x77, + (byte)0xae, (byte)0xf2, (byte)0x67, (byte)0x57, + (byte)0x48, (byte)0xcf, (byte)0x69, (byte)0x71 + }; + + /* + * Test vector 6 from OpenJDK: AES-128, 96-bit IV, + * no plaintext, 16-byte AAD, 128-bit tag + */ + byte[] key2 = new byte[] { + (byte)0x77, (byte)0xbe, (byte)0x63, (byte)0x70, + (byte)0x89, (byte)0x71, (byte)0xc4, (byte)0xe2, + (byte)0x40, (byte)0xd1, (byte)0xcb, (byte)0x79, + (byte)0xe8, (byte)0xd7, (byte)0x7f, (byte)0xeb + }; + byte[] iv2 = new byte[] { + (byte)0xe0, (byte)0xe0, (byte)0x0f, (byte)0x19, + (byte)0xfe, (byte)0xd7, (byte)0xba, (byte)0x01, + (byte)0x36, (byte)0xa7, (byte)0x97, (byte)0xf3 + }; + byte[] aad2 = new byte[] { + (byte)0x7a, (byte)0x43, (byte)0xec, (byte)0x1d, + (byte)0x9c, (byte)0x0a, (byte)0x5a, (byte)0x78, + (byte)0xa0, (byte)0xb1, (byte)0x65, (byte)0x33, + (byte)0xa6, (byte)0x21, (byte)0x3c, (byte)0xab + }; + byte[] expectedTag2 = new byte[] { + (byte)0x20, (byte)0x9f, (byte)0xcc, (byte)0x8d, + (byte)0x36, (byte)0x75, (byte)0xed, (byte)0x93, + (byte)0x8e, (byte)0x9c, (byte)0x71, (byte)0x66, + (byte)0x70, (byte)0x9d, (byte)0xd9, (byte)0x46 + }; + + /* skip test if AES-128 is not compiled in native library */ + if (!FeatureDetect.Aes128Enabled()) { + return; + } + + /* Test case 1: null plaintext, no AAD */ + AesGcm enc = new AesGcm(); + enc.setKey(key1); + byte[] tag = new byte[expectedTag1.length]; + byte[] ciphertext = enc.encrypt(null, iv1, tag, null); + + /* Should return null/empty ciphertext since input was null */ + assertTrue("Ciphertext should be null or empty when input is null", + ciphertext == null || ciphertext.length == 0); + + /* Tag should match expected value */ + assertArrayEquals("Tag should match expected value for null " + + "plaintext, no AAD", expectedTag1, tag); + enc.releaseNativeStruct(); + + /* Test case 2: null plaintext, with AAD */ + enc = new AesGcm(); + enc.setKey(key2); + tag = new byte[expectedTag2.length]; + ciphertext = enc.encrypt(null, iv2, tag, aad2); + + /* Should return null/empty ciphertext since input was null */ + assertTrue("Ciphertext should be null or empty when input is null", + ciphertext == null || ciphertext.length == 0); + + /* Tag should match expected value */ + assertArrayEquals("Tag should match expected value for null " + + "plaintext with AAD", expectedTag2, tag); + + /* Test decryption with null ciphertext */ + AesGcm dec = new AesGcm(); + dec.setKey(key2); + byte[] plaintext = dec.decrypt(null, iv2, tag, aad2); + + /* Should return null/empty plaintext since ciphertext was null */ + assertTrue("Plaintext should be null or empty when ciphertext " + + "is null", plaintext == null || plaintext.length == 0); + + enc.releaseNativeStruct(); + dec.releaseNativeStruct(); + } }