From acf3abb19bb2a2075e874d6852d7ee3a63f6d873 Mon Sep 17 00:00:00 2001 From: zzz <zzz@mail.i2p> Date: Fri, 27 Mar 2020 16:55:53 +0000 Subject: [PATCH] Ratchet: Updates - Modify NextKey, start of support (WIP) - Don't expect DSM reply to ECIES destinations - Debug setting to always sent ack request --- .../crypto/ratchet/ECIESAEADEngine.java | 33 +++++++++++++------ .../router/crypto/ratchet/NextSessionKey.java | 26 ++++++++++++--- .../router/crypto/ratchet/RatchetPayload.java | 22 ++++++++----- .../i2p/router/crypto/ratchet/RatchetSKM.java | 1 + .../router/crypto/ratchet/RatchetTagSet.java | 26 +++++++++++++++ .../OutboundClientMessageOneShotJob.java | 3 +- 6 files changed, 87 insertions(+), 24 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 19c74065b2..ab0cdd98d9 100644 --- a/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java +++ b/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java @@ -62,6 +62,8 @@ public final class ECIESAEADEngine { private static final int MIN_ENCRYPTED_SIZE = MIN_ES_SIZE; private static final byte[] NULLPK = new byte[KEYLEN]; private static final int MAXPAD = 16; + // debug, send ACKREQ in every ES + private static final boolean ACKREQ_IN_ES = false; private static final String INFO_0 = "SessionReplyTags"; private static final String INFO_6 = "AttachPayloadKDF"; @@ -603,7 +605,7 @@ public final class ECIESAEADEngine { } 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, replyDI); + byte rv[] = encryptExistingSession(cloves, target, re, replyDI); return rv; } @@ -640,7 +642,7 @@ public final class ECIESAEADEngine { if (_log.shouldDebug()) _log.debug("State before encrypt new session: " + state); - byte[] payload = createPayload(cloves, cloves.getExpiration(), replyDI); + byte[] payload = createPayload(cloves, cloves.getExpiration(), replyDI, null); byte[] enc = new byte[KEYLEN + KEYLEN + MACLEN + payload.length + MACLEN]; try { @@ -699,7 +701,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, replyDI); + byte[] payload = createPayload(cloves, 0, replyDI, null); // part 1 - tag and empty payload byte[] enc = new byte[TAGLEN + KEYLEN + MACLEN + payload.length + MACLEN]; @@ -762,11 +764,14 @@ public final class ECIESAEADEngine { * @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, + private byte[] encryptExistingSession(CloveSet cloves, PublicKey target, RatchetEntry re, DeliveryInstructions replyDI) { - byte rawTag[] = currentTag.getData(); - byte[] payload = createPayload(cloves, 0, replyDI); + // + if (ACKREQ_IN_ES && replyDI == null) + replyDI = new DeliveryInstructions(); + byte rawTag[] = re.tag.getData(); + byte[] payload = createPayload(cloves, 0, replyDI, re.nextKey); + SessionKeyAndNonce key = re.key; byte encr[] = encryptAEADBlock(rawTag, payload, key, key.getNonce()); System.arraycopy(rawTag, 0, encr, 0, TAGLEN); return encr; @@ -843,12 +848,12 @@ public final class ECIESAEADEngine { public void gotAck(int id, int n) { if (_log.shouldDebug()) - _log.debug("Got ACK block: " + n); + _log.debug("Got ACK block: " + id + " / " + n); } public void gotAckRequest(int id, DeliveryInstructions di) { if (_log.shouldDebug()) - _log.debug("Got ACK REQUEST block: " + di); + _log.debug("Got ACK REQUEST block: " + id + " / " + di); ackRequested = true; } @@ -872,13 +877,16 @@ 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, DeliveryInstructions replyDI) { + private byte[] createPayload(CloveSet cloves, long expiration, + DeliveryInstructions replyDI, NextSessionKey nextKey) { int count = cloves.getCloveCount(); int numblocks = count + 1; if (expiration > 0) numblocks++; if (replyDI != null) numblocks++; + if (nextKey != null) + numblocks++; int len = 0; List<Block> blocks = new ArrayList<Block>(numblocks); if (expiration > 0) { @@ -886,6 +894,11 @@ public final class ECIESAEADEngine { blocks.add(block); len += block.getTotalLength(); } + if (nextKey != null) { + Block block = new NextKeyBlock(nextKey); + blocks.add(block); + len += block.getTotalLength(); + } for (int i = 0; i < count; i++) { GarlicClove clove = cloves.getClove(i); Block block = new GarlicBlock(clove); diff --git a/router/java/src/net/i2p/router/crypto/ratchet/NextSessionKey.java b/router/java/src/net/i2p/router/crypto/ratchet/NextSessionKey.java index e7e208d218..eaa3057d8d 100644 --- a/router/java/src/net/i2p/router/crypto/ratchet/NextSessionKey.java +++ b/router/java/src/net/i2p/router/crypto/ratchet/NextSessionKey.java @@ -1,30 +1,46 @@ package net.i2p.router.crypto.ratchet; -import net.i2p.data.SessionKey; +import net.i2p.crypto.EncType; +import net.i2p.data.PublicKey; /** - * A session key and key ID. + * A X25519 key and key ID. * * @since 0.9.44 */ -class NextSessionKey extends SessionKey { +class NextSessionKey extends PublicKey { private final int _id; + private final boolean _isReverse, _isRequest; - public NextSessionKey(byte[] data, int id) { - super(data); + public NextSessionKey(byte[] data, int id, boolean isReverse, boolean isRequest) { + super(EncType.ECIES_X25519, data); _id = id; + _isReverse = isReverse; + _isRequest = isRequest; } public int getID() { return _id; } + /** @since 0.9.46 */ + public boolean isReverse() { + return _isReverse; + } + + /** @since 0.9.46 */ + public boolean isRequest() { + return _isRequest; + } + @Override public String toString() { StringBuilder buf = new StringBuilder(64); buf.append("[NextSessionKey: "); buf.append(toBase64()); buf.append(" ID: ").append(_id); + buf.append(" reverse? ").append(_isReverse); + buf.append(" request? ").append(_isRequest); buf.append(']'); return buf.toString(); } 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 836d1db443..c6b065a6fc 100644 --- a/router/java/src/net/i2p/router/crypto/ratchet/RatchetPayload.java +++ b/router/java/src/net/i2p/router/crypto/ratchet/RatchetPayload.java @@ -136,12 +136,14 @@ class RatchetPayload { case BLOCK_NEXTKEY: { - if (len != 34) + if (len != 35) throw new IOException("Bad length for NEXTKEY: " + len); - int id = (int) DataHelper.fromLong(payload, i, 2); + boolean isReverse = (payload[i] & 0x01) != 0; + boolean isRequest = (payload[i] & 0x02) != 0; + int id = (int) DataHelper.fromLong(payload, i + 1, 2); byte[] data = new byte[32]; - System.arraycopy(payload, i + 2, data, 0, 32); - NextSessionKey nsk = new NextSessionKey(data, id); + System.arraycopy(payload, i + 3, data, 0, 32); + NextSessionKey nsk = new NextSessionKey(data, id, isReverse, isRequest); cb.gotNextKey(nsk); } break; @@ -350,13 +352,17 @@ class RatchetPayload { } public int getDataLength() { - return 34; + return 35; } public int writeData(byte[] tgt, int off) { - DataHelper.toLong(tgt, off, 2, next.getID()); - System.arraycopy(next.getData(), 0, tgt, off + 2, 32); - return off + 34; + if (next.isReverse()) + tgt[off] = 0x01; + if (next.isRequest()) + tgt[off] |= 0x02; + DataHelper.toLong(tgt, off + 1, 2, next.getID()); + System.arraycopy(next.getData(), 0, tgt, off + 3, 32); + return off + 35; } } diff --git a/router/java/src/net/i2p/router/crypto/ratchet/RatchetSKM.java b/router/java/src/net/i2p/router/crypto/ratchet/RatchetSKM.java index 0c699bba4b..4c0b2e8162 100644 --- a/router/java/src/net/i2p/router/crypto/ratchet/RatchetSKM.java +++ b/router/java/src/net/i2p/router/crypto/ratchet/RatchetSKM.java @@ -1100,6 +1100,7 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener set.setDate(now); SessionKeyAndNonce skn = set.consumeNextKey(); // TODO key ID and PN + // TODO next key return new RatchetEntry(tag, skn, 0, 0); } else if (_log.shouldInfo()) { _log.info("Removing empty " + set); diff --git a/router/java/src/net/i2p/router/crypto/ratchet/RatchetTagSet.java b/router/java/src/net/i2p/router/crypto/ratchet/RatchetTagSet.java index f72e52f12a..8107ca478c 100644 --- a/router/java/src/net/i2p/router/crypto/ratchet/RatchetTagSet.java +++ b/router/java/src/net/i2p/router/crypto/ratchet/RatchetTagSet.java @@ -13,6 +13,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.KeyPair; import net.i2p.crypto.TagSetHandle; import net.i2p.data.Base64; import net.i2p.data.DataHelper; @@ -56,6 +57,9 @@ class RatchetTagSet implements TagSetHandle { private final byte[] _symmkey_constant; private int _lastTag = -1; private int _lastKey = -1; + private KeyPair _nextKeys; + private NextSessionKey _nextKey; + private boolean _nextKeyAcked; private static final String INFO_1 = "KDFDHRatchetStep"; private static final String INFO_2 = "TagAndKeyGenKeys"; @@ -65,6 +69,7 @@ class RatchetTagSet implements TagSetHandle { private static final byte[] ZEROLEN = new byte[0]; private static final int TAGLEN = RatchetSessionTag.LENGTH; private static final int MAX = 65535; + private static final int LOW = 50; /** * Outbound NSR Tagset @@ -241,6 +246,27 @@ class RatchetTagSet implements TagSetHandle { return MAX - nextKey; } + /** + * Next Key if applicable + * null if remaining is sufficient + * + * @return key or null + * @since 0.9.46 + */ + public NextSessionKey getNextKey() { + if (remaining() > LOW) + return null; + if (_nextKeyAcked) // maybe not needed, keep sending until unused + return null; + if (_nextKeys == null) { + _nextKeys = I2PAppContext.getGlobalContext().keyGenerator().generatePKIKeys(EncType.ECIES_X25519); + boolean isIB = _sessionTags != null; + // TODO request only needed every other time + _nextKey = new NextSessionKey(_nextKeys.getPublic().getData(), 0, isIB, !isIB); + } + return _nextKey; + } + /** * tags still available * inbound only diff --git a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java index 689339b5db..05f43b7c0a 100644 --- a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java +++ b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java @@ -678,7 +678,8 @@ public class OutboundClientMessageOneShotJob extends JobImpl { SendSuccessJob onReply = null; SendTimeoutJob onFail = null; ReplySelector selector = null; - if (wantACK) { + + if (wantACK && _encryptionKey.getType() == EncType.ELGAMAL_2048) { TagSetHandle tsh = null; if (!tags.isEmpty()) { SessionKeyManager skm = getContext().clientManager().getClientSessionKeyManager(_from.calculateHash()); -- GitLab