I2P changes for ratchet
Some checks failed
Java CI / build (push) Has been cancelled
Java CI / javadoc-latest (push) Has been cancelled
Java CI / build-java7 (push) Has been cancelled
Java with IzPack Snapshot Setup / setup (push) Has been cancelled
Sync Primary Repository to GitHub Mirror / sync (push) Has been cancelled

This commit is contained in:
zzz
2025-06-13 08:56:52 -04:00
parent d664af0a32
commit e3b176b676
26 changed files with 1483 additions and 73 deletions

View File

@ -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)

View File

@ -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);
%>
<tr>
<th colspan="2" <%=ehdisabled%>>
@ -617,7 +620,25 @@
<option title="<%=intl._t("This is the default, recommended option")%>" value="4" <%=((has4 && !has0) ? " selected=\"selected\"" : edisabled)%> >
ECIES-X25519</option>
<option value="4,0" <%=((has0 && has4) ? " selected=\"selected\"" : edisabled)%> >
<%=intl._t("Both encryption types")%></option>
ECIES-X25519 + ElGamal-2048</option>
<%
if (editBean.isAdvanced()) {
%>
<option title="<%=intl._t("experimental")%>" value="5" <%=((has5 && !has4) ? " selected=\"selected\"" : edisabled)%> >
MLKEM512-X25519</option>
<option title="<%=intl._t("experimental")%>" value="6" <%=((has6 && !has4) ? " selected=\"selected\"" : edisabled)%> >
MLKEM768-X25519</option>
<option title="<%=intl._t("experimental")%>" value="7" <%=((has7 && !has4) ? " selected=\"selected\"" : edisabled)%> >
MLKEM1024-X25519</option>
<option title="<%=intl._t("experimental")%>" value="5,4" <%=((has5 && has4) ? " selected=\"selected\"" : edisabled)%> >
MLKEM512-X25519 + ECIES-X25519</option>
<option title="<%=intl._t("experimental")%>" value="6,4" <%=((has6 && has4) ? " selected=\"selected\"" : edisabled)%> >
MLKEM768-X25519 + ECIES-X25519</option>
<option title="<%=intl._t("experimental")%>" value="7,4" <%=((has7 && has4) ? " selected=\"selected\"" : edisabled)%> >
MLKEM1024-X25519 + ECIES-X25519</option>
<%
} // isAdvanced()
%>
</select>
</td>
</tr>

View File

@ -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\" ";
%>
<tr>
@ -690,7 +693,25 @@
<option title="<%=intl._t("This is the default, recommended option")%>" value="4" <%=((has4 && !has0) ? " selected=\"selected\"" : edisabled)%> >
ECIES-X25519</option>
<option value="4,0" <%=((has0 && has4) ? " selected=\"selected\"" : edisabled)%> >
<%=intl._t("Both encryption types")%></option>
ECIES-X25519 + ElGamal-2048</option>
<%
if (editBean.isAdvanced()) {
%>
<option title="<%=intl._t("experimental")%>" value="5" <%=((has5 && !has4) ? " selected=\"selected\"" : edisabled)%> >
MLKEM512-X25519</option>
<option title="<%=intl._t("experimental")%>" value="6" <%=((has6 && !has4) ? " selected=\"selected\"" : edisabled)%> >
MLKEM768-X25519</option>
<option title="<%=intl._t("experimental")%>" value="7" <%=((has7 && !has4) ? " selected=\"selected\"" : edisabled)%> >
MLKEM1024-X25519</option>
<option title="<%=intl._t("experimental")%>" value="5,4" <%=((has5 && has4) ? " selected=\"selected\"" : edisabled)%> >
MLKEM512-X25519 + ECIES-X25519</option>
<option title="<%=intl._t("experimental")%>" value="6,4" <%=((has6 && has4) ? " selected=\"selected\"" : edisabled)%> >
MLKEM768-X25519 + ECIES-X25519</option>
<option title="<%=intl._t("experimental")%>" value="7,4" <%=((has7 && has4) ? " selected=\"selected\"" : edisabled)%> >
MLKEM1024-X25519 + ECIES-X25519</option>
<%
} // isAdvanced()
%>
</select>
</td>
</tr>

View File

@ -887,7 +887,7 @@
windowtitle="I2P Anonymous Network - Java Documentation - API Version ${api.version}">
<group title="Core SDK (i2p.jar)" packages="net.i2p:net.i2p.*:net.i2p.client:net.i2p.client.*:net.i2p.internal:net.i2p.internal.*:freenet.support.CPUInformation:gnu.crypto.*:gnu.getopt:gnu.gettext:com.nettgryppa.security:net.i2p.apache.http.conn.ssl:net.i2p.apache.http.conn.util:net.i2p.apache.http.util:org.json.simple:com.southernstorm.noise.crypto.x25519:com.southernstorm.noise.crypto.chacha20:org.minidns:org.minidns.*" />
<group title="Streaming Library" packages="net.i2p.client.streaming:net.i2p.client.streaming.impl" />
<group title="Router" packages="net.i2p.router:net.i2p.router.*:net.i2p.data.i2np:net.i2p.data.router:org.cybergarage:org.cybergarage.*:org.freenetproject:org.xlattice.crypto.filters:com.maxmind.*:com.southernstorm.noise.*" />
<group title="Router" packages="net.i2p.router:net.i2p.router.*:net.i2p.data.i2np:net.i2p.data.router:org.cybergarage:org.cybergarage.*:org.freenetproject:org.xlattice.crypto.filters:com.maxmind.*:com.southernstorm.noise.*:org.bouncycastle:org.bouncycastle.*" />
<group title="Router Console" packages="net.i2p.router.web:net.i2p.router.web.*:net.i2p.router.update:edu.internet2.ndt:net.i2p.router.news:com.vuze.*" />
<!-- apps and bridges starting here, alphabetical please -->
<group title="Addressbook Application" packages="net.i2p.addressbook:net.i2p.router.naming:net.metanotion:net.metanotion.*" />

View File

@ -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;

View File

@ -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 {

View File

@ -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++) {

View File

@ -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.
*/

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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<EncType> SET_BOTH = Collections.unmodifiableSet(EnumSet.of(EncType.ELGAMAL_2048, EncType.ECIES_X25519));
private static final Set<EncType> SET_NONE = Collections.emptySet();
/**
* Unmodifiable, PQ only
* @since public since 0.9.67
*/
public static final Set<EncType> SET_PQ1 = Collections.unmodifiableSet(EnumSet.of(EncType.MLKEM512_X25519));
/**
* Unmodifiable, PQ only
* @since public since 0.9.67
*/
public static final Set<EncType> SET_PQ2 = Collections.unmodifiableSet(EnumSet.of(EncType.MLKEM768_X25519));
/**
* Unmodifiable, PQ only
* @since public since 0.9.67
*/
public static final Set<EncType> 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<EncType> 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<EncType> 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<EncType> 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<EncType> 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<PrivateKey> 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<EncType> 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;
}
}

View File

@ -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);

View File

@ -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<EncType> 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())

View File

@ -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;
}
}

View File

@ -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<SessionTag> sessionTags) {
return null;
}
/**
* ElG only
*/
@Override
public void tagsReceived(SessionKey key, Set<SessionTag> sessionTags) {
}
/**
* ElG only
*/
@Override
public void tagsReceived(SessionKey key, Set<SessionTag> 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) {
}
}

View File

@ -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<PublicKey, OutboundSession>(64);
_pendingOutboundSessions = new HashMap<PublicKey, List<OutboundSession>>(64);
_inboundTagSets = new ConcurrentHashMap<RatchetSessionTag, RatchetTagSet>(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<RatchetTagSet> 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

View File

@ -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, &gt; 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);

View File

@ -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;

View File

@ -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))

View File

@ -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,

View File

@ -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 &&

View File

@ -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);
}

View File

@ -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())

View File

@ -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;