Skip to content

Commit 92a868c

Browse files
committed
Add DTLS 1.3 record number encryption mask primitive for AES and ChaCha20 (RFC 9147 4.2.3), relates to github #1468.
1 parent 5e8aadb commit 92a868c

7 files changed

Lines changed: 348 additions & 0 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package org.bouncycastle.tls.crypto.impl;
2+
3+
import java.io.IOException;
4+
5+
/**
6+
* Record number encryption mask generator for DTLS 1.3 (RFC 9147 4.2.3).
7+
* <p>
8+
* The mask is derived from the first 16 bytes of the protected record using the block function underlying the
9+
* negotiated AEAD (AES-ECB for AES-based AEADs, the ChaCha20 block function for ChaCha20-Poly1305), and is XORed
10+
* with the sequence number bytes of the unified header.
11+
* </p>
12+
*/
13+
public interface TlsRecordNumberMask
14+
{
15+
/**
16+
* Set the sequence number key ("sn" traffic key) for this direction.
17+
*/
18+
void setKey(byte[] key, int keyOff, int keyLen) throws IOException;
19+
20+
/**
21+
* Generate the 16-byte mask for a record whose ciphertext starts at 'ciphertextOff'. At least 16 bytes of
22+
* ciphertext must be available. The mask is written to mask[maskOff..maskOff + 16).
23+
*/
24+
void generateMask(byte[] ciphertext, int ciphertextOff, byte[] mask, int maskOff) throws IOException;
25+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package org.bouncycastle.tls.crypto.impl.bc;
2+
3+
import java.io.IOException;
4+
5+
import org.bouncycastle.crypto.BlockCipher;
6+
import org.bouncycastle.crypto.params.KeyParameter;
7+
import org.bouncycastle.tls.AlertDescription;
8+
import org.bouncycastle.tls.TlsFatalAlert;
9+
import org.bouncycastle.tls.crypto.impl.TlsRecordNumberMask;
10+
11+
/**
12+
* RFC 9147 4.2.3: Mask = AES-ECB(sn_key, Ciphertext[0..15]).
13+
*/
14+
public class BcTlsAESRecordNumberMask
15+
implements TlsRecordNumberMask
16+
{
17+
private final BlockCipher cipher;
18+
19+
public BcTlsAESRecordNumberMask(BlockCipher cipher)
20+
{
21+
this.cipher = cipher;
22+
}
23+
24+
public void setKey(byte[] key, int keyOff, int keyLen) throws IOException
25+
{
26+
try
27+
{
28+
cipher.init(true, new KeyParameter(key, keyOff, keyLen));
29+
}
30+
catch (RuntimeException e)
31+
{
32+
throw new TlsFatalAlert(AlertDescription.internal_error, e);
33+
}
34+
}
35+
36+
public void generateMask(byte[] ciphertext, int ciphertextOff, byte[] mask, int maskOff) throws IOException
37+
{
38+
try
39+
{
40+
cipher.processBlock(ciphertext, ciphertextOff, mask, maskOff);
41+
}
42+
catch (RuntimeException e)
43+
{
44+
throw new TlsFatalAlert(AlertDescription.internal_error, e);
45+
}
46+
}
47+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package org.bouncycastle.tls.crypto.impl.bc;
2+
3+
import java.io.IOException;
4+
5+
import org.bouncycastle.crypto.engines.ChaCha7539Engine;
6+
import org.bouncycastle.crypto.params.KeyParameter;
7+
import org.bouncycastle.crypto.params.ParametersWithIV;
8+
import org.bouncycastle.tls.AlertDescription;
9+
import org.bouncycastle.tls.TlsFatalAlert;
10+
import org.bouncycastle.tls.crypto.impl.TlsRecordNumberMask;
11+
import org.bouncycastle.util.Pack;
12+
13+
/**
14+
* RFC 9147 4.2.3: Mask = ChaCha20(sn_key, Ciphertext[0..3], Ciphertext[4..15]), i.e. the ChaCha20 block
15+
* selected by the 32-bit little-endian counter in the first 4 ciphertext bytes and the 96-bit nonce in the
16+
* following 12 bytes.
17+
*/
18+
public class BcTlsChaCha20RecordNumberMask
19+
implements TlsRecordNumberMask
20+
{
21+
private static final byte[] ZEROES = new byte[16];
22+
23+
private final ChaCha7539Engine cipher = new ChaCha7539Engine();
24+
25+
private KeyParameter key;
26+
27+
public void setKey(byte[] key, int keyOff, int keyLen) throws IOException
28+
{
29+
this.key = new KeyParameter(key, keyOff, keyLen);
30+
}
31+
32+
public void generateMask(byte[] ciphertext, int ciphertextOff, byte[] mask, int maskOff) throws IOException
33+
{
34+
if (null == key)
35+
{
36+
throw new TlsFatalAlert(AlertDescription.internal_error);
37+
}
38+
39+
try
40+
{
41+
long counter = Pack.littleEndianToInt(ciphertext, ciphertextOff) & 0xFFFFFFFFL;
42+
43+
byte[] nonce = new byte[12];
44+
System.arraycopy(ciphertext, ciphertextOff + 4, nonce, 0, 12);
45+
46+
cipher.init(true, new ParametersWithIV(key, nonce));
47+
cipher.skip(counter * 64L);
48+
cipher.processBytes(ZEROES, 0, 16, mask, maskOff);
49+
}
50+
catch (RuntimeException e)
51+
{
52+
throw new TlsFatalAlert(AlertDescription.internal_error, e);
53+
}
54+
}
55+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package org.bouncycastle.tls.crypto.impl.jcajce;
2+
3+
import java.io.IOException;
4+
import java.security.GeneralSecurityException;
5+
6+
import javax.crypto.Cipher;
7+
import javax.crypto.spec.SecretKeySpec;
8+
9+
import org.bouncycastle.jcajce.util.JcaJceHelper;
10+
import org.bouncycastle.tls.AlertDescription;
11+
import org.bouncycastle.tls.TlsFatalAlert;
12+
import org.bouncycastle.tls.crypto.impl.TlsRecordNumberMask;
13+
14+
/**
15+
* RFC 9147 4.2.3: Mask = AES-ECB(sn_key, Ciphertext[0..15]), via a JCA "AES/ECB/NoPadding" cipher.
16+
*/
17+
public class JceAESRecordNumberMask
18+
implements TlsRecordNumberMask
19+
{
20+
private final Cipher cipher;
21+
22+
public JceAESRecordNumberMask(JcaJceHelper helper) throws GeneralSecurityException
23+
{
24+
this.cipher = helper.createCipher("AES/ECB/NoPadding");
25+
}
26+
27+
public void setKey(byte[] key, int keyOff, int keyLen) throws IOException
28+
{
29+
try
30+
{
31+
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, keyOff, keyLen, "AES"));
32+
}
33+
catch (GeneralSecurityException e)
34+
{
35+
throw new TlsFatalAlert(AlertDescription.internal_error, e);
36+
}
37+
}
38+
39+
public void generateMask(byte[] ciphertext, int ciphertextOff, byte[] mask, int maskOff) throws IOException
40+
{
41+
try
42+
{
43+
int len = cipher.doFinal(ciphertext, ciphertextOff, 16, mask, maskOff);
44+
if (16 != len)
45+
{
46+
throw new TlsFatalAlert(AlertDescription.internal_error);
47+
}
48+
}
49+
catch (GeneralSecurityException e)
50+
{
51+
throw new TlsFatalAlert(AlertDescription.internal_error, e);
52+
}
53+
}
54+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package org.bouncycastle.tls.crypto.impl.jcajce;
2+
3+
import java.io.IOException;
4+
5+
import org.bouncycastle.crypto.engines.ChaCha7539Engine;
6+
import org.bouncycastle.crypto.params.KeyParameter;
7+
import org.bouncycastle.crypto.params.ParametersWithIV;
8+
import org.bouncycastle.tls.AlertDescription;
9+
import org.bouncycastle.tls.TlsFatalAlert;
10+
import org.bouncycastle.tls.crypto.impl.TlsRecordNumberMask;
11+
import org.bouncycastle.util.Pack;
12+
13+
/**
14+
* RFC 9147 4.2.3: Mask = ChaCha20(sn_key, Ciphertext[0..3], Ciphertext[4..15]).
15+
* <p>
16+
* The JCA has no ChaCha20 cipher able to position the block counter on the Java versions this module targets
17+
* (the base sources compile for Java 8 and below; {@code ChaCha20ParameterSpec}, which carries a counter, only
18+
* arrived in Java 11), so the lightweight engine is used here, as {@link JcaNonceGenerator} does for its DRBG.
19+
* </p>
20+
*/
21+
public class JceChaCha20RecordNumberMask
22+
implements TlsRecordNumberMask
23+
{
24+
private static final byte[] ZEROES = new byte[16];
25+
26+
private final ChaCha7539Engine cipher = new ChaCha7539Engine();
27+
28+
private KeyParameter key;
29+
30+
public void setKey(byte[] key, int keyOff, int keyLen) throws IOException
31+
{
32+
this.key = new KeyParameter(key, keyOff, keyLen);
33+
}
34+
35+
public void generateMask(byte[] ciphertext, int ciphertextOff, byte[] mask, int maskOff) throws IOException
36+
{
37+
if (null == key)
38+
{
39+
throw new TlsFatalAlert(AlertDescription.internal_error);
40+
}
41+
42+
try
43+
{
44+
long counter = Pack.littleEndianToInt(ciphertext, ciphertextOff) & 0xFFFFFFFFL;
45+
46+
byte[] nonce = new byte[12];
47+
System.arraycopy(ciphertext, ciphertextOff + 4, nonce, 0, 12);
48+
49+
cipher.init(true, new ParametersWithIV(key, nonce));
50+
cipher.skip(counter * 64L);
51+
cipher.processBytes(ZEROES, 0, 16, mask, maskOff);
52+
}
53+
catch (RuntimeException e)
54+
{
55+
throw new TlsFatalAlert(AlertDescription.internal_error, e);
56+
}
57+
}
58+
}

‎tls/src/test/java/org/bouncycastle/tls/AllTests.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public static Test suite()
2424
suite.addTestSuite(Add13CertificateStatusTest.class);
2525
suite.addTestSuite(CheckTlsFeaturesExtensionTest.class);
2626
suite.addTestSuite(DTLSReassemblerTest.class);
27+
suite.addTestSuite(DTLSRecordNumberMaskTest.class);
2728
suite.addTestSuite(SpreadCertificateStatusTest.class);
2829

2930
return new BCTestSetup(suite);
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package org.bouncycastle.tls;
2+
3+
import java.security.SecureRandom;
4+
5+
import org.bouncycastle.crypto.engines.AESEngine;
6+
import org.bouncycastle.crypto.engines.ChaCha7539Engine;
7+
import org.bouncycastle.crypto.params.KeyParameter;
8+
import org.bouncycastle.crypto.params.ParametersWithIV;
9+
import org.bouncycastle.jcajce.util.DefaultJcaJceHelper;
10+
import org.bouncycastle.tls.crypto.impl.TlsRecordNumberMask;
11+
import org.bouncycastle.tls.crypto.impl.bc.BcTlsAESRecordNumberMask;
12+
import org.bouncycastle.tls.crypto.impl.bc.BcTlsChaCha20RecordNumberMask;
13+
import org.bouncycastle.tls.crypto.impl.jcajce.JceAESRecordNumberMask;
14+
import org.bouncycastle.tls.crypto.impl.jcajce.JceChaCha20RecordNumberMask;
15+
import org.bouncycastle.util.Arrays;
16+
17+
import junit.framework.TestCase;
18+
19+
/**
20+
* RFC 9147 4.2.3 record number encryption masks: AES-ECB over the first 16 ciphertext bytes, or the ChaCha20
21+
* block selected by the first 4 ciphertext bytes (counter) and the next 12 (nonce). Both backends must agree
22+
* with an independent lightweight reference.
23+
*/
24+
public class DTLSRecordNumberMaskTest
25+
extends TestCase
26+
{
27+
private static final SecureRandom RANDOM = new SecureRandom();
28+
29+
public void testAESMaskMatchesECBReference() throws Exception
30+
{
31+
byte[] key = new byte[16];
32+
byte[] ciphertext = new byte[40];
33+
RANDOM.nextBytes(key);
34+
RANDOM.nextBytes(ciphertext);
35+
36+
AESEngine reference = new AESEngine();
37+
reference.init(true, new KeyParameter(key));
38+
byte[] expected = new byte[16];
39+
reference.processBlock(ciphertext, 8, expected, 0);
40+
41+
TlsRecordNumberMask bc = new BcTlsAESRecordNumberMask(new AESEngine());
42+
bc.setKey(key, 0, key.length);
43+
byte[] actualBc = new byte[16];
44+
bc.generateMask(ciphertext, 8, actualBc, 0);
45+
assertTrue(Arrays.areEqual(expected, actualBc));
46+
47+
TlsRecordNumberMask jce = new JceAESRecordNumberMask(new DefaultJcaJceHelper());
48+
jce.setKey(key, 0, key.length);
49+
byte[] actualJce = new byte[16];
50+
jce.generateMask(ciphertext, 8, actualJce, 0);
51+
assertTrue(Arrays.areEqual(expected, actualJce));
52+
}
53+
54+
public void testChaCha20MaskMatchesKeystreamReference() throws Exception
55+
{
56+
byte[] key = new byte[32];
57+
byte[] ciphertext = new byte[32];
58+
RANDOM.nextBytes(key);
59+
RANDOM.nextBytes(ciphertext);
60+
61+
// counter = 3 (little-endian), so the mask is keystream bytes [192, 208)
62+
ciphertext[0] = 3;
63+
ciphertext[1] = 0;
64+
ciphertext[2] = 0;
65+
ciphertext[3] = 0;
66+
67+
byte[] nonce = Arrays.copyOfRange(ciphertext, 4, 16);
68+
ChaCha7539Engine reference = new ChaCha7539Engine();
69+
reference.init(true, new ParametersWithIV(new KeyParameter(key), nonce));
70+
byte[] stream = new byte[3 * 64 + 16];
71+
reference.processBytes(stream, 0, stream.length, stream, 0);
72+
byte[] expected = Arrays.copyOfRange(stream, 192, 208);
73+
74+
TlsRecordNumberMask bc = new BcTlsChaCha20RecordNumberMask();
75+
bc.setKey(key, 0, key.length);
76+
byte[] actualBc = new byte[16];
77+
bc.generateMask(ciphertext, 0, actualBc, 0);
78+
assertTrue(Arrays.areEqual(expected, actualBc));
79+
80+
TlsRecordNumberMask jce = new JceChaCha20RecordNumberMask();
81+
jce.setKey(key, 0, key.length);
82+
byte[] actualJce = new byte[16];
83+
jce.generateMask(ciphertext, 0, actualJce, 0);
84+
assertTrue(Arrays.areEqual(expected, actualJce));
85+
}
86+
87+
public void testChaCha20MaskLargeCounter() throws Exception
88+
{
89+
byte[] key = new byte[32];
90+
byte[] ciphertext = new byte[16];
91+
RANDOM.nextBytes(key);
92+
RANDOM.nextBytes(ciphertext);
93+
ciphertext[3] = (byte)0xFF; // counter near 2^32
94+
95+
TlsRecordNumberMask bc = new BcTlsChaCha20RecordNumberMask();
96+
bc.setKey(key, 0, key.length);
97+
byte[] a = new byte[16];
98+
bc.generateMask(ciphertext, 0, a, 0);
99+
100+
TlsRecordNumberMask jce = new JceChaCha20RecordNumberMask();
101+
jce.setKey(key, 0, key.length);
102+
byte[] b = new byte[16];
103+
jce.generateMask(ciphertext, 0, b, 0);
104+
105+
assertTrue(Arrays.areEqual(a, b));
106+
assertFalse(Arrays.areEqual(new byte[16], a));
107+
}
108+
}

0 commit comments

Comments
 (0)