From a7d9ca920f35f51840c0ecd599b00ba957720dc8 Mon Sep 17 00:00:00 2001 From: zzz <zzz@i2pmail.org> Date: Fri, 16 Jul 2021 12:28:04 -0400 Subject: [PATCH] Prop 157 updates - Don't require AES keys for short records - Derive keys from noise ck - Use derived keys to garlic-encrypt reply at OBEP - Register reply key with SKM - Only use short message for client tunnels if client supports EC - Set nonce for chacha/poly reply record - Add tagsReceived() for single tag to MuxedSKM - Add extended TunnelCreatorConfig.toStringFull() - BRR toString() enhancements - Test enhancements --- .../net/i2p/data/i2np/BuildRequestRecord.java | 106 ++++++++++++++++-- .../i2p/data/i2np/BuildResponseRecord.java | 72 ++++++++++-- .../i2p/router/crypto/ratchet/MuxedSKM.java | 10 ++ .../router/tunnel/TunnelCreatorConfig.java | 41 +++++++ .../i2p/router/tunnel/pool/BuildHandler.java | 33 +++--- .../tunnel/pool/BuildMessageGenerator.java | 23 ++-- .../router/tunnel/pool/BuildReplyHandler.java | 6 +- .../router/tunnel/pool/BuildRequestor.java | 73 ++++++++++-- .../pool/BuildMessageTestStandalone.java | 49 ++++---- 9 files changed, 338 insertions(+), 75 deletions(-) diff --git a/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java b/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java index 595b140b6a..8a66b86607 100644 --- a/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java +++ b/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java @@ -9,6 +9,7 @@ import com.southernstorm.noise.protocol.HandshakeState; import net.i2p.I2PAppContext; import net.i2p.crypto.EncType; +import net.i2p.crypto.HKDF; import net.i2p.crypto.KeyFactory; import net.i2p.data.Base64; import net.i2p.data.ByteArray; @@ -20,12 +21,14 @@ import net.i2p.data.PublicKey; import net.i2p.data.SessionKey; import net.i2p.util.Log; import net.i2p.router.RouterContext; +import net.i2p.router.crypto.ratchet.RatchetSessionTag; +import net.i2p.router.networkdb.kademlia.MessageWrapper.OneTimeSession; /** * As of 0.9.48, supports two formats. * As of 0.9.51, supports three formats. * The original 222-byte ElGamal format, the new 464-byte ECIES format, - * and the newest 172-byte ECIES format. + * and the newest 154-byte ECIES format. * See proposal 152 and 157 for details on the new formats. * * None of the readXXX() calls are cached. For efficiency, @@ -128,6 +131,10 @@ public class BuildRequestRecord { private final boolean _isEC; private SessionKey _chachaReplyKey; private byte[] _chachaReplyAD; + // derived keys for short records + private SessionKey _derivedLayerKey; + private SessionKey _derivedIVKey; + private OneTimeSession _derivedGarlicKeys; /** * If set in the flag byte, any peer may send a message into this tunnel, but if @@ -195,6 +202,12 @@ public class BuildRequestRecord { // 16 byte trunc. hash, 32 byte eph. key, 16 byte MAC private static final int LENGTH_EC_SHORT = ShortTunnelBuildMessage.SHORT_RECORD_SIZE - (16 + 32 + 16); private static final int MAX_OPTIONS_LENGTH_SHORT = LENGTH_EC_SHORT - OFF_OPTIONS_SHORT; // includes options length + // short record HKDF + private static final byte[] ZEROLEN = new byte[0]; + private static final String INFO_1 = "SMTunnelReplyKey"; + private static final String INFO_2 = "SMTunnelLayerKey"; + private static final String INFO_3 = "TunnelLayerIVKey"; + private static final String INFO_4 = "RGarlicKeyAndTag"; private static final boolean TEST = false; private static KeyFactory TESTKF; @@ -226,6 +239,8 @@ public class BuildRequestRecord { * Tunnel layer encryption key that the current hop should use */ public SessionKey readLayerKey() { + if (_data.length == LENGTH_EC_SHORT) + return _derivedLayerKey; byte key[] = new byte[SessionKey.KEYSIZE_BYTES]; int off = _isEC ? OFF_LAYER_KEY_EC : OFF_LAYER_KEY; System.arraycopy(_data, off, key, 0, SessionKey.KEYSIZE_BYTES); @@ -236,6 +251,8 @@ public class BuildRequestRecord { * Tunnel IV encryption key that the current hop should use */ public SessionKey readIVKey() { + if (_data.length == LENGTH_EC_SHORT) + return _derivedIVKey; byte key[] = new byte[SessionKey.KEYSIZE_BYTES]; int off = _isEC ? OFF_IV_KEY_EC : OFF_IV_KEY; System.arraycopy(_data, off, key, 0, SessionKey.KEYSIZE_BYTES); @@ -345,7 +362,7 @@ public class BuildRequestRecord { return null; } } - + /** * ECIES short record only. * @return 0 for ElGamal or ECIES long record @@ -357,6 +374,15 @@ public class BuildRequestRecord { return 0; } + /** + * ECIES short OBEP record only. + * @return null for ElGamal or ECIES long record or non-OBEP + * @since 0.9.51 + */ + public OneTimeSession readGarlicKeys() { + return _derivedGarlicKeys; + } + /** * Encrypt the record to the specified peer. The result is formatted as: <pre> * bytes 0-15: truncated SHA-256 of the current hop's identity (the toPeer parameter) @@ -385,8 +411,10 @@ public class BuildRequestRecord { * Encrypt the record to the specified peer. ECIES only. * The ChaCha reply key and IV will be available via the getters * after this call. + * For short records, derived keys will be available via + * readLayerKey(), readIVKey(), and readGarlicKeys() after this call. * See class javadocs for format. - * See proposal 152. + * See proposals 152 and 157. * * @return non-null * @since 0.9.48 @@ -406,9 +434,37 @@ public class BuildRequestRecord { state.start(); state.writeMessage(out, PEER_SIZE, _data, 0, _data.length); EncryptedBuildRecord rv = isShort ? new ShortEncryptedBuildRecord(out) : new EncryptedBuildRecord(out); - _chachaReplyKey = new SessionKey(state.getChainingKey()); _chachaReplyAD = new byte[32]; System.arraycopy(state.getHandshakeHash(), 0, _chachaReplyAD, 0, 32); + byte[] ck = state.getChainingKey(); + if (isShort) { + byte[] crk = new byte[32]; + byte[] newck = new byte[32]; + HKDF hkdf = new HKDF(ctx); + hkdf.calculate(ck, ZEROLEN, INFO_1, newck, crk, 0); + _chachaReplyKey = new SessionKey(crk); + System.arraycopy(newck, 0, ck, 0, 32); + byte[] dlk = new byte[32]; + hkdf.calculate(ck, ZEROLEN, INFO_2, newck, dlk, 0); + _derivedLayerKey = new SessionKey(dlk); + boolean isOBEP = readIsOutboundEndpoint(); + if (isOBEP) { + System.arraycopy(newck, 0, ck, 0, 32); + byte[] divk = new byte[32]; + hkdf.calculate(ck, ZEROLEN, INFO_3, newck, divk, 0); + _derivedIVKey = new SessionKey(divk); + System.arraycopy(newck, 0, ck, 0, 32); + byte[] dgk = new byte[32]; + hkdf.calculate(ck, ZEROLEN, INFO_4, newck, dgk, 0); + SessionKey sdgk = new SessionKey(dgk); + RatchetSessionTag rst = new RatchetSessionTag(newck); + _derivedGarlicKeys = new OneTimeSession(sdgk, rst); + } else { + _derivedIVKey = new SessionKey(newck); + } + } else { + _chachaReplyKey = new SessionKey(ck); + } return rv; } catch (GeneralSecurityException gse) { throw new IllegalStateException("failed", gse); @@ -491,9 +547,37 @@ public class BuildRequestRecord { decrypted = new byte[isShort ? LENGTH_EC_SHORT : LENGTH_EC]; state.readMessage(encrypted, PEER_SIZE, len - PEER_SIZE, decrypted, 0); - _chachaReplyKey = new SessionKey(state.getChainingKey()); _chachaReplyAD = new byte[32]; System.arraycopy(state.getHandshakeHash(), 0, _chachaReplyAD, 0, 32); + byte[] ck = state.getChainingKey(); + if (isShort) { + byte[] crk = new byte[32]; + byte[] newck = new byte[32]; + HKDF hkdf = new HKDF(ctx); + hkdf.calculate(ck, ZEROLEN, INFO_1, newck, crk, 0); + _chachaReplyKey = new SessionKey(crk); + System.arraycopy(newck, 0, ck, 0, 32); + byte[] dlk = new byte[32]; + hkdf.calculate(ck, ZEROLEN, INFO_2, newck, dlk, 0); + _derivedLayerKey = new SessionKey(dlk); + boolean isOBEP = (decrypted[OFF_FLAG_EC_SHORT] & FLAG_OUTBOUND_ENDPOINT) != 0; + if (isOBEP) { + System.arraycopy(newck, 0, ck, 0, 32); + byte[] divk = new byte[32]; + hkdf.calculate(ck, ZEROLEN, INFO_3, newck, divk, 0); + _derivedIVKey = new SessionKey(divk); + System.arraycopy(newck, 0, ck, 0, 32); + byte[] dgk = new byte[32]; + hkdf.calculate(ck, ZEROLEN, INFO_4, newck, dgk, 0); + SessionKey sdgk = new SessionKey(dgk); + RatchetSessionTag rst = new RatchetSessionTag(newck); + _derivedGarlicKeys = new OneTimeSession(sdgk, rst); + } else { + _derivedIVKey = new SessionKey(newck); + } + } else { + _chachaReplyKey = new SessionKey(ck); + } } catch (GeneralSecurityException gse) { if (state != null) { Log log = ctx.logManager().getLog(BuildRequestRecord.class); @@ -690,7 +774,7 @@ public class BuildRequestRecord { StringBuilder buf = new StringBuilder(256); buf.append(_isEC ? "ECIES" : "ElGamal"); if (_data.length == LENGTH_EC_SHORT) - buf.append(" short "); + buf.append(" short"); buf.append(" BRR "); boolean isIBGW = readIsInboundGateway(); boolean isOBEP = readIsOutboundEndpoint(); @@ -704,10 +788,10 @@ public class BuildRequestRecord { .append(" out: ").append(readNextTunnelId()); } buf.append(" to: ").append(readNextIdentity()); + buf.append(" layer key: ").append(readLayerKey()) + .append(" IV key: ").append(readIVKey()); if (_data.length != LENGTH_EC_SHORT) { - buf.append(" layer key: ").append(readLayerKey()) - .append(" IV key: ").append(readIVKey()) - .append(" reply key: ").append(readReplyKey()) + buf.append(" reply key: ").append(readReplyKey()) .append(" reply IV: ").append(Base64.encode(readReplyIV())); } buf.append(" time: ").append(DataHelper.formatTime(readRequestTime())) @@ -719,6 +803,10 @@ public class BuildRequestRecord { buf.append(" chacha reply key: ").append(_chachaReplyKey) .append(" chacha reply IV: ").append(Base64.encode(_chachaReplyAD)); } + if (_derivedGarlicKeys != null) { + buf.append(" garlic reply key: ").append(_derivedGarlicKeys.key) + .append(" garlic reply tag: ").append(_derivedGarlicKeys.rtag); + } } // to chase i2pd bug //buf.append('\n').append(net.i2p.util.HexDump.dump(readReplyKey().getData())); diff --git a/router/java/src/net/i2p/data/i2np/BuildResponseRecord.java b/router/java/src/net/i2p/data/i2np/BuildResponseRecord.java index 9d6a6a3224..18cf50a057 100644 --- a/router/java/src/net/i2p/data/i2np/BuildResponseRecord.java +++ b/router/java/src/net/i2p/data/i2np/BuildResponseRecord.java @@ -84,12 +84,13 @@ public class BuildResponseRecord { * @param status the response 0-255 * @param replyAD 32 bytes * @param options 116 bytes max when serialized + * @param slot the slot number, 0-7 * @return a 218-byte response record * @throws IllegalArgumentException if options too big or on encryption failure * @since 0.9.51 */ public static ShortEncryptedBuildRecord createShort(I2PAppContext ctx, int status, SessionKey replyKey, - byte replyAD[], Properties options) { + byte replyAD[], Properties options, int slot) { byte rv[] = new byte[ShortTunnelBuildMessage.SHORT_RECORD_SIZE]; int off; try { @@ -103,7 +104,7 @@ public class BuildResponseRecord { else if (sz < 0) throw new IllegalArgumentException("options"); rv[ShortTunnelBuildMessage.SHORT_RECORD_SIZE - 17] = (byte) status; - boolean ok = encryptAEADBlock(replyAD, rv, replyKey); + boolean ok = encryptAEADBlock(replyAD, rv, replyKey, slot); if (!ok) throw new IllegalArgumentException("encrypt fail"); return new ShortEncryptedBuildRecord(rv); @@ -111,14 +112,16 @@ public class BuildResponseRecord { /** * Encrypts in place. - * Handles both standard (528) and short (218) byte records as of 0.9.51. + * Handles standard (528) byte records only. * * @param ad non-null - * @param data 528 or 218 bytes, data will be encrypted in place. + * @param data 528 bytes, data will be encrypted in place. * @return success * @since 0.9.48 */ private static final boolean encryptAEADBlock(byte[] ad, byte data[], SessionKey key) { + if (data.length != EncryptedBuildRecord.LENGTH) + throw new IllegalArgumentException(); ChaChaPolyCipherState chacha = new ChaChaPolyCipherState(); chacha.initializeKey(key.getData(), 0); try { @@ -131,19 +134,74 @@ public class BuildResponseRecord { /* * ChaCha/Poly only for ECIES routers. - * Handles both standard (528) and short (218) byte records as of 0.9.51. + * Handles standard (528) byte records only. * Decrypts in place. - * Status will be rec.getData()[511 or 219]. + * Status will be rec.getData()[511]. * Properties will be at rec.getData()[0]. * - * @param rec 528 or 218 bytes, data will be decrypted in place. + * @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) { + if (rec.length() != EncryptedBuildRecord.LENGTH) + throw new IllegalArgumentException(); + 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, rec.length()); + } catch (GeneralSecurityException e) { + return false; + } + return true; + } + + /** + * Encrypts in place. + * Handles short (218) byte records only. + * + * @param ad non-null + * @param data 218 bytes, data will be encrypted in place. + * @param nonce the slot number, 0-7 + * @return success + * @since 0.9.51 + */ + private static final boolean encryptAEADBlock(byte[] ad, byte data[], SessionKey key, int nonce) { + if (data.length != ShortEncryptedBuildRecord.LENGTH || nonce < 0 || nonce > 7) + throw new IllegalArgumentException(); + ChaChaPolyCipherState chacha = new ChaChaPolyCipherState(); + chacha.initializeKey(key.getData(), 0); + chacha.setNonce(nonce); + try { + chacha.encryptWithAd(ad, data, 0, data, 0, data.length - 16); + } catch (GeneralSecurityException e) { + return false; + } + return true; + } + + /* + * ChaCha/Poly only for ECIES routers. + * Handles short (218) byte records only. + * Decrypts in place. + * Status will be rec.getData()[201]. + * Properties will be at rec.getData()[0]. + * + * @param rec 218 bytes, data will be decrypted in place. + * @param ad non-null + * @param nonce the slot number, 0-7 + * @return success + * @since 0.9.51 + */ + public static boolean decrypt(EncryptedBuildRecord rec, SessionKey key, byte[] ad, int nonce) { + if (rec.length() != ShortEncryptedBuildRecord.LENGTH || nonce < 0 || nonce > 7) + throw new IllegalArgumentException(); ChaChaPolyCipherState chacha = new ChaChaPolyCipherState(); chacha.initializeKey(key.getData(), 0); + chacha.setNonce(nonce); try { // this is safe to do in-place, it checks the mac before starting decryption byte[] data = rec.getData(); diff --git a/router/java/src/net/i2p/router/crypto/ratchet/MuxedSKM.java b/router/java/src/net/i2p/router/crypto/ratchet/MuxedSKM.java index 903392b24c..26277b58ce 100644 --- a/router/java/src/net/i2p/router/crypto/ratchet/MuxedSKM.java +++ b/router/java/src/net/i2p/router/crypto/ratchet/MuxedSKM.java @@ -215,6 +215,16 @@ public class MuxedSKM extends SessionKeyManager { _elg.tagsReceived(key, sessionTags, expire); } + /** + * EC only + * One time session + * @param expire time from now + * @since 0.9.51 + */ + public void tagsReceived(SessionKey key, RatchetSessionTag tag, long expire) { + _ec.tagsReceived(key, tag, expire); + } + @Override public SessionKey consumeTag(SessionTag tag) { SessionKey rv = _elg.consumeTag(tag); diff --git a/router/java/src/net/i2p/router/tunnel/TunnelCreatorConfig.java b/router/java/src/net/i2p/router/tunnel/TunnelCreatorConfig.java index a6716f04e4..47364a8563 100644 --- a/router/java/src/net/i2p/router/tunnel/TunnelCreatorConfig.java +++ b/router/java/src/net/i2p/router/tunnel/TunnelCreatorConfig.java @@ -10,6 +10,7 @@ import net.i2p.data.SessionKey; import net.i2p.data.TunnelId; import net.i2p.router.RouterContext; import net.i2p.router.TunnelInfo; +import net.i2p.router.networkdb.kademlia.MessageWrapper.OneTimeSession; /** * Coordinate the info that the tunnel creator keeps track of, including what @@ -45,6 +46,8 @@ public abstract class TunnelCreatorConfig implements TunnelInfo { private byte[][] _ChaReplyADs; private final SessionKey[] _AESReplyKeys; private final byte[][] _AESReplyIVs; + // short record OBEP only + private OneTimeSession _garlicReplyKeys; /** * IV length for {@link #getAESReplyIV} @@ -270,6 +273,7 @@ public abstract class TunnelCreatorConfig implements TunnelInfo { /** * Key to encrypt the reply sent for the tunnel creation crypto. + * Null for short build record. * * @return key or null * @throws IllegalArgumentException if iv not 16 bytes @@ -279,6 +283,7 @@ public abstract class TunnelCreatorConfig implements TunnelInfo { /** * IV used to encrypt the reply sent for the tunnel creation crypto. + * Null for short build record. * * @return 16 bytes or null * @since 0.9.48 moved from HopConfig @@ -340,6 +345,24 @@ public abstract class TunnelCreatorConfig implements TunnelInfo { return _ChaReplyADs[hop]; } + /** + * ECIES short OBEP record only. + * @return null for ElGamal or ECIES long record or non-OBEP + * @since 0.9.51 + */ + public void setGarlicReplyKeys(OneTimeSession keys) { + _garlicReplyKeys = keys; + } + + /** + * ECIES short OBEP record only. + * @return null for ElGamal or ECIES long record or non-OBEP + * @since 0.9.51 + */ + public OneTimeSession getGarlicReplyKeys() { + return _garlicReplyKeys; + } + @Override public String toString() { // H0:1235-->H1:2345-->H2:2345 @@ -382,4 +405,22 @@ public abstract class TunnelCreatorConfig implements TunnelInfo { buf.append(" with ").append(_failures).append(" failures"); return buf.toString(); } + + /** + * @since 0.9.51 + */ + public String toStringFull() { + StringBuilder buf = new StringBuilder(1024); + buf.append(toString()); + for (int i = 0; i < _peers.length; i++) { + if (i == 0) + buf.append("\nGW "); + else if (i == _peers.length - 1) + buf.append("\nEP "); + else + buf.append("\nHop ").append(i); + buf.append(": ").append(_config[i]); + } + return buf.toString(); + } } 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 90ae30fa58..9cf128ae5e 100644 --- a/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java +++ b/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java @@ -34,6 +34,7 @@ import net.i2p.router.OutNetMessage; import net.i2p.router.RouterContext; import net.i2p.router.crypto.ratchet.RatchetSessionTag; import net.i2p.router.networkdb.kademlia.MessageWrapper; +import net.i2p.router.networkdb.kademlia.MessageWrapper.OneTimeSession; import net.i2p.router.peermanager.TunnelHistory; import net.i2p.router.tunnel.HopConfig; import net.i2p.router.tunnel.TunnelDispatcher; @@ -956,6 +957,14 @@ class BuildHandler implements Runnable { + " after " + recvDelay + " with " + response + " from " + (from != null ? from : "tunnel") + ": " + req); + int records = state.msg.getRecordCount(); + int ourSlot = -1; + for (int j = 0; j < records; j++) { + if (state.msg.getRecord(j) == null) { + ourSlot = j; + break; + } + } EncryptedBuildRecord reply; if (isEC) { // TODO options @@ -966,29 +975,14 @@ class BuildHandler implements Runnable { _log.warn("Unsupported STBM"); return; } - if (isOutEnd) { - // reply will be sent in plaintext in a OTBRM, see below - reply = null; - } else { - reply = BuildResponseRecord.createShort(_context, response, req.getChaChaReplyKey(), req.getChaChaReplyAD(), props); - } + reply = BuildResponseRecord.createShort(_context, response, req.getChaChaReplyKey(), req.getChaChaReplyAD(), props, ourSlot); } else { 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++) { - if (state.msg.getRecord(j) == null) { - ourSlot = j; - if (!(isOutEnd && state.msg.getType() == ShortTunnelBuildMessage.MESSAGE_TYPE)) - state.msg.setRecord(j, reply); - // else reply will be sent in plaintext - break; - } - } + state.msg.setRecord(ourSlot, reply); if (_log.shouldLog(Log.DEBUG)) _log.debug("Read slot " + ourSlot + " containing: " + req @@ -1026,8 +1020,9 @@ class BuildHandler implements Runnable { I2NPMessage outMessage; if (state.msg.getType() == ShortTunnelBuildMessage.MESSAGE_TYPE) { // garlic encrypt - SessionKey sk = null; // TODO - RatchetSessionTag st = null; // TODO + OneTimeSession ots = req.readGarlicKeys(); + SessionKey sk = ots.key; + RatchetSessionTag st = ots.rtag; outMessage = MessageWrapper.wrap(_context, replyMsg, sk, st); if (outMessage == null) { if (_log.shouldWarn()) diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildMessageGenerator.java b/router/java/src/net/i2p/router/tunnel/pool/BuildMessageGenerator.java index 1ab9117db9..b000cd73f4 100644 --- a/router/java/src/net/i2p/router/tunnel/pool/BuildMessageGenerator.java +++ b/router/java/src/net/i2p/router/tunnel/pool/BuildMessageGenerator.java @@ -46,7 +46,6 @@ abstract class BuildMessageGenerator { boolean isEC = peerKey.getType() == EncType.ECIES_X25519; BuildRequestRecord req; if ( (!cfg.isInbound()) && (hop + 1 == cfg.getLength()) ) //outbound endpoint - /// TODO if isEC && isShort req = createUnencryptedRecord(ctx, cfg, hop, replyRouter, replyTunnel, isEC, isShort); else req = createUnencryptedRecord(ctx, cfg, hop, null, -1, isEC, isShort); @@ -55,8 +54,15 @@ abstract class BuildMessageGenerator { Hash peer = cfg.getPeer(hop); if (isEC) { erec = req.encryptECIESRecord(ctx, peerKey, peer); - // TODO if isShort, set derived keys in coonfig cfg.setChaChaReplyKeys(hop, req.getChaChaReplyKey(), req.getChaChaReplyAD()); + if (isShort) { + // save derived keys + HopConfig hopConfig = cfg.getConfig(hop); + hopConfig.setLayerKey(req.readLayerKey()); + hopConfig.setIVKey(req.readIVKey()); + if (!cfg.isInbound() && hop + 1 == cfg.getLength()) //outbound endpoint + cfg.setGarlicReplyKeys(req.readGarlicKeys()); + } } else { erec = req.encryptRecord(ctx, peerKey, peer); } @@ -114,11 +120,6 @@ abstract class BuildMessageGenerator { } SessionKey layerKey = hopConfig.getLayerKey(); SessionKey ivKey = hopConfig.getIVKey(); - SessionKey replyKey = cfg.getAESReplyKey(hop); - byte iv[] = cfg.getAESReplyIV(hop); - if (iv == null) { - throw new IllegalStateException(); - } boolean isInGW = (cfg.isInbound() && (hop == 0)); boolean isOutEnd = (!cfg.isInbound() && (hop + 1 >= cfg.getLength())); @@ -137,11 +138,19 @@ abstract class BuildMessageGenerator { nextMsgId, isInGW, isOutEnd, EmptyProperties.INSTANCE); } else { + SessionKey replyKey = cfg.getAESReplyKey(hop); + byte iv[] = cfg.getAESReplyIV(hop); + if (iv == null) + throw new IllegalStateException(); rec = new BuildRequestRecord(ctx, recvTunnelId, nextTunnelId, nextPeer, nextMsgId, layerKey, ivKey, replyKey, iv, isInGW, isOutEnd, EmptyProperties.INSTANCE); } } else { + SessionKey replyKey = cfg.getAESReplyKey(hop); + byte iv[] = cfg.getAESReplyIV(hop); + if (iv == null) + throw new IllegalStateException(); rec = new BuildRequestRecord(ctx, recvTunnelId, peer, nextTunnelId, nextPeer, nextMsgId, layerKey, ivKey, replyKey, iv, isInGW, isOutEnd); diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildReplyHandler.java b/router/java/src/net/i2p/router/tunnel/pool/BuildReplyHandler.java index c689ca5006..fba4cb490f 100644 --- a/router/java/src/net/i2p/router/tunnel/pool/BuildReplyHandler.java +++ b/router/java/src/net/i2p/router/tunnel/pool/BuildReplyHandler.java @@ -159,7 +159,11 @@ class BuildReplyHandler { 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); + boolean ok; + if (isShort) + ok = BuildResponseRecord.decrypt(rec, replyKey, replyIV, recordNum); + else + ok = BuildResponseRecord.decrypt(rec, replyKey, replyIV); if (!ok) { if (log.shouldWarn()) log.debug(reply.getUniqueId() + ": chacha reply decrypt fail on " + recordNum + "/" + hop); 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 360bb5e5fa..9297993843 100644 --- a/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java +++ b/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java @@ -5,22 +5,27 @@ import java.util.Collections; import java.util.List; import net.i2p.crypto.EncType; +import net.i2p.crypto.SessionKeyManager; import net.i2p.data.DataHelper; import net.i2p.data.Hash; import net.i2p.data.PublicKey; -import net.i2p.data.router.RouterInfo; import net.i2p.data.TunnelId; import net.i2p.data.i2np.I2NPMessage; import net.i2p.data.i2np.ShortTunnelBuildMessage; import net.i2p.data.i2np.TunnelBuildMessage; import net.i2p.data.i2np.VariableTunnelBuildMessage; +import net.i2p.data.router.RouterInfo; import net.i2p.router.JobImpl; +import net.i2p.router.LeaseSetKeys; import net.i2p.router.OutNetMessage; import net.i2p.router.RouterContext; 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.MuxedSKM; import net.i2p.router.networkdb.kademlia.MessageWrapper; +import net.i2p.router.networkdb.kademlia.MessageWrapper.OneTimeSession; import net.i2p.router.tunnel.HopConfig; import net.i2p.router.tunnel.TunnelCreatorConfig; import net.i2p.util.Log; @@ -142,16 +147,51 @@ abstract class BuildRequestor { TunnelManagerFacade mgr = ctx.tunnelManager(); boolean isInbound = settings.isInbound(); if (settings.isExploratory() || !usePairedTunnels(ctx)) { - if (isInbound) + if (isInbound) { pairedTunnel = mgr.selectOutboundExploratoryTunnel(farEnd); - else + } else { pairedTunnel = mgr.selectInboundExploratoryTunnel(farEnd); + if (pairedTunnel != null) { + OneTimeSession ots = cfg.getGarlicReplyKeys(); + if (ots != null) { + SessionKeyManager skm = ctx.sessionKeyManager(); + RatchetSKM rskm = (RatchetSKM) skm; + rskm.tagsReceived(ots.key, ots.rtag, 2 * BUILD_MSG_TIMEOUT); + cfg.setGarlicReplyKeys(null); + } + } + } } else { // building a client tunnel - if (isInbound) - pairedTunnel = mgr.selectOutboundTunnel(settings.getDestination(), farEnd); - else - pairedTunnel = mgr.selectInboundTunnel(settings.getDestination(), farEnd); + Hash from = settings.getDestination(); + if (isInbound) { + pairedTunnel = mgr.selectOutboundTunnel(from, farEnd); + } else { + pairedTunnel = mgr.selectInboundTunnel(from, farEnd); + if (pairedTunnel != null) { + OneTimeSession ots = cfg.getGarlicReplyKeys(); + if (ots != null) { + SessionKeyManager skm = ctx.clientManager().getClientSessionKeyManager(from); + if (skm != null) { + if (skm instanceof RatchetSKM) { + RatchetSKM rskm = (RatchetSKM) skm; + rskm.tagsReceived(ots.key, ots.rtag, 2 * BUILD_MSG_TIMEOUT); + cfg.setGarlicReplyKeys(null); + } else if (skm instanceof MuxedSKM) { + MuxedSKM mskm = (MuxedSKM) skm; + mskm.tagsReceived(ots.key, ots.rtag, 2 * BUILD_MSG_TIMEOUT); + cfg.setGarlicReplyKeys(null); + } else { + // ElG-only won't work, fall back to expl. + pairedTunnel = null; + } + } else { + // no client SKM, fall back to expl. + pairedTunnel = null; + } + } + } + } if (pairedTunnel == null) { if (isInbound) { // random more reliable than closest ?? @@ -177,6 +217,15 @@ abstract class BuildRequestor { // ditto pairedTunnel = null; } + if (pairedTunnel != null) { + OneTimeSession ots = cfg.getGarlicReplyKeys(); + if (ots != null) { + SessionKeyManager skm = ctx.sessionKeyManager(); + RatchetSKM rskm = (RatchetSKM) skm; + rskm.tagsReceived(ots.key, ots.rtag, 2 * BUILD_MSG_TIMEOUT); + cfg.setGarlicReplyKeys(null); + } + } } if (pairedTunnel != null && log.shouldLog(Log.INFO)) log.info("Couldn't find a paired tunnel for " + cfg + ", using exploratory tunnel"); @@ -296,6 +345,16 @@ abstract class BuildRequestor { Hash replyRouter; boolean useVariable = SEND_VARIABLE && cfg.getLength() <= MEDIUM_RECORDS; boolean useShortTBM = SEND_SHORT && ctx.keyManager().getPublicKey().getType() == EncType.ECIES_X25519; + if (useShortTBM && !pool.getSettings().isExploratory()) { + // pool must be EC also + LeaseSetKeys lsk = ctx.keyManager().getKeys(pool.getSettings().getDestination()); + if (lsk != null) { + if (!lsk.isSupported(EncType.ECIES_X25519)) + useShortTBM = false; + } else { + useShortTBM = false; + } + } if (cfg.isInbound()) { //replyTunnel = 0; // as above diff --git a/router/java/test/junit/net/i2p/router/tunnel/pool/BuildMessageTestStandalone.java b/router/java/test/junit/net/i2p/router/tunnel/pool/BuildMessageTestStandalone.java index f375543028..2e92bb6eee 100644 --- a/router/java/test/junit/net/i2p/router/tunnel/pool/BuildMessageTestStandalone.java +++ b/router/java/test/junit/net/i2p/router/tunnel/pool/BuildMessageTestStandalone.java @@ -64,6 +64,9 @@ public class BuildMessageTestStandalone extends TestCase { */ private void x_testBuildMessage(RouterContext ctx, int testType) { Log log = ctx.logManager().getLog(getClass()); + log.debug("\n================================================================" + + "\nTest " + testType + + "\n================================================================"); // set our keys to avoid NPE KeyPair kpr = ctx.keyGenerator().generatePKIKeys((testType == 1 || testType == 4) ? EncType.ELGAMAL_2048 : EncType.ECIES_X25519); PublicKey k1 = kpr.getPublic(); @@ -102,7 +105,7 @@ public class BuildMessageTestStandalone extends TestCase { log.debug("\n================================================================" + "\nMessage fully encrypted" + - "\n" + cfg + + "\n" + cfg.toStringFull() + "\n================================================================"); if (testType == 3 || testType == 6) { @@ -137,6 +140,12 @@ public class BuildMessageTestStandalone extends TestCase { long time = req.readRequestTime(); long now = (ctx.clock().now() / (60l*60l*1000l)) * (60*60*1000); int ourSlot = -1; + for (int j = 0; j < TunnelBuildMessage.MAX_RECORD_COUNT; j++) { + if (msg.getRecord(j) == null) { + ourSlot = j; + break; + } + } EncryptedBuildRecord reply; if (testType == 1 || testType == 4) { @@ -144,25 +153,9 @@ public class BuildMessageTestStandalone extends TestCase { } else if (testType == 2 || testType == 5) { reply = BuildResponseRecord.create(ctx, 0, req.getChaChaReplyKey(), req.getChaChaReplyAD(), EmptyProperties.INSTANCE); } else { - reply = BuildResponseRecord.createShort(ctx, 0, req.getChaChaReplyKey(), req.getChaChaReplyAD(), EmptyProperties.INSTANCE); - } - if (testType != 3 || i != cfg.getLength() - 1) { - for (int j = 0; j < TunnelBuildMessage.MAX_RECORD_COUNT; j++) { - if (msg.getRecord(j) == null) { - ourSlot = j; - msg.setRecord(j, reply); - break; - } - } - } else { - for (int j = 0; j < TunnelBuildMessage.MAX_RECORD_COUNT; j++) { - if (msg.getRecord(j) == null) { - ourSlot = j; - msg.setRecord(j, reply); - break; - } - } + reply = BuildResponseRecord.createShort(ctx, 0, req.getChaChaReplyKey(), req.getChaChaReplyAD(), EmptyProperties.INSTANCE, ourSlot); } + msg.setRecord(ourSlot, reply); if (testType == 1 || testType == 4) { log.debug("Read slot " + ourSlot + " containing hop " + i + " @ " + _peers[i].toBase64() @@ -233,7 +226,7 @@ public class BuildMessageTestStandalone extends TestCase { } log.debug("\n================================================================" + - "\nAll peers agree? " + allAgree + + "\nTest " + testType + " complete, all peers agree? " + allAgree + "\n================================================================"); assertTrue("All peers agree", allAgree); } @@ -289,12 +282,18 @@ public class BuildMessageTestStandalone extends TestCase { HopConfig hop = cfg.getConfig(i); hop.setCreation(now); hop.setExpiration(now+10*60*1000); - hop.setIVKey(ctx.keyGenerator().generateSessionKey()); - hop.setLayerKey(ctx.keyGenerator().generateSessionKey()); - byte iv[] = new byte[BuildRequestRecord.IV_SIZE]; - Arrays.fill(iv, (byte)i); // consistent for repeatability - cfg.setAESReplyKeys(i, ctx.keyGenerator().generateSessionKey(), iv); + if (testType != 3 && testType != 6) { + hop.setIVKey(ctx.keyGenerator().generateSessionKey()); + hop.setLayerKey(ctx.keyGenerator().generateSessionKey()); + byte iv[] = new byte[BuildRequestRecord.IV_SIZE]; + Arrays.fill(iv, (byte)i); // consistent for repeatability + cfg.setAESReplyKeys(i, ctx.keyGenerator().generateSessionKey(), iv); + } hop.setReceiveTunnelId(new TunnelId(i+1)); + if (i != _peers.length - 1) { + hop.setSendTo(_peers[i + 1]); + hop.setSendTunnelId(new TunnelId(i+2)); + } } return cfg; } -- GitLab