From 7707c4bb944d6480fea865bd2e9f39a6245b0563 Mon Sep 17 00:00:00 2001 From: zzz <zzz@mail.i2p> Date: Sun, 15 Mar 2020 18:40:01 +0000 Subject: [PATCH] Ratchet: Stub out ack and ack request blocks --- .../crypto/ratchet/ECIESAEADEngine.java | 52 +++++++++--- .../router/crypto/ratchet/RatchetPayload.java | 83 +++++++++++++++++++ .../router/message/GarlicMessageBuilder.java | 12 ++- .../OutboundClientMessageJobHelper.java | 29 ++++++- 4 files changed, 158 insertions(+), 18 deletions(-) diff --git a/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java b/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java index 8d24b05180..7438605925 100644 --- a/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java +++ b/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java @@ -26,6 +26,7 @@ import net.i2p.data.PrivateKey; import net.i2p.data.PublicKey; import net.i2p.data.SessionKey; import net.i2p.data.SessionTag; +import net.i2p.data.i2np.DeliveryInstructions; import net.i2p.data.i2np.GarlicClove; import static net.i2p.router.crypto.ratchet.RatchetPayload.*; import net.i2p.router.RouterContext; @@ -554,13 +555,14 @@ public final class ECIESAEADEngine { * * @param target public key to which the data should be encrypted. * @param priv local private key to encrypt with, from the leaseset + * @param replyDI non-null to request an ack, or null * @return encrypted data or null on failure * */ public byte[] encrypt(CloveSet cloves, PublicKey target, PrivateKey priv, - RatchetSKM keyManager) { + RatchetSKM keyManager, DeliveryInstructions replyDI) { try { - return x_encrypt(cloves, target, priv, keyManager); + return x_encrypt(cloves, target, priv, keyManager, replyDI); } catch (Exception e) { _log.error("ECIES encrypt error", e); return null; @@ -568,7 +570,7 @@ public final class ECIESAEADEngine { } private byte[] x_encrypt(CloveSet cloves, PublicKey target, PrivateKey priv, - RatchetSKM keyManager) { + RatchetSKM keyManager, DeliveryInstructions replyDI) { if (target.getType() != EncType.ECIES_X25519) throw new IllegalArgumentException(); if (Arrays.equals(target.getData(), NULLPK)) { @@ -581,7 +583,7 @@ public final class ECIESAEADEngine { if (re == null) { if (_log.shouldDebug()) _log.debug("Encrypting as NS to " + target); - return encryptNewSession(cloves, target, priv, keyManager); + return encryptNewSession(cloves, target, priv, keyManager, replyDI); } HandshakeState state = re.key.getHandshakeState(); @@ -595,11 +597,11 @@ public final class ECIESAEADEngine { } if (_log.shouldDebug()) _log.debug("Encrypting as NSR to " + target + " with tag " + re.tag.toBase64()); - return encryptNewSessionReply(cloves, target, state, re.tag, keyManager); + return encryptNewSessionReply(cloves, target, state, re.tag, keyManager, replyDI); } if (_log.shouldDebug()) _log.debug("Encrypting as ES to " + target + " with key " + re.key + " and tag " + re.tag.toBase64()); - byte rv[] = encryptExistingSession(cloves, target, re.key, re.tag); + byte rv[] = encryptExistingSession(cloves, target, re.key, re.tag, replyDI); return rv; } @@ -618,10 +620,11 @@ public final class ECIESAEADEngine { * - 16 byte MAC * </pre> * + * @param replyDI non-null to request an ack, or null * @return encrypted data or null on failure */ private byte[] encryptNewSession(CloveSet cloves, PublicKey target, PrivateKey priv, - RatchetSKM keyManager) { + RatchetSKM keyManager, DeliveryInstructions replyDI) { HandshakeState state; try { state = new HandshakeState(HandshakeState.PATTERN_ID_IK, HandshakeState.INITIATOR, _edhThread); @@ -635,7 +638,7 @@ public final class ECIESAEADEngine { if (_log.shouldDebug()) _log.debug("State before encrypt new session: " + state); - byte[] payload = createPayload(cloves, cloves.getExpiration()); + byte[] payload = createPayload(cloves, cloves.getExpiration(), replyDI); byte[] enc = new byte[KEYLEN + KEYLEN + MACLEN + payload.length + MACLEN]; try { @@ -681,10 +684,12 @@ public final class ECIESAEADEngine { * </pre> * * @param state must have already been cloned + * @param replyDI non-null to request an ack, or null * @return encrypted data or null on failure */ private byte[] encryptNewSessionReply(CloveSet cloves, PublicKey target, HandshakeState state, - RatchetSessionTag currentTag, RatchetSKM keyManager) { + RatchetSessionTag currentTag, RatchetSKM keyManager, + DeliveryInstructions replyDI) { if (_log.shouldDebug()) _log.debug("State before encrypt new session reply: " + state); byte[] tag = currentTag.getData(); @@ -692,7 +697,7 @@ public final class ECIESAEADEngine { if (_log.shouldDebug()) _log.debug("State after mixhash tag before encrypt new session reply: " + state); - byte[] payload = createPayload(cloves, 0); + byte[] payload = createPayload(cloves, 0, replyDI); // part 1 - tag and empty payload byte[] enc = new byte[TAGLEN + KEYLEN + MACLEN + payload.length + MACLEN]; @@ -752,12 +757,14 @@ public final class ECIESAEADEngine { * </pre> * * @param target unused, this is AEAD encrypt only using the session key and tag + * @param replyDI non-null to request an ack, or null * @return encrypted data or null on failure */ private byte[] encryptExistingSession(CloveSet cloves, PublicKey target, SessionKeyAndNonce key, - RatchetSessionTag currentTag) { + RatchetSessionTag currentTag, + DeliveryInstructions replyDI) { byte rawTag[] = currentTag.getData(); - byte[] payload = createPayload(cloves, 0); + byte[] payload = createPayload(cloves, 0, replyDI); byte encr[] = encryptAEADBlock(rawTag, payload, key, key.getNonce()); System.arraycopy(rawTag, 0, encr, 0, TAGLEN); return encr; @@ -831,6 +838,16 @@ public final class ECIESAEADEngine { nextKey = next; } + public void gotAck(int id, int n) { + if (_log.shouldDebug()) + _log.debug("Got ACK block: " + n); + } + + public void gotAckRequest(int id, DeliveryInstructions di) { + if (_log.shouldDebug()) + _log.debug("Got ACK REQUEST block: " + di); + } + public void gotTermination(int reason, long count) { if (_log.shouldDebug()) _log.debug("Got TERMINATION block, reason: " + reason + " count: " + count); @@ -849,12 +866,15 @@ public final class ECIESAEADEngine { /** * @param expiration if greater than zero, add a DateTime block + * @param replyDI non-null to request an ack, or null */ - private byte[] createPayload(CloveSet cloves, long expiration) { + private byte[] createPayload(CloveSet cloves, long expiration, DeliveryInstructions replyDI) { int count = cloves.getCloveCount(); int numblocks = count + 1; if (expiration > 0) numblocks++; + if (replyDI != null) + numblocks++; int len = 0; List<Block> blocks = new ArrayList<Block>(numblocks); if (expiration > 0) { @@ -868,6 +888,12 @@ public final class ECIESAEADEngine { blocks.add(block); len += block.getTotalLength(); } + if (replyDI != null) { + // put after the cloves so recipient has any LS garlic + Block block = new AckRequestBlock(0, replyDI); + blocks.add(block); + len += block.getTotalLength(); + } int padlen = 1 + _context.random().nextInt(MAXPAD); // random data //Block block = new PaddingBlock(_context, padlen); diff --git a/router/java/src/net/i2p/router/crypto/ratchet/RatchetPayload.java b/router/java/src/net/i2p/router/crypto/ratchet/RatchetPayload.java index 5e272f8f33..0290131c45 100644 --- a/router/java/src/net/i2p/router/crypto/ratchet/RatchetPayload.java +++ b/router/java/src/net/i2p/router/crypto/ratchet/RatchetPayload.java @@ -8,6 +8,7 @@ import java.util.List; import net.i2p.I2PAppContext; import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; +import net.i2p.data.i2np.DeliveryInstructions; import net.i2p.data.i2np.GarlicClove; import net.i2p.data.i2np.GarlicMessage; import net.i2p.data.i2np.I2NPMessage; @@ -61,6 +62,16 @@ class RatchetPayload { */ public void gotNextKey(NextSessionKey nextKey); + /** + * @since 0.9.46 + */ + public void gotAck(int id, int n); + + /** + * @since 0.9.46 + */ + public void gotAckRequest(int id, DeliveryInstructions di); + /** * For stats. * @param paddingLength the number of padding bytes, not including the 3-byte block header @@ -123,6 +134,7 @@ class RatchetPayload { break; case BLOCK_NEXTKEY: + { if (len != 34) throw new IOException("Bad length for NEXTKEY: " + len); int id = (int) DataHelper.fromLong(payload, i, 2); @@ -130,6 +142,30 @@ class RatchetPayload { System.arraycopy(payload, i + 2, data, 0, 32); NextSessionKey nsk = new NextSessionKey(data, id); cb.gotNextKey(nsk); + } + break; + + case BLOCK_ACKKEY: + { + if (len < 4 || (len % 4) != 0) + throw new IOException("Bad length for REPLYDI: " + len); + for (int j = i; j < i + len; j += 4) { + int id = (int) DataHelper.fromLong(payload, j, 2); + int n = (int) DataHelper.fromLong(payload, j + 2, 2); + cb.gotAck(id, n); + } + } + break; + + case BLOCK_REPLYDI: + { + if (len < 6) + throw new IOException("Bad length for REPLYDI: " + len); + int id = (int) DataHelper.fromLong(payload, i, 4); + DeliveryInstructions di = new DeliveryInstructions(); + di.readBytes(payload, i + 5); + cb.gotAckRequest(id, di); + } break; case BLOCK_TERMINATION: @@ -318,6 +354,53 @@ class RatchetPayload { } } + /** + * @since 0.9.46 + */ + public static class AckBlock extends Block { + private final byte[] data; + + public AckBlock(int keyID, int n) { + super(BLOCK_ACKKEY); + data = new byte[4]; + DataHelper.toLong(data, 0, 2, keyID); + DataHelper.toLong(data, 2, 2, n); + } + + public int getDataLength() { + return 4; + } + + public int writeData(byte[] tgt, int off) { + System.arraycopy(data, 0, tgt, off, data.length); + return off + data.length; + } + } + + /** + * @since 0.9.46 + */ + public static class AckRequestBlock extends Block { + private final byte[] data; + + public AckRequestBlock(int sessionID, DeliveryInstructions di) { + super(BLOCK_REPLYDI); + data = new byte[5 + di.getSize()]; + DataHelper.toLong(data, 0, 4, sessionID); + // flag is zero + di.writeBytes(data, 5); + } + + public int getDataLength() { + return data.length; + } + + public int writeData(byte[] tgt, int off) { + System.arraycopy(data, 0, tgt, off, data.length); + return off + data.length; + } + } + public static class TerminationBlock extends Block { private final byte rsn; private final long rcvd; diff --git a/router/java/src/net/i2p/router/message/GarlicMessageBuilder.java b/router/java/src/net/i2p/router/message/GarlicMessageBuilder.java index 424a774c68..dbd612734d 100644 --- a/router/java/src/net/i2p/router/message/GarlicMessageBuilder.java +++ b/router/java/src/net/i2p/router/message/GarlicMessageBuilder.java @@ -24,6 +24,7 @@ import net.i2p.data.PrivateKey; import net.i2p.data.PublicKey; import net.i2p.data.SessionKey; import net.i2p.data.SessionTag; +import net.i2p.data.i2np.DeliveryInstructions; import net.i2p.data.i2np.GarlicClove; import net.i2p.data.i2np.GarlicMessage; import net.i2p.data.i2np.I2NPMessage; @@ -251,12 +252,14 @@ public class GarlicMessageBuilder { * @param config how/what to wrap * @param target public key of the location being garlic routed to (may be null if we * know the encryptKey and encryptTag) + * @param replyDI non-null to request an ack, or null * @return null if expired or on other errors * @throws IllegalArgumentException on error * @since 0.9.44 */ static GarlicMessage buildECIESMessage(RouterContext ctx, GarlicConfig config, - PublicKey target, Hash from, SessionKeyManager skm) { + PublicKey target, Hash from, SessionKeyManager skm, + DeliveryInstructions replyDI) { PublicKey key = config.getRecipientPublicKey(); if (key.getType() != EncType.ECIES_X25519) throw new IllegalArgumentException(); @@ -286,7 +289,7 @@ public class GarlicMessageBuilder { log.warn("No SKM for " + from.toBase32()); return null; } - byte encData[] = ctx.eciesEngine().encrypt(cloveSet, target, priv, rskm); + byte encData[] = ctx.eciesEngine().encrypt(cloveSet, target, priv, rskm, replyDI); if (encData == null) { if (log.shouldWarn()) log.warn("Encrypt fail for " + from.toBase32()); @@ -438,6 +441,11 @@ public class GarlicMessageBuilder { return rv; } + /** + * Build a single clove + * + * @since 0.9.44 + */ private static GarlicClove buildECIESClove(RouterContext ctx, PayloadGarlicConfig config) { GarlicClove clove = new GarlicClove(ctx); clove.setData(config.getPayload()); diff --git a/router/java/src/net/i2p/router/message/OutboundClientMessageJobHelper.java b/router/java/src/net/i2p/router/message/OutboundClientMessageJobHelper.java index bfe007f6ef..9c906c5e19 100644 --- a/router/java/src/net/i2p/router/message/OutboundClientMessageJobHelper.java +++ b/router/java/src/net/i2p/router/message/OutboundClientMessageJobHelper.java @@ -118,13 +118,36 @@ class OutboundClientMessageJobHelper { SessionKeyManager skm = ctx.clientManager().getClientSessionKeyManager(from); if (skm == null) return null; + boolean isECIES = recipientPK.getType() == EncType.ECIES_X25519; + // force ack off if ECIES + boolean ackInGarlic = isECIES ? false : requireAck; GarlicConfig config = createGarlicConfig(ctx, replyToken, expiration, recipientPK, dataClove, - from, dest, replyTunnel, requireAck, bundledReplyLeaseSet, skm); + from, dest, replyTunnel, ackInGarlic, bundledReplyLeaseSet, skm); if (config == null) return null; GarlicMessage msg; - if (recipientPK.getType() == EncType.ECIES_X25519) { - msg = GarlicMessageBuilder.buildECIESMessage(ctx, config, recipientPK, from, skm); + if (isECIES) { + DeliveryInstructions di; + if (requireAck) { + // setup reply DI + di = new DeliveryInstructions(); + if (bundledReplyLeaseSet != null) { + di.setDeliveryMode(DeliveryInstructions.DELIVERY_MODE_DESTINATION); + di.setDestination(from); + } else if (replyTunnel != null) { + di.setDeliveryMode(DeliveryInstructions.DELIVERY_MODE_TUNNEL); + TunnelId replyToTunnelId = replyTunnel.getReceiveTunnelId(0); + Hash replyToTunnelRouter = replyTunnel.getPeer(0); + di.setRouter(replyToTunnelRouter); + di.setTunnelId(replyToTunnelId); + } else { + // shouldn't happen + di = null; + } + } else { + di = null; + } + msg = GarlicMessageBuilder.buildECIESMessage(ctx, config, recipientPK, from, skm, di); } else { // no use sending tags unless we have a reply token set up already int tagsToSend = replyToken >= 0 ? (tagsToSendOverride > 0 ? tagsToSendOverride : skm.getTagsToSend()) : 0; -- GitLab