diff --git a/README_JCE.md b/README_JCE.md index d01f2a87..e6fd56a9 100644 --- a/README_JCE.md +++ b/README_JCE.md @@ -559,8 +559,8 @@ the services below are not registered. Usage details: - All four opmodes are supported: `WRAP_MODE`/`UNWRAP_MODE` for `wrap()`/ - `unwrap()`, and `ENCRYPT_MODE`/`DECRYPT_MODE` for `doFinal()` over raw - data (`update()` buffers, all output comes from `doFinal()`). + `unwrap()`, and `ENCRYPT_MODE`/`DECRYPT_MODE` for `doFinal()` over raw data + (`update()` buffers and returns `null`, all output comes from `doFinal()`). - Input to wrap must be at least 16 bytes and a multiple of 8 bytes, and wrapped input to unwrap at least 24 bytes and a multiple of 8 (`IllegalBlockSizeException` otherwise). Wrapped output is 8 bytes longer @@ -569,10 +569,12 @@ Usage details: no parameters the default `A6A6A6A6A6A6A6A6` is used and wolfJCE never generates a random IV. An alternative value may be given as an 8-byte `IvParameterSpec` (or an `AlgorithmParameters("AES")` holding one) and - must then be supplied to the unwrapping side as well. `getIV()` and - `getParameters()` return `null` unless an IV was set explicitly, in which - case `getParameters()` is an `AlgorithmParameters("AES")` holding the - 8-byte IV, as with SunJCE 17+. That object can be passed to another + must then be supplied to the unwrapping side as well. Like SunJCE, on + Java 17+ `getIV()` returns the default IV after `init()`, and + `getParameters()` returns it even before `init()`. On older Java versions, + where SunJCE accepts no AES Key Wrap parameters, both return `null` unless + an IV was set. `getParameters()` is an `AlgorithmParameters("AES")` + holding the 8-byte IV. That object can be passed to another provider's `Cipher.init()` (which uses `getParameterSpec()`). Its DER encoding, `04 08 `, is the same one SunJCE produces, but SunJCE's own DER decoder only accepts 16-byte AES IVs, where wolfJCE decodes both. @@ -1124,15 +1126,6 @@ Castle. SunJCE (JDK 17+) throws `IllegalBlockSizeException` with the same message in this case. `UNWRAP_MODE` is unaffected: both providers throw `InvalidKeyException` for every `unwrap()` failure. -`getParameters()` on an AES Key Wrap Cipher returns `null` unless an IV was -set explicitly (JDK 8 and Bouncy Castle behavior), whereas SunJCE 17+ returns -`AlgorithmParameters` holding the default IV. This keeps -`otherCipher.init(mode, key, wolfCipher.getParameters())` working against -providers that accept no AES Key Wrap parameters at all. With an explicit IV -both providers return `AlgorithmParameters("AES")` with the same `04 08 ` -DER encoding; `AlgorithmParameters.getInstance("AES", "SunJCE")` rejects that -encoding (even its own), while wolfJCE accepts both 8 and 16 byte IVs. - #### Cipher `unwrap()` With a Null or Empty Algorithm Name `Cipher.unwrap()` on any wolfJCE Cipher throws `NoSuchAlgorithmException` diff --git a/src/main/java/com/wolfssl/provider/jce/WolfCryptCipher.java b/src/main/java/com/wolfssl/provider/jce/WolfCryptCipher.java index 923270f4..6d65850c 100644 --- a/src/main/java/com/wolfssl/provider/jce/WolfCryptCipher.java +++ b/src/main/java/com/wolfssl/provider/jce/WolfCryptCipher.java @@ -207,6 +207,17 @@ private static final class DefaultIvRandom { /* Size of generated AES-GCM IV, 96 bits */ private static final int GCM_INTERNAL_IV_SIZE = 12; + /* RFC 3394 default AES Key Wrap IV */ + private static final byte[] KW_DEFAULT_IV = { + (byte)0xA6, (byte)0xA6, (byte)0xA6, (byte)0xA6, + (byte)0xA6, (byte)0xA6, (byte)0xA6, (byte)0xA6 + }; + + /* Report default AES Key Wrap IV on Java 17+, matching JDK behavior. + * Older JDKs return null and reject all AES Key Wrap parameters. */ + private static final boolean KW_REPORT_DEFAULT_IV = + WolfCryptUtil.getJavaVersion() >= 17; + /* Set when the AES-GCM IV is generated natively, cleared whenever the * native structs are recreated */ private boolean gcmInternalIv = false; @@ -1072,16 +1083,20 @@ else if (this.cipherType == CipherType.WC_DES3) { break; case WC_KW: - /* Only return an explicitly-set IV. null is the RFC 3394 - * default. Always use wolfJCE AES parameters, JDK 8/11 - * SunJCE rejects 8-byte IVs. */ - if (this.iv != null) { + /* Set IV, or the default before init() on Java 17+. + * Always use wolfJCE AES parameters, JDK 8/11 AES + * parameters reject 8-byte IVs. */ + paramIv = this.iv; + if (paramIv == null && KW_REPORT_DEFAULT_IV) { + paramIv = KW_DEFAULT_IV; + } + if (paramIv != null) { Provider wolf = Security.getProvider("wolfJCE"); if (wolf == null) { wolf = FallbackProvider.INSTANCE; } params = AlgorithmParameters.getInstance("AES", wolf); - params.init(new IvParameterSpec(this.iv)); + params.init(new IvParameterSpec(paramIv)); } break; @@ -1192,7 +1207,7 @@ private void wolfCryptSetIV(AlgorithmParameterSpec spec, this.cipherMode == CipherMode.WC_KW) { if (spec == null) { - this.iv = null; + this.iv = KW_REPORT_DEFAULT_IV ? KW_DEFAULT_IV.clone() : null; } else if (spec instanceof IvParameterSpec) { byte[] kwIv = ((IvParameterSpec)spec).getIV(); @@ -1875,9 +1890,13 @@ private byte[] wolfCryptUpdate(byte[] input, int inputOffset, int len) } /* Some algos/modes keep data buffered until the doFinal() call, like - * RSA or AES-GCM/CCM without stream mode compiled natively. Just - * return an empty byte array in those cases here. */ + * RSA or AES-GCM/CCM without stream mode compiled natively. Return + * an empty byte array, or null for AES Key Wrap matching JDK + * behavior. */ if (isNoOpUpdate(bufferedLen)) { + if (this.cipherMode == CipherMode.WC_KW) { + return null; + } return new byte[0]; } diff --git a/src/main/java/com/wolfssl/provider/jce/WolfCryptUtil.java b/src/main/java/com/wolfssl/provider/jce/WolfCryptUtil.java index 521329af..a0cfd453 100644 --- a/src/main/java/com/wolfssl/provider/jce/WolfCryptUtil.java +++ b/src/main/java/com/wolfssl/provider/jce/WolfCryptUtil.java @@ -1378,4 +1378,29 @@ public static boolean constantTimeEquals(char[] a, char[] b) { return (diff == 0); } + + /** + * Get the Java major version from java.specification.version - "1.8" on + * Java 8, "17" on Java 17, "0.9" on Android. + * + * @return Java major version, or 0 on Android or if unknown + */ + public static int getJavaVersion() { + try { + String[] ver = System.getProperty( + "java.specification.version", "0").split("\\."); + + int major = Integer.parseInt(ver[0]); + if (major == 1 && ver.length > 1) { + major = Integer.parseInt(ver[1]); + } + + return major; + + } catch (RuntimeException e) { + /* SecurityException or malformed version, never throw since + * WolfCryptCipher calls this from its static initializer */ + return 0; + } + } } diff --git a/src/test/java/com/wolfssl/provider/jce/test/WolfCryptCipherAesWrapTest.java b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptCipherAesWrapTest.java index dce9fa1f..ec65f249 100644 --- a/src/test/java/com/wolfssl/provider/jce/test/WolfCryptCipherAesWrapTest.java +++ b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptCipherAesWrapTest.java @@ -70,6 +70,7 @@ import java.security.spec.ECGenParameterSpec; import com.wolfssl.provider.jce.WolfCryptProvider; +import com.wolfssl.provider.jce.WolfCryptUtil; import com.wolfssl.wolfcrypt.AesKeyWrap; import com.wolfssl.wolfcrypt.FeatureDetect; import com.wolfssl.wolfcrypt.test.TimedTestWatcher; @@ -144,6 +145,10 @@ public class WolfCryptCipherAesWrapTest { /* Alternative 8-byte IV */ private static final byte[] ALT_IV = Util.h2b("0011223344556677"); + /* IV reported when none set: the default on Java 17+, null on older Java */ + private static final byte[] REPORTED_DEFAULT_IV = + (WolfCryptUtil.getJavaVersion() >= 17) ? DEFAULT_IV : null; + @Rule(order = Integer.MIN_VALUE) public TestRule testWatcher = TimedTestWatcher.create(); @@ -237,6 +242,16 @@ private static byte[] randomBytes(int len) { return b; } + /* IV held by AlgorithmParameters, or null */ + private static byte[] paramsIv(AlgorithmParameters params) + throws Exception { + + if (params == null) { + return null; + } + return params.getParameterSpec(IvParameterSpec.class).getIV(); + } + /** * Names SunJCE (JDK 8 and 17+), Bouncy Castle, and NIST OID callers * expect to resolve, plus transformation forms that exercise @@ -354,17 +369,14 @@ private void runKat(byte[] kek, byte[] data, byte[] expected) assertArrayEquals("doFinal() decrypt does not match vector", data, dec.doFinal(expected)); - /* ENCRYPT_MODE / DECRYPT_MODE, update() then doFinal() */ - byte[] part = enc.update(data, 0, 8); - assertTrue("update() must buffer, not emit output", - part == null || part.length == 0); - part = enc.update(data, 8, data.length - 8); - assertTrue(part == null || part.length == 0); + /* ENCRYPT_MODE / DECRYPT_MODE, update() then doFinal(). update() + * buffers and returns null. */ + assertNull(enc.update(data, 0, 8)); + assertNull(enc.update(data, 8, data.length - 8)); assertArrayEquals("update()+doFinal() encrypt does not match", expected, enc.doFinal()); - part = dec.update(expected, 0, 8); - assertTrue(part == null || part.length == 0); + assertNull(dec.update(expected, 0, 8)); assertArrayEquals("update()+doFinal() decrypt does not match", data, dec.doFinal(expected, 8, expected.length - 8)); } @@ -777,8 +789,7 @@ public void testEncryptDecryptRoundTripVariousLengths() /* chunked update() */ int half = (sz / 2) & ~7; - byte[] u = enc.update(data, 0, half); - assertTrue(u == null || u.length == 0); + assertNull(enc.update(data, 0, half)); assertArrayEquals(ct, enc.doFinal(data, half, sz - half)); } } @@ -911,11 +922,12 @@ public void testDefaultIvIsRfc3394Default() throws Exception { assumeAes128(); Cipher c = Cipher.getInstance("AESWrap", jceProvider); + assertNull("getIV() must be null before init()", c.getIV()); + assertArrayEquals(REPORTED_DEFAULT_IV, paramsIv(c.getParameters())); c.init(Cipher.WRAP_MODE, aesKey(KEK_128)); - assertNull("getIV() must be null when no IV was set", c.getIV()); - assertNull("getParameters() must be null when no IV was set", - c.getParameters()); + assertArrayEquals(REPORTED_DEFAULT_IV, c.getIV()); + assertArrayEquals(REPORTED_DEFAULT_IV, paramsIv(c.getParameters())); byte[] a = c.wrap(aesKey(DATA_128)); c.init(Cipher.WRAP_MODE, aesKey(KEK_128), @@ -925,7 +937,7 @@ public void testDefaultIvIsRfc3394Default() throws Exception { c.init(Cipher.WRAP_MODE, aesKey(KEK_128), (AlgorithmParameterSpec)null); - assertNull(c.getIV()); + assertArrayEquals(REPORTED_DEFAULT_IV, c.getIV()); byte[] d = c.wrap(aesKey(DATA_128)); assertArrayEquals(WRAP_128_KEK128, a); @@ -934,11 +946,11 @@ public void testDefaultIvIsRfc3394Default() throws Exception { /* A SecureRandom must never be used to invent an IV */ c.init(Cipher.WRAP_MODE, aesKey(KEK_128), secureRandom); - assertNull(c.getIV()); + assertArrayEquals(REPORTED_DEFAULT_IV, c.getIV()); assertArrayEquals(WRAP_128_KEK128, c.wrap(aesKey(DATA_128))); c.init(Cipher.ENCRYPT_MODE, aesKey(KEK_128), secureRandom); - assertNull(c.getIV()); + assertArrayEquals(REPORTED_DEFAULT_IV, c.getIV()); assertArrayEquals(WRAP_128_KEK128, c.doFinal(DATA_128)); } @@ -1738,9 +1750,12 @@ public void testInteropWrapUnwrap() throws Exception { wolfWrap.init(Cipher.WRAP_MODE, kek); byte[] wrapped = wolfWrap.wrap(key); + /* Our getParameters() must init the interop provider on + * any Java version */ Cipher otherUnwrap = Cipher.getInstance("AESWrap", interopProvider); - otherUnwrap.init(Cipher.UNWRAP_MODE, kek); + otherUnwrap.init(Cipher.UNWRAP_MODE, kek, + wolfWrap.getParameters()); Key out = otherUnwrap.unwrap(wrapped, "AES", Cipher.SECRET_KEY); assertArrayEquals("wolfJCE wrap -> " + interopProvider + @@ -1751,6 +1766,10 @@ public void testInteropWrapUnwrap() throws Exception { Cipher otherWrap = Cipher.getInstance("AESWrap", interopProvider); otherWrap.init(Cipher.WRAP_MODE, kek); + if (interopProvider.equals("SunJCE")) { + assertArrayEquals("getIV() must match SunJCE", + otherWrap.getIV(), wolfWrap.getIV()); + } byte[] wrapped2 = otherWrap.wrap(key); assertArrayEquals("both providers must produce identical " + "wrapped output", wrapped, wrapped2); @@ -2123,9 +2142,10 @@ public void testGetParametersWithoutRegisteredProvider() assertArrayEquals(DATA_128, u.unwrap(wrapped, "AES", Cipher.SECRET_KEY).getEncoded()); - /* no IV set, no parameters regardless of provider lookup */ + /* no IV set, default IV reported per Java version */ c.init(Cipher.WRAP_MODE, aesKey(KEK_128)); - assertNull(c.getParameters()); + assertArrayEquals(REPORTED_DEFAULT_IV, + paramsIv(c.getParameters())); } finally { Security.insertProviderAt(saved, savedPos); diff --git a/src/test/java/com/wolfssl/provider/jce/test/WolfCryptUtilTest.java b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptUtilTest.java index c2ffd233..6700207d 100644 --- a/src/test/java/com/wolfssl/provider/jce/test/WolfCryptUtilTest.java +++ b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptUtilTest.java @@ -1660,4 +1660,37 @@ public void testConstantTimeEquals() { assertFalse(WolfCryptUtil.constantTimeEquals(a, "passwordpasswordX".toCharArray())); } + + @Test + public void testGetJavaVersion() { + final String prop = "java.specification.version"; + String[] specs = { "1.8", "11", "17", "0.9", "", "abc", "." }; + int[] expected = { 8, 11, 17, 0, 0, 0, 0 }; + + int ver = WolfCryptUtil.getJavaVersion(); + + /* 0 on Android, otherwise Java 8+ */ + assertTrue("unexpected Java version " + ver, ver == 0 || ver >= 8); + + /* Mutates a global System property, restored in finally */ + String orig = System.getProperty(prop); + try { + for (int i = 0; i < specs.length; i++) { + System.setProperty(prop, specs[i]); + /* Android ignores changes to this property */ + Assume.assumeTrue(prop + " is read-only", + specs[i].equals(System.getProperty(prop))); + assertEquals("\"" + specs[i] + "\"", expected[i], + WolfCryptUtil.getJavaVersion()); + } + + System.clearProperty(prop); + assertEquals(0, WolfCryptUtil.getJavaVersion()); + + } finally { + if (orig != null) { + System.setProperty(prop, orig); + } + } + } }