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;