diff --git a/core/java/src/net/i2p/data/DataHelper.java b/core/java/src/net/i2p/data/DataHelper.java index 39f766e6fa..7352595544 100644 --- a/core/java/src/net/i2p/data/DataHelper.java +++ b/core/java/src/net/i2p/data/DataHelper.java @@ -302,19 +302,22 @@ public class DataHelper { * * Properties from the defaults table of props (if any) are not written out by this method. * - * @deprecated unused - * * @param target returned array as specified in data structure spec * @param props source may be null * @return new offset * @throws DataFormatException if any string is over 255 bytes long, or if the total length * (not including the two length bytes) is greater than 65535 bytes. + * @since un-deprecated in 0.9.48 */ - @Deprecated public static int toProperties(byte target[], int offset, Properties props) throws DataFormatException, IOException { - if (props != null) { - OrderedProperties p = new OrderedProperties(); - p.putAll(props); + if (props != null && !props.isEmpty()) { + Properties p; + if (props instanceof OrderedProperties) { + p = props; + } else { + p = new OrderedProperties(); + p.putAll(props); + } ByteArrayOutputStream baos = new ByteArrayOutputStream(p.size() * 64); for (Map.Entry entry : p.entrySet()) { String key = (String) entry.getKey(); diff --git a/history.txt b/history.txt index fb78190157..219fce1101 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,12 @@ +2020-10-03 zzz + * Router: Support building tunnels through ECIES routers (proposal 152) + +2020-09-28 zzz + * Router: Don't unregister a message without a selector (ticket #2771) + +2020-09-27 zzz + * Streaming: Fix tag option handling + 2020-09-26 zzz * JBigI: GMP 6.2.0 for linux 64 bit Zen and Zen2 (ticket #1869) diff --git a/router/java/src/com/southernstorm/noise/protocol/HandshakeState.java b/router/java/src/com/southernstorm/noise/protocol/HandshakeState.java index a173d3663d..1d6a31a65d 100644 --- a/router/java/src/com/southernstorm/noise/protocol/HandshakeState.java +++ b/router/java/src/com/southernstorm/noise/protocol/HandshakeState.java @@ -130,16 +130,19 @@ public class HandshakeState implements Destroyable, Cloneable { public static final String protocolName = "Noise_XKaesobfse+hs2+hs3_25519_ChaChaPoly_SHA256"; public static final String protocolName2 = "Noise_IKelg2+hs2_25519_ChaChaPoly_SHA256"; + public static final String protocolName3 = "Noise_N_25519_ChaChaPoly_SHA256"; private static final String prefix; private final String patternId; public static final String PATTERN_ID_XK = "XK"; public static final String PATTERN_ID_IK = "IK"; + public static final String PATTERN_ID_N = "N"; private static 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; static { // Parse the protocol name into its components. @@ -169,13 +172,21 @@ public class HandshakeState implements Destroyable, Cloneable { PATTERN_IK = Pattern.lookup(id); if (PATTERN_IK == null) throw new IllegalArgumentException("Handshake pattern is not recognized"); + // N + components = protocolName3.split("_"); + id = components[1]; + if (!PATTERN_ID_N.equals(id)) + throw new IllegalArgumentException(); + PATTERN_N = Pattern.lookup(id); + if (PATTERN_N == null) + throw new IllegalArgumentException("Handshake pattern is not recognized"); } /** * Creates a new Noise handshake. * Noise protocol name is hardcoded. * - * @param patternId XK or IK + * @param patternId XK, IK, or N * @param role The role, HandshakeState.INITIATOR or HandshakeState.RESPONDER. * @param xdh The key pair factory for ephemeral keys * @@ -192,6 +203,8 @@ public class HandshakeState implements Destroyable, Cloneable { pattern = PATTERN_XK; else if (patternId.equals(PATTERN_ID_IK)) pattern = PATTERN_IK; + else if (patternId.equals(PATTERN_ID_N)) + pattern = PATTERN_N; else throw new IllegalArgumentException("Handshake pattern is not recognized"); short flags = pattern[0]; diff --git a/router/java/src/com/southernstorm/noise/protocol/Pattern.java b/router/java/src/com/southernstorm/noise/protocol/Pattern.java index 27daaa208c..2b205e4450 100644 --- a/router/java/src/com/southernstorm/noise/protocol/Pattern.java +++ b/router/java/src/com/southernstorm/noise/protocol/Pattern.java @@ -54,6 +54,15 @@ class Pattern { public static final short FLAG_REMOTE_HYBRID = 0x1000; public static final short FLAG_REMOTE_HYBRID_REQ = 0x2000; + private static final short[] noise_pattern_N = { + FLAG_LOCAL_EPHEMERAL | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_REQUIRED, + + E, + ES + }; + private static final short[] noise_pattern_XK = { FLAG_LOCAL_STATIC | FLAG_LOCAL_EPHEMERAL | @@ -96,7 +105,9 @@ class Pattern { */ public static short[] lookup(String name) { - if (name.equals("XK")) + if (name.equals("N")) + return noise_pattern_N; + else if (name.equals("XK")) return noise_pattern_XK; else if (name.equals("IK")) return noise_pattern_IK; diff --git a/router/java/src/com/southernstorm/noise/protocol/SymmetricState.java b/router/java/src/com/southernstorm/noise/protocol/SymmetricState.java index 7883255ce9..cf19dec29e 100644 --- a/router/java/src/com/southernstorm/noise/protocol/SymmetricState.java +++ b/router/java/src/com/southernstorm/noise/protocol/SymmetricState.java @@ -39,10 +39,12 @@ class SymmetricState implements Destroyable, Cloneable { // precalculated hash of the Noise name private static final byte[] INIT_HASH_XK; private static final byte[] INIT_HASH_IK; + private static final byte[] INIT_HASH_N; static { INIT_HASH_XK = initHash(HandshakeState.protocolName); INIT_HASH_IK = initHash(HandshakeState.protocolName2); + INIT_HASH_N = initHash(HandshakeState.protocolName3); } /** @@ -102,6 +104,8 @@ class SymmetricState implements Destroyable, Cloneable { initHash = INIT_HASH_XK; else if (patternId.equals(HandshakeState.PATTERN_ID_IK)) initHash = INIT_HASH_IK; + else if (patternId.equals(HandshakeState.PATTERN_ID_N)) + initHash = INIT_HASH_N; else throw new IllegalArgumentException("Handshake pattern is not recognized"); System.arraycopy(initHash, 0, h, 0, hashLength); diff --git a/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java b/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java index b4456cb209..bd5085667f 100644 --- a/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java +++ b/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java @@ -1,8 +1,15 @@ package net.i2p.data.i2np; -import java.util.Date; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.Properties; + +import com.southernstorm.noise.protocol.HandshakeState; import net.i2p.I2PAppContext; +import net.i2p.crypto.EncType; +import net.i2p.crypto.KeyFactory; import net.i2p.data.Base64; import net.i2p.data.ByteArray; import net.i2p.data.DataFormatException; @@ -11,8 +18,18 @@ import net.i2p.data.Hash; import net.i2p.data.PrivateKey; import net.i2p.data.PublicKey; import net.i2p.data.SessionKey; +import net.i2p.router.RouterContext; /** + * As of 0.9.48, supports two formats. + * The original 222-byte ElGamal format and the new 464-byte ECIES format. + * See proposal 152 for details on the new format. + * + * None of the readXXX() calls are cached. For efficiency, + * they should only be called once. + * + * Original ElGamal format: + * * Holds the unencrypted 222-byte tunnel request record, * with a constructor for ElGamal decryption and a method for ElGamal encryption. * Iterative AES encryption/decryption is done elsewhere. @@ -39,9 +56,45 @@ import net.i2p.data.SessionKey; * bytes 16-527: ElGamal encrypted block (discarding zero bytes at elg[0] and elg[257]) * * + * New ECIES format, ref: proposal 152: + * + * Holds the unencrypted 464-byte tunnel request record, + * with a constructor for ECIES decryption and a method for ECIES encryption. + * Iterative AES encryption/decryption is done elsewhere. + * + * Cleartext: + *
+ *   bytes     0-3: tunnel ID to receive messages as, nonzero
+ *   bytes     4-7: next tunnel ID, nonzero
+ *   bytes    8-39: next router identity hash
+ *   bytes   40-71: AES-256 tunnel layer key
+ *   bytes  72-103: AES-256 tunnel IV key
+ *   bytes 104-135: AES-256 reply key
+ *   bytes 136-151: AES-256 reply IV
+ *   byte      152: flags
+ *   bytes 153-155: more flags, unused, set to 0 for compatibility
+ *   bytes 156-159: request time (in minutes since the epoch, rounded down)
+ *   bytes 160-163: request expiration (in seconds since creation)
+ *   bytes 164-167: next message ID
+ *   bytes   168-x: tunnel build options (Mapping)
+ *   bytes     x-x: other data as implied by flags or options
+ *   bytes   x-463: random padding
+ * 
+ * + * Encrypted: + *
+ *   bytes    0-15: Hop's truncated identity hash
+ *   bytes   16-47: Sender's ephemeral X25519 public key
+ *   bytes  48-511: ChaCha20 encrypted BuildRequestRecord
+ *   bytes 512-527: Poly1305 MAC
+ * 
+ * */ public class BuildRequestRecord { private final byte[] _data; + private final boolean _isEC; + private SessionKey _chachaReplyKey; + private byte[] _chachaReplyAD; /** * If set in the flag byte, any peer may send a message into this tunnel, but if @@ -61,10 +114,11 @@ public class BuildRequestRecord { public static final int PEER_SIZE = 16; /** - * @return 222 bytes, non-null + * @return 222 (ElG) or 464 (ECIES) bytes, non-null */ public byte[] getData() { return _data; } + // Original ElGamal format private static final int OFF_RECV_TUNNEL = 0; private static final int OFF_OUR_IDENT = OFF_RECV_TUNNEL + 4; private static final int OFF_SEND_TUNNEL = OFF_OUR_IDENT + Hash.HASH_LENGTH; @@ -79,7 +133,23 @@ public class BuildRequestRecord { private static final int PADDING_SIZE = 29; // 222 private static final int LENGTH = OFF_SEND_MSG_ID + 4 + PADDING_SIZE; + + // New ECIES format + private static final int OFF_SEND_TUNNEL_EC = OFF_OUR_IDENT; + private static final int OFF_SEND_IDENT_EC = OFF_SEND_TUNNEL_EC + 4; + private static final int OFF_LAYER_KEY_EC = OFF_SEND_IDENT_EC + Hash.HASH_LENGTH; + private static final int OFF_IV_KEY_EC = OFF_LAYER_KEY_EC + SessionKey.KEYSIZE_BYTES; + public static final int OFF_REPLY_KEY_EC = OFF_IV_KEY_EC + SessionKey.KEYSIZE_BYTES; + private static final int OFF_REPLY_IV_EC = OFF_REPLY_KEY_EC + SessionKey.KEYSIZE_BYTES; + private static final int OFF_FLAG_EC = OFF_REPLY_IV_EC + IV_SIZE; + private static final int OFF_REQ_TIME_EC = OFF_FLAG_EC + 4; + private static final int OFF_SEND_MSG_ID_EC = OFF_REQ_TIME_EC + 4; + private static final int OFF_OPTIONS = OFF_SEND_MSG_ID_EC + 4; + private static final int LENGTH_EC = 464; + private static final int MAX_OPTIONS_LENGTH = LENGTH_EC - OFF_OPTIONS; // includes options length + private static final boolean TEST = false; + private static KeyFactory TESTKF; /** what tunnel ID should this receive messages on */ public long readReceiveTunnelId() { @@ -91,7 +161,8 @@ public class BuildRequestRecord { * this specifies the tunnel ID to which the reply should be sent. */ public long readNextTunnelId() { - return DataHelper.fromLong(_data, OFF_SEND_TUNNEL, 4); + int off = _isEC ? OFF_SEND_TUNNEL_EC : OFF_SEND_TUNNEL; + return DataHelper.fromLong(_data, off, 4); } /** @@ -99,10 +170,8 @@ public class BuildRequestRecord { * the gateway to which the reply should be sent. */ public Hash readNextIdentity() { - //byte rv[] = new byte[Hash.HASH_LENGTH]; - //System.arraycopy(_data, OFF_SEND_IDENT, rv, 0, Hash.HASH_LENGTH); - //return new Hash(rv); - return Hash.create(_data, OFF_SEND_IDENT); + int off = _isEC ? OFF_SEND_IDENT_EC : OFF_SEND_IDENT; + return Hash.create(_data, off); } /** @@ -110,7 +179,8 @@ public class BuildRequestRecord { */ public SessionKey readLayerKey() { byte key[] = new byte[SessionKey.KEYSIZE_BYTES]; - System.arraycopy(_data, OFF_LAYER_KEY, key, 0, SessionKey.KEYSIZE_BYTES); + int off = _isEC ? OFF_LAYER_KEY_EC : OFF_LAYER_KEY; + System.arraycopy(_data, off, key, 0, SessionKey.KEYSIZE_BYTES); return new SessionKey(key); } @@ -119,7 +189,8 @@ public class BuildRequestRecord { */ public SessionKey readIVKey() { byte key[] = new byte[SessionKey.KEYSIZE_BYTES]; - System.arraycopy(_data, OFF_IV_KEY, key, 0, SessionKey.KEYSIZE_BYTES); + int off = _isEC ? OFF_IV_KEY_EC : OFF_IV_KEY; + System.arraycopy(_data, off, key, 0, SessionKey.KEYSIZE_BYTES); return new SessionKey(key); } @@ -128,7 +199,8 @@ public class BuildRequestRecord { */ public SessionKey readReplyKey() { byte key[] = new byte[SessionKey.KEYSIZE_BYTES]; - System.arraycopy(_data, OFF_REPLY_KEY, key, 0, SessionKey.KEYSIZE_BYTES); + int off = _isEC ? OFF_REPLY_KEY_EC : OFF_REPLY_KEY; + System.arraycopy(_data, off, key, 0, SessionKey.KEYSIZE_BYTES); return new SessionKey(key); } @@ -137,7 +209,8 @@ public class BuildRequestRecord { */ public byte[] readReplyIV() { byte iv[] = new byte[IV_SIZE]; - System.arraycopy(_data, OFF_REPLY_IV, iv, 0, IV_SIZE); + int off = _isEC ? OFF_REPLY_IV_EC : OFF_REPLY_IV; + System.arraycopy(_data, off, iv, 0, IV_SIZE); return iv; } @@ -147,7 +220,8 @@ public class BuildRequestRecord { * */ public boolean readIsInboundGateway() { - return (_data[OFF_FLAG] & FLAG_UNRESTRICTED_PREV) != 0; + int off = _isEC ? OFF_FLAG_EC : OFF_FLAG; + return (_data[off] & FLAG_UNRESTRICTED_PREV) != 0; } /** @@ -155,14 +229,18 @@ public class BuildRequestRecord { * fields refer to where the reply should be sent. */ public boolean readIsOutboundEndpoint() { - return (_data[OFF_FLAG] & FLAG_OUTBOUND_ENDPOINT) != 0; + int off = _isEC ? OFF_FLAG_EC : OFF_FLAG; + return (_data[off] & FLAG_OUTBOUND_ENDPOINT) != 0; } /** - * Time that the request was sent (ms), truncated to the nearest hour. + * For ElGamal, time that the request was sent (ms), truncated to the nearest hour. + * For ECIES, time that the request was sent (ms), truncated to the nearest minute. * This ignores leap seconds. */ public long readRequestTime() { + if (_isEC) + return DataHelper.fromLong(_data, OFF_REQ_TIME_EC, 4) * (60 * 1000L); return DataHelper.fromLong(_data, OFF_REQ_TIME, 4) * (60 * 60 * 1000L); } @@ -171,7 +249,26 @@ public class BuildRequestRecord { * this specifies the message ID with which the reply should be sent. */ public long readReplyMessageId() { - return DataHelper.fromLong(_data, OFF_SEND_MSG_ID, 4); + int off = _isEC ? OFF_SEND_MSG_ID_EC : OFF_SEND_MSG_ID; + return DataHelper.fromLong(_data, off, 4); + } + + /** + * ECIES only. + * @return null for ElGamal or on error + * @since 0.9.48 + */ + public Properties readOptions() { + if (!_isEC) + return null; + ByteArrayInputStream in = new ByteArrayInputStream(_data, OFF_OPTIONS, MAX_OPTIONS_LENGTH); + try { + return DataHelper.readProperties(in, null); + } catch (DataFormatException dfe) { + return null; + } catch (IOException ioe) { + return null; + } } /** @@ -180,9 +277,14 @@ public class BuildRequestRecord { * bytes 15-527: ElGamal-2048 encrypted block * * + * ElGamal only + * * @return non-null */ public EncryptedBuildRecord encryptRecord(I2PAppContext ctx, PublicKey toKey, Hash toPeer) { + EncType type = toKey.getType(); + if (type != EncType.ELGAMAL_2048) + throw new IllegalArgumentException(); byte[] out = new byte[EncryptedBuildRecord.LENGTH]; System.arraycopy(toPeer.getData(), 0, out, 0, PEER_SIZE); byte encrypted[] = ctx.elGamalEngine().encrypt(_data, toKey); @@ -193,6 +295,63 @@ public class BuildRequestRecord { return new EncryptedBuildRecord(out); } + /** + * Encrypt the record to the specified peer. ECIES only. + * The ChaCha reply key and IV will be available via the getters + * after this call. + * See class javadocs for format. + * See proposal 152. + * + * @return non-null + * @since 0.9.48 + */ + public EncryptedBuildRecord encryptECIESRecord(RouterContext ctx, PublicKey toKey, Hash toPeer) { + EncType type = toKey.getType(); + if (type != EncType.ECIES_X25519) + throw new IllegalArgumentException(); + byte[] out = new byte[EncryptedBuildRecord.LENGTH]; + System.arraycopy(toPeer.getData(), 0, out, 0, PEER_SIZE); + HandshakeState state = null; + try { + KeyFactory kf = TEST ? TESTKF : ctx.commSystem().getXDHFactory(); + state = new HandshakeState(HandshakeState.PATTERN_ID_N, HandshakeState.INITIATOR, kf); + state.getRemotePublicKey().setPublicKey(toKey.getData(), 0); + state.start(); + state.writeMessage(out, PEER_SIZE, _data, 0, LENGTH_EC); + EncryptedBuildRecord rv = new EncryptedBuildRecord(out); + _chachaReplyKey = new SessionKey(state.getChainingKey()); + _chachaReplyAD = new byte[32]; + System.arraycopy(state.getHandshakeHash(), 0, _chachaReplyAD, 0, 32); + return rv; + } catch (GeneralSecurityException gse) { + throw new IllegalStateException("failed", gse); + } finally { + if (state != null) + state.destroy(); + } + } + + /** + * Valid after calling encryptECIESRecord() or after the decrypting constructor + * with an ECIES private key. + * See proposal 152. + * + * @return null if no ECIES encrypt/decrypt operation was performed + * @since 0.9.48 + */ + public SessionKey getChaChaReplyKey() { return _chachaReplyKey; } + + /** + * Valid after calling encryptECIESRecord() or after the decrypting constructor + * with an ECIES private key. + * See proposal 152. + * + * @return null if no ECIES encrypt/decrypt operation was performed + * @since 0.9.48 + */ + public byte[] getChaChaReplyAD() { return _chachaReplyAD; } + + /** * Decrypt the data from the specified record, writing the decrypted record into this instance's * data buffer @@ -200,28 +359,61 @@ public class BuildRequestRecord { * Caller MUST check that first 16 bytes of our hash matches first 16 bytes of encryptedRecord * before calling this. Not checked here. * + * The ChaCha reply key and IV will be available via the getters + * after this call if ourKey is ECIES. + * * @throws DataFormatException on decrypt fail - * @since 0.9.18, was decryptRecord() + * @since 0.9.48 */ - public BuildRequestRecord(I2PAppContext ctx, PrivateKey ourKey, + public BuildRequestRecord(RouterContext ctx, PrivateKey ourKey, EncryptedBuildRecord encryptedRecord) throws DataFormatException { + byte decrypted[]; + EncType type = ourKey.getType(); + if (type == EncType.ELGAMAL_2048) { byte preDecrypt[] = new byte[514]; System.arraycopy(encryptedRecord.getData(), PEER_SIZE, preDecrypt, 1, 256); System.arraycopy(encryptedRecord.getData(), PEER_SIZE + 256, preDecrypt, 258, 256); - byte decrypted[] = ctx.elGamalEngine().decrypt(preDecrypt, ourKey); - if (decrypted != null) { - _data = decrypted; - } else { - throw new DataFormatException("decrypt fail"); + decrypted = ctx.elGamalEngine().decrypt(preDecrypt, ourKey); + _isEC = false; + } else if (type == EncType.ECIES_X25519) { + HandshakeState state = null; + try { + KeyFactory kf = TEST ? TESTKF : ctx.commSystem().getXDHFactory(); + state = new HandshakeState(HandshakeState.PATTERN_ID_N, HandshakeState.RESPONDER, kf); + state.getLocalKeyPair().setPublicKey(ourKey.toPublic().getData(), 0); + state.getLocalKeyPair().setPrivateKey(ourKey.getData(), 0); + state.start(); + decrypted = new byte[LENGTH_EC]; + state.readMessage(encryptedRecord.getData(), PEER_SIZE, EncryptedBuildRecord.LENGTH - PEER_SIZE, + decrypted, 0); + _chachaReplyKey = new SessionKey(state.getChainingKey()); + _chachaReplyAD = new byte[32]; + System.arraycopy(state.getHandshakeHash(), 0, _chachaReplyAD, 0, 32); + } catch (GeneralSecurityException gse) { + throw new DataFormatException("decrypt fail", gse); + } finally { + if (state != null) + state.destroy(); } + _isEC = true; + } else { + throw new DataFormatException("Unsupported EncType " + type); + } + if (decrypted != null) { + _data = decrypted; + } else { + throw new DataFormatException("decrypt fail"); + } } /** * Populate this instance with data. A new buffer is created to contain the data, with the * necessary randomized padding. * + * ElGamal only. ECIES constructor below. + * * @param receiveTunnelId tunnel the current hop will receive messages on - * @param peer current hop's identity + * @param peer current hop's identity, unused, no read() method * @param nextTunnelId id for the next hop, or where we send the reply (if we are the outbound endpoint) * @param nextHop next hop's identity, or where we send the reply (if we are the outbound endpoint) * @param nextMsgId message ID to use when sending on to the next hop (or for the reply) @@ -238,6 +430,7 @@ public class BuildRequestRecord { boolean isOutEndpoint) { byte buf[] = new byte[LENGTH]; _data = buf; + _isEC = false; /* bytes 0-3: tunnel ID to receive messages as * bytes 4-35: local router identity hash @@ -272,10 +465,61 @@ public class BuildRequestRecord { DataHelper.toLong(buf, OFF_REQ_TIME, 4, truncatedHour); DataHelper.toLong(buf, OFF_SEND_MSG_ID, 4, nextMsgId); ctx.random().nextBytes(buf, OFF_SEND_MSG_ID+4, PADDING_SIZE); + } + + /** + * Populate this instance with data. A new buffer is created to contain the data, with the + * necessary randomized padding. + * + * ECIES only. ElGamal constructor above. + * + * @param receiveTunnelId tunnel the current hop will receive messages on + * @param nextTunnelId id for the next hop, or where we send the reply (if we are the outbound endpoint) + * @param nextHop next hop's identity, or where we send the reply (if we are the outbound endpoint) + * @param nextMsgId message ID to use when sending on to the next hop (or for the reply) + * @param layerKey tunnel layer key to be used by the peer + * @param ivKey tunnel IV key to be used by the peer + * @param replyKey key to be used when encrypting the reply to this build request + * @param iv iv to be used when encrypting the reply to this build request + * @param isInGateway are we the gateway of an inbound tunnel? + * @param isOutEndpoint are we the endpoint of an outbound tunnel? + * @param options 296 bytes max when serialized + * @since 0.9.48 + * @throws IllegalArgumentException if options too long + */ + public BuildRequestRecord(I2PAppContext ctx, long receiveTunnelId, long nextTunnelId, Hash nextHop, long nextMsgId, + SessionKey layerKey, SessionKey ivKey, SessionKey replyKey, byte iv[], boolean isInGateway, + boolean isOutEndpoint, Properties options) { + byte buf[] = new byte[LENGTH_EC]; + _data = buf; + _isEC = true; - byte wroteIV[] = readReplyIV(); - if (!DataHelper.eq(iv, wroteIV)) - throw new RuntimeException("foo"); + DataHelper.toLong(buf, OFF_RECV_TUNNEL, 4, receiveTunnelId); + DataHelper.toLong(buf, OFF_SEND_TUNNEL_EC, 4, nextTunnelId); + System.arraycopy(nextHop.getData(), 0, buf, OFF_SEND_IDENT_EC, Hash.HASH_LENGTH); + System.arraycopy(layerKey.getData(), 0, buf, OFF_LAYER_KEY_EC, SessionKey.KEYSIZE_BYTES); + System.arraycopy(ivKey.getData(), 0, buf, OFF_IV_KEY_EC, SessionKey.KEYSIZE_BYTES); + System.arraycopy(replyKey.getData(), 0, buf, OFF_REPLY_KEY_EC, SessionKey.KEYSIZE_BYTES); + System.arraycopy(iv, 0, buf, OFF_REPLY_IV_EC, IV_SIZE); + if (isInGateway) + buf[OFF_FLAG_EC] |= FLAG_UNRESTRICTED_PREV; + else if (isOutEndpoint) + buf[OFF_FLAG_EC] |= FLAG_OUTBOUND_ENDPOINT; + long truncatedMinute = ctx.clock().now(); + // prevent hop identification at top of the minute + truncatedMinute -= ctx.random().nextInt(2048); + // this ignores leap seconds + truncatedMinute /= (60*1000L); + DataHelper.toLong(buf, OFF_REQ_TIME_EC, 4, truncatedMinute); + DataHelper.toLong(buf, OFF_SEND_MSG_ID_EC, 4, nextMsgId); + try { + int off = DataHelper.toProperties(buf, OFF_OPTIONS, options); + int sz = LENGTH_EC - off; + if (sz > 0) + ctx.random().nextBytes(buf, off, sz); + } catch (Exception e) { + throw new IllegalArgumentException("options", e); + } } /** @@ -284,7 +528,8 @@ public class BuildRequestRecord { @Override public String toString() { StringBuilder buf = new StringBuilder(256); - buf.append("BRR "); + buf.append(_isEC ? "ECIES" : "ElGamal"); + buf.append(" BRR "); boolean isIBGW = readIsInboundGateway(); boolean isOBEP = readIsOutboundEndpoint(); if (isIBGW) { @@ -301,10 +546,57 @@ public class BuildRequestRecord { .append(" IV key: ").append(readIVKey()) .append(" reply key: ").append(readReplyKey()) .append(" reply IV: ").append(Base64.encode(readReplyIV())) - .append(" hour: ").append(new Date(readRequestTime())) + .append(" time: ").append(DataHelper.formatTime(readRequestTime())) .append(" reply msg id: ").append(readReplyMessageId()); + if (_isEC) { + buf.append(" options: ").append(readOptions()); + if (_chachaReplyKey != null) { + buf.append(" chacha reply key: ").append(_chachaReplyKey) + .append(" chacha reply IV: ").append(Base64.encode(_chachaReplyAD)); + } + } // to chase i2pd bug //buf.append('\n').append(net.i2p.util.HexDump.dump(readReplyKey().getData())); return buf.toString(); } + +/**** + public static void main(String[] args) throws Exception { + RouterContext ctx = new RouterContext(null); + TESTKF = new net.i2p.router.transport.crypto.X25519KeyFactory(ctx); + byte[] h = new byte[32]; + ctx.random().nextBytes(h); + Hash bh = new Hash(h); + SessionKey k1 = ctx.keyGenerator().generateSessionKey(); + SessionKey k2 = ctx.keyGenerator().generateSessionKey(); + SessionKey k3 = ctx.keyGenerator().generateSessionKey(); + byte[] iv = new byte[16]; + ctx.random().nextBytes(iv); + Properties props = new Properties(); + props.setProperty("foo", "bar"); + BuildRequestRecord brr = new BuildRequestRecord(ctx, 1, 2, bh, 3, k1, k2, k3, iv, false, false, props); + System.out.println(brr.toString()); + System.out.println("\nplaintext request:\n" + net.i2p.util.HexDump.dump(brr.getData())); + net.i2p.crypto.KeyPair kp = ctx.keyGenerator().generatePKIKeys(net.i2p.crypto.EncType.ECIES_X25519); + PublicKey bpub = kp.getPublic(); + PrivateKey bpriv = kp.getPrivate(); + EncryptedBuildRecord record = brr.encryptECIESRecord(ctx, bpub, bh); + System.out.println("\nencrypted request:\n" + net.i2p.util.HexDump.dump(record.getData())); + System.out.println("reply key: " + brr.getChaChaReplyKey()); + System.out.println("reply IV: " + net.i2p.data.Base64.encode(brr.getChaChaReplyAD())); + BuildRequestRecord brr2 = new BuildRequestRecord(ctx, bpriv, record); + System.out.println(brr2.toString()); + System.out.println("\nreply key: " + brr2.getChaChaReplyKey()); + System.out.println("reply IV: " + net.i2p.data.Base64.encode(brr2.getChaChaReplyAD())); + props.setProperty("yes", "no"); + EncryptedBuildRecord ebr = BuildResponseRecord.create(ctx, 1, brr.getChaChaReplyKey(), brr.getChaChaReplyAD(), props); + System.out.println("\nencrypted reply:\n" + net.i2p.util.HexDump.dump(ebr.getData())); + BuildResponseRecord.decrypt(ebr, brr2.getChaChaReplyKey(), brr2.getChaChaReplyAD()); + System.out.println("\nplaintext reply:\n" + net.i2p.util.HexDump.dump(ebr.getData())); + Properties p2 = new net.i2p.util.OrderedProperties(); + DataHelper.fromProperties(ebr.getData(), 0, p2); + System.out.println("reply props: " + p2); + System.out.println("reply status: " + (ebr.getData()[511] & 0xff)); + } +****/ } diff --git a/router/java/src/net/i2p/data/i2np/BuildResponseRecord.java b/router/java/src/net/i2p/data/i2np/BuildResponseRecord.java index ea53655513..0ccb4e8295 100644 --- a/router/java/src/net/i2p/data/i2np/BuildResponseRecord.java +++ b/router/java/src/net/i2p/data/i2np/BuildResponseRecord.java @@ -1,10 +1,14 @@ package net.i2p.data.i2np; +import java.security.GeneralSecurityException; +import java.util.Properties; + +import com.southernstorm.noise.protocol.ChaChaPolyCipherState; + import net.i2p.I2PAppContext; import net.i2p.data.DataHelper; import net.i2p.data.Hash; import net.i2p.data.SessionKey; -//import net.i2p.util.Log; /** * Class that creates an encrypted tunnel build message record. @@ -22,7 +26,8 @@ import net.i2p.data.SessionKey; public class BuildResponseRecord { /** - * Create a new encrypted response + * Create a new encrypted response. + * AES only for ElGamal routers. * * @param status the response 0-255 * @param replyIV 16 bytes @@ -31,17 +36,85 @@ public class BuildResponseRecord { */ public static EncryptedBuildRecord create(I2PAppContext ctx, int status, SessionKey replyKey, byte replyIV[], long responseMessageId) { - //Log log = ctx.logManager().getLog(BuildResponseRecord.class); byte rv[] = new byte[TunnelBuildReplyMessage.RECORD_SIZE]; ctx.random().nextBytes(rv, Hash.HASH_LENGTH, TunnelBuildReplyMessage.RECORD_SIZE - Hash.HASH_LENGTH - 1); rv[TunnelBuildMessage.RECORD_SIZE-1] = (byte) status; // rv = AES(SHA256(padding+status) + padding + status, replyKey, replyIV) ctx.sha().calculateHash(rv, Hash.HASH_LENGTH, rv.length - Hash.HASH_LENGTH, rv, 0); - //if (log.shouldLog(Log.DEBUG)) - // log.debug(responseMessageId + ": before encrypt: " + Base64.encode(rv, 0, 128) + " with " + replyKey.toBase64() + "/" + Base64.encode(replyIV)); ctx.aes().encrypt(rv, 0, rv, 0, replyKey, replyIV, rv.length); - //if (log.shouldLog(Log.DEBUG)) - // log.debug(responseMessageId + ": after encrypt: " + Base64.encode(rv, 0, 128)); return new EncryptedBuildRecord(rv); } + + /** + * Create a new encrypted response. + * ChaCha/Poly only for ECIES routers. + * + * @param status the response 0-255 + * @param replyAD 32 bytes + * @param options 511 bytes max when serialized + * @return a 528-byte response record + * @throws IllegalArgumentException if options too big or on encryption failure + * @since 0.9.48 + */ + public static EncryptedBuildRecord create(I2PAppContext ctx, int status, SessionKey replyKey, + byte replyAD[], Properties options) { + byte rv[] = new byte[TunnelBuildReplyMessage.RECORD_SIZE]; + int off; + try { + off = DataHelper.toProperties(rv, 0, options); + } catch (Exception e) { + throw new IllegalArgumentException("options", e); + } + int sz = TunnelBuildReplyMessage.RECORD_SIZE - off - 1; + if (sz > 0) + ctx.random().nextBytes(rv, off, sz); + else if (sz < 0) + throw new IllegalArgumentException("options"); + rv[TunnelBuildMessage.RECORD_SIZE - 17] = (byte) status; + boolean ok = encryptAEADBlock(replyAD, rv, replyKey); + if (!ok) + throw new IllegalArgumentException("encrypt fail"); + return new EncryptedBuildRecord(rv); + } + + /** + * Encrypts in place + * @param ad non-null + * @return success + * @since 0.9.48 + */ + private static final boolean encryptAEADBlock(byte[] ad, byte data[], SessionKey key) { + ChaChaPolyCipherState chacha = new ChaChaPolyCipherState(); + chacha.initializeKey(key.getData(), 0); + try { + chacha.encryptWithAd(ad, data, 0, data, 0, TunnelBuildReplyMessage.RECORD_SIZE - 16); + } catch (GeneralSecurityException e) { + return false; + } + return true; + } + + /* + * ChaCha/Poly only for ECIES routers. + * Decrypts in place in bytes 0-511. + * Status will be rec.getData()[511]. + * Properties will be at rec.getData()[0]. + * + * @param rec 528 bytes, data will be decrypted in place. + * @param ad non-null + * @return success + * @since 0.9.48 + */ + public static boolean decrypt(EncryptedBuildRecord rec, SessionKey key, byte[] ad) { + ChaChaPolyCipherState chacha = new ChaChaPolyCipherState(); + chacha.initializeKey(key.getData(), 0); + try { + // this is safe to do in-place, it checks the mac before starting decryption + byte[] data = rec.getData(); + chacha.decryptWithAd(ad, data, 0, data, 0, TunnelBuildReplyMessage.RECORD_SIZE); + } catch (GeneralSecurityException e) { + return false; + } + return true; + } } diff --git a/router/java/src/net/i2p/router/LeaseSetKeys.java b/router/java/src/net/i2p/router/LeaseSetKeys.java index 537e36d316..ca55f9f5cb 100644 --- a/router/java/src/net/i2p/router/LeaseSetKeys.java +++ b/router/java/src/net/i2p/router/LeaseSetKeys.java @@ -32,9 +32,17 @@ public class LeaseSetKeys { * @since 0.9.44 */ public static final Set SET_ELG = Collections.unmodifiableSet(EnumSet.of(EncType.ELGAMAL_2048)); + /** + * Unmodifiable, ECIES-X25519 only + * @since public since 0.9.46 + */ public static final Set SET_EC = Collections.unmodifiableSet(EnumSet.of(EncType.ECIES_X25519)); - private static final Set SET_BOTH = Collections.unmodifiableSet(EnumSet.of(EncType.ELGAMAL_2048, EncType.ECIES_X25519)); - private static final Set SET_NONE = Collections.unmodifiableSet(EnumSet.noneOf(EncType.class)); + /** + * Unmodifiable, ElGamal and ECIES-X25519. + * @since public since 0.9.48 + */ + public static final Set SET_BOTH = Collections.unmodifiableSet(EnumSet.of(EncType.ELGAMAL_2048, EncType.ECIES_X25519)); + private static final Set SET_NONE = Collections.emptySet(); /** * Client with a single key diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index f6e6df3a5c..4e28a2c8d2 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -18,7 +18,7 @@ public class RouterVersion { /** deprecated */ public final static String ID = "Monotone"; public final static String VERSION = CoreVersion.VERSION; - public final static long BUILD = 4; + public final static long BUILD = 5; /** for example "-test" */ public final static String EXTRA = ""; diff --git a/router/java/src/net/i2p/router/tunnel/BuildMessageGenerator.java b/router/java/src/net/i2p/router/tunnel/BuildMessageGenerator.java index cf42a8fbb1..0cc9b55231 100644 --- a/router/java/src/net/i2p/router/tunnel/BuildMessageGenerator.java +++ b/router/java/src/net/i2p/router/tunnel/BuildMessageGenerator.java @@ -3,6 +3,8 @@ package net.i2p.router.tunnel; import java.util.List; import net.i2p.I2PAppContext; +import net.i2p.crypto.EncType; +import net.i2p.data.EmptyProperties; import net.i2p.data.Hash; import net.i2p.data.PublicKey; import net.i2p.data.SessionKey; @@ -10,45 +12,13 @@ import net.i2p.data.i2np.BuildRequestRecord; import net.i2p.data.i2np.EncryptedBuildRecord; import net.i2p.data.i2np.I2NPMessage; import net.i2p.data.i2np.TunnelBuildMessage; +import net.i2p.router.RouterContext; /** * Fill in the encrypted BuildRequestRecords in a TunnelBuildMessage */ public abstract class BuildMessageGenerator { - /** return null if it is unable to find a router's public key (etc) */ -/**** - public TunnelBuildMessage createInbound(RouterContext ctx, TunnelCreatorConfig cfg) { - return create(ctx, cfg, null, -1); - } -****/ - - /** return null if it is unable to find a router's public key (etc) */ -/**** - public TunnelBuildMessage createOutbound(RouterContext ctx, TunnelCreatorConfig cfg, Hash replyRouter, long replyTunnel) { - return create(ctx, cfg, replyRouter, replyTunnel); - } -****/ - -/**** - private TunnelBuildMessage create(RouterContext ctx, TunnelCreatorConfig cfg, Hash replyRouter, long replyTunnel) { - TunnelBuildMessage msg = new TunnelBuildMessage(ctx); - List order = new ArrayList(ORDER.length); - for (int i = 0; i < ORDER.length; i++) order.add(ORDER[i]); - Collections.shuffle(order, ctx.random()); - for (int i = 0; i < ORDER.length; i++) { - int hop = ((Integer)order.get(i)).intValue(); - Hash peer = cfg.getPeer(hop); - RouterInfo ri = ctx.netDb().lookupRouterInfoLocally(peer); - if (ri == null) - return null; - createRecord(i, hop, msg, cfg, replyRouter, replyTunnel, ctx, ri.getIdentity().getPublicKey()); - } - layeredEncrypt(ctx, msg, cfg, order); - return msg; - } -****/ - /** * Place the asymmetrically encrypted record in the specified record slot, * containing the hop's configuration (as well as the reply info, if it is an outbound endpoint) @@ -61,29 +31,35 @@ public abstract class BuildMessageGenerator { */ public static void createRecord(int recordNum, int hop, TunnelBuildMessage msg, TunnelCreatorConfig cfg, Hash replyRouter, - long replyTunnel, I2PAppContext ctx, PublicKey peerKey) { - //Log log = ctx.logManager().getLog(BuildMessageGenerator.class); + long replyTunnel, RouterContext ctx, PublicKey peerKey) { EncryptedBuildRecord erec; if (peerKey != null) { - BuildRequestRecord req = null; + boolean isEC = peerKey.getType() == EncType.ECIES_X25519; + BuildRequestRecord req; if ( (!cfg.isInbound()) && (hop + 1 == cfg.getLength()) ) //outbound endpoint - req = createUnencryptedRecord(ctx, cfg, hop, replyRouter, replyTunnel); + req = createUnencryptedRecord(ctx, cfg, hop, replyRouter, replyTunnel, isEC); else - req = createUnencryptedRecord(ctx, cfg, hop, null, -1); + req = createUnencryptedRecord(ctx, cfg, hop, null, -1, isEC); if (req == null) throw new IllegalArgumentException("hop bigger than config"); Hash peer = cfg.getPeer(hop); - //if (log.shouldLog(Log.DEBUG)) - // log.debug("Record " + recordNum + "/" + hop + "/" + peer.toBase64() - // + ": unencrypted = " + Base64.encode(req.getData().getData())); - erec = req.encryptRecord(ctx, peerKey, peer); - //if (log.shouldLog(Log.DEBUG)) - // log.debug("Record " + recordNum + "/" + hop + ": encrypted = " + Base64.encode(encrypted)); + if (isEC) { + erec = req.encryptECIESRecord(ctx, peerKey, peer); + cfg.setChaChaReplyKeys(hop, req.getChaChaReplyKey(), req.getChaChaReplyAD()); + } else { + erec = req.encryptRecord(ctx, peerKey, peer); + } } else { - //if (log.shouldLog(Log.DEBUG)) - // log.debug("Record " + recordNum + "/" + hop + "/ is blank/random"); byte encrypted[] = new byte[TunnelBuildMessage.RECORD_SIZE]; - ctx.random().nextBytes(encrypted); + if (cfg.isInbound() && hop + 1 == cfg.getLength()) { // IBEP + System.arraycopy(cfg.getPeer(hop).getData(), 0, encrypted, 0, BuildRequestRecord.PEER_SIZE); + ctx.random().nextBytes(encrypted, BuildRequestRecord.PEER_SIZE, TunnelBuildMessage.RECORD_SIZE - BuildRequestRecord.PEER_SIZE); + byte[] h = new byte[Hash.HASH_LENGTH]; + ctx.sha().calculateHash(encrypted, 0, TunnelBuildMessage.RECORD_SIZE, h, 0); + cfg.setBlankHash(new Hash(h)); + } else { + ctx.random().nextBytes(encrypted); + } erec = new EncryptedBuildRecord(encrypted); } msg.setRecord(recordNum, erec); @@ -93,8 +69,7 @@ public abstract class BuildMessageGenerator { * Returns null if hop >= cfg.length */ private static BuildRequestRecord createUnencryptedRecord(I2PAppContext ctx, TunnelCreatorConfig cfg, int hop, - Hash replyRouter, long replyTunnel) { - //Log log = ctx.logManager().getLog(BuildMessageGenerator.class); + Hash replyRouter, long replyTunnel, boolean isEC) { if (hop < cfg.getLength()) { // ok, now lets fill in some data HopConfig hopConfig = cfg.getConfig(hop); @@ -138,15 +113,17 @@ public abstract class BuildMessageGenerator { // dont care about these intermediary hops nextMsgId = ctx.random().nextLong(I2NPMessage.MAX_ID_VALUE); } - - //if (log.shouldLog(Log.DEBUG)) - // log.debug("Hop " + hop + " has the next message ID of " + nextMsgId + " for " + cfg - // + " with replyKey " + replyKey.toBase64() + " and replyIV " + Base64.encode(iv)); - - BuildRequestRecord rec= new BuildRequestRecord(ctx, recvTunnelId, peer, nextTunnelId, nextPeer, - nextMsgId, layerKey, ivKey, replyKey, - iv, isInGW, isOutEnd); - + BuildRequestRecord rec; + if (isEC) { + // TODO pass properties from cfg + rec = new BuildRequestRecord(ctx, recvTunnelId, nextTunnelId, nextPeer, + nextMsgId, layerKey, ivKey, replyKey, + iv, isInGW, isOutEnd, EmptyProperties.INSTANCE); + } else { + rec = new BuildRequestRecord(ctx, recvTunnelId, peer, nextTunnelId, nextPeer, + nextMsgId, layerKey, ivKey, replyKey, + iv, isInGW, isOutEnd); + } return rec; } else { return null; @@ -163,34 +140,25 @@ public abstract class BuildMessageGenerator { */ public static void layeredEncrypt(I2PAppContext ctx, TunnelBuildMessage msg, TunnelCreatorConfig cfg, List order) { - //Log log = ctx.logManager().getLog(BuildMessageGenerator.class); // encrypt the records so that the right elements will be visible at the right time for (int i = 0; i < msg.getRecordCount(); i++) { EncryptedBuildRecord rec = msg.getRecord(i); Integer hopNum = order.get(i); int hop = hopNum.intValue(); - if ( (isBlank(cfg, hop)) || (!cfg.isInbound() && hop == 1) ) { - //if (log.shouldLog(Log.DEBUG)) - // log.debug(msg.getUniqueId() + ": not pre-decrypting record " + i + "/" + hop + " for " + cfg); + if ((isBlank(cfg, hop) && !(cfg.isInbound() && hop + 1 == cfg.getLength())) || + (!cfg.isInbound() && hop == 1)) { continue; } - //if (log.shouldLog(Log.DEBUG)) - // log.debug(msg.getUniqueId() + ": pre-decrypting record " + i + "/" + hop + " for " + cfg); // ok, now decrypt the record with all of the reply keys from cfg.getConfig(0) through hop-1 int stop = (cfg.isInbound() ? 0 : 1); for (int j = hop-1; j >= stop; j--) { HopConfig hopConfig = cfg.getConfig(j); SessionKey key = hopConfig.getReplyKey(); byte iv[] = hopConfig.getReplyIV(); - //if (log.shouldLog(Log.DEBUG)) - // log.debug(msg.getUniqueId() + ": pre-decrypting record " + i + "/" + hop + " for " + cfg - // + " with " + key.toBase64() + "/" + Base64.encode(iv)); // corrupts the SDS ctx.aes().decrypt(rec.getData(), 0, rec.getData(), 0, key, iv, TunnelBuildMessage.RECORD_SIZE); } } - //if (log.shouldLog(Log.DEBUG)) - // log.debug(msg.getUniqueId() + ": done pre-decrypting all records for " + cfg); } public static boolean isBlank(TunnelCreatorConfig cfg, int hop) { diff --git a/router/java/src/net/i2p/router/tunnel/BuildMessageProcessor.java b/router/java/src/net/i2p/router/tunnel/BuildMessageProcessor.java index 4240407904..f499bca36a 100644 --- a/router/java/src/net/i2p/router/tunnel/BuildMessageProcessor.java +++ b/router/java/src/net/i2p/router/tunnel/BuildMessageProcessor.java @@ -1,6 +1,5 @@ package net.i2p.router.tunnel; -import net.i2p.I2PAppContext; import net.i2p.crypto.EncType; import net.i2p.data.Base64; import net.i2p.data.DataFormatException; @@ -11,6 +10,7 @@ import net.i2p.data.SessionKey; import net.i2p.data.i2np.BuildRequestRecord; import net.i2p.data.i2np.EncryptedBuildRecord; import net.i2p.data.i2np.TunnelBuildMessage; +import net.i2p.router.RouterContext; import net.i2p.router.RouterThrottleImpl; import net.i2p.router.util.DecayingBloomFilter; import net.i2p.router.util.DecayingHashSet; @@ -28,11 +28,11 @@ import net.i2p.util.SystemVersion; * */ public class BuildMessageProcessor { - private final I2PAppContext ctx; + private final RouterContext ctx; private final Log log; private final DecayingBloomFilter _filter; - public BuildMessageProcessor(I2PAppContext ctx) { + public BuildMessageProcessor(RouterContext ctx) { this.ctx = ctx; log = ctx.logManager().getLog(getClass()); _filter = selectFilter(); @@ -89,10 +89,6 @@ public class BuildMessageProcessor { * @return the current hop's decrypted record or null on failure */ public BuildRequestRecord decrypt(TunnelBuildMessage msg, Hash ourHash, PrivateKey privKey) { - // TODO proposal 152 - if (privKey.getType() != EncType.ELGAMAL_2048) - return null; - BuildRequestRecord rv = null; int ourHop = -1; long beforeActualDecrypt = 0; @@ -101,8 +97,7 @@ public class BuildMessageProcessor { long beforeLoop = System.currentTimeMillis(); for (int i = 0; i < msg.getRecordCount(); i++) { EncryptedBuildRecord rec = msg.getRecord(i); - int len = BuildRequestRecord.PEER_SIZE; - boolean eq = DataHelper.eq(ourHashData, 0, rec.getData(), 0, len); + boolean eq = DataHelper.eq(ourHashData, 0, rec.getData(), 0, BuildRequestRecord.PEER_SIZE); if (eq) { beforeActualDecrypt = System.currentTimeMillis(); try { @@ -121,7 +116,9 @@ public class BuildMessageProcessor { // The spec says to feed the 32-byte AES-256 reply key into the Bloom filter. // But we were using the first 32 bytes of the encrypted reply. // Fixed in 0.9.24 - boolean isDup = _filter.add(rv.getData(), BuildRequestRecord.OFF_REPLY_KEY, 32); + boolean isEC = ctx.keyManager().getPrivateKey().getType() == EncType.ECIES_X25519; + int off = isEC ? BuildRequestRecord.OFF_REPLY_KEY_EC : BuildRequestRecord.OFF_REPLY_KEY; + boolean isDup = _filter.add(rv.getData(), off, 32); if (isDup) { if (log.shouldLog(Log.WARN)) log.warn(msg.getUniqueId() + ": Dup record: " + rv); @@ -135,8 +132,10 @@ public class BuildMessageProcessor { // TODO should we keep looking for a second match and fail if found? break; } catch (DataFormatException dfe) { + // For ECIES routers, this is relatively common due to old routers that don't + // check enc type sending us ElG requests if (log.shouldLog(Log.WARN)) - log.warn(msg.getUniqueId() + ": Matching record decrypt failure", dfe); + log.warn(msg.getUniqueId() + ": Matching record decrypt failure " + privKey.getType(), dfe); // on the microscopic chance that there's another router // out there with the same first 16 bytes, go around again continue; @@ -146,7 +145,7 @@ public class BuildMessageProcessor { if (rv == null) { // none of the records matched, b0rk if (log.shouldLog(Log.WARN)) - log.warn(msg.getUniqueId() + ": No matching record"); + log.warn(msg.getUniqueId() + ": No record decrypted"); return null; } diff --git a/router/java/src/net/i2p/router/tunnel/BuildReplyHandler.java b/router/java/src/net/i2p/router/tunnel/BuildReplyHandler.java index 8514d6db1a..6ba5681e3f 100644 --- a/router/java/src/net/i2p/router/tunnel/BuildReplyHandler.java +++ b/router/java/src/net/i2p/router/tunnel/BuildReplyHandler.java @@ -7,6 +7,7 @@ import net.i2p.data.Base64; import net.i2p.data.DataHelper; import net.i2p.data.Hash; import net.i2p.data.SessionKey; +import net.i2p.data.i2np.BuildResponseRecord; import net.i2p.data.i2np.EncryptedBuildRecord; import net.i2p.data.i2np.TunnelBuildReplyMessage; import net.i2p.util.Log; @@ -50,19 +51,34 @@ public class BuildReplyHandler { for (int i = 0; i < rv.length; i++) { int hop = recordOrder.get(i).intValue(); if (BuildMessageGenerator.isBlank(cfg, hop)) { - // self... + // self or unused... if (log.shouldLog(Log.DEBUG)) - log.debug(reply.getUniqueId() + ": no need to decrypt record " + i + "/" + hop + ", as its out of range: " + cfg); - rv[i] = 0; + log.debug(reply.getUniqueId() + ": skipping record " + i + "/" + hop + " for: " + cfg); + if (cfg.isInbound() && hop + 1 == cfg.getLength()) { // IBEP + byte[] h1 = new byte[Hash.HASH_LENGTH]; + ctx.sha().calculateHash(reply.getRecord(i).getData(), 0, TunnelBuildReplyMessage.RECORD_SIZE, h1, 0); + // get stored hash put here by BuildMessageGenerator + Hash h2 = cfg.getBlankHash(); + if (h2 != null && DataHelper.eq(h1, h2.getData())) { + rv[i] = 0; + } else { + if (log.shouldWarn()) + log.warn("IBEP record corrupt on " + cfg); + // Caller doesn't check value for this hop so fail the whole thing + return null; + } + } else { + rv[i] = 0; + } } else { int ok = decryptRecord(reply, cfg, i, hop); if (ok == -1) { if (log.shouldLog(Log.WARN)) - log.warn(reply.getUniqueId() + ": decrypt record " + i + "/" + hop + " was not ok: " + cfg); + log.warn(reply.getUniqueId() + ": decrypt record " + i + "/" + hop + " fail: " + cfg); return null; } else { if (log.shouldLog(Log.DEBUG)) - log.debug(reply.getUniqueId() + ": decrypt record " + i + "/" + hop + " was ok: " + ok + " for " + cfg); + log.debug(reply.getUniqueId() + ": decrypt record " + i + "/" + hop + " success: " + ok + " for " + cfg); } rv[i] = ok; } @@ -75,22 +91,23 @@ public class BuildReplyHandler { * * Note that this layer-decrypts the build records in-place. * Do not call this more than once for a given message. + * Do not call for blank hops. * * @return the status 0-255, or -1 on decrypt failure */ private int decryptRecord(TunnelBuildReplyMessage reply, TunnelCreatorConfig cfg, int recordNum, int hop) { - if (BuildMessageGenerator.isBlank(cfg, hop)) { - if (log.shouldLog(Log.DEBUG)) - log.debug(reply.getUniqueId() + ": Record " + recordNum + "/" + hop + " is fake, so consider it valid..."); - return 0; - } EncryptedBuildRecord rec = reply.getRecord(recordNum); byte[] data = rec.getData(); int start = cfg.getLength() - 1; if (cfg.isInbound()) start--; // the last hop in an inbound tunnel response doesn't actually encrypt + int end = hop; + boolean isEC = cfg.isEC(hop); + // chacha decrypt after the loop + if (isEC) + end++; // do we need to adjust this for the endpoint? - for (int j = start; j >= hop; j--) { + for (int j = start; j >= end; j--) { HopConfig hopConfig = cfg.getConfig(j); SessionKey replyKey = hopConfig.getReplyKey(); byte replyIV[] = hopConfig.getReplyIV(); @@ -106,24 +123,41 @@ public class BuildReplyHandler { } // ok, all of the layered encryption is stripped, so lets verify it // (formatted per BuildResponseRecord.create) - // don't cache the result - //Hash h = ctx.sha().calculateHash(data, off + Hash.HASH_LENGTH, TunnelBuildReplyMessage.RECORD_SIZE-Hash.HASH_LENGTH); - byte[] h = SimpleByteCache.acquire(Hash.HASH_LENGTH); - ctx.sha().calculateHash(data, Hash.HASH_LENGTH, TunnelBuildReplyMessage.RECORD_SIZE-Hash.HASH_LENGTH, h, 0); - boolean ok = DataHelper.eq(h, 0, data, 0, Hash.HASH_LENGTH); - if (!ok) { - if (log.shouldLog(Log.DEBUG)) - log.debug(reply.getUniqueId() + ": Failed verification on " + recordNum + "/" + hop + ": " + Base64.encode(h) + " calculated, " + - Base64.encode(data, 0, Hash.HASH_LENGTH) + " expected\n" + - "Record: " + Base64.encode(data, Hash.HASH_LENGTH, TunnelBuildReplyMessage.RECORD_SIZE-Hash.HASH_LENGTH)); - SimpleByteCache.release(h); - return -1; + int rv; + if (isEC) { + // For last iteration, do ChaCha instead + SessionKey replyKey = cfg.getChaChaReplyKey(hop); + byte[] replyIV = cfg.getChaChaReplyAD(hop); + if (log.shouldDebug()) + log.debug(reply.getUniqueId() + ": Decrypting chacha/poly record " + recordNum + "/" + hop + " with replyKey " + + replyKey.toBase64() + "/" + Base64.encode(replyIV) + ": " + cfg); + boolean ok = BuildResponseRecord.decrypt(rec, replyKey, replyIV); + if (!ok) { + if (log.shouldWarn()) + log.debug(reply.getUniqueId() + ": chacha reply decrypt fail on " + recordNum + "/" + hop); + return -1; + } + // reply properties TODO + rv = data[TunnelBuildReplyMessage.RECORD_SIZE - 17] & 0xff; } else { + // don't cache the result + //Hash h = ctx.sha().calculateHash(data, off + Hash.HASH_LENGTH, TunnelBuildReplyMessage.RECORD_SIZE-Hash.HASH_LENGTH); + byte[] h = SimpleByteCache.acquire(Hash.HASH_LENGTH); + ctx.sha().calculateHash(data, Hash.HASH_LENGTH, TunnelBuildReplyMessage.RECORD_SIZE-Hash.HASH_LENGTH, h, 0); + boolean ok = DataHelper.eq(h, 0, data, 0, Hash.HASH_LENGTH); + if (!ok) { + if (log.shouldWarn()) + log.warn(reply.getUniqueId() + ": sha256 reply verify fail on " + recordNum + "/" + hop + ": " + Base64.encode(h) + " calculated, " + + Base64.encode(data, 0, Hash.HASH_LENGTH) + " expected\n" + + "Record: " + Base64.encode(data, Hash.HASH_LENGTH, TunnelBuildReplyMessage.RECORD_SIZE-Hash.HASH_LENGTH)); + SimpleByteCache.release(h); + return -1; + } SimpleByteCache.release(h); - int rv = data[TunnelBuildReplyMessage.RECORD_SIZE - 1] & 0xff; - if (log.shouldLog(Log.DEBUG)) - log.debug(reply.getUniqueId() + ": Verified: " + rv + " for record " + recordNum + "/" + hop); - return rv; + rv = data[TunnelBuildReplyMessage.RECORD_SIZE - 1] & 0xff; } + if (log.shouldLog(Log.DEBUG)) + log.debug(reply.getUniqueId() + ": Verified: " + rv + " for record " + recordNum + "/" + hop); + return rv; } } diff --git a/router/java/src/net/i2p/router/tunnel/HopConfig.java b/router/java/src/net/i2p/router/tunnel/HopConfig.java index 63b3d10269..b19e7b1bb0 100644 --- a/router/java/src/net/i2p/router/tunnel/HopConfig.java +++ b/router/java/src/net/i2p/router/tunnel/HopConfig.java @@ -10,6 +10,9 @@ import net.i2p.data.TunnelId; /** * Defines the general configuration for a hop in a tunnel. * + * This is used for both participating tunnels and tunnels we create. + * Data only stored for tunnels we create should be in + * TunnelCreatorConfig to save space. */ public class HopConfig { private byte _receiveTunnelId[]; @@ -87,19 +90,26 @@ public class HopConfig { public SessionKey getIVKey() { return _ivKey; } public void setIVKey(SessionKey key) { _ivKey = key; } - /** key to encrypt the reply sent for the new tunnel creation crypto */ + /** + * Key to encrypt the reply sent for the tunnel creation crypto. + * Not used for participating tunnels, will return null, + * candidate for moving to TunnelCreatorConfig. + * @return key or null + */ public SessionKey getReplyKey() { return _replyKey; } public void setReplyKey(SessionKey key) { _replyKey = key; } /** - * IV used to encrypt the reply sent for the new tunnel creation crypto + * IV used to encrypt the reply sent for the tunnel creation crypto. + * Not used for participating tunnels, will return null, + * candidate for moving to TunnelCreatorConfig. * - * @return 16 bytes + * @return 16 bytes or null */ public byte[] getReplyIV() { return _replyIV; } /** - * IV used to encrypt the reply sent for the new tunnel creation crypto + * IV used to encrypt the reply sent for the tunnel creation crypto * * @throws IllegalArgumentException if not 16 bytes */ diff --git a/router/java/src/net/i2p/router/tunnel/TunnelCreatorConfig.java b/router/java/src/net/i2p/router/tunnel/TunnelCreatorConfig.java index 07a5e84d2a..175f275446 100644 --- a/router/java/src/net/i2p/router/tunnel/TunnelCreatorConfig.java +++ b/router/java/src/net/i2p/router/tunnel/TunnelCreatorConfig.java @@ -6,6 +6,7 @@ import java.util.Properties; import net.i2p.data.Base64; import net.i2p.data.Hash; +import net.i2p.data.SessionKey; import net.i2p.data.TunnelId; import net.i2p.router.RouterContext; import net.i2p.router.TunnelInfo; @@ -39,6 +40,10 @@ public abstract class TunnelCreatorConfig implements TunnelInfo { //private final double _peakThroughput[] = new double[THROUGHPUT_COUNT]; private long _peakThroughputCurrentTotal; private long _peakThroughputLastCoallesce = System.currentTimeMillis(); + private Hash _blankHash; + private SessionKey[] _replyKeys; + private byte[][] _replyADs; + // Make configurable? - but can't easily get to pool options from here private static final int MAX_CONSECUTIVE_TEST_FAILURES = 3; @@ -238,6 +243,61 @@ public abstract class TunnelCreatorConfig implements TunnelInfo { */ public void setPriority(int priority) { _priority = priority; } + /** + * Checksum for blank record + * @since 0.9.48 + */ + public Hash getBlankHash() { return _blankHash; } + + /** + * Checksum for blank record + * @since 0.9.48 + */ + public void setBlankHash(Hash h) { _blankHash = h; } + + /** + * Set ECIES reply key and IV + * @since 0.9.48 + */ + public void setChaChaReplyKeys(int hop, SessionKey key, byte[] ad) { + if (_replyKeys == null) { + _replyKeys = new SessionKey[_config.length]; + _replyADs = new byte[_config.length][]; + } + _replyKeys[hop] = key; + _replyADs[hop] = ad; + } + + /** + * Is it an ECIES hop? + * @since 0.9.48 + */ + public boolean isEC(int hop) { + if (_replyKeys == null) + return false; + return _replyKeys[hop] != null; + } + + /** + * Get ECIES reply key + * @since 0.9.48 + */ + public SessionKey getChaChaReplyKey(int hop) { + if (_replyKeys == null) + return null; + return _replyKeys[hop]; + } + + /** + * Get ECIES reply AD + * @since 0.9.48 + */ + public byte[] getChaChaReplyAD(int hop) { + if (_replyADs == null) + return null; + return _replyADs[hop]; + } + @Override public String toString() { // H0:1235-->H1:2345-->H2:2345 @@ -253,7 +313,7 @@ public abstract class TunnelCreatorConfig implements TunnelInfo { buf.append(": GW "); for (int i = 0; i < _peers.length; i++) { buf.append(_peers[i].toBase64().substring(0,4)); - buf.append(':'); + buf.append(isEC(i) ? " EC:" : " ElG:"); if (_config[i].getReceiveTunnel() != null) buf.append(_config[i].getReceiveTunnel()); else diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java b/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java index 18e69bb31d..791ed87984 100644 --- a/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java +++ b/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java @@ -1,12 +1,15 @@ package net.i2p.router.tunnel.pool; import java.util.List; +import java.util.Properties; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicInteger; +import net.i2p.crypto.EncType; import net.i2p.data.Base64; import net.i2p.data.DataHelper; +import net.i2p.data.EmptyProperties; import net.i2p.data.Hash; import net.i2p.data.router.RouterIdentity; import net.i2p.data.router.RouterInfo; @@ -96,6 +99,7 @@ class BuildHandler implements Runnable { private static final long MAX_REQUEST_FUTURE = 5*60*1000; /** must be > 1 hour due to rouding down */ private static final long MAX_REQUEST_AGE = 65*60*1000; + private static final long MAX_REQUEST_AGE_ECIES = 8*60*1000; private static final long JOB_LAG_LIMIT_TUNNEL = 350; @@ -441,6 +445,7 @@ class BuildHandler implements Runnable { _context.statManager().addRateData("tunnel.corruptBuildReply", 1); // don't leak _exec.buildComplete(cfg); + // TODO blame everybody } } @@ -718,13 +723,24 @@ class BuildHandler implements Runnable { } } - // time is in hours, rounded down. - // tunnel-alt-creation.html specifies that this is enforced +/- 1 hour but it was not. - // As of 0.9.16, allow + 5 minutes to - 65 minutes. long time = req.readRequestTime(); long now = _context.clock().now(); - long roundedNow = (now / (60l*60l*1000l)) * (60*60*1000); - long timeDiff = roundedNow - time; + boolean isEC = _context.keyManager().getPrivateKey().getType() == EncType.ECIES_X25519; + long timeDiff; + long maxAge; + if (isEC) { + // time is in minutes, rounded down. + long roundedNow = (now / (60*1000L)) * (60*1000); + timeDiff = roundedNow - time; + maxAge = MAX_REQUEST_AGE_ECIES; + } else { + // time is in hours, rounded down. + // tunnel-alt-creation.html specifies that this is enforced +/- 1 hour but it was not. + // As of 0.9.16, allow + 5 minutes to - 65 minutes. + long roundedNow = (now / (60*60*1000L)) * (60*60*1000); + timeDiff = roundedNow - time; + maxAge = MAX_REQUEST_AGE; + } if (timeDiff > MAX_REQUEST_AGE) { _context.statManager().addRateData("tunnel.rejectTooOld", 1); if (_log.shouldLog(Log.WARN)) @@ -915,7 +931,14 @@ class BuildHandler implements Runnable { + " after " + recvDelay + " with " + response + " from " + (from != null ? from : "tunnel") + ": " + req); - EncryptedBuildRecord reply = BuildResponseRecord.create(_context, response, req.readReplyKey(), req.readReplyIV(), state.msg.getUniqueId()); + EncryptedBuildRecord reply; + if (isEC) { + // TODO options + Properties props = EmptyProperties.INSTANCE; + reply = BuildResponseRecord.create(_context, response, req.getChaChaReplyKey(), req.getChaChaReplyAD(), props); + } else { + reply = BuildResponseRecord.create(_context, response, req.readReplyKey(), req.readReplyIV(), state.msg.getUniqueId()); + } int records = state.msg.getRecordCount(); int ourSlot = -1; for (int j = 0; j < records; j++) { diff --git a/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java b/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java index fb4d8cce51..9d6f69a9d3 100644 --- a/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java +++ b/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java @@ -19,6 +19,7 @@ import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; import net.i2p.data.Hash; import net.i2p.data.router.RouterInfo; +import net.i2p.router.LeaseSetKeys; import net.i2p.router.Router; import net.i2p.router.RouterContext; import net.i2p.router.TunnelPoolSettings; @@ -97,7 +98,7 @@ public abstract class TunnelPeerSelector extends ConnectChecker { /** * For debugging, also possibly for restricted routes? * Needs analysis and testing - * @return should always be false + * @return usually false */ protected boolean shouldSelectExplicit(TunnelPoolSettings settings) { if (settings.isExploratory()) return false; @@ -116,7 +117,7 @@ public abstract class TunnelPeerSelector extends ConnectChecker { /** * For debugging, also possibly for restricted routes? * Needs analysis and testing - * @return should always be false + * @return the peers */ protected List selectExplicit(TunnelPoolSettings settings, int length) { String peers = null; @@ -138,8 +139,7 @@ public abstract class TunnelPeerSelector extends ConnectChecker { if (ctx.profileOrganizer().isSelectable(peer)) { rv.add(peer); } else { - if (log.shouldLog(Log.DEBUG)) - log.debug("Explicit peer is not selectable: " + peerStr); + log.logAlways(Log.WARN, "Explicit peer is not selectable: " + peerStr); } } catch (DataFormatException dfe) { if (log.shouldLog(Log.ERROR)) @@ -148,10 +148,24 @@ public abstract class TunnelPeerSelector extends ConnectChecker { } int sz = rv.size(); - Collections.shuffle(rv, ctx.random()); + if (sz == 0) { + log.logAlways(Log.WARN, "No valid explicit peers found, building zero hop"); + } else if (sz > 1) { + Collections.shuffle(rv, ctx.random()); + } - while (rv.size() > length) + while (rv.size() > length) { rv.remove(0); + } + if (rv.size() < length) { + int more = length - rv.size(); + Set exclude = getExclude(settings.isInbound(), settings.isExploratory()); + exclude.addAll(rv); + Set matches = new HashSet(more); + ctx.profileOrganizer().selectFastPeers(more, exclude, matches, 0); + rv.addAll(matches); + Collections.shuffle(rv, ctx.random()); + } if (log.shouldLog(Log.INFO)) { StringBuilder buf = new StringBuilder(); @@ -471,7 +485,8 @@ public abstract class TunnelPeerSelector extends ConnectChecker { maxLen++; if (cap.length() <= maxLen) return true; - if (peer.getIdentity().getPublicKey().getType() != EncType.ELGAMAL_2048) + EncType type = peer.getIdentity().getPublicKey().getType(); + if (!LeaseSetKeys.SET_BOTH.contains(type)) return true; // otherwise, it contains flags we aren't trying to focus on, diff --git a/router/java/test/junit/net/i2p/router/tunnel/BuildMessageTestStandalone.java b/router/java/test/junit/net/i2p/router/tunnel/BuildMessageTestStandalone.java index 7f7e3f4efa..649cbe7ba9 100644 --- a/router/java/test/junit/net/i2p/router/tunnel/BuildMessageTestStandalone.java +++ b/router/java/test/junit/net/i2p/router/tunnel/BuildMessageTestStandalone.java @@ -18,6 +18,7 @@ import net.i2p.data.i2np.BuildResponseRecord; import net.i2p.data.i2np.EncryptedBuildRecord; import net.i2p.data.i2np.TunnelBuildMessage; import net.i2p.data.i2np.TunnelBuildReplyMessage; +import net.i2p.router.RouterContext; import net.i2p.util.Log; /** @@ -41,7 +42,7 @@ public class BuildMessageTestStandalone extends TestCase { private long _replyTunnel; public void testBuildMessage() { - I2PAppContext ctx = I2PAppContext.getGlobalContext(); + RouterContext ctx = new RouterContext(null); Log log = ctx.logManager().getLog(getClass()); List order = pickOrder();