Skip to content

Commit 058cc86

Browse files
committed
CryptoProWrapEngine now diversifies a copy of the KEK rather than the caller's KeyParameter array, and with no S-box uses the GOST 28147 default S-box instead of failing with a NullPointerException.
1 parent 07ce709 commit 058cc86

6 files changed

Lines changed: 111 additions & 7 deletions

File tree

‎core/src/main/java/org/bouncycastle/crypto/engines/CryptoProWrapEngine.java‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,20 @@
77
import org.bouncycastle.crypto.params.ParametersWithRandom;
88
import org.bouncycastle.crypto.params.ParametersWithSBox;
99
import org.bouncycastle.crypto.params.ParametersWithUKM;
10+
import org.bouncycastle.util.Arrays;
1011
import org.bouncycastle.util.Pack;
1112

1213
public class CryptoProWrapEngine
1314
extends GOST28147WrapEngine
1415
{
1516
public void init(boolean forWrapping, CipherParameters param)
1617
{
18+
/*
19+
* The random is stripped on both sides of the UKM on purpose: UKM(Random(..)) is the documented
20+
* order, and Random(UKM(..)) is the earlier one, kept for existing callers. Not a pattern to copy -
21+
* see the org.bouncycastle.crypto.params package documentation. This repeats GOST28147WrapEngine,
22+
* whose init is handed a rebuilt UKM(..) with no random, so the unwrapping has to happen here too.
23+
*/
1724
if (param instanceof ParametersWithRandom)
1825
{
1926
ParametersWithRandom pr = (ParametersWithRandom)param;
@@ -41,7 +48,8 @@ public void init(boolean forWrapping, CipherParameters param)
4148
kParam = (KeyParameter)inner;
4249
}
4350

44-
kParam = new KeyParameter(cryptoProDiversify(kParam.getKey(), pU.getUKM(), sBox));
51+
// diversify a copy - getKey() is the caller's own array
52+
kParam = new KeyParameter(cryptoProDiversify(Arrays.clone(kParam.getKey()), pU.getUKM(), sBox));
4553

4654
if (sBox != null)
4755
{
@@ -99,7 +107,14 @@ private static byte[] cryptoProDiversify(byte[] K, byte[] ukm, byte[] sBox)
99107

100108
GCFBBlockCipher c = new GCFBBlockCipher(new GOST28147Engine());
101109

102-
c.init(true, new ParametersWithIV(new ParametersWithSBox(new KeyParameter(K), sBox), s));
110+
// with no S-box, the engine's default - the one the wrap itself then uses
111+
CipherParameters kParam = new KeyParameter(K);
112+
if (sBox != null)
113+
{
114+
kParam = new ParametersWithSBox(kParam, sBox);
115+
}
116+
117+
c.init(true, new ParametersWithIV(kParam, s));
103118

104119
c.processBlock(K, 0, K, 0);
105120
c.processBlock(K, 8, K, 8);

‎core/src/main/java/org/bouncycastle/crypto/engines/GOST28147WrapEngine.java‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ public class GOST28147WrapEngine
1717

1818
public void init(boolean forWrapping, CipherParameters param)
1919
{
20+
/*
21+
* The random is stripped on both sides of the UKM on purpose: UKM(Random(..)) is the documented
22+
* order, and Random(UKM(..)) is the earlier one, kept for existing callers. Not a pattern to copy -
23+
* see the org.bouncycastle.crypto.params package documentation. CryptoProWrapEngine does the same.
24+
*/
2025
if (param instanceof ParametersWithRandom)
2126
{
2227
ParametersWithRandom pr = (ParametersWithRandom)param;
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package org.bouncycastle.crypto.test;
2+
3+
import org.bouncycastle.crypto.CipherParameters;
4+
import org.bouncycastle.crypto.engines.CryptoProWrapEngine;
5+
import org.bouncycastle.crypto.engines.GOST28147Engine;
6+
import org.bouncycastle.crypto.params.KeyParameter;
7+
import org.bouncycastle.crypto.params.ParametersWithSBox;
8+
import org.bouncycastle.crypto.params.ParametersWithUKM;
9+
import org.bouncycastle.util.encoders.Hex;
10+
import org.bouncycastle.util.test.SimpleTest;
11+
12+
/**
13+
* {@link CryptoProWrapEngine} (RFC 4357 sec. 6.3, with the sec. 6.5 KEK diversification) given no
14+
* S-box uses the GOST 28147 engine's default one throughout, and never modifies the caller's key.
15+
*/
16+
public class CryptoProWrapEngineTest
17+
extends SimpleTest
18+
{
19+
private static final byte[] KEK = Hex.decode("8b2f3a41c6d5e7091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f7081");
20+
private static final byte[] CEK = Hex.decode("0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20");
21+
private static final byte[] UKM = Hex.decode("1d80603c8544c727");
22+
23+
public String getName()
24+
{
25+
return "CryptoProWrapEngine";
26+
}
27+
28+
public void performTest()
29+
throws Exception
30+
{
31+
defaultSBoxTest();
32+
33+
keyUnmodifiedTest(null);
34+
keyUnmodifiedTest(GOST28147Engine.getSBox("E-A"));
35+
}
36+
37+
private void defaultSBoxTest()
38+
throws Exception
39+
{
40+
CryptoProWrapEngine engine = new CryptoProWrapEngine();
41+
42+
engine.init(true, new ParametersWithUKM(
43+
new ParametersWithSBox(new KeyParameter(KEK), GOST28147Engine.getSBox("Default")), UKM));
44+
byte[] expected = engine.wrap(CEK, 0, CEK.length);
45+
46+
engine.init(true, new ParametersWithUKM(new KeyParameter(KEK), UKM));
47+
isTrue("no S-box wrap differs from the default S-box", areEqual(expected, engine.wrap(CEK, 0, CEK.length)));
48+
49+
engine.init(false, new ParametersWithUKM(new KeyParameter(KEK), UKM));
50+
isTrue("no S-box unwrap failed", areEqual(CEK, engine.unwrap(expected, 0, expected.length)));
51+
}
52+
53+
private void keyUnmodifiedTest(byte[] sBox)
54+
throws Exception
55+
{
56+
String label = sBox == null ? "no S-box" : "S-box";
57+
58+
KeyParameter key = new KeyParameter(KEK);
59+
CipherParameters kParam = key;
60+
if (sBox != null)
61+
{
62+
kParam = new ParametersWithSBox(kParam, sBox);
63+
}
64+
ParametersWithUKM params = new ParametersWithUKM(kParam, UKM);
65+
66+
CryptoProWrapEngine engine = new CryptoProWrapEngine();
67+
68+
engine.init(true, params);
69+
byte[] wrapped = engine.wrap(CEK, 0, CEK.length);
70+
isTrue(label + ": caller's key modified by init", areEqual(KEK, key.getKey()));
71+
72+
engine.init(true, params);
73+
isTrue(label + ": wrap differs on re-init with the same parameters",
74+
areEqual(wrapped, engine.wrap(CEK, 0, CEK.length)));
75+
76+
engine.init(false, params);
77+
isTrue(label + ": unwrap with the same parameters failed", areEqual(CEK, engine.unwrap(wrapped, 0, wrapped.length)));
78+
isTrue(label + ": caller's key modified by re-init", areEqual(KEK, key.getKey()));
79+
}
80+
81+
public static void main(String[] args)
82+
{
83+
runTest(new CryptoProWrapEngineTest());
84+
}
85+
}

‎core/src/test/java/org/bouncycastle/crypto/test/RegressionTest.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ public class RegressionTest
159159
new SM2SignerTest(),
160160
new ShortWrapCipherTextTest(),
161161
new UKMParametersNestingTest(),
162+
new CryptoProWrapEngineTest(),
162163
new SM4Test(),
163164
new SM9KEMTest(),
164165
new SM9EngineTest(),

‎core/src/test/java/org/bouncycastle/crypto/test/UKMParametersNestingTest.java‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import org.bouncycastle.crypto.params.ParametersWithRandom;
1919
import org.bouncycastle.crypto.params.ParametersWithSBox;
2020
import org.bouncycastle.crypto.params.ParametersWithUKM;
21-
import org.bouncycastle.util.Arrays;
2221
import org.bouncycastle.util.encoders.Hex;
2322
import org.bouncycastle.util.test.SimpleTest;
2423

@@ -47,6 +46,7 @@ public void performTest()
4746
{
4847
wrapTest("GOST28147Wrap", new GOST28147WrapEngine(), null);
4948
wrapTest("GOST28147Wrap/SBox", new GOST28147WrapEngine(), GOST28147Engine.getSBox("E-A"));
49+
wrapTest("CryptoProWrap", new CryptoProWrapEngine(), null);
5050
wrapTest("CryptoProWrap/SBox", new CryptoProWrapEngine(), GOST28147Engine.getSBox("E-A"));
5151

5252
vkoTest();
@@ -73,12 +73,9 @@ private void wrapTest(String label, Wrapper wrapper, byte[] sBox)
7373
}
7474
}
7575

76-
/*
77-
* A fresh key each time: CryptoProWrapEngine diversifies the KeyParameter's bytes in place.
78-
*/
7976
private static CipherParameters createWrapParameters(int nesting, byte[] sBox)
8077
{
81-
CipherParameters key = new KeyParameter(Arrays.clone(KEK));
78+
CipherParameters key = new KeyParameter(KEK);
8279
if (sBox != null)
8380
{
8481
key = new ParametersWithSBox(key, sBox);

‎docs/releasenotes.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ Date: 2026, TBD
6262
- JceKeyAgreeRecipientInfoGenerator created its 1-pass ECMQV ephemeral key pair once per generator rather than once per message, so a generator used for more than one message sent the same ephemeral key in each and derived the same key-encryption key for every recipient each time, withdrawing the ephemeral contribution RFC 5753 sec. 3.2 relies on. The messages were well formed and decrypted correctly. A fresh key pair is now generated for each KeyAgreeRecipientInfo, still shared by all of its recipients.
6363
- The ML-KEM KeyGenerator (KEMGenerateSpec/KEMExtractSpec), Cipher (wrap/unwrap), javax.crypto.KEM and KeyFactory.translateKey services accepted only BC's own ML-KEM key objects, and the KeyGenerator failed with a ClassCastException at generateKey() rather than at init, so an ML-KEM key from another provider could not be used with BC even though its standard encoding was one BC reads. This broke BCJSSE handshakes over the ML-KEM and hybrid groups whenever another provider ahead of BC decoded the peer's key or generated the ephemeral key pair. A foreign key is now converted from its X.509 or PKCS#8 encoding, with the usual parameter-set checks, and an unusable one is rejected at init (github #2466).
6464
- The raw JCA provider bounded the PBKDF2 iteration count taken from an encoding (org.bouncycastle.pbe.max_iteration_count, default 10,000,000) but not the counts of the legacy PBES1 (PKCS#5 scheme 1) and PKCS#12 PBE families beside it. Their AlgorithmParameters (PKCS12PBE and its OID aliases, PBKDF1) accepted any count, narrowing one beyond the int range with intValue() so that 2^32 arrived as 0, and every Cipher, Mac and SecretKeyFactory derivation ran with whatever count it was given - including a count decoded by another provider's AlgorithmParameters, as when javax.crypto.EncryptedPrivateKeyInfo.getKeySpec() decrypts a PKCS#12 PBE-protected key with BC. As these schemes carry the count in unauthenticated parameters and derive before anything can be checked, a supplied blob could hold a derivation for tens of minutes. The parameter parse now rejects a negative, beyond-int or over-limit count, and the derivations reject a negative or over-limit count, under the same property as PBKDF2. The PKCS#12 key store derives through the same code, so a org.bouncycastle.pkcs12.max_it_count raised above 10,000,000 now needs org.bouncycastle.pbe.max_iteration_count raised with it.
65+
- The light-weight CryptoProWrapEngine (RFC 4357 sec. 6.3) diversified the key encryption key in the caller's own array, so after init the KeyParameter it was given held the diversified key, and initialising again with the same parameters - to unwrap what had just been wrapped, say - diversified it a second time and used a different key. It also failed with a NullPointerException when given no S-box, although init has a branch for that case. It now diversifies a copy, and given no S-box uses the GOST 28147 engine's default S-box for the diversification, the one the wrap itself then uses. The provider's GOST 28147 key wrap ciphers were unaffected, as they always supply an S-box and build a new KeyParameter on every init.
6566

6667
### 2.1.3 Additional Features and Functionality
6768

0 commit comments

Comments
 (0)