diff --git a/LICENSE.txt b/LICENSE.txt
index 8af9cfc4c..861c68cf4 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -128,6 +128,10 @@ Public domain except as listed below:
Copyright (C) 2006 The Android Open Source Project
See licenses/LICENSE-Apache2.0.txt
+ ML-KEM:
+ Copyright (c) 2000-2023 The Legion Of The Bouncy Castle Inc. (https://www.bouncycastle.org)
+ See licenses/LICENSE-Bouncycastle.txt
+
Installer:
(not included in distribution packages)
diff --git a/apps/i2ptunnel/jsp/editClient.jsi b/apps/i2ptunnel/jsp/editClient.jsi
index 3f2d3d7df..10daa693b 100644
--- a/apps/i2ptunnel/jsp/editClient.jsi
+++ b/apps/i2ptunnel/jsp/editClient.jsi
@@ -604,6 +604,9 @@
<%
boolean has0 = editBean.hasEncType(curTunnel, 0);
boolean has4 = editBean.hasEncType(curTunnel, 4);
+ boolean has5 = editBean.hasEncType(curTunnel, 5);
+ boolean has6 = editBean.hasEncType(curTunnel, 6);
+ boolean has7 = editBean.hasEncType(curTunnel, 7);
%>
>
@@ -617,7 +620,25 @@
+ ECIES-X25519 + ElGamal-2048
+<%
+ if (editBean.isAdvanced()) {
+%>
+
+
+
+
+
+
+<%
+ } // isAdvanced()
+%>
|
diff --git a/apps/i2ptunnel/jsp/editServer.jsi b/apps/i2ptunnel/jsp/editServer.jsi
index 3012a15fa..60a8b4013 100644
--- a/apps/i2ptunnel/jsp/editServer.jsi
+++ b/apps/i2ptunnel/jsp/editServer.jsi
@@ -676,6 +676,9 @@
<%
boolean has0 = editBean.hasEncType(curTunnel, 0);
boolean has4 = editBean.hasEncType(curTunnel, 4);
+ boolean has5 = editBean.hasEncType(curTunnel, 5);
+ boolean has6 = editBean.hasEncType(curTunnel, 6);
+ boolean has7 = editBean.hasEncType(curTunnel, 7);
String edisabled = canChangeEncType ? "" : " disabled=\"disabled\" ";
%>
@@ -690,7 +693,25 @@
+ ECIES-X25519 + ElGamal-2048
+<%
+ if (editBean.isAdvanced()) {
+%>
+
+
+
+
+
+
+<%
+ } // isAdvanced()
+%>
diff --git a/build.xml b/build.xml
index de8b84837..9da1445fe 100644
--- a/build.xml
+++ b/build.xml
@@ -887,7 +887,7 @@
windowtitle="I2P Anonymous Network - Java Documentation - API Version ${api.version}">
-
+
diff --git a/core/java/src/net/i2p/crypto/EncAlgo.java b/core/java/src/net/i2p/crypto/EncAlgo.java
index cc2c53382..bc73da1da 100644
--- a/core/java/src/net/i2p/crypto/EncAlgo.java
+++ b/core/java/src/net/i2p/crypto/EncAlgo.java
@@ -11,7 +11,14 @@ public enum EncAlgo {
EC("EC"),
/** @since 0.9.38 */
- ECIES("ECIES");
+ ECIES("ECIES"),
+
+ /** @since 0.9.67 */
+ ECIES_MLKEM("ECIES-MLKEM"),
+
+ /** @since 0.9.67 */
+ ECIES_MLKEM_INT("ECIES-MLKEM-Internal");
+
private final String name;
diff --git a/core/java/src/net/i2p/crypto/EncType.java b/core/java/src/net/i2p/crypto/EncType.java
index dda7f0d82..f9699d433 100644
--- a/core/java/src/net/i2p/crypto/EncType.java
+++ b/core/java/src/net/i2p/crypto/EncType.java
@@ -52,14 +52,83 @@ public enum EncType {
* Pubkey 32 bytes; privkey 32 bytes
* @since 0.9.38
*/
- ECIES_X25519(4, 32, 32, EncAlgo.ECIES, "EC/None/NoPadding", X25519_SPEC, "0.9.38");
+ ECIES_X25519(4, 32, 32, EncAlgo.ECIES, "EC/None/NoPadding", X25519_SPEC, "0.9.38"),
+
+ /**
+ * Proposal 169.
+ * Pubkey 32 bytes; privkey 32 bytes
+ * @since 0.9.67
+ */
+ MLKEM512_X25519(5, 32, 32, EncAlgo.ECIES_MLKEM, "EC/None/NoPadding", X25519_SPEC, "0.9.67"),
+
+ /**
+ * Proposal 169.
+ * Pubkey 32 bytes; privkey 32 bytes
+ * @since 0.9.67
+ */
+ MLKEM768_X25519(6, 32, 32, EncAlgo.ECIES_MLKEM, "EC/None/NoPadding", X25519_SPEC, "0.9.67"),
+
+ /**
+ * Proposal 169.
+ * Pubkey 32 bytes; privkey 32 bytes
+ * @since 0.9.67
+ */
+ MLKEM1024_X25519(7, 32, 32, EncAlgo.ECIES_MLKEM, "EC/None/NoPadding", X25519_SPEC, "0.9.67"),
+
+ /**
+ * For internal use only (Alice side)
+ * Proposal 169.
+ * Pubkey 800 bytes; privkey 1632 bytes
+ * @since 0.9.67
+ */
+ MLKEM512_X25519_INT(100005, 800, 1632, EncAlgo.ECIES_MLKEM_INT, "EC/None/NoPadding", X25519_SPEC, "0.9.67"),
+
+ /**
+ * For internal use only (Alice side)
+ * Proposal 169.
+ * Pubkey 1184 bytes; privkey 2400 bytes
+ * @since 0.9.67
+ */
+ MLKEM768_X25519_INT(100006, 1184, 2400, EncAlgo.ECIES_MLKEM_INT, "EC/None/NoPadding", X25519_SPEC, "0.9.67"),
+
+ /**
+ * For internal use only (Alice side)
+ * Proposal 169.
+ * Pubkey 1568 bytes; privkey 3168 bytes
+ * @since 0.9.67
+ */
+ MLKEM1024_X25519_INT(100007, 1568, 3168, EncAlgo.ECIES_MLKEM_INT, "EC/None/NoPadding", X25519_SPEC, "0.9.67"),
+
+ /**
+ * For internal use only (Bob side ciphertext)
+ * Proposal 169.
+ * Pubkey 768 bytes; privkey 0
+ * @since 0.9.67
+ */
+ MLKEM512_X25519_CT(100008, 768, 0, EncAlgo.ECIES_MLKEM_INT, "EC/None/NoPadding", X25519_SPEC, "0.9.67"),
+
+ /**
+ * For internal use only (Bob side ciphertext)
+ * Proposal 169.
+ * Pubkey 1088 bytes; privkey 0
+ * @since 0.9.67
+ */
+ MLKEM768_X25519_CT(100009, 1088, 0, EncAlgo.ECIES_MLKEM_INT, "EC/None/NoPadding", X25519_SPEC, "0.9.67"),
+
+ /**
+ * For internal use only (Bob side ciphertext)
+ * Proposal 169.
+ * Pubkey 1568 bytes; privkey 0
+ * @since 0.9.67
+ */
+ MLKEM1024_X25519_CT(100010, 1568, 0, EncAlgo.ECIES_MLKEM_INT, "EC/None/NoPadding", X25519_SPEC, "0.9.67");
private final int code, pubkeyLen, privkeyLen;
private final EncAlgo base;
private final String algoName, since;
private final AlgorithmParameterSpec params;
- private final boolean isAvail;
+ private final boolean isAvail, isPQ;
/**
*
@@ -68,7 +137,7 @@ public enum EncType {
*/
EncType(int cod, int pubLen, int privLen, EncAlgo baseAlgo,
String transformation, AlgorithmParameterSpec pSpec, String supportedSince) {
- if (pubLen > 256)
+ if (pubLen > 256 && baseAlgo != EncAlgo.ECIES_MLKEM_INT)
throw new IllegalArgumentException("fixup PublicKey for longer keys");
code = cod;
pubkeyLen = pubLen;
@@ -78,6 +147,7 @@ public enum EncType {
params = pSpec;
since = supportedSince;
isAvail = x_isAvailable();
+ isPQ = base == EncAlgo.ECIES_MLKEM;
}
/** the unique identifier for this type */
@@ -120,11 +190,16 @@ public enum EncType {
}
private boolean x_isAvailable() {
- if (ELGAMAL_2048 == this)
- return true;
- // EC types are placeholders for now
- if (base == EncAlgo.EC)
- return false;
+ switch (base) {
+ case ELGAMAL:
+ return true;
+
+ // EC types are placeholders for now
+ case EC:
+ // internal types
+ case ECIES_MLKEM_INT:
+ return false;
+ }
try {
getParams();
} catch (InvalidParameterSpecException e) {
@@ -154,6 +229,14 @@ public enum EncType {
return type.isAvailable();
}
+ /**
+ * @since 0.9.67
+ * @return true if this is a PQ type
+ */
+ public boolean isPQ() {
+ return isPQ;
+ }
+
private static final EncType[] BY_CODE;
static {
diff --git a/core/java/src/net/i2p/crypto/KeyGenerator.java b/core/java/src/net/i2p/crypto/KeyGenerator.java
index 497807f4f..d7364f416 100644
--- a/core/java/src/net/i2p/crypto/KeyGenerator.java
+++ b/core/java/src/net/i2p/crypto/KeyGenerator.java
@@ -196,6 +196,9 @@ public final class KeyGenerator {
break;
case ECIES_X25519:
+ case MLKEM512_X25519:
+ case MLKEM768_X25519:
+ case MLKEM1024_X25519:
byte[] bpriv = new byte[32];
do {
_context.random().nextBytes(bpriv);
@@ -238,6 +241,9 @@ public final class KeyGenerator {
break;
case ECIES_X25519:
+ case MLKEM512_X25519:
+ case MLKEM768_X25519:
+ case MLKEM1024_X25519:
data = new byte[32];
Curve25519.eval(data, 0, priv.getData(), null);
break;
@@ -342,6 +348,7 @@ public final class KeyGenerator {
}
java.security.PublicKey pubkey = kp.getPublic();
java.security.PrivateKey privkey = kp.getPrivate();
+
SimpleDataStructure[] keys = new SimpleDataStructure[2];
keys[0] = SigUtil.fromJavaKey(pubkey, type);
keys[1] = SigUtil.fromJavaKey(privkey, type);
@@ -476,6 +483,7 @@ public final class KeyGenerator {
System.out.println(type + " private-to-public test PASSED");
else
System.out.println(type + " private-to-public test FAILED");
+
//System.out.println("privkey " + keys[1]);
MessageDigest md = type.getDigestInstance();
for (int i = 0; i < runs; i++) {
diff --git a/licenses/LICENSE-Bouncycastle.txt b/licenses/LICENSE-Bouncycastle.txt
new file mode 100644
index 000000000..bec737cc3
--- /dev/null
+++ b/licenses/LICENSE-Bouncycastle.txt
@@ -0,0 +1,21 @@
+/**
+ * The Bouncy Castle License
+ *
+ * Copyright (c) 2000-2023 The Legion Of The Bouncy Castle Inc. (https://www.bouncycastle.org)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+ * and associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or substantial
+ * portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
diff --git a/router/java/src/com/southernstorm/noise/protocol/HandshakeState.java b/router/java/src/com/southernstorm/noise/protocol/HandshakeState.java
index 0a35ddcc2..0957aa619 100644
--- a/router/java/src/com/southernstorm/noise/protocol/HandshakeState.java
+++ b/router/java/src/com/southernstorm/noise/protocol/HandshakeState.java
@@ -39,12 +39,15 @@ public class HandshakeState implements Destroyable, Cloneable {
private final boolean isInitiator;
private DHState localKeyPair;
private DHState localEphemeral;
+ private DHState localHybrid;
private DHState remotePublicKey;
private DHState remoteEphemeral;
+ private DHState remoteHybrid;
private int action;
private final int requirements;
private int patternIndex;
private boolean wasCloned;
+ private boolean isDestroyed;
/**
* Enumerated value that indicates that the handshake object
@@ -139,6 +142,14 @@ public class HandshakeState implements Destroyable, Cloneable {
public static final String protocolName3 = "Noise_N_25519_ChaChaPoly_SHA256";
/** SSU2 */
public static final String protocolName4 = "Noise_XKchaobfse+hs1+hs2+hs3_25519_ChaChaPoly_SHA256";
+ /**
+ * Hybrid Ratchet
+ * @since 0.9.67
+ */
+ public static final String protocolName5 = "Noise_IKhfselg2_25519+MLKEM512_ChaChaPoly_SHA256";
+ public static final String protocolName6 = "Noise_IKhfselg2_25519+MLKEM768_ChaChaPoly_SHA256";
+ public static final String protocolName7 = "Noise_IKhfselg2_25519+MLKEM1024_ChaChaPoly_SHA256";
+
private static final String prefix;
private final String patternId;
/** NTCP2 */
@@ -151,13 +162,24 @@ public class HandshakeState implements Destroyable, Cloneable {
public static final String PATTERN_ID_N_NO_RESPONSE = "N!";
/** SSU2 */
public static final String PATTERN_ID_XK_SSU2 = "XK-SSU2";
- private static String dh;
+ /** Hybrid Base */
+ private static final String PATTERN_ID_IKHFS = "IKhfs";
+ /**
+ * Hybrid Ratchet
+ * @since 0.9.67
+ */
+ public static final String PATTERN_ID_IKHFS_512 = "IKhfs512";
+ public static final String PATTERN_ID_IKHFS_768 = "IKhfs768";
+ public static final String PATTERN_ID_IKHFS_1024 = "IKhfs1024";
+
+ private static final String dh;
private static final String cipher;
private static final String hash;
private final short[] pattern;
private static final short[] PATTERN_XK;
private static final short[] PATTERN_IK;
private static final short[] PATTERN_N;
+ private static final short[] PATTERN_IKHFS;
static {
// Parse the protocol name into its components.
@@ -200,11 +222,28 @@ public class HandshakeState implements Destroyable, Cloneable {
id = components[1].substring(0, 2);
if (!PATTERN_ID_XK.equals(id))
throw new IllegalArgumentException();
+ // IK Hybrid
+ components = protocolName5.split("_");
+ id = components[1].substring(0, 5);
+ if (!PATTERN_ID_IKHFS.equals(id))
+ throw new IllegalArgumentException();
+ PATTERN_IKHFS = Pattern.lookup(id);
+ if (PATTERN_IKHFS == null)
+ throw new IllegalArgumentException("Handshake pattern is not recognized");
+ components = protocolName6.split("_");
+ id = components[1].substring(0, 5);
+ if (!PATTERN_ID_IKHFS.equals(id))
+ throw new IllegalArgumentException();
+ components = protocolName7.split("_");
+ id = components[1].substring(0, 5);
+ if (!PATTERN_ID_IKHFS.equals(id))
+ throw new IllegalArgumentException();
}
/**
* Creates a new Noise handshake.
* Noise protocol name is hardcoded.
+ * Not for PQ Alice side.
*
* @param patternId XK, IK, or N
* @param role The role, HandshakeState.INITIATOR or HandshakeState.RESPONDER.
@@ -217,6 +256,26 @@ public class HandshakeState implements Destroyable, Cloneable {
* that is specified in the protocolName is not supported.
*/
public HandshakeState(String patternId, int role, KeyFactory xdh) throws NoSuchAlgorithmException
+ {
+ this(patternId, role, xdh, null);
+ }
+
+ /**
+ * Creates a new Noise handshake.
+ * Noise protocol name is hardcoded.
+ *
+ * @param patternId XK, IK, or N
+ * @param role The role, HandshakeState.INITIATOR or HandshakeState.RESPONDER.
+ * @param xdh The key pair factory for ephemeral keys
+ * @param hdh The key pair factory for hybrid keys, Alice side only, or null for Bob or non-hybrid
+ *
+ * @throws IllegalArgumentException The protocolName is not
+ * formatted correctly, or the role is not recognized.
+ *
+ * @throws NoSuchAlgorithmException One of the cryptographic algorithms
+ * that is specified in the protocolName is not supported.
+ */
+ public HandshakeState(String patternId, int role, KeyFactory xdh, KeyFactory hdh) throws NoSuchAlgorithmException
{
this.patternId = patternId;
if (patternId.equals(PATTERN_ID_XK))
@@ -229,6 +288,10 @@ public class HandshakeState implements Destroyable, Cloneable {
pattern = PATTERN_N;
else if (patternId.equals(PATTERN_ID_XK_SSU2))
pattern = PATTERN_XK;
+ else if (patternId.equals(PATTERN_ID_IKHFS_512) ||
+ patternId.equals(PATTERN_ID_IKHFS_768) ||
+ patternId.equals(PATTERN_ID_IKHFS_1024))
+ pattern = PATTERN_IKHFS;
else
throw new IllegalArgumentException("Handshake pattern is not recognized");
short flags = pattern[0];
@@ -256,10 +319,18 @@ public class HandshakeState implements Destroyable, Cloneable {
localKeyPair = new Curve25519DHState(xdh);
if ((flags & Pattern.FLAG_LOCAL_EPHEMERAL) != 0)
localEphemeral = new Curve25519DHState(xdh);
+ if ((flags & Pattern.FLAG_LOCAL_HYBRID) != 0) {
+ if (isInitiator && hdh == null)
+ throw new IllegalArgumentException("Hybrid patterns require hybrid key generator");
+ localHybrid = isInitiator ? new MLKEMDHState(hdh, patternId) : new MLKEMDHState(false, patternId);
+ }
if ((flags & Pattern.FLAG_REMOTE_STATIC) != 0)
remotePublicKey = new Curve25519DHState(xdh);
if ((flags & Pattern.FLAG_REMOTE_EPHEMERAL) != 0)
remoteEphemeral = new Curve25519DHState(xdh);
+ if ((flags & Pattern.FLAG_REMOTE_HYBRID) != 0) {
+ remoteHybrid = new MLKEMDHState(!isInitiator, patternId);
+ }
}
@@ -294,6 +365,23 @@ public class HandshakeState implements Destroyable, Cloneable {
remotePublicKey = o.remotePublicKey.clone();
if (o.remoteEphemeral != null)
remoteEphemeral = o.remoteEphemeral.clone();
+ if (o.localHybrid != null) {
+ if (isInitiator) {
+ // always save Alice's local keys
+ localHybrid = o.localHybrid.clone();
+ } else {
+ if (o.wasCloned) {
+ // new keys after first time for Bob
+ localHybrid = o.localHybrid.clone();
+ } else {
+ // first time for Bob, use the eph. keys previously generated
+ localHybrid = o.localHybrid;
+ o.wasCloned = true;
+ }
+ }
+ }
+ if (o.remoteHybrid != null)
+ remoteHybrid = o.remoteHybrid.clone();
action = o.action;
if (action == SPLIT || action == COMPLETE)
throw new CloneNotSupportedException("clone after NSR");
@@ -419,6 +507,32 @@ public class HandshakeState implements Destroyable, Cloneable {
return false;
}
+ /**
+ * Gets the keypair object for the local hybrid key.
+ *
+ * I2P
+ *
+ * @return The keypair, or null if a local hybrid key is not required or has not been generated.
+ * @since 0.9.67
+ */
+ public DHState getLocalHybridKeyPair()
+ {
+ return localHybrid;
+ }
+
+ /**
+ * Gets the keypair object for the remote hybrid key.
+ *
+ * I2P
+ *
+ * @return The keypair, or null if a remote hybrid key is not required or has not been generated.
+ * @since 0.9.67
+ */
+ public DHState getRemoteHybridKeyPair()
+ {
+ return remoteHybrid;
+ }
+
// Empty value for when the prologue is not supplied.
private static final byte[] emptyPrologue = new byte [0];
@@ -472,17 +586,11 @@ public class HandshakeState implements Destroyable, Cloneable {
if (isInitiator) {
if ((requirements & LOCAL_PREMSG) != 0)
symmetric.mixPublicKey(localKeyPair);
- if ((requirements & FALLBACK_PREMSG) != 0) {
- symmetric.mixPublicKey(remoteEphemeral);
- }
if ((requirements & REMOTE_PREMSG) != 0)
symmetric.mixPublicKey(remotePublicKey);
} else {
if ((requirements & REMOTE_PREMSG) != 0)
symmetric.mixPublicKey(remotePublicKey);
- if ((requirements & FALLBACK_PREMSG) != 0) {
- symmetric.mixPublicKey(localEphemeral);
- }
if ((requirements & LOCAL_PREMSG) != 0)
symmetric.mixPublicKey(localKeyPair);
}
@@ -675,6 +783,54 @@ public class HandshakeState implements Destroyable, Cloneable {
}
break;
+ case Pattern.F:
+ {
+ // Generate a local hybrid keypair and add the public
+ // key to the message. If we are running fixed vector tests,
+ // then a fixed hybrid key may have already been provided.
+ if (localHybrid == null)
+ throw new IllegalStateException("Pattern definition error");
+ byte[] shared = null;
+ if (isInitiator) {
+ // Only Alice generates a keypair
+ localHybrid.generateKeyPair();
+ } else {
+ // Only Bob. We have to do the FF part here,
+ // so we split up mixDH()
+ // and do the localHybrid.calculate() first
+ // and the mixKey() after.
+ // mixDH(localHybrid, remoteHybrid)
+ // First part
+ len = localHybrid.getSharedKeyLength();
+ shared = new byte [len];
+ // this creates the ciphertext and puts it in localHybrid.publicKey
+ // IllegalArgumentException will be thrown here on bad remote key
+ localHybrid.calculate(shared, 0, remoteHybrid);
+ }
+ len = localHybrid.getPublicKeyLength();
+ macLen = symmetric.getMACLength();
+ if (space < (len + macLen))
+ throw new ShortBufferException();
+ localHybrid.getPublicKey(message, messagePosn);
+ messagePosn += symmetric.encryptAndHash(message, messagePosn, message, messagePosn, len);
+ if (!isInitiator) {
+ // Second part
+ // We do the rest of the FF part here while we have the shared key
+ symmetric.mixKey(shared, 0, shared.length);
+ Noise.destroy(shared);
+ }
+ }
+ break;
+
+
+ case Pattern.FF:
+ {
+ // DH operation with initiator and responder hybrid keys.
+ // We are Bob.
+ // This is a NOOP, we did the mixDH() in Pattern.F above.
+ }
+ break;
+
default:
{
// Unknown token code. Abort.
@@ -857,6 +1013,35 @@ public class HandshakeState implements Destroyable, Cloneable {
mixDH(localKeyPair, remotePublicKey);
}
break;
+
+ case Pattern.F:
+ {
+ // Decrypt and read the remote hybrid ephemeral key.
+ if (remoteHybrid == null)
+ throw new IllegalStateException("Pattern definition error");
+ len = remoteHybrid.getPublicKeyLength();
+ macLen = symmetric.getMACLength();
+ if (space < (len + macLen))
+ throw new ShortBufferException();
+ byte[] temp = new byte [len];
+ try {
+ if (symmetric.decryptAndHash(message, messageOffset, temp, 0, len + macLen) != len)
+ throw new ShortBufferException();
+ remoteHybrid.setPublicKey(temp, 0);
+ } finally {
+ Noise.destroy(temp);
+ }
+ messageOffset += len + macLen;
+ }
+ break;
+
+ case Pattern.FF:
+ {
+ // DH operation with initiator and responder hybrid keys.
+ // We are Alice.
+ mixDH(localHybrid, remoteHybrid);
+ }
+ break;
default:
{
@@ -953,17 +1138,22 @@ public class HandshakeState implements Destroyable, Cloneable {
}
@Override
- public void destroy() {
+ public synchronized void destroy() {
+ isDestroyed = true;
if (symmetric != null)
symmetric.destroy();
if (localKeyPair != null)
localKeyPair.destroy();
if (localEphemeral != null)
localEphemeral.destroy();
+ if (localHybrid != null)
+ localHybrid.destroy();
if (remotePublicKey != null)
remotePublicKey.destroy();
if (remoteEphemeral != null)
remoteEphemeral.destroy();
+ if (remoteHybrid != null)
+ remoteHybrid.destroy();
}
/**
@@ -992,11 +1182,6 @@ public class HandshakeState implements Destroyable, Cloneable {
requirements |= REMOTE_REQUIRED;
requirements |= REMOTE_PREMSG;
}
- if ((flags & (Pattern.FLAG_REMOTE_EPHEM_REQ |
- Pattern.FLAG_LOCAL_EPHEM_REQ)) != 0) {
- if (isFallback)
- requirements |= FALLBACK_PREMSG;
- }
return requirements;
}
@@ -1022,6 +1207,8 @@ public class HandshakeState implements Destroyable, Cloneable {
*/
@Override
public synchronized HandshakeState clone() throws CloneNotSupportedException {
+ if (isDestroyed)
+ throw new IllegalStateException("destroyed");
return new HandshakeState(this);
}
@@ -1085,6 +1272,34 @@ public class HandshakeState implements Destroyable, Cloneable {
}
buf.append('\n');
+ dh = localHybrid;
+ if (dh != null) {
+ buf.append("Local hybrid public key (e1/ekem1) : ");
+ if (dh != null && dh.hasPublicKey()) {
+ tmp = new byte[dh.getPublicKeyLength()];
+ dh.getPublicKey(tmp, 0);
+ buf.append(tmp.length).append(" bytes ");
+ buf.append(net.i2p.data.Base64.encode(tmp));
+ } else {
+ buf.append("null");
+ }
+ buf.append('\n');
+ }
+
+ dh = remoteHybrid;
+ if (dh != null) {
+ buf.append("Remote hybrid public key (e1/ekem1) : ");
+ if (dh != null && dh.hasPublicKey()) {
+ tmp = new byte[dh.getPublicKeyLength()];
+ dh.getPublicKey(tmp, 0);
+ buf.append(tmp.length).append(" bytes ");
+ buf.append(net.i2p.data.Base64.encode(tmp));
+ } else {
+ buf.append("null");
+ }
+ buf.append('\n');
+ }
+
return buf.toString();
}
}
diff --git a/router/java/src/com/southernstorm/noise/protocol/MLKEMDHState.java b/router/java/src/com/southernstorm/noise/protocol/MLKEMDHState.java
new file mode 100644
index 000000000..9811886e9
--- /dev/null
+++ b/router/java/src/com/southernstorm/noise/protocol/MLKEMDHState.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2016 Southern Storm Software, Pty Ltd.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+package com.southernstorm.noise.protocol;
+
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+
+import net.i2p.crypto.KeyFactory;
+import net.i2p.crypto.KeyPair;
+import net.i2p.crypto.EncType;
+import net.i2p.router.crypto.pqc.MLKEM;
+
+/**
+ * Implementation of the MLKEM algorithm for the Noise protocol.
+ *
+ * @since 0.9.67
+ */
+class MLKEMDHState implements DHState, Cloneable {
+
+ private final EncType type;
+ private final byte[] publicKey;
+ private final byte[] privateKey;
+ private int mode;
+ private final KeyFactory _hdh;
+
+ /**
+ * Bob local/remote or Alice remote side, do not call generateKeyPair()
+ * @param isAlice true for Bob remote side, false for Bob local side and Alice remote side
+ */
+ public MLKEMDHState(boolean isAlice, String patternId)
+ {
+ this(isAlice, null, patternId);
+ }
+
+ /**
+ * Alice local side
+ */
+ public MLKEMDHState(KeyFactory hdh, String patternId)
+ {
+ this(true, hdh, patternId);
+ }
+
+ /**
+ * Internal
+ */
+ private MLKEMDHState(boolean isAlice, KeyFactory hdh, String patternId)
+ {
+ if (patternId.equals(HandshakeState.PATTERN_ID_IKHFS_512)) {
+ type = isAlice ? EncType.MLKEM512_X25519_INT : EncType.MLKEM512_X25519_CT;
+ } else if (patternId.equals(HandshakeState.PATTERN_ID_IKHFS_768)) {
+ type = isAlice ? EncType.MLKEM768_X25519_INT : EncType.MLKEM768_X25519_CT;
+ } else if (patternId.equals(HandshakeState.PATTERN_ID_IKHFS_1024)) {
+ type = isAlice ? EncType.MLKEM1024_X25519_INT : EncType.MLKEM1024_X25519_CT;
+ } else {
+ throw new IllegalArgumentException("Handshake pattern is not recognized");
+ }
+ publicKey = new byte [type.getPubkeyLen()];
+ privateKey = isAlice ? new byte [type.getPrivkeyLen()] : null;
+ mode = 0;
+ _hdh = hdh;
+ }
+
+ @Override
+ public void destroy() {
+ clearKey();
+ }
+
+ @Override
+ public String getDHName() {
+ return "MLKEM";
+ }
+
+ /**
+ * Note: Alice/Bob sizes are different
+ */
+ @Override
+ public int getPublicKeyLength() {
+ return type.getPubkeyLen();
+ }
+
+ /**
+ * Note: Alice/Bob sizes are different
+ * @return 0 for Bob
+ * @deprecated
+ */
+ @Deprecated
+ @Override
+ public int getPrivateKeyLength() {
+ return type.getPrivkeyLen();
+ }
+
+ @Override
+ public int getSharedKeyLength() {
+ return 32;
+ }
+
+ /**
+ * Alice local side ONLY
+ */
+ @Override
+ public void generateKeyPair() {
+ if (_hdh == null)
+ throw new IllegalStateException("Don't keygen PQ on Bob side");
+ KeyPair kp = _hdh.getKeys();
+ System.arraycopy(kp.getPrivate().getData(), 0, privateKey, 0, type.getPrivkeyLen());
+ System.arraycopy(kp.getPublic().getData(), 0, publicKey, 0, type.getPubkeyLen());
+ mode = 0x03;
+ }
+
+ @Override
+ public void getPublicKey(byte[] key, int offset) {
+ System.arraycopy(publicKey, 0, key, offset, type.getPubkeyLen());
+ }
+
+ @Override
+ public void setPublicKey(byte[] key, int offset) {
+ System.arraycopy(key, offset, publicKey, 0, type.getPubkeyLen());
+ if (privateKey != null)
+ Arrays.fill(privateKey, (byte)0);
+ mode = 0x01;
+ }
+
+ /**
+ * @deprecated
+ */
+ @Deprecated
+ @Override
+ public void getPrivateKey(byte[] key, int offset) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * @deprecated
+ */
+ @Deprecated
+ @Override
+ public void setPrivateKey(byte[] key, int offset) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * @deprecated
+ */
+ @Deprecated
+ @Override
+ public void setKeys(byte[] privkey, int privoffset, byte[] pubkey, int puboffset) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setToNullPublicKey() {
+ Arrays.fill(publicKey, (byte)0);
+ if (privateKey != null)
+ Arrays.fill(privateKey, (byte)0);
+ mode = 0x01;
+ }
+
+ @Override
+ public void clearKey() {
+ Noise.destroy(publicKey);
+ if (privateKey != null)
+ Noise.destroy(privateKey);
+ mode = 0;
+ }
+
+ @Override
+ public boolean hasPublicKey() {
+ return (mode & 0x01) != 0;
+ }
+
+ @Override
+ public boolean hasPrivateKey() {
+ return (mode & 0x02) != 0;
+ }
+
+ @Override
+ public boolean isNullPublicKey() {
+ if ((mode & 0x01) == 0)
+ return false;
+ int temp = 0;
+ for (int index = 0; index < publicKey.length; ++index)
+ temp |= publicKey[index];
+ return temp == 0;
+ }
+
+ /**
+ * I2P
+ */
+ @Override
+ public boolean hasEncodedPublicKey() {
+ return false;
+ }
+
+ /**
+ * @deprecated
+ */
+ @Deprecated
+ @Override
+ public void getEncodedPublicKey(byte[] key, int offset) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Side effect: If we are Bob, copies the ciphertext to our public key
+ * so it may be written out in the message.
+ *
+ * @throws IllegalArgumentException on bad public key modulus
+ */
+ @Override
+ public void calculate(byte[] sharedKey, int offset, DHState publicDH) {
+ if (!(publicDH instanceof MLKEMDHState))
+ throw new IllegalArgumentException("Incompatible DH algorithms");
+ try {
+ if (hasPrivateKey()) {
+ // we are Alice
+ byte[] sk = MLKEM.decaps(type, ((MLKEMDHState)publicDH).publicKey, privateKey);
+ System.arraycopy(sk, 0, sharedKey, offset, sk.length);
+ } else if (!hasPublicKey()) {
+ // we are Bob
+ byte[][] rv = MLKEM.encaps(type, ((MLKEMDHState)publicDH).publicKey);
+ byte[] ct = rv[0];
+ byte[] sk = rv[1];
+ System.arraycopy(sk, 0, sharedKey, offset, sk.length);
+ setPublicKey(ct, 0);
+ } else {
+ throw new IllegalStateException();
+ }
+ System.out.println("Calculated shared PQ key: " + net.i2p.data.Base64.encode(sharedKey, offset, 32));
+ } catch (GeneralSecurityException gse) {
+ throw new IllegalArgumentException(gse);
+ }
+ }
+
+ @Override
+ public void copyFrom(DHState other) {
+ if (!(other instanceof MLKEMDHState))
+ throw new IllegalStateException("Mismatched DH key objects");
+ if (other == this)
+ return;
+ MLKEMDHState dh = (MLKEMDHState)other;
+ if (dh.privateKey != null)
+ System.arraycopy(dh.privateKey, 0, privateKey, 0, type.getPrivkeyLen());
+ if (dh.publicKey != null)
+ System.arraycopy(dh.publicKey, 0, publicKey, 0, type.getPubkeyLen());
+ mode = dh.mode;
+ }
+
+ /**
+ * I2P
+ */
+ @Override
+ public MLKEMDHState clone() throws CloneNotSupportedException {
+ return (MLKEMDHState) super.clone();
+ }
+}
diff --git a/router/java/src/com/southernstorm/noise/protocol/Pattern.java b/router/java/src/com/southernstorm/noise/protocol/Pattern.java
index 2b205e445..d809a23f3 100644
--- a/router/java/src/com/southernstorm/noise/protocol/Pattern.java
+++ b/router/java/src/com/southernstorm/noise/protocol/Pattern.java
@@ -97,6 +97,31 @@ class Pattern {
SE
};
+ /**
+ * @since 0.9.67
+ */
+ private static final short[] noise_pattern_IKhfs = {
+ FLAG_LOCAL_STATIC |
+ FLAG_LOCAL_EPHEMERAL |
+ FLAG_LOCAL_HYBRID |
+ FLAG_REMOTE_STATIC |
+ FLAG_REMOTE_EPHEMERAL |
+ FLAG_REMOTE_HYBRID |
+ FLAG_REMOTE_REQUIRED,
+
+ E,
+ ES,
+ F,
+ S,
+ SS,
+ FLIP_DIR,
+ E,
+ EE,
+ F,
+ FF,
+ SE
+ };
+
/**
* Look up the description information for a pattern.
*
@@ -111,6 +136,8 @@ class Pattern {
return noise_pattern_XK;
else if (name.equals("IK"))
return noise_pattern_IK;
+ else if (name.equals("IKhfs"))
+ return noise_pattern_IKhfs;
return null;
}
diff --git a/router/java/src/com/southernstorm/noise/protocol/SymmetricState.java b/router/java/src/com/southernstorm/noise/protocol/SymmetricState.java
index 68f641e89..82864cedd 100644
--- a/router/java/src/com/southernstorm/noise/protocol/SymmetricState.java
+++ b/router/java/src/com/southernstorm/noise/protocol/SymmetricState.java
@@ -41,17 +41,26 @@ class SymmetricState implements Destroyable, Cloneable {
private static final byte[] INIT_CK_IK;
private static final byte[] INIT_CK_N;
private static final byte[] INIT_CK_XK_SSU2;
+ private static final byte[] INIT_CK_IKHFS_512;
+ private static final byte[] INIT_CK_IKHFS_768;
+ private static final byte[] INIT_CK_IKHFS_1024;
// precalculated hash of the hash of the Noise name = mixHash(nullPrologue)
private static final byte[] INIT_HASH_XK = new byte[32];
private static final byte[] INIT_HASH_IK = new byte[32];
private static final byte[] INIT_HASH_N = new byte[32];
private static final byte[] INIT_HASH_XK_SSU2 = new byte[32];
+ private static final byte[] INIT_HASH_IKHFS_512 = new byte[32];
+ private static final byte[] INIT_HASH_IKHFS_768 = new byte[32];
+ private static final byte[] INIT_HASH_IKHFS_1024 = new byte[32];
static {
INIT_CK_XK = initHash(HandshakeState.protocolName);
INIT_CK_IK = initHash(HandshakeState.protocolName2);
INIT_CK_N = initHash(HandshakeState.protocolName3);
INIT_CK_XK_SSU2 = initHash(HandshakeState.protocolName4);
+ INIT_CK_IKHFS_512 = initHash(HandshakeState.protocolName5);
+ INIT_CK_IKHFS_768 = initHash(HandshakeState.protocolName6);
+ INIT_CK_IKHFS_1024 = initHash(HandshakeState.protocolName7);
try {
MessageDigest md = Noise.createHash("SHA256");
md.update(INIT_CK_XK, 0, 32);
@@ -62,6 +71,12 @@ class SymmetricState implements Destroyable, Cloneable {
md.digest(INIT_HASH_N, 0, 32);
md.update(INIT_CK_XK_SSU2, 0, 32);
md.digest(INIT_HASH_XK_SSU2, 0, 32);
+ md.update(INIT_CK_IKHFS_512, 0, 32);
+ md.digest(INIT_HASH_IKHFS_512, 0, 32);
+ md.update(INIT_CK_IKHFS_768, 0, 32);
+ md.digest(INIT_HASH_IKHFS_768, 0, 32);
+ md.update(INIT_CK_IKHFS_1024, 0, 32);
+ md.digest(INIT_HASH_IKHFS_1024, 0, 32);
Noise.releaseHash(md);
} catch (Exception e) {
throw new IllegalStateException(e);
@@ -136,6 +151,15 @@ class SymmetricState implements Destroyable, Cloneable {
} else if (patternId.equals(HandshakeState.PATTERN_ID_XK_SSU2)) {
initCK = INIT_CK_XK_SSU2;
initHash = INIT_HASH_XK_SSU2;
+ } else if (patternId.equals(HandshakeState.PATTERN_ID_IKHFS_512)) {
+ initCK = INIT_CK_IKHFS_512;
+ initHash = INIT_HASH_IKHFS_512;
+ } else if (patternId.equals(HandshakeState.PATTERN_ID_IKHFS_768)) {
+ initCK = INIT_CK_IKHFS_768;
+ initHash = INIT_HASH_IKHFS_768;
+ } else if (patternId.equals(HandshakeState.PATTERN_ID_IKHFS_1024)) {
+ initCK = INIT_CK_IKHFS_1024;
+ initHash = INIT_HASH_IKHFS_1024;
} else {
throw new IllegalArgumentException("Handshake pattern is not recognized");
}
@@ -319,6 +343,7 @@ class SymmetricState implements Destroyable, Cloneable {
*/
public int decryptAndHash(byte[] ciphertext, int ciphertextOffset, byte[] plaintext, int plaintextOffset, int length) throws ShortBufferException, BadPaddingException
{
+ // NOTE: This updates the hash, even on failure
System.arraycopy(h, 0, prev_h, 0, h.length);
mixHash(ciphertext, ciphertextOffset, length);
return cipher.decryptWithAd(prev_h, ciphertext, ciphertextOffset, plaintext, plaintextOffset, length);
diff --git a/router/java/src/net/i2p/router/LeaseSetKeys.java b/router/java/src/net/i2p/router/LeaseSetKeys.java
index ca55f9f5c..f45460b64 100644
--- a/router/java/src/net/i2p/router/LeaseSetKeys.java
+++ b/router/java/src/net/i2p/router/LeaseSetKeys.java
@@ -26,6 +26,7 @@ public class LeaseSetKeys {
private final SigningPrivateKey _revocationKey;
private final PrivateKey _decryptionKey;
private final PrivateKey _decryptionKeyEC;
+ private final PrivateKey _decryptionKeyPQ;
/**
* Unmodifiable, ElGamal only
@@ -43,6 +44,41 @@ public class LeaseSetKeys {
*/
public static final Set SET_BOTH = Collections.unmodifiableSet(EnumSet.of(EncType.ELGAMAL_2048, EncType.ECIES_X25519));
private static final Set SET_NONE = Collections.emptySet();
+ /**
+ * Unmodifiable, PQ only
+ * @since public since 0.9.67
+ */
+ public static final Set SET_PQ1 = Collections.unmodifiableSet(EnumSet.of(EncType.MLKEM512_X25519));
+ /**
+ * Unmodifiable, PQ only
+ * @since public since 0.9.67
+ */
+ public static final Set SET_PQ2 = Collections.unmodifiableSet(EnumSet.of(EncType.MLKEM768_X25519));
+ /**
+ * Unmodifiable, PQ only
+ * @since public since 0.9.67
+ */
+ public static final Set SET_PQ3 = Collections.unmodifiableSet(EnumSet.of(EncType.MLKEM1024_X25519));
+ /**
+ * Unmodifiable, ECIES-X25519 and PQ only
+ * @since public since 0.9.67
+ */
+ public static final Set SET_EC_PQ1 = Collections.unmodifiableSet(EnumSet.of(EncType.ECIES_X25519, EncType.MLKEM512_X25519));
+ /**
+ * Unmodifiable, ECIES-X25519 and PQ only
+ * @since public since 0.9.67
+ */
+ public static final Set SET_EC_PQ2 = Collections.unmodifiableSet(EnumSet.of(EncType.ECIES_X25519, EncType.MLKEM768_X25519));
+ /**
+ * Unmodifiable, ECIES-X25519 and PQ only
+ * @since public since 0.9.67
+ */
+ public static final Set SET_EC_PQ3 = Collections.unmodifiableSet(EnumSet.of(EncType.ECIES_X25519, EncType.MLKEM1024_X25519));
+ /**
+ * Unmodifiable, ECIES-X25519 and PQ only
+ * @since public since 0.9.67
+ */
+ public static final Set SET_EC_PQ_ALL = Collections.unmodifiableSet(EnumSet.of(EncType.ECIES_X25519, EncType.MLKEM512_X25519, EncType.MLKEM768_X25519, EncType.MLKEM1024_X25519));
/**
* Client with a single key
@@ -57,9 +93,15 @@ public class LeaseSetKeys {
if (type == EncType.ELGAMAL_2048) {
_decryptionKey = decryptionKey;
_decryptionKeyEC = null;
+ _decryptionKeyPQ = null;
} else if (type == EncType.ECIES_X25519) {
_decryptionKey = null;
_decryptionKeyEC = decryptionKey;
+ _decryptionKeyPQ = null;
+ } else if (type.isPQ()) {
+ _decryptionKey = null;
+ _decryptionKeyEC =null;
+ _decryptionKeyPQ = decryptionKey;
} else {
throw new IllegalArgumentException("Unknown type " + type);
}
@@ -68,9 +110,13 @@ public class LeaseSetKeys {
/**
* Client with multiple keys
*
+ * The ONLY valid combinations are X25519 + ElG or X25519 + (MLKEM512 OR MLKEM768 OR MLKEM1024).
+ * Other combinations will throw IllegalArgumentException.
+ *
* @param dest unused
* @param revocationKey unused, may be null
* @param decryptionKeys non-null, non-empty
+ * @throws IllegalArgumentException
* @since 0.9.44
*/
public LeaseSetKeys(Destination dest, SigningPrivateKey revocationKey, List decryptionKeys) {
@@ -79,22 +125,32 @@ public class LeaseSetKeys {
_revocationKey = revocationKey;
PrivateKey elg = null;
PrivateKey ec = null;
+ PrivateKey pq = null;
for (PrivateKey pk : decryptionKeys) {
EncType type = pk.getType();
if (type == EncType.ELGAMAL_2048) {
if (elg != null)
throw new IllegalArgumentException("Multiple keys same type");
+ if (pq != null)
+ throw new IllegalArgumentException("Invalid combination ElG + PQ");
elg = pk;
} else if (type == EncType.ECIES_X25519) {
if (ec != null)
throw new IllegalArgumentException("Multiple keys same type");
ec = pk;
+ } else if (type.isPQ()) {
+ if (pq != null)
+ throw new IllegalArgumentException("Multiple keys same type");
+ if (elg != null)
+ throw new IllegalArgumentException("Invalid combination ElG + PQ");
+ pq = pk;
} else {
throw new IllegalArgumentException("Unknown type " + type);
}
}
_decryptionKey = elg;
_decryptionKeyEC = ec;
+ _decryptionKeyPQ = pq;
}
/**
@@ -128,9 +184,17 @@ public class LeaseSetKeys {
return _decryptionKey;
if (type == EncType.ECIES_X25519)
return _decryptionKeyEC;
+ if (type.isPQ() && _decryptionKeyPQ != null && _decryptionKeyPQ.getType() == type)
+ return _decryptionKeyPQ;
return null;
}
+ /**
+ * @return PQ key (any type) or null if the LS does not support PQ
+ * @since 0.9.67
+ */
+ public PrivateKey getPQDecryptionKey() { return _decryptionKeyPQ; }
+
/**
* Do we support this type of encryption?
*
@@ -141,6 +205,8 @@ public class LeaseSetKeys {
return _decryptionKey != null;
if (type == EncType.ECIES_X25519)
return _decryptionKeyEC != null;
+ if (type.isPQ())
+ return _decryptionKeyPQ != null && _decryptionKeyPQ.getType() == type;
return false;
}
@@ -152,6 +218,27 @@ public class LeaseSetKeys {
public Set getSupportedEncryption() {
if (_decryptionKey != null)
return (_decryptionKeyEC != null) ? SET_BOTH : SET_ELG;
+ if (_decryptionKeyPQ != null) {
+ if (_decryptionKeyEC != null) {
+ switch (_decryptionKeyPQ.getType()) {
+ case MLKEM512_X25519:
+ return SET_EC_PQ1;
+ case MLKEM768_X25519:
+ return SET_EC_PQ2;
+ case MLKEM1024_X25519:
+ return SET_EC_PQ3;
+ }
+ } else {
+ switch (_decryptionKeyPQ.getType()) {
+ case MLKEM512_X25519:
+ return SET_PQ1;
+ case MLKEM768_X25519:
+ return SET_PQ2;
+ case MLKEM1024_X25519:
+ return SET_PQ3;
+ }
+ }
+ }
return (_decryptionKeyEC != null) ? SET_EC : SET_NONE;
}
}
diff --git a/router/java/src/net/i2p/router/client/ClientConnectionRunner.java b/router/java/src/net/i2p/router/client/ClientConnectionRunner.java
index 3ae5442b6..0dff4a7a4 100644
--- a/router/java/src/net/i2p/router/client/ClientConnectionRunner.java
+++ b/router/java/src/net/i2p/router/client/ClientConnectionRunner.java
@@ -26,6 +26,7 @@ import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import net.i2p.client.I2PClient;
+import net.i2p.crypto.EncType;
import net.i2p.crypto.SessionKeyManager;
import net.i2p.data.DatabaseEntry;
import net.i2p.data.DataHelper;
@@ -50,6 +51,7 @@ import net.i2p.router.JobImpl;
import net.i2p.router.RouterContext;
import net.i2p.router.crypto.TransientSessionKeyManager;
import net.i2p.router.crypto.ratchet.RatchetSKM;
+import net.i2p.router.crypto.ratchet.MuxedPQSKM;
import net.i2p.router.crypto.ratchet.MuxedSKM;
import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade;
import net.i2p.util.ConcurrentHashSet;
@@ -605,6 +607,8 @@ class ClientConnectionRunner {
int thresh = TransientSessionKeyManager.LOW_THRESHOLD;
boolean hasElg = false;
boolean hasEC = false;
+ boolean hasPQ = false;
+ int pqType = 0;
// router may be null in unit tests, avoid NPEs in ratchet
// we won't actually be using any SKM anyway
if (opts != null && _context.router() != null) {
@@ -620,10 +624,18 @@ class ClientConnectionRunner {
if (senc != null) {
String[] senca = DataHelper.split(senc, ",");
for (String sencaa : senca) {
- if (sencaa.equals("0"))
+ if (sencaa.equals("0")) {
hasElg = true;
- else if (sencaa.equals("4"))
+ } else if (sencaa.equals("4")) {
hasEC = true;
+ } else if (sencaa.equals("5") || sencaa.equals("6") || sencaa.equals("7")) {
+ if (hasPQ) {
+ _log.error("Bad encryption type combination in i2cp.leaseSetEncType for " + dest.toBase32());
+ return SessionStatusMessage.STATUS_INVALID;
+ }
+ pqType = Integer.parseInt(sencaa);
+ hasPQ = true;
+ }
}
} else {
hasElg = true;
@@ -632,6 +644,10 @@ class ClientConnectionRunner {
hasElg = true;
}
if (hasElg) {
+ if (hasPQ) {
+ _log.error("Bad encryption type combination in i2cp.leaseSetEncType for " + dest.toBase32());
+ return SessionStatusMessage.STATUS_INVALID;
+ }
TransientSessionKeyManager tskm = new TransientSessionKeyManager(_context, tags, thresh);
if (hasEC) {
RatchetSKM rskm = new RatchetSKM(_context, dest);
@@ -639,6 +655,16 @@ class ClientConnectionRunner {
} else {
_sessionKeyManager = tskm;
}
+ } else if (hasPQ) {
+ if (hasEC) {
+ // ECIES
+ RatchetSKM rskm1 = new RatchetSKM(_context, dest);
+ // PQ
+ RatchetSKM rskm2 = new RatchetSKM(_context, dest, EncType.getByCode(pqType));
+ _sessionKeyManager = new MuxedPQSKM(rskm1, rskm2);
+ } else {
+ _sessionKeyManager = new RatchetSKM(_context, dest, EncType.getByCode(pqType));
+ }
} else {
if (hasEC) {
_sessionKeyManager = new RatchetSKM(_context, dest);
diff --git a/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java b/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java
index 54ac7825b..f608f445c 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java
@@ -16,6 +16,7 @@ import com.southernstorm.noise.protocol.HandshakeState;
import net.i2p.crypto.EncType;
import net.i2p.crypto.HKDF;
+import net.i2p.crypto.KeyFactory;
import net.i2p.crypto.SessionKeyManager;
import net.i2p.data.Base64;
import net.i2p.data.Certificate;
@@ -33,6 +34,7 @@ import net.i2p.data.SessionTag;
import net.i2p.data.i2np.DatabaseStoreMessage;
import net.i2p.data.i2np.GarlicClove;
import net.i2p.data.i2np.I2NPMessage;
+import net.i2p.router.crypto.pqc.MLKEM;
import static net.i2p.router.crypto.ratchet.RatchetPayload.*;
import net.i2p.router.LeaseSetKeys;
import net.i2p.router.Router;
@@ -53,6 +55,7 @@ public final class ECIESAEADEngine {
private final RouterContext _context;
private final Log _log;
private final MuxedEngine _muxedEngine;
+ private final MuxedPQEngine _muxedPQEngine;
private final HKDF _hkdf;
private final Elg2KeyFactory _edhThread;
private boolean _isRunning;
@@ -86,6 +89,24 @@ public final class ECIESAEADEngine {
private static final String INFO_0 = "SessionReplyTags";
private static final String INFO_6 = "AttachPayloadKDF";
+ // These are the min sizes for the MLKEM New Session Message.
+ // It contains an extra MLKEM key and MAC.
+ // 112
+ private static final int NS_MLKEM_OVERHEAD = NS_OVERHEAD + MACLEN;
+ // 800 + 112 + 7 = 919
+ private static final int MIN_NS_MLKEM512_SIZE = EncType.MLKEM512_X25519_INT.getPubkeyLen() + NS_MLKEM_OVERHEAD + DATETIME_SIZE;
+ // 1184 + 112 + 7 = 1303
+ private static final int MIN_NS_MLKEM768_SIZE = EncType.MLKEM768_X25519_INT.getPubkeyLen() + NS_MLKEM_OVERHEAD + DATETIME_SIZE;
+ // 1568 + 112 + 7 = 1687
+ private static final int MIN_NS_MLKEM1024_SIZE = EncType.MLKEM1024_X25519_INT.getPubkeyLen() + NS_MLKEM_OVERHEAD + DATETIME_SIZE;
+ // 856
+ private static final int MIN_NSR_MLKEM512_SIZE = EncType.MLKEM512_X25519_CT.getPubkeyLen() + MIN_NSR_SIZE + MACLEN;
+ // 1176
+ private static final int MIN_NSR_MLKEM768_SIZE = EncType.MLKEM768_X25519_CT.getPubkeyLen() + MIN_NSR_SIZE + MACLEN;
+ // 1656
+ private static final int MIN_NSR_MLKEM1024_SIZE = EncType.MLKEM1024_X25519_CT.getPubkeyLen() + MIN_NSR_SIZE + MACLEN;
+
+
/**
* Caller MUST call startup() to get threaded generation.
* Will still work without, will just generate inline.
@@ -96,6 +117,7 @@ public final class ECIESAEADEngine {
_context = ctx;
_log = _context.logManager().getLog(ECIESAEADEngine.class);
_muxedEngine = new MuxedEngine(ctx);
+ _muxedPQEngine = new MuxedPQEngine(ctx);
_hkdf = new HKDF(ctx);
_edhThread = new Elg2KeyFactory(ctx);
@@ -147,6 +169,18 @@ public final class ECIESAEADEngine {
return _muxedEngine.decrypt(data, elgKey, ecKey, keyManager);
}
+ /**
+ * Try to decrypt the message with one or both of the given private keys
+ *
+ * @param ecKey must be EC, non-null
+ * @param pqKey must be PQ, non-null
+ * @return decrypted data or null on failure
+ * @since 0.9.67
+ */
+ public CloveSet decrypt(byte data[], PrivateKey ecKey, PrivateKey pqKey, MuxedPQSKM keyManager) throws DataFormatException {
+ return _muxedPQEngine.decrypt(data, ecKey, pqKey, keyManager);
+ }
+
/**
* Decrypt the message using the given private key
* and using tags from the specified key manager.
@@ -175,8 +209,7 @@ public final class ECIESAEADEngine {
private CloveSet x_decrypt(byte data[], PrivateKey targetPrivateKey,
RatchetSKM keyManager) throws DataFormatException {
- if (targetPrivateKey.getType() != EncType.ECIES_X25519)
- throw new IllegalArgumentException();
+ checkType(targetPrivateKey.getType());
if (data == null) {
if (_log.shouldLog(Log.ERROR)) _log.error("Null data being decrypted?");
return null;
@@ -267,14 +300,22 @@ public final class ECIESAEADEngine {
if (shouldDebug)
_log.debug("Decrypting ES with tag: " + st.toBase64() + " key: " + key + ": " + data.length + " bytes");
decrypted = decryptExistingSession(tag, data, key, targetPrivateKey, keyManager);
- } else if (data.length >= MIN_NSR_SIZE) {
- if (shouldDebug)
- _log.debug("Decrypting NSR with tag: " + st.toBase64() + " key: " + key + ": " + data.length + " bytes");
- decrypted = decryptNewSessionReply(tag, data, state, keyManager);
} else {
- decrypted = null;
- if (_log.shouldWarn())
- _log.warn("ECIES decrypt fail, tag found but no state and too small for NSR: " + data.length + " bytes");
+ // it's important not to attempt decryption for too-short packets,
+ // because Noise will destroy() the handshake state on failure,
+ // and we don't clone() on the first one, so it's fatal.
+ EncType type = targetPrivateKey.getType();
+ int min = getMinNSRSize(type);
+ if (data.length >= min) {
+ if (shouldDebug)
+ _log.debug("Decrypting NSR with tag: " + st.toBase64() + " key: " + key + ": " + data.length + " bytes");
+ decrypted = decryptNewSessionReply(tag, data, state, keyManager);
+ } else {
+ decrypted = null;
+ if (_log.shouldWarn())
+ _log.warn("NSR decrypt fail, tag: " + st.toBase64() + " but packet too small: " + data.length + " bytes, min is " +
+ min + " on state " + state);
+ }
}
if (decrypted != null) {
_context.statManager().updateFrequency("crypto.eciesAEAD.decryptExistingSession");
@@ -316,8 +357,10 @@ public final class ECIESAEADEngine {
private CloveSet x_decryptSlow(byte data[], PrivateKey targetPrivateKey,
RatchetSKM keyManager) throws DataFormatException {
CloveSet decrypted;
+ EncType type = targetPrivateKey.getType();
+ int minns = getMinNSSize(type);
boolean isRouter = keyManager.getDestination() == null;
- if (data.length >= MIN_NS_SIZE || (isRouter && data.length >= MIN_NS_N_SIZE)) {
+ if (data.length >= minns || (isRouter && data.length >= MIN_NS_N_SIZE)) {
if (isRouter)
decrypted = decryptNewSession_N(data, targetPrivateKey, keyManager);
else
@@ -333,11 +376,116 @@ public final class ECIESAEADEngine {
} else {
decrypted = null;
if (_log.shouldDebug())
- _log.debug("ECIES decrypt fail, too small for NS: " + data.length + " bytes");
+ _log.debug("ECIES decrypt fail, too small for NS: " + data.length + " bytes, min is " +
+ minns + " for type " + type);
}
return decrypted;
}
+ /**
+ * @throws IllegalArgumentException if unsupported
+ * @since 0.9.67
+ */
+ private static void checkType(EncType type) {
+ switch(type) {
+ case ECIES_X25519:
+ case MLKEM512_X25519:
+ case MLKEM768_X25519:
+ case MLKEM1024_X25519:
+ return;
+ default:
+ throw new IllegalArgumentException("Unsupported key type " + type);
+ }
+ }
+
+ /**
+ * @since 0.9.67
+ */
+ private static String getNoisePattern(EncType type) {
+ switch(type) {
+ case ECIES_X25519:
+ return HandshakeState.PATTERN_ID_IK;
+ case MLKEM512_X25519:
+ return HandshakeState.PATTERN_ID_IKHFS_512;
+ case MLKEM768_X25519:
+ return HandshakeState.PATTERN_ID_IKHFS_768;
+ case MLKEM1024_X25519:
+ return HandshakeState.PATTERN_ID_IKHFS_1024;
+ default:
+ throw new IllegalArgumentException("No pattern for " + type);
+ }
+ }
+
+ /**
+ * @since 0.9.67
+ */
+ private static KeyFactory getHybridKeyFactory(EncType type) {
+ switch(type) {
+ case MLKEM512_X25519:
+ return MLKEM.MLKEM512KeyFactory;
+ case MLKEM768_X25519:
+ return MLKEM.MLKEM768KeyFactory;
+ case MLKEM1024_X25519:
+ return MLKEM.MLKEM1024KeyFactory;
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * @since 0.9.67
+ */
+ private static int getMinNSSize(EncType type) {
+ switch(type) {
+ case ECIES_X25519:
+ return MIN_NS_SIZE;
+ case MLKEM512_X25519:
+ return MIN_NS_MLKEM512_SIZE;
+ case MLKEM768_X25519:
+ return MIN_NS_MLKEM768_SIZE;
+ case MLKEM1024_X25519:
+ return MIN_NS_MLKEM1024_SIZE;
+ default:
+ throw new IllegalArgumentException("No pattern for " + type);
+ }
+ }
+
+ /**
+ * @since 0.9.67
+ */
+ private static int getMinNSRSize(EncType type) {
+ switch(type) {
+ case ECIES_X25519:
+ return MIN_NSR_SIZE;
+ case MLKEM512_X25519:
+ return MIN_NSR_MLKEM512_SIZE;
+ case MLKEM768_X25519:
+ return MIN_NSR_MLKEM768_SIZE;
+ case MLKEM1024_X25519:
+ return MIN_NSR_MLKEM1024_SIZE;
+ default:
+ throw new IllegalArgumentException("No pattern for " + type);
+ }
+ }
+
+ /**
+ * @since 0.9.67
+ */
+ private static Set getEncTypeSet(EncType type) {
+ switch(type) {
+ case ECIES_X25519:
+ return LeaseSetKeys.SET_EC;
+ case MLKEM512_X25519:
+ return LeaseSetKeys.SET_PQ1;
+ case MLKEM768_X25519:
+ return LeaseSetKeys.SET_PQ2;
+ case MLKEM1024_X25519:
+ return LeaseSetKeys.SET_PQ3;
+ default:
+ throw new IllegalArgumentException("No pattern for " + type);
+ }
+ }
+
/**
* scenario 1: New Session Message
*
@@ -383,8 +531,12 @@ public final class ECIESAEADEngine {
System.arraycopy(pk.getData(), 0, data, 0, KEYLEN);
HandshakeState state;
+ EncType type = targetPrivateKey.getType();
try {
- state = new HandshakeState(HandshakeState.PATTERN_ID_IK, HandshakeState.RESPONDER, _edhThread);
+ String pattern = getNoisePattern(type);
+ // Bob does not need a key factory
+ //state = new HandshakeState(pattern, HandshakeState.RESPONDER, _edhThread, getHybridKeyFactory(type));
+ state = new HandshakeState(pattern, HandshakeState.RESPONDER, _edhThread);
} catch (GeneralSecurityException gse) {
throw new IllegalStateException("bad proto", gse);
}
@@ -392,9 +544,13 @@ public final class ECIESAEADEngine {
targetPrivateKey.toPublic().getData(), 0);
state.start();
if (_log.shouldDebug())
- _log.debug("State before decrypt new session: " + state);
+ _log.debug("State before decrypt new session (" + data.length + " bytes) " + state);
int payloadlen = data.length - (KEYLEN + KEYLEN + MACLEN + MACLEN);
+ DHState hyb = state.getRemoteHybridKeyPair();
+ if (hyb != null) {
+ payloadlen -= hyb.getPublicKeyLength() + MACLEN;
+ }
byte[] payload = new byte[payloadlen];
try {
state.readMessage(data, 0, data.length, payload, 0);
@@ -402,7 +558,7 @@ public final class ECIESAEADEngine {
// we'll get this a lot on muxed SKM
// logged at INFO in caller
if (_log.shouldDebug())
- _log.debug("Decrypt fail NS, state at failure: " + state, gse);
+ _log.debug("Decrypt fail NS " + data.length + " bytes, state at failure: " + state, gse);
// restore original data for subsequent ElG attempt
System.arraycopy(xx, 0, data, 0, KEYLEN - 1);
data[KEYLEN - 1] = xx31;
@@ -467,7 +623,7 @@ public final class ECIESAEADEngine {
state.destroy();
} else {
// tell the SKM
- PublicKey alice = new PublicKey(EncType.ECIES_X25519, alicePK);
+ PublicKey alice = new PublicKey(type, alicePK);
keyManager.createSession(alice, null, state, null);
setResponseTimerNS(alice, pc.cloveSet, keyManager);
}
@@ -638,8 +794,13 @@ public final class ECIESAEADEngine {
state.mixHash(tag, 0, TAGLEN);
if (_log.shouldDebug())
_log.debug("State after mixhash tag before decrypt new session reply: " + state);
+ int tmplen = 48;
+ DHState hyb = state.getRemoteHybridKeyPair();
+ if (hyb != null) {
+ tmplen += hyb.getPublicKeyLength() + MACLEN;
+ }
try {
- state.readMessage(data, 8, 48, ZEROLEN, 0);
+ state.readMessage(data, 8, tmplen, ZEROLEN, 0);
} catch (GeneralSecurityException gse) {
if (_log.shouldWarn()) {
_log.warn("Decrypt fail NSR part 1", gse);
@@ -667,9 +828,16 @@ public final class ECIESAEADEngine {
byte[] encpayloadkey = new byte[32];
_hkdf.calculate(split.k_ba.getData(), ZEROLEN, INFO_6, encpayloadkey);
rcvr.initializeKey(encpayloadkey, 0);
- byte[] payload = new byte[data.length - (TAGLEN + KEYLEN + MACLEN + MACLEN)];
+ int off = TAGLEN + KEYLEN + MACLEN;
+ int plen = data.length - (TAGLEN + KEYLEN + MACLEN + MACLEN);
+ if (hyb != null) {
+ int len = hyb.getPublicKeyLength() + MACLEN;
+ off += len;
+ plen -= len;
+ }
+ byte[] payload = new byte[plen];
try {
- rcvr.decryptWithAd(hash, data, TAGLEN + KEYLEN + MACLEN, payload, 0, payload.length + MACLEN);
+ rcvr.decryptWithAd(hash, data, off, payload, 0, plen + MACLEN);
} catch (GeneralSecurityException gse) {
if (_log.shouldWarn()) {
_log.warn("Decrypt fail NSR part 2", gse);
@@ -714,7 +882,7 @@ public final class ECIESAEADEngine {
}
// tell the SKM
- PublicKey bob = new PublicKey(EncType.ECIES_X25519, bobPK);
+ PublicKey bob = new PublicKey(keyManager.getType(), bobPK);
keyManager.updateSession(bob, oldState, state, null, split);
if (pc == null)
@@ -872,8 +1040,7 @@ public final class ECIESAEADEngine {
private byte[] x_encrypt(CloveSet cloves, PublicKey target, Destination to, PrivateKey priv,
RatchetSKM keyManager,
ReplyCallback callback) {
- if (target.getType() != EncType.ECIES_X25519)
- throw new IllegalArgumentException();
+ checkType(target.getType());
if (Arrays.equals(target.getData(), NULLPK)) {
if (_log.shouldWarn())
_log.warn("Zero static key target");
@@ -904,6 +1071,7 @@ public final class ECIESAEADEngine {
}
if (_log.shouldDebug())
_log.debug("Encrypting as NSR to " + target + " with tag " + re.tag.toBase64());
+// trash old state if this throws IAE???
return encryptNewSessionReply(cloves, target, state, re.tag, keyManager, callback);
}
byte rv[] = encryptExistingSession(cloves, target, re, callback, keyManager);
@@ -943,9 +1111,13 @@ public final class ECIESAEADEngine {
private byte[] encryptNewSession(CloveSet cloves, PublicKey target, Destination to, PrivateKey priv,
RatchetSKM keyManager,
ReplyCallback callback) {
+ EncType type = target.getType();
+ if (type != priv.getType())
+ throw new IllegalArgumentException("Key mismatch " + target + ' ' + priv);
HandshakeState state;
try {
- state = new HandshakeState(HandshakeState.PATTERN_ID_IK, HandshakeState.INITIATOR, _edhThread);
+ String pattern = getNoisePattern(target.getType());
+ state = new HandshakeState(pattern, HandshakeState.INITIATOR, _edhThread, getHybridKeyFactory(type));
} catch (GeneralSecurityException gse) {
throw new IllegalStateException("bad proto", gse);
}
@@ -962,7 +1134,12 @@ public final class ECIESAEADEngine {
byte[] payload = createPayload(cloves, cloves.getExpiration(), NS_OVERHEAD);
- byte[] enc = new byte[KEYLEN + KEYLEN + MACLEN + payload.length + MACLEN];
+ int enclen = KEYLEN + KEYLEN + MACLEN + payload.length + MACLEN;
+ DHState hyb = state.getLocalHybridKeyPair();
+ if (hyb != null) {
+ enclen += hyb.getPublicKeyLength() + MACLEN;
+ }
+ byte[] enc = new byte[enclen];
try {
state.writeMessage(enc, 0, payload, 0, payload.length);
} catch (GeneralSecurityException gse) {
@@ -1071,7 +1248,12 @@ public final class ECIESAEADEngine {
byte[] payload = createPayload(cloves, 0, NSR_OVERHEAD);
// part 1 - tag and empty payload
- byte[] enc = new byte[TAGLEN + KEYLEN + MACLEN + payload.length + MACLEN];
+ int enclen = TAGLEN + KEYLEN + MACLEN + payload.length + MACLEN;
+ DHState hyb = state.getLocalHybridKeyPair();
+ if (hyb != null) {
+ enclen += hyb.getPublicKeyLength() + MACLEN;
+ }
+ byte[] enc = new byte[enclen];
System.arraycopy(tag, 0, enc, 0, TAGLEN);
try {
state.writeMessage(enc, TAGLEN, ZEROLEN, 0, 0);
@@ -1103,8 +1285,12 @@ public final class ECIESAEADEngine {
byte[] encpayloadkey = new byte[32];
_hkdf.calculate(split.k_ba.getData(), ZEROLEN, INFO_6, encpayloadkey);
sender.initializeKey(encpayloadkey, 0);
+ int off = TAGLEN + KEYLEN + MACLEN;
+ if (hyb != null) {
+ off += hyb.getPublicKeyLength() + MACLEN;
+ }
try {
- sender.encryptWithAd(hash, payload, 0, enc, TAGLEN + KEYLEN + MACLEN, payload.length);
+ sender.encryptWithAd(hash, payload, 0, enc, off, payload.length);
} catch (GeneralSecurityException gse) {
if (_log.shouldWarn())
_log.warn("Encrypt fail NSR part 2", gse);
@@ -1488,7 +1674,7 @@ public final class ECIESAEADEngine {
return;
if (!ls2.isCurrent(Router.CLOCK_FUDGE_FACTOR))
continue;
- PublicKey pk = ls2.getEncryptionKey(LeaseSetKeys.SET_EC);
+ PublicKey pk = ls2.getEncryptionKey(getEncTypeSet(from.getType()));
if (!from.equals(pk))
continue;
if (!ls2.verifySignature())
diff --git a/router/java/src/net/i2p/router/crypto/ratchet/MuxedPQEngine.java b/router/java/src/net/i2p/router/crypto/ratchet/MuxedPQEngine.java
new file mode 100644
index 000000000..2d9eaec23
--- /dev/null
+++ b/router/java/src/net/i2p/router/crypto/ratchet/MuxedPQEngine.java
@@ -0,0 +1,98 @@
+package net.i2p.router.crypto.ratchet;
+
+import net.i2p.crypto.EncAlgo;
+import net.i2p.crypto.EncType;
+import net.i2p.data.DataFormatException;
+import net.i2p.data.PrivateKey;
+import net.i2p.router.RouterContext;
+import net.i2p.router.message.CloveSet;
+import net.i2p.util.Log;
+
+/**
+ * Both EC and PQ
+ *
+ * Handles the actual decryption using the
+ * supplied keys and data.
+ *
+ * @since 0.9.67
+ */
+final class MuxedPQEngine {
+ private final RouterContext _context;
+ private final Log _log;
+
+ public MuxedPQEngine(RouterContext ctx) {
+ _context = ctx;
+ _log = _context.logManager().getLog(MuxedPQEngine.class);
+ }
+
+ /**
+ * Decrypt the message with the given private keys
+ *
+ * @param ecKey must be EC, non-null
+ * @param pqKey must be PQ, non-null
+ * @return decrypted data or null on failure
+ */
+ public CloveSet decrypt(byte data[], PrivateKey ecKey, PrivateKey pqKey, MuxedPQSKM keyManager) throws DataFormatException {
+ if (ecKey.getType() != EncType.ECIES_X25519 ||
+ pqKey.getType().getBaseAlgorithm() != EncAlgo.ECIES_MLKEM)
+ throw new IllegalArgumentException();
+ final boolean debug = _log.shouldDebug();
+ CloveSet rv = null;
+ // Try in-order from fastest to slowest
+ boolean preferRatchet = keyManager.preferRatchet();
+ if (preferRatchet) {
+ // Ratchet Tag
+ rv = _context.eciesEngine().decryptFast(data, ecKey, keyManager.getECSKM());
+ if (rv != null)
+ return rv;
+ if (debug)
+ _log.debug("Ratchet tag not found before PQ");
+ }
+ // PQ
+ // Ratchet Tag
+ rv = _context.eciesEngine().decryptFast(data, pqKey, keyManager.getPQSKM());
+ if (rv != null)
+ return rv;
+ if (debug)
+ _log.debug("PQ tag not found");
+ if (!preferRatchet) {
+ // Ratchet Tag
+ rv = _context.eciesEngine().decryptFast(data, ecKey, keyManager.getECSKM());
+ if (rv != null)
+ return rv;
+ if (debug)
+ _log.debug("Ratchet tag not found after PQ");
+ }
+
+ if (preferRatchet) {
+ // Ratchet DH
+ rv = _context.eciesEngine().decryptSlow(data, ecKey, keyManager.getECSKM());
+ boolean ok = rv != null;
+ keyManager.reportDecryptResult(true, ok);
+ if (ok)
+ return rv;
+ if (debug)
+ _log.debug("Ratchet NS decrypt failed before PQ");
+ }
+
+ // PQ DH
+ // Minimum size checks for the larger New Session message are in ECIESAEADEngine.x_decryptSlow().
+ rv = _context.eciesEngine().decryptSlow(data, pqKey, keyManager.getPQSKM());
+ boolean isok = rv != null;
+ keyManager.reportDecryptResult(false, isok);
+ if (isok)
+ return rv;
+ if (debug)
+ _log.debug("PQ NS decrypt failed");
+
+ if (!preferRatchet) {
+ // Ratchet DH
+ rv = _context.eciesEngine().decryptSlow(data, ecKey, keyManager.getECSKM());
+ boolean ok = rv != null;
+ keyManager.reportDecryptResult(true, ok);
+ if (!ok && debug)
+ _log.debug("Ratchet NS decrypt failed after PQ");
+ }
+ return rv;
+ }
+}
diff --git a/router/java/src/net/i2p/router/crypto/ratchet/MuxedPQSKM.java b/router/java/src/net/i2p/router/crypto/ratchet/MuxedPQSKM.java
new file mode 100644
index 000000000..dfb4ee7d7
--- /dev/null
+++ b/router/java/src/net/i2p/router/crypto/ratchet/MuxedPQSKM.java
@@ -0,0 +1,231 @@
+package net.i2p.router.crypto.ratchet;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import net.i2p.I2PAppContext;
+import net.i2p.crypto.EncType;
+import net.i2p.crypto.TagSetHandle;
+import net.i2p.crypto.SessionKeyManager;
+import net.i2p.data.PublicKey;
+import net.i2p.data.SessionKey;
+import net.i2p.data.SessionTag;
+
+/**
+ * Both EC and PQ
+ *
+ * @since 0.9.67
+ */
+public class MuxedPQSKM extends SessionKeyManager {
+
+ private final RatchetSKM _ec;
+ private final RatchetSKM _pq;
+ private final AtomicInteger _ecCounter = new AtomicInteger();
+ private final AtomicInteger _pqCounter = new AtomicInteger();
+ // PQ is about this much slower than EC
+ private static final int PQ_SLOW_FACTOR = 2;
+ private static final int RESTART_COUNTERS = 500;
+
+ public MuxedPQSKM(RatchetSKM ec, RatchetSKM pq) {
+ _ec = ec;
+ _pq = pq;
+ }
+
+ public RatchetSKM getECSKM() { return _ec; }
+
+ public RatchetSKM getPQSKM() { return _pq; }
+
+ /**
+ * Should we try the Ratchet slow decrypt before PQ slow decrypt?
+ * Adaptive test based on previous mix of traffic for this SKM,
+ * as reported by reportDecryptResult().
+ */
+ boolean preferRatchet() {
+ int ec = _ecCounter.get();
+ int pq = _pqCounter.get();
+ if (ec > RESTART_COUNTERS / 10 &&
+ pq > RESTART_COUNTERS / 10 &&
+ ec + pq > RESTART_COUNTERS) {
+ _ecCounter.set(0);
+ _pqCounter.set(0);
+ return true;
+ }
+ return ec >= pq / PQ_SLOW_FACTOR;
+ }
+
+ /**
+ * Report the result of a slow decrypt attempt.
+ *
+ * @param isRatchet true for EC, false for PQ
+ * @param success true for successful decrypt
+ */
+ void reportDecryptResult(boolean isRatchet, boolean success) {
+ if (success) {
+ if (isRatchet)
+ _ecCounter.incrementAndGet();
+ else
+ _pqCounter.incrementAndGet();
+ }
+ }
+
+ /**
+ * ElG only
+ */
+ @Override
+ public SessionKey getCurrentKey(PublicKey target) {
+ return null;
+ }
+
+ /**
+ * ElG only
+ */
+ @Override
+ public SessionKey getCurrentOrNewKey(PublicKey target) {
+ return null;
+ }
+
+ /**
+ * ElG only
+ */
+ @Override
+ public void createSession(PublicKey target, SessionKey key) {
+ }
+
+ /**
+ * ElG only
+ */
+ @Override
+ public SessionKey createSession(PublicKey target) {
+ return null;
+ }
+
+ /**
+ * ElG only
+ */
+ @Override
+ public SessionTag consumeNextAvailableTag(PublicKey target, SessionKey key) {
+ return null;
+ }
+
+ /**
+ * EC/PQ
+ */
+ public RatchetEntry consumeNextAvailableTag(PublicKey target) {
+ EncType type = target.getType();
+ if (type == EncType.ECIES_X25519)
+ return _ec.consumeNextAvailableTag(target);
+ else
+ return _pq.consumeNextAvailableTag(target);
+ }
+
+ @Override
+ public int getTagsToSend() { return 0; };
+
+ @Override
+ public int getLowThreshold() { return 0; };
+
+ /**
+ * ElG only
+ */
+ @Override
+ public boolean shouldSendTags(PublicKey target, SessionKey key) {
+ return false;
+ }
+
+ /**
+ * ElG only
+ */
+ @Override
+ public boolean shouldSendTags(PublicKey target, SessionKey key, int lowThreshold) {
+ return false;
+ }
+
+ @Override
+ public int getAvailableTags(PublicKey target, SessionKey key) {
+ EncType type = target.getType();
+ if (type == EncType.ECIES_X25519)
+ return _ec.getAvailableTags(target, key);
+ else
+ return _pq.getAvailableTags(target, key);
+ }
+
+ @Override
+ public long getAvailableTimeLeft(PublicKey target, SessionKey key) {
+ EncType type = target.getType();
+ if (type == EncType.ECIES_X25519)
+ return _ec.getAvailableTimeLeft(target, key);
+ else
+ return _pq.getAvailableTimeLeft(target, key);
+ }
+
+ /**
+ * ElG only
+ */
+ @Override
+ public TagSetHandle tagsDelivered(PublicKey target, SessionKey key, Set sessionTags) {
+ return null;
+ }
+
+ /**
+ * ElG only
+ */
+ @Override
+ public void tagsReceived(SessionKey key, Set sessionTags) {
+ }
+
+ /**
+ * ElG only
+ */
+ @Override
+ public void tagsReceived(SessionKey key, Set sessionTags, long expire) {
+ }
+
+ /**
+ * EC only.
+ * One time session
+ * We do not support PQ one-time sessions on MuxedPQSKM.
+ *
+ * @param expire time from now
+ */
+ public void tagsReceived(SessionKey key, RatchetSessionTag tag, long expire) {
+ _ec.tagsReceived(key, tag, expire);
+ }
+
+ @Override
+ public SessionKey consumeTag(SessionTag tag) {
+ RatchetSessionTag rstag = new RatchetSessionTag(tag.getData());
+ SessionKey rv = _ec.consumeTag(rstag);
+ if (rv == null) {
+ rv = _pq.consumeTag(rstag);
+ }
+ return rv;
+ }
+
+ @Override
+ public void shutdown() {
+ _ec.shutdown();
+ _pq.shutdown();
+ }
+
+ @Override
+ public void renderStatusHTML(Writer out) throws IOException {
+ _ec.renderStatusHTML(out);
+ _pq.renderStatusHTML(out);
+ }
+
+ /**
+ * ElG only
+ */
+ @Override
+ public void failTags(PublicKey target, SessionKey key, TagSetHandle ts) {
+ }
+
+ /**
+ * ElG only
+ */
+ @Override
+ public void tagsAcked(PublicKey target, SessionKey key, TagSetHandle ts) {
+ }
+}
diff --git a/router/java/src/net/i2p/router/crypto/ratchet/RatchetSKM.java b/router/java/src/net/i2p/router/crypto/ratchet/RatchetSKM.java
index d0e74d230..ff6730ebd 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/RatchetSKM.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/RatchetSKM.java
@@ -54,6 +54,7 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
private final HKDF _hkdf;
private final DecayingHashSet _replayFilter;
private final Destination _destination;
+ private final EncType _type;
/**
* Let outbound session tags sit around for this long before expiring them.
@@ -83,7 +84,17 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
* @since 0.9.48
*/
public RatchetSKM(RouterContext context) {
- this(context, null);
+ this(context, null, EncType.ECIES_X25519);
+ }
+
+ /**
+ * ECIES only.
+ *
+ * @param dest null for router's SKM only
+ * @since 0.9.48
+ */
+ public RatchetSKM(RouterContext context, Destination dest) {
+ this(context, dest, EncType.ECIES_X25519);
}
/**
@@ -91,12 +102,15 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
* client manager.
*
* @param dest null for router's SKM only
+ * @param type the encryption type
+ * @since 0.9.67
*/
- public RatchetSKM(RouterContext context, Destination dest) {
+ public RatchetSKM(RouterContext context, Destination dest, EncType type) {
super(context);
_log = context.logManager().getLog(RatchetSKM.class);
_context = context;
_destination = dest;
+ _type = type;
_outboundSessions = new ConcurrentHashMap(64);
_pendingOutboundSessions = new HashMap>(64);
_inboundTagSets = new ConcurrentHashMap(128);
@@ -146,6 +160,15 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
return _destination;
}
+ /**
+ * The EncType for this SKM
+ *
+ * @since 0.9.67
+ */
+ public EncType getType() {
+ return _type;
+ }
+
/** RatchetTagSet */
private Set getRatchetTagSets() {
synchronized (_inboundTagSets) {
@@ -201,8 +224,8 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
*/
boolean createSession(PublicKey target, Destination d, HandshakeState state, ReplyCallback callback) {
EncType type = target.getType();
- if (type != EncType.ECIES_X25519)
- throw new IllegalArgumentException("Bad public key type " + type);
+ if (type != _type)
+ throw new IllegalArgumentException("Bad public key type " + type + " expected " + _type);
OutboundSession sess = new OutboundSession(target, d, null, state, callback);
boolean isInbound = state.getRole() == HandshakeState.RESPONDER;
if (isInbound) {
@@ -247,8 +270,8 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
boolean updateSession(PublicKey target, HandshakeState oldState, HandshakeState state,
ReplyCallback callback, SplitKeys split) {
EncType type = target.getType();
- if (type != EncType.ECIES_X25519)
- throw new IllegalArgumentException("Bad public key type " + type);
+ if (type != _type)
+ throw new IllegalArgumentException("Bad public key type " + type + " expected " + _type);
boolean isInbound = state.getRole() == HandshakeState.RESPONDER;
if (isInbound) {
// we are Bob, NSR sent
diff --git a/router/java/src/net/i2p/router/message/GarlicMessageBuilder.java b/router/java/src/net/i2p/router/message/GarlicMessageBuilder.java
index 7527d9a1f..d10df77ee 100644
--- a/router/java/src/net/i2p/router/message/GarlicMessageBuilder.java
+++ b/router/java/src/net/i2p/router/message/GarlicMessageBuilder.java
@@ -32,6 +32,7 @@ import net.i2p.data.router.RouterIdentity;
import net.i2p.data.router.RouterInfo;
import net.i2p.router.LeaseSetKeys;
import net.i2p.router.RouterContext;
+import net.i2p.router.crypto.ratchet.MuxedPQSKM;
import net.i2p.router.crypto.ratchet.MuxedSKM;
import net.i2p.router.crypto.ratchet.RatchetSKM;
import net.i2p.router.crypto.ratchet.RatchetSessionTag;
@@ -46,13 +47,13 @@ import net.i2p.util.Log;
public class GarlicMessageBuilder {
/**
- * ELGAMAL_2048 only.
+ * ELGAMAL_2048 only; returns false for others
*
* @param local non-null; do not use this method for the router's SessionKeyManager
* @param minTagOverride 0 for no override, > 0 to override SKM's settings
*/
static boolean needsTags(RouterContext ctx, PublicKey key, Hash local, int minTagOverride) {
- if (key.getType() == EncType.ECIES_X25519)
+ if (LeaseSetKeys.SET_EC_PQ_ALL.contains(key.getType()))
return false;
SessionKeyManager skm = ctx.clientManager().getClientSessionKeyManager(local);
if (skm == null)
@@ -275,7 +276,7 @@ public class GarlicMessageBuilder {
}
/**
- * ECIES_X25519 only.
+ * ECIES_X25519 and PQ only.
* Called by OCMJH only.
*
* @param ctx scope
@@ -289,7 +290,8 @@ public class GarlicMessageBuilder {
Hash from, Destination to, SessionKeyManager skm,
ReplyCallback callback) {
PublicKey key = config.getRecipientPublicKey();
- if (key.getType() != EncType.ECIES_X25519)
+ EncType type = key.getType();
+ if (!LeaseSetKeys.SET_EC_PQ_ALL.contains(type))
throw new IllegalArgumentException();
Log log = ctx.logManager().getLog(GarlicMessageBuilder.class);
GarlicMessage msg = new GarlicMessage(ctx);
@@ -300,7 +302,7 @@ public class GarlicMessageBuilder {
log.warn("No LSK for " + from.toBase32());
return null;
}
- PrivateKey priv = lsk.getDecryptionKey(EncType.ECIES_X25519);
+ PrivateKey priv = lsk.getDecryptionKey(type);
if (priv == null) {
if (log.shouldWarn())
log.warn("No key for " + from.toBase32());
@@ -312,6 +314,9 @@ public class GarlicMessageBuilder {
rskm = (RatchetSKM) skm;
} else if (skm instanceof MuxedSKM) {
rskm = ((MuxedSKM) skm).getECSKM();
+ } else if (skm instanceof MuxedPQSKM) {
+ MuxedPQSKM mskm = (MuxedPQSKM) skm;
+ rskm = type.isPQ() ? mskm.getPQSKM() : mskm.getECSKM();
} else {
if (log.shouldWarn())
log.warn("No SKM for " + from.toBase32());
@@ -338,7 +343,7 @@ public class GarlicMessageBuilder {
/**
* Encrypt from an anonymous source.
- * ECIES_X25519 only.
+ * ECIES_X25519 only. PQ not supported.
* Called by MessageWrapper only.
*
* @param ctx scope
@@ -348,7 +353,7 @@ public class GarlicMessageBuilder {
*/
public static GarlicMessage buildECIESMessage(RouterContext ctx, GarlicConfig config) {
PublicKey key = config.getRecipientPublicKey();
- if (key.getType() != EncType.ECIES_X25519)
+ if (!LeaseSetKeys.SET_EC_PQ_ALL.contains(key.getType()))
throw new IllegalArgumentException();
Log log = ctx.logManager().getLog(GarlicMessageBuilder.class);
GarlicMessage msg = new GarlicMessage(ctx);
diff --git a/router/java/src/net/i2p/router/message/GarlicMessageParser.java b/router/java/src/net/i2p/router/message/GarlicMessageParser.java
index 7aec9cafc..0ddc911d7 100644
--- a/router/java/src/net/i2p/router/message/GarlicMessageParser.java
+++ b/router/java/src/net/i2p/router/message/GarlicMessageParser.java
@@ -19,6 +19,7 @@ import net.i2p.data.PrivateKey;
import net.i2p.data.i2np.GarlicClove;
import net.i2p.data.i2np.GarlicMessage;
import net.i2p.router.RouterContext;
+import net.i2p.router.crypto.ratchet.MuxedPQSKM;
import net.i2p.router.crypto.ratchet.MuxedSKM;
import net.i2p.router.crypto.ratchet.RatchetSKM;
import net.i2p.util.Log;
@@ -44,7 +45,7 @@ public class GarlicMessageParser {
}
/**
- * Supports both ELGAMAL_2048 and ECIES_X25519.
+ * Supports ELGAMAL_2048, ECIES_X25519, and PQ
*
* @param encryptionKey either type
* @param skm use tags from this session key manager
@@ -65,6 +66,8 @@ public class GarlicMessageParser {
rskm = (RatchetSKM) skm;
} else if (skm instanceof MuxedSKM) {
rskm = ((MuxedSKM) skm).getECSKM();
+ } else if (skm instanceof MuxedPQSKM) {
+ rskm = ((MuxedPQSKM) skm).getECSKM();
} else {
if (_log.shouldWarn())
_log.warn("No SKM to decrypt ECIES");
@@ -80,6 +83,27 @@ public class GarlicMessageParser {
_log.warn("ECIES decrypt fail");
return null;
}
+ } else if (type.isPQ()) {
+ RatchetSKM rskm;
+ if (skm instanceof RatchetSKM) {
+ rskm = (RatchetSKM) skm;
+ } else if (skm instanceof MuxedPQSKM) {
+ rskm = ((MuxedPQSKM) skm).getPQSKM();
+ } else {
+ if (_log.shouldWarn())
+ _log.warn("No SKM to decrypt PQ");
+ return null;
+ }
+ CloveSet rv = _context.eciesEngine().decrypt(encData, encryptionKey, rskm);
+ if (rv != null) {
+ if (_log.shouldDebug())
+ _log.debug("PQ decrypt success, cloves: " + rv.getCloveCount());
+ return rv;
+ } else {
+ if (_log.shouldWarn())
+ _log.warn("PQ decrypt fail");
+ return null;
+ }
} else {
if (_log.shouldWarn())
_log.warn("Can't decrypt with key type " + type);
@@ -112,7 +136,7 @@ public class GarlicMessageParser {
/**
* Supports both ELGAMAL_2048 and ECIES_X25519.
*
- * @param elgKey must be ElG, non-null
+ * @param elgKey must be ElG OR PQ, non-null
* @param ecKey must be EC, non-null
* @param skm use tags from this session key manager
* @return null on error
@@ -125,6 +149,10 @@ public class GarlicMessageParser {
if (skm instanceof MuxedSKM) {
MuxedSKM mskm = (MuxedSKM) skm;
rv = _context.eciesEngine().decrypt(encData, elgKey, ecKey, mskm);
+ } else if (skm instanceof MuxedPQSKM) {
+ MuxedPQSKM mskm = (MuxedPQSKM) skm;
+ // EC is first
+ rv = _context.eciesEngine().decrypt(encData, ecKey, elgKey, mskm);
} else if (skm instanceof RatchetSKM) {
// unlikely, if we have two keys we should have a MuxedSKM
RatchetSKM rskm = (RatchetSKM) skm;
diff --git a/router/java/src/net/i2p/router/message/GarlicMessageReceiver.java b/router/java/src/net/i2p/router/message/GarlicMessageReceiver.java
index d2a1600cf..6105f9c0d 100644
--- a/router/java/src/net/i2p/router/message/GarlicMessageReceiver.java
+++ b/router/java/src/net/i2p/router/message/GarlicMessageReceiver.java
@@ -65,15 +65,24 @@ public class GarlicMessageReceiver {
if (keys != null && skm != null) {
decryptionKey = keys.getDecryptionKey();
decryptionKey2 = keys.getDecryptionKey(EncType.ECIES_X25519);
- if (decryptionKey == null && decryptionKey2 == null) {
+ // this will return any of the PQ types
+ PrivateKey decryptionKey3 = keys.getPQDecryptionKey();
+ if (decryptionKey == null && decryptionKey2 == null && decryptionKey3 == null) {
if (_log.shouldWarn())
_log.warn("No key to decrypt for " + _clientDestination.toBase32());
return;
}
+ // ElG + PQ disallowed
if (decryptionKey == null) {
// swap
- decryptionKey = decryptionKey2;
- decryptionKey2 = null;
+ if (decryptionKey3 != null) {
+ // PQ first if present
+ decryptionKey = decryptionKey3;
+ } else {
+ // EC only
+ decryptionKey = decryptionKey2;
+ decryptionKey2 = null;
+ }
}
} else {
if (_log.shouldLog(Log.WARN))
diff --git a/router/java/src/net/i2p/router/message/OutboundClientMessageJobHelper.java b/router/java/src/net/i2p/router/message/OutboundClientMessageJobHelper.java
index 0be622904..e0fe82b31 100644
--- a/router/java/src/net/i2p/router/message/OutboundClientMessageJobHelper.java
+++ b/router/java/src/net/i2p/router/message/OutboundClientMessageJobHelper.java
@@ -124,7 +124,7 @@ class OutboundClientMessageJobHelper {
SessionKeyManager skm = ctx.clientManager().getClientSessionKeyManager(from);
if (skm == null)
return null;
- boolean isECIES = recipientPK.getType() == EncType.ECIES_X25519;
+ boolean isECIES = recipientPK.getType() != EncType.ELGAMAL_2048;
// force ack off if ECIES
boolean ackInGarlic = isECIES ? false : requireAck;
GarlicConfig config = createGarlicConfig(ctx, replyToken, expiration, recipientPK, dataClove,
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java
index 844e3edd0..f5d2cd895 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java
@@ -359,7 +359,7 @@ public class IterativeSearchJob extends FloodSearchJob {
}
LeaseSetKeys lsk = ctx.keyManager().getKeys(_fromLocalDest);
supportsRatchet = lsk != null &&
- lsk.isSupported(EncType.ECIES_X25519) &&
+ (lsk.isSupported(EncType.ECIES_X25519) || lsk.getPQDecryptionKey() != null) &&
DatabaseLookupMessage.supportsRatchetReplies(ri);
supportsElGamal = !supportsRatchet &&
lsk != null &&
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/MessageWrapper.java b/router/java/src/net/i2p/router/networkdb/kademlia/MessageWrapper.java
index 8d0468801..63e0cee7a 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/MessageWrapper.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/MessageWrapper.java
@@ -17,6 +17,7 @@ import net.i2p.data.i2np.I2NPMessage;
import net.i2p.data.router.RouterInfo;
import net.i2p.router.RouterContext;
import net.i2p.router.crypto.TransientSessionKeyManager;
+import net.i2p.router.crypto.ratchet.MuxedPQSKM;
import net.i2p.router.crypto.ratchet.MuxedSKM;
import net.i2p.router.crypto.ratchet.RatchetSKM;
import net.i2p.router.crypto.ratchet.RatchetSessionTag;
@@ -242,6 +243,8 @@ public class MessageWrapper {
rskm = (RatchetSKM) skm;
} else if (skm instanceof MuxedSKM) {
rskm = ((MuxedSKM) skm).getECSKM();
+ } else if (skm instanceof MuxedPQSKM) {
+ rskm = ((MuxedPQSKM) skm).getECSKM();
} else {
throw new IllegalStateException("skm not a ratchet " + skm);
}
diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java b/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java
index 59285498b..237fa6daf 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java
@@ -27,6 +27,7 @@ import net.i2p.router.TunnelInfo;
import net.i2p.router.TunnelManagerFacade;
import net.i2p.router.TunnelPoolSettings;
import net.i2p.router.crypto.ratchet.RatchetSKM;
+import net.i2p.router.crypto.ratchet.MuxedPQSKM;
import net.i2p.router.crypto.ratchet.MuxedSKM;
import net.i2p.router.networkdb.kademlia.MessageWrapper;
import net.i2p.router.networkdb.kademlia.MessageWrapper.OneTimeSession;
@@ -318,6 +319,9 @@ abstract class BuildRequestor {
} else if (replySKM instanceof MuxedSKM) {
MuxedSKM mskm = (MuxedSKM) replySKM;
mskm.tagsReceived(ots.key, ots.rtag, 2 * BUILD_MSG_TIMEOUT);
+ } else if (replySKM instanceof MuxedPQSKM) {
+ MuxedPQSKM mskm = (MuxedPQSKM) replySKM;
+ mskm.tagsReceived(ots.key, ots.rtag, 2 * BUILD_MSG_TIMEOUT);
} else {
// non-EC client, shouldn't happen, checked at top of createTunnelBuildMessage() below
if (log.shouldWarn())
diff --git a/router/java/src/net/i2p/router/tunnel/pool/TestJob.java b/router/java/src/net/i2p/router/tunnel/pool/TestJob.java
index f6a3528ef..37a7f3aba 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/TestJob.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/TestJob.java
@@ -13,6 +13,7 @@ import net.i2p.router.OutNetMessage;
import net.i2p.router.ReplyJob;
import net.i2p.router.RouterContext;
import net.i2p.router.TunnelInfo;
+import net.i2p.router.crypto.ratchet.MuxedPQSKM;
import net.i2p.router.crypto.ratchet.MuxedSKM;
import net.i2p.router.crypto.ratchet.RatchetSessionTag;
import net.i2p.router.crypto.ratchet.RatchetSKM;
@@ -368,6 +369,8 @@ class TestJob extends JobImpl {
rskm = (RatchetSKM) skm;
} else if (skm instanceof MuxedSKM) {
rskm = ((MuxedSKM) skm).getECSKM();
+ } else if (skm instanceof MuxedPQSKM) {
+ rskm = ((MuxedPQSKM) skm).getECSKM();
} else {
// shouldn't happen
rskm = null;