Merge pull request #294 from cconlon/kwDefaultIvJava17

JCE: match SunJCE AES Key Wrap default IV and update() behavior
pull/296/head
Ruby Martin 2026-10-13 10:40:24 -05:00 committed by GitHub
commit 64d4d29015
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 132 additions and 42 deletions

View File

@ -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 <iv>`, 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 <iv>`
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`

View File

@ -207,6 +207,17 @@ public class WolfCryptCipher extends CipherSpi {
/* 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 @@ public class WolfCryptCipher extends CipherSpi {
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 @@ public class WolfCryptCipher extends CipherSpi {
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 @@ public class WolfCryptCipher extends CipherSpi {
}
/* 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];
}

View File

@ -1378,4 +1378,29 @@ public class WolfCryptUtil {
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;
}
}
}

View File

@ -70,6 +70,7 @@ import java.security.spec.AlgorithmParameterSpec;
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 @@ public class WolfCryptCipherAesWrapTest {
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 @@ public class WolfCryptCipherAesWrapTest {
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 class WolfCryptCipherAesWrapTest {
/* 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 class WolfCryptCipherAesWrapTest {
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 class WolfCryptCipherAesWrapTest {
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 class WolfCryptCipherAesWrapTest {
/* 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 class WolfCryptCipherAesWrapTest {
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 class WolfCryptCipherAesWrapTest {
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 class WolfCryptCipherAesWrapTest {
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);

View File

@ -1660,4 +1660,37 @@ public class WolfCryptUtilTest {
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);
}
}
}
}