Skip to content

Commit 6170224

Browse files
committed
WIP: Add a concrete Kyber implementation
1 parent 11e1d5c commit 6170224

10 files changed

+307
-9
lines changed

pom.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,26 @@
1414
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
1515
</properties>
1616

17+
<repositories>
18+
<repository>
19+
<id>jitpack.io</id>
20+
<url>https://jitpack.io</url>
21+
</repository>
22+
</repositories>
23+
1724
<dependencies>
1825
<dependency>
1926
<groupId>com.google.code.findbugs</groupId>
2027
<artifactId>jsr305</artifactId>
2128
<version>3.0.2</version>
2229
</dependency>
2330

31+
<dependency>
32+
<groupId>com.github.fisherstevenk</groupId>
33+
<artifactId>kyberJCE</artifactId>
34+
<version>v3.0.0</version>
35+
</dependency>
36+
2437
<dependency>
2538
<groupId>com.fasterxml.jackson.core</groupId>
2639
<artifactId>jackson-databind</artifactId>

src/main/java/com/eatthepath/noise/NoiseHandshake.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -823,7 +823,7 @@ public int writeMessage(@Nullable final byte[] payload,
823823
localKeyEncapsulationKeyPair = keyEncapsulationMechanism.generateKeyPair();
824824

825825
final EncapsulatedKey encapsulatedKey =
826-
keyEncapsulationMechanism.encapsulate(localKeyEncapsulationKeyPair.getPrivate(), remoteEphemeralPublicKey);
826+
keyEncapsulationMechanism.encapsulate(localKeyEncapsulationKeyPair.getPrivate(), remoteKeyEncapsulationPublicKey);
827827

828828
try {
829829
offset += encryptAndHash(encapsulatedKey.encapsulation(),
@@ -995,7 +995,7 @@ public int writeMessage(@Nullable final ByteBuffer payload,
995995
localKeyEncapsulationKeyPair = keyEncapsulationMechanism.generateKeyPair();
996996

997997
final EncapsulatedKey encapsulatedKey =
998-
keyEncapsulationMechanism.encapsulate(localKeyEncapsulationKeyPair.getPrivate(), remoteEphemeralPublicKey);
998+
keyEncapsulationMechanism.encapsulate(localKeyEncapsulationKeyPair.getPrivate(), remoteKeyEncapsulationPublicKey);
999999

10001000
try {
10011001
bytesWritten += encryptAndHash(ByteBuffer.wrap(encapsulatedKey.encapsulation()), message);
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package com.eatthepath.noise.component;
2+
3+
import com.swiftcryptollc.crypto.interfaces.KyberPublicKey;
4+
import com.swiftcryptollc.crypto.provider.KyberCipherText;
5+
import com.swiftcryptollc.crypto.provider.KyberDecrypted;
6+
import com.swiftcryptollc.crypto.provider.KyberEncrypted;
7+
import com.swiftcryptollc.crypto.provider.KyberKeySize;
8+
import com.swiftcryptollc.crypto.provider.kyber.KyberParams;
9+
import com.swiftcryptollc.crypto.spec.KyberPublicKeySpec;
10+
11+
import javax.crypto.KeyAgreement;
12+
import java.security.*;
13+
import java.security.spec.InvalidKeySpecException;
14+
15+
abstract class AbstractKyberKeyEncapsulationMechanism implements NoiseKeyEncapsulationMechanism {
16+
17+
private final KeyAgreement keyAgreement;
18+
private final KeyPairGenerator keyPairGenerator;
19+
private final KeyFactory keyFactory;
20+
21+
AbstractKyberKeyEncapsulationMechanism(final KeyAgreement keyAgreement,
22+
final KeyPairGenerator keyPairGenerator,
23+
final KeyFactory keyFactory) {
24+
25+
this.keyAgreement = keyAgreement;
26+
this.keyPairGenerator = keyPairGenerator;
27+
this.keyFactory = keyFactory;
28+
}
29+
30+
protected abstract KyberKeySize getKyberKeySize();
31+
32+
@Override
33+
public KeyPair generateKeyPair() {
34+
return keyPairGenerator.generateKeyPair();
35+
}
36+
37+
@Override
38+
public EncapsulatedKey encapsulate(final PrivateKey privateKey, final PublicKey publicKey) {
39+
try {
40+
keyAgreement.init(privateKey);
41+
} catch (final InvalidKeyException e) {
42+
throw new IllegalArgumentException("Invalid private key for encapsulation", e);
43+
}
44+
45+
try {
46+
final KyberEncrypted kyberEncrypted = (KyberEncrypted) keyAgreement.doPhase(publicKey, true);
47+
48+
return new EncapsulatedKey(kyberEncrypted.getSecretKey().getS(), kyberEncrypted.getCipherText().getC());
49+
} catch (final InvalidKeyException e) {
50+
throw new IllegalArgumentException("Invalid public key for encapsulation", e);
51+
}
52+
}
53+
54+
@Override
55+
public byte[] decapsulate(final PrivateKey privateKey, final byte[] encapsulation) {
56+
try {
57+
keyAgreement.init(privateKey);
58+
} catch (final InvalidKeyException e) {
59+
throw new IllegalArgumentException("Invalid private key for decapsulation", e);
60+
}
61+
62+
try {
63+
final KyberCipherText kyberCiphertext =
64+
new KyberCipherText(encapsulation, KyberParams.default_p, KyberParams.default_g);
65+
66+
return ((KyberDecrypted) keyAgreement.doPhase(kyberCiphertext, true)).getSecretKey().getS();
67+
} catch (final InvalidKeyException e) {
68+
throw new IllegalArgumentException("Invalid ciphertext for decapsulation", e);
69+
}
70+
}
71+
72+
@Override
73+
public byte[] serializePublicKey(final PublicKey publicKey) {
74+
if (publicKey instanceof final KyberPublicKey kyberPublicKey) {
75+
return kyberPublicKey.getY();
76+
}
77+
78+
throw new IllegalArgumentException("Illegal public key type: " + publicKey.getClass());
79+
}
80+
81+
@Override
82+
public PublicKey deserializePublicKey(final byte[] publicKeyBytes) {
83+
try {
84+
return keyFactory.generatePublic(
85+
new KyberPublicKeySpec(publicKeyBytes, KyberParams.default_p, KyberParams.default_g, getKyberKeySize()));
86+
} catch (final InvalidKeySpecException e) {
87+
throw new IllegalArgumentException("Could not parse public key bytes as a Kyber public key", e);
88+
}
89+
}
90+
}

src/main/java/com/eatthepath/noise/component/AbstractXECKeyAgreement.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ abstract class AbstractXECKeyAgreement implements NoiseKeyAgreement {
1414
private final KeyFactory keyFactory;
1515

1616
protected AbstractXECKeyAgreement(final KeyAgreement keyAgreement,
17-
final KeyPairGenerator keyPairGenerator,
18-
final KeyFactory keyFactory) {
17+
final KeyPairGenerator keyPairGenerator,
18+
final KeyFactory keyFactory) {
1919

2020
this.keyAgreement = keyAgreement;
2121
this.keyPairGenerator = keyPairGenerator;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
package com.eatthepath.noise.component;
22

3+
import java.security.Key;
4+
35
public record EncapsulatedKey(byte[] sharedSecret, byte[] encapsulation) {
46
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.eatthepath.noise.component;
2+
3+
import com.swiftcryptollc.crypto.provider.KyberKeySize;
4+
import com.swiftcryptollc.crypto.provider.kyber.KyberParams;
5+
6+
import javax.crypto.KeyAgreement;
7+
import java.security.KeyFactory;
8+
import java.security.KeyPairGenerator;
9+
import java.security.NoSuchAlgorithmException;
10+
11+
public class Kyber1024KeyEncapsulationMechanism extends AbstractKyberKeyEncapsulationMechanism {
12+
13+
public Kyber1024KeyEncapsulationMechanism() throws NoSuchAlgorithmException {
14+
super(KeyAgreement.getInstance("Kyber"), KeyPairGenerator.getInstance("Kyber1024"), KeyFactory.getInstance("Kyber"));
15+
}
16+
17+
@Override
18+
protected KyberKeySize getKyberKeySize() {
19+
return KyberKeySize.KEY_1024;
20+
}
21+
22+
@Override
23+
public String getName() {
24+
return "Kyber1024";
25+
}
26+
27+
@Override
28+
public int getPublicKeyLength() {
29+
return KyberParams.Kyber1024PKBytes;
30+
}
31+
32+
@Override
33+
public int getEncapsulationLength() {
34+
return KyberParams.Kyber1024CTBytes;
35+
}
36+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.eatthepath.noise.component;
2+
3+
import com.swiftcryptollc.crypto.provider.KyberKeySize;
4+
import com.swiftcryptollc.crypto.provider.kyber.KyberParams;
5+
6+
import javax.crypto.KeyAgreement;
7+
import java.security.KeyFactory;
8+
import java.security.KeyPairGenerator;
9+
import java.security.NoSuchAlgorithmException;
10+
11+
class Kyber512KeyEncapsulationMechanism extends AbstractKyberKeyEncapsulationMechanism {
12+
13+
public Kyber512KeyEncapsulationMechanism() throws NoSuchAlgorithmException {
14+
super(KeyAgreement.getInstance("Kyber"), KeyPairGenerator.getInstance("Kyber512"), KeyFactory.getInstance("Kyber"));
15+
}
16+
17+
@Override
18+
protected KyberKeySize getKyberKeySize() {
19+
return KyberKeySize.KEY_512;
20+
}
21+
22+
@Override
23+
public String getName() {
24+
return "Kyber512";
25+
}
26+
27+
@Override
28+
public int getPublicKeyLength() {
29+
return KyberParams.Kyber512PKBytes;
30+
}
31+
32+
@Override
33+
public int getEncapsulationLength() {
34+
return KyberParams.Kyber512CTBytes;
35+
}
36+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.eatthepath.noise.component;
2+
3+
import com.swiftcryptollc.crypto.provider.KyberKeySize;
4+
import com.swiftcryptollc.crypto.provider.kyber.KyberParams;
5+
6+
import javax.crypto.KeyAgreement;
7+
import java.security.KeyFactory;
8+
import java.security.KeyPairGenerator;
9+
import java.security.NoSuchAlgorithmException;
10+
11+
class Kyber768KeyEncapsulationMechanism extends AbstractKyberKeyEncapsulationMechanism {
12+
public Kyber768KeyEncapsulationMechanism() throws NoSuchAlgorithmException {
13+
super(KeyAgreement.getInstance("Kyber"), KeyPairGenerator.getInstance("Kyber768"), KeyFactory.getInstance("Kyber"));
14+
}
15+
16+
@Override
17+
protected KyberKeySize getKyberKeySize() {
18+
return KyberKeySize.KEY_768;
19+
}
20+
21+
@Override
22+
public String getName() {
23+
return "Kyber768";
24+
}
25+
26+
@Override
27+
public int getPublicKeyLength() {
28+
return KyberParams.Kyber768PKBytes;
29+
}
30+
31+
@Override
32+
public int getEncapsulationLength() {
33+
return KyberParams.Kyber768CTBytes;
34+
}
35+
}

src/main/java/com/eatthepath/noise/component/NoiseKeyEncapsulationMechanism.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package com.eatthepath.noise.component;
22

3-
import java.security.KeyPair;
4-
import java.security.PrivateKey;
5-
import java.security.PublicKey;
3+
import java.security.*;
64

75
public interface NoiseKeyEncapsulationMechanism {
86

9-
static NoiseKeyEncapsulationMechanism getInstance(final String name) {
10-
throw new IllegalArgumentException("Unrecognized key encapsulation method name: " + name);
7+
static NoiseKeyEncapsulationMechanism getInstance(final String name) throws NoSuchAlgorithmException {
8+
return switch (name) {
9+
case "Kyber512" -> new Kyber512KeyEncapsulationMechanism();
10+
case "Kyber768" -> new Kyber768KeyEncapsulationMechanism();
11+
case "Kyber1024" -> new Kyber1024KeyEncapsulationMechanism();
12+
default -> throw new IllegalArgumentException("Unrecognized key encapsulation method name: " + name);
13+
};
1114
}
1215

1316
String getName();

src/test/java/com/eatthepath/noise/NoiseProtocolIntegrationTest.java

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,21 @@
66
import com.fasterxml.jackson.databind.ObjectMapper;
77
import com.fasterxml.jackson.databind.ObjectReader;
88
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
9+
import com.swiftcryptollc.crypto.provider.KyberJCE;
910
import org.junit.jupiter.api.Assumptions;
1011
import org.junit.jupiter.api.Named;
1112
import org.junit.jupiter.params.ParameterizedTest;
1213
import org.junit.jupiter.params.provider.Arguments;
1314
import org.junit.jupiter.params.provider.MethodSource;
15+
import org.junit.jupiter.params.provider.ValueSource;
1416
import org.opentest4j.TestAbortedException;
1517

1618
import javax.annotation.Nullable;
1719
import javax.crypto.AEADBadTagException;
1820
import java.io.IOException;
1921
import java.io.InputStream;
2022
import java.nio.ByteBuffer;
23+
import java.nio.charset.StandardCharsets;
2124
import java.security.*;
2225
import java.security.spec.NamedParameterSpec;
2326
import java.util.List;
@@ -665,4 +668,84 @@ public void nextBytes(final byte[] bytes) {
665668
throw new RuntimeException(e);
666669
}
667670
}
671+
672+
@ParameterizedTest
673+
@ValueSource(strings = {"Kyber512", "Kyber768", "Kyber1024"})
674+
void hackyHfsTest(final String keyEncapsulationMechanism) throws NoSuchAlgorithmException, AEADBadTagException {
675+
Security.addProvider(new KyberJCE());
676+
677+
final NoiseHandshake initiatorHandshake = NoiseHandshakeBuilder.forNNHfsInitiator()
678+
.setKeyAgreement("25519")
679+
.setKeyEncapsulationMechanism(keyEncapsulationMechanism)
680+
.setCipher("AESGCM")
681+
.setHash("SHA256")
682+
.build();
683+
684+
final NoiseHandshake responderHandshake = NoiseHandshakeBuilder.forNNHfsResponder()
685+
.setKeyAgreement("25519")
686+
.setKeyEncapsulationMechanism(keyEncapsulationMechanism)
687+
.setCipher("AESGCM")
688+
.setHash("SHA256")
689+
.build();
690+
691+
// -> e (with an empty payload)
692+
final byte[] initiatorEMessage = initiatorHandshake.writeMessage((byte[]) null);
693+
responderHandshake.readMessage(initiatorEMessage);
694+
695+
// <- e, ee (with an empty payload)
696+
final byte[] responderEEeMessage = responderHandshake.writeMessage((byte[]) null);
697+
initiatorHandshake.readMessage(responderEEeMessage);
698+
699+
assertTrue(initiatorHandshake.isDone());
700+
assertTrue(responderHandshake.isDone());
701+
702+
final NoiseTransport initiatorTransport = initiatorHandshake.toTransport();
703+
final NoiseTransport responderTransport = responderHandshake.toTransport();
704+
705+
final byte[] originalPlaintext = "Original payload!".getBytes(StandardCharsets.UTF_8);
706+
final byte[] originalCiphertext = initiatorTransport.writeMessage(originalPlaintext);
707+
final byte[] decryptedPlaintext = responderTransport.readMessage(originalCiphertext);
708+
709+
assertArrayEquals(originalPlaintext, decryptedPlaintext);
710+
}
711+
712+
@ParameterizedTest
713+
@ValueSource(strings = {"Kyber512", "Kyber768", "Kyber1024"})
714+
void hackyHfsByteBufferTest(final String keyEncapsulationMechanism) throws NoSuchAlgorithmException, AEADBadTagException {
715+
Security.addProvider(new KyberJCE());
716+
717+
final NoiseHandshake initiatorHandshake = NoiseHandshakeBuilder.forNNHfsInitiator()
718+
.setKeyAgreement("25519")
719+
.setKeyEncapsulationMechanism(keyEncapsulationMechanism)
720+
.setCipher("AESGCM")
721+
.setHash("SHA256")
722+
.build();
723+
724+
final NoiseHandshake responderHandshake = NoiseHandshakeBuilder.forNNHfsResponder()
725+
.setKeyAgreement("25519")
726+
.setKeyEncapsulationMechanism(keyEncapsulationMechanism)
727+
.setCipher("AESGCM")
728+
.setHash("SHA256")
729+
.build();
730+
731+
// -> e (with an empty payload)
732+
final ByteBuffer initiatorEMessage = initiatorHandshake.writeMessage((ByteBuffer) null);
733+
responderHandshake.readMessage(initiatorEMessage);
734+
735+
// <- e, ee (with an empty payload)
736+
final ByteBuffer responderEEeMessage = responderHandshake.writeMessage((ByteBuffer) null);
737+
initiatorHandshake.readMessage(responderEEeMessage);
738+
739+
assertTrue(initiatorHandshake.isDone());
740+
assertTrue(responderHandshake.isDone());
741+
742+
final NoiseTransport initiatorTransport = initiatorHandshake.toTransport();
743+
final NoiseTransport responderTransport = responderHandshake.toTransport();
744+
745+
final ByteBuffer originalPlaintext = ByteBuffer.wrap("Original payload!".getBytes(StandardCharsets.UTF_8));
746+
final ByteBuffer originalCiphertext = initiatorTransport.writeMessage(originalPlaintext);
747+
final ByteBuffer decryptedPlaintext = responderTransport.readMessage(originalCiphertext);
748+
749+
assertEquals(originalPlaintext.rewind(), decryptedPlaintext);
750+
}
668751
}

0 commit comments

Comments
 (0)