Skip to content

Commit 01fc3c4

Browse files
committed
Add a concrete DHKEM implementation
1 parent 776ca78 commit 01fc3c4

File tree

3 files changed

+163
-2
lines changed

3 files changed

+163
-2
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package com.eatthepath.noise.component;
2+
3+
import javax.crypto.DecapsulateException;
4+
import javax.crypto.KEM;
5+
import javax.crypto.SecretKey;
6+
import javax.crypto.spec.SecretKeySpec;
7+
import java.security.*;
8+
9+
public class DhkemKeyEncapsulationMechanism implements NoiseKeyEncapsulationMechanism {
10+
11+
private final KEM kem;
12+
private final KeyPairGenerator keyPairGenerator;
13+
private final KeyFactory keyFactory;
14+
15+
public DhkemKeyEncapsulationMechanism() throws NoSuchAlgorithmException {
16+
this.keyPairGenerator = KeyPairGenerator.getInstance("X25519");
17+
this.keyFactory = KeyFactory.getInstance("X25519");
18+
this.kem = KEM.getInstance("DHKEM");
19+
}
20+
21+
@Override
22+
public String getName() {
23+
return "DHKEM";
24+
}
25+
26+
@Override
27+
public KeyPair generateKeyPair() {
28+
return keyPairGenerator.generateKeyPair();
29+
}
30+
31+
@Override
32+
public KEM.Encapsulated encapsulate(final PublicKey publicKey) {
33+
try {
34+
return kem.newEncapsulator(publicKey).encapsulate();
35+
} catch (final InvalidKeyException e) {
36+
throw new IllegalArgumentException("Invalid public key for encapsulation", e);
37+
}
38+
}
39+
40+
@Override
41+
public byte[] decapsulate(final PrivateKey privateKey, final byte[] encapsulation) {
42+
try {
43+
return serializeSharedSecret(kem.newDecapsulator(privateKey).decapsulate(encapsulation));
44+
} catch (final DecapsulateException e) {
45+
throw new IllegalArgumentException("Invalid encapsulation", e);
46+
} catch (final InvalidKeyException e) {
47+
throw new IllegalArgumentException("Invalid private key for decapsulation", e);
48+
}
49+
}
50+
51+
@Override
52+
public int getPublicKeyLength() {
53+
return 32;
54+
}
55+
56+
@Override
57+
public int getEncapsulationLength() {
58+
return 32;
59+
}
60+
61+
@Override
62+
public byte[] serializePublicKey(final PublicKey publicKey) {
63+
return XECUtil.serializePublicKey(publicKey, getPublicKeyLength(), XECUtil.X25519_X509_PREFIX);
64+
}
65+
66+
@Override
67+
public PublicKey deserializePublicKey(final byte[] publicKeyBytes) {
68+
return XECUtil.deserializePublicKey(publicKeyBytes, getPublicKeyLength(), XECUtil.X25519_X509_PREFIX, keyFactory);
69+
}
70+
71+
@Override
72+
public byte[] serializeSharedSecret(final SecretKey sharedSecret) {
73+
// For DHKEM, the shared secret has a "raw" encoding
74+
return sharedSecret.getEncoded();
75+
}
76+
77+
@Override
78+
public SecretKey deserializeSharedSecret(final byte[] sharedSecretBytes) {
79+
return new SecretKeySpec(sharedSecretBytes, "Generic");
80+
}
81+
}

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@
44
import javax.crypto.KEM;
55
import javax.crypto.SecretKey;
66
import java.security.KeyPair;
7+
import java.security.NoSuchAlgorithmException;
78
import java.security.PrivateKey;
89
import java.security.PublicKey;
910

1011
public interface NoiseKeyEncapsulationMechanism {
1112

12-
static NoiseKeyEncapsulationMechanism getInstance(final String name) {
13-
throw new IllegalArgumentException("Unrecognized key encapsulation method name: " + name);
13+
static NoiseKeyEncapsulationMechanism getInstance(final String name) throws NoSuchAlgorithmException {
14+
return switch (name) {
15+
case "DHKEM" -> new DhkemKeyEncapsulationMechanism();
16+
default -> throw new IllegalArgumentException("Unrecognized key encapsulation method name: " + name);
17+
};
1418
}
1519

1620
String getName();

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

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
99
import org.junit.jupiter.api.Assumptions;
1010
import org.junit.jupiter.api.Named;
11+
import org.junit.jupiter.api.Test;
1112
import org.junit.jupiter.params.ParameterizedTest;
1213
import org.junit.jupiter.params.provider.Arguments;
1314
import org.junit.jupiter.params.provider.MethodSource;
@@ -18,6 +19,7 @@
1819
import java.io.IOException;
1920
import java.io.InputStream;
2021
import java.nio.ByteBuffer;
22+
import java.nio.charset.StandardCharsets;
2123
import java.security.*;
2224
import java.security.spec.NamedParameterSpec;
2325
import java.util.List;
@@ -665,4 +667,78 @@ public void nextBytes(final byte[] bytes) {
665667
throw new RuntimeException(e);
666668
}
667669
}
670+
671+
@Test
672+
void dhkemHfs() throws NoSuchAlgorithmException, AEADBadTagException {
673+
final NoiseHandshake initiatorHandshake = NoiseHandshakeBuilder.forNNHfsInitiator()
674+
.setKeyAgreement("25519")
675+
.setKeyEncapsulationMechanism("DHKEM")
676+
.setCipher("AESGCM")
677+
.setHash("SHA256")
678+
.build();
679+
680+
final NoiseHandshake responderHandshake = NoiseHandshakeBuilder.forNNHfsResponder()
681+
.setKeyAgreement("25519")
682+
.setKeyEncapsulationMechanism("DHKEM")
683+
.setCipher("AESGCM")
684+
.setHash("SHA256")
685+
.build();
686+
687+
// -> e (with an empty payload)
688+
final byte[] initiatorEMessage = initiatorHandshake.writeMessage((byte[]) null);
689+
responderHandshake.readMessage(initiatorEMessage);
690+
691+
// <- e, ee (with an empty payload)
692+
final byte[] responderEEeMessage = responderHandshake.writeMessage((byte[]) null);
693+
initiatorHandshake.readMessage(responderEEeMessage);
694+
695+
assertTrue(initiatorHandshake.isDone());
696+
assertTrue(responderHandshake.isDone());
697+
698+
final NoiseTransport initiatorTransport = initiatorHandshake.toTransport();
699+
final NoiseTransport responderTransport = responderHandshake.toTransport();
700+
701+
final byte[] originalPlaintext = "Original payload!".getBytes(StandardCharsets.UTF_8);
702+
final byte[] originalCiphertext = initiatorTransport.writeMessage(originalPlaintext);
703+
final byte[] decryptedPlaintext = responderTransport.readMessage(originalCiphertext);
704+
705+
assertArrayEquals(originalPlaintext, decryptedPlaintext);
706+
}
707+
708+
@Test
709+
void dhkemHfsByteBuffer() throws NoSuchAlgorithmException, AEADBadTagException {
710+
final NoiseHandshake initiatorHandshake = NoiseHandshakeBuilder.forNNHfsInitiator()
711+
.setKeyAgreement("25519")
712+
.setKeyEncapsulationMechanism("DHKEM")
713+
.setCipher("AESGCM")
714+
.setHash("SHA256")
715+
.build();
716+
717+
final NoiseHandshake responderHandshake = NoiseHandshakeBuilder.forNNHfsResponder()
718+
.setKeyAgreement("25519")
719+
.setKeyEncapsulationMechanism("DHKEM")
720+
.setCipher("AESGCM")
721+
.setHash("SHA256")
722+
.build();
723+
724+
// -> e (with an empty payload)
725+
final ByteBuffer initiatorEMessage = initiatorHandshake.writeMessage((ByteBuffer) null);
726+
responderHandshake.readMessage(initiatorEMessage);
727+
728+
// <- e, ee (with an empty payload)
729+
final ByteBuffer responderEEeMessage = responderHandshake.writeMessage((ByteBuffer) null);
730+
initiatorHandshake.readMessage(responderEEeMessage);
731+
732+
assertTrue(initiatorHandshake.isDone());
733+
assertTrue(responderHandshake.isDone());
734+
735+
final NoiseTransport initiatorTransport = initiatorHandshake.toTransport();
736+
final NoiseTransport responderTransport = responderHandshake.toTransport();
737+
738+
final ByteBuffer originalPlaintext = ByteBuffer.wrap("Original payload!".getBytes(StandardCharsets.UTF_8));
739+
final ByteBuffer originalCiphertext = initiatorTransport.writeMessage(originalPlaintext);
740+
final ByteBuffer decryptedPlaintext = responderTransport.readMessage(originalCiphertext);
741+
742+
assertEquals(originalPlaintext.rewind(), decryptedPlaintext);
743+
}
668744
}

0 commit comments

Comments
 (0)