Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 8 additions & 15 deletions README_JCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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`
Expand Down
35 changes: 27 additions & 8 deletions src/main/java/com/wolfssl/provider/jce/WolfCryptCipher.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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];
}

Expand Down
25 changes: 25 additions & 0 deletions src/main/java/com/wolfssl/provider/jce/WolfCryptUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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));
}
}
Expand Down Expand Up @@ -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),
Expand All @@ -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);
Expand All @@ -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));
}

Expand Down Expand Up @@ -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 +
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
33 changes: 33 additions & 0 deletions src/test/java/com/wolfssl/provider/jce/test/WolfCryptUtilTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
}
Loading