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 1a203eec00fa92653905fe46dfc58e4d8f739f56..42248f2dba5aa9b8c200a520920ecfc04bd506e0 100644 --- a/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java +++ b/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java @@ -510,6 +510,11 @@ public final class ECIESAEADEngine { if (_log.shouldWarn()) _log.warn("No garlic block in ES payload"); } + if (pc.nextKeys != null) { + for (NextSessionKey nextKey : pc.nextKeys) { + keyManager.nextKeyReceived(remote, nextKey); + } + } if (pc.ackRequested) { keyManager.ackRequested(remote, key.getID(), nonce); } @@ -667,7 +672,7 @@ public final class ECIESAEADEngine { if (_log.shouldDebug()) _log.debug("State before encrypt new session: " + state); - byte[] payload = createPayload(cloves, cloves.getExpiration(), false, null, null); + byte[] payload = createPayload(cloves, cloves.getExpiration()); byte[] enc = new byte[KEYLEN + KEYLEN + MACLEN + payload.length + MACLEN]; try { @@ -726,7 +731,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, false, null, null); + byte[] payload = createPayload(cloves, 0); // part 1 - tag and empty payload byte[] enc = new byte[TAGLEN + KEYLEN + MACLEN + payload.length + MACLEN]; @@ -793,7 +798,7 @@ public final class ECIESAEADEngine { RatchetSKM keyManager) { boolean ackreq = callback != null || ACKREQ_IN_ES; byte rawTag[] = re.tag.getData(); - byte[] payload = createPayload(cloves, 0, ackreq, re.nextKey, re.acksToSend); + byte[] payload = createPayload(cloves, 0, ackreq, re.nextForwardKey, re.nextReverseKey, re.acksToSend); SessionKeyAndNonce key = re.key; int nonce = key.getNonce(); byte encr[] = encryptAEADBlock(rawTag, payload, key, nonce); @@ -823,7 +828,7 @@ public final class ECIESAEADEngine { */ public byte[] encrypt(CloveSet cloves, SessionKey key, RatchetSessionTag tag) { byte rawTag[] = tag.getData(); - byte[] payload = createPayload(cloves, 0, false, null, null); + byte[] payload = createPayload(cloves, 0); byte encr[] = encryptAEADBlock(rawTag, payload, key, 0); System.arraycopy(rawTag, 0, encr, 0, TAGLEN); return encr; @@ -857,7 +862,7 @@ public final class ECIESAEADEngine { return enc; } - private static final PrivateKey doDH(PrivateKey privkey, PublicKey pubkey) { + static final PrivateKey doDH(PrivateKey privkey, PublicKey pubkey) { byte[] dh = new byte[KEYLEN]; Curve25519.eval(dh, 0, privkey.getData(), pubkey.getData()); return new PrivateKey(EncType.ECIES_X25519, dh); @@ -868,11 +873,13 @@ public final class ECIESAEADEngine { ///////////////////////////////////////////////////////// private class PLCallback implements RatchetPayload.PayloadCallback { + /** non null, may be empty */ public final List<GarlicClove> cloveSet = new ArrayList<GarlicClove>(3); private final RatchetSKM skm; private final PublicKey remote; public long datetime; - public NextSessionKey nextKey; + /** null or non-empty */ + public List<NextSessionKey> nextKeys; public boolean ackRequested; /** @@ -920,7 +927,11 @@ public final class ECIESAEADEngine { public void gotNextKey(NextSessionKey next) { if (_log.shouldDebug()) _log.debug("Got NEXTKEY block: " + next); - nextKey = next; + // could have both a forward and reverse. + // shouldn't have two forwards or two reverses + if (nextKeys == null) + nextKeys = new ArrayList<NextSessionKey>(2); + nextKeys.add(next); } public void gotAck(int id, int n) { @@ -954,21 +965,33 @@ public final class ECIESAEADEngine { } } + /** + * @param expiration if greater than zero, add a DateTime block + * @since 0.9.46 + */ + private byte[] createPayload(CloveSet cloves, long expiration) { + return createPayload(cloves, expiration, false, null, null, null); + } + /** * @param expiration if greater than zero, add a DateTime block * @param ackreq to request an ack, must be false for NS/NSR + * @param nextKey1 may be null + * @param nextKey2 may be null * @param acksTOSend may be null */ private byte[] createPayload(CloveSet cloves, long expiration, - boolean ackreq, NextSessionKey nextKey, - List<Integer> acksToSend) { + boolean ackreq, NextSessionKey nextKey1, + NextSessionKey nextKey2, List<Integer> acksToSend) { int count = cloves.getCloveCount(); int numblocks = count + 1; if (expiration > 0) numblocks++; if (ackreq) numblocks++; - if (nextKey != null) + if (nextKey1 != null) + numblocks++; + if (nextKey2 != null) numblocks++; if (acksToSend != null) numblocks++; @@ -979,8 +1002,13 @@ public final class ECIESAEADEngine { blocks.add(block); len += block.getTotalLength(); } - if (nextKey != null) { - Block block = new NextKeyBlock(nextKey); + if (nextKey1 != null) { + Block block = new NextKeyBlock(nextKey1); + blocks.add(block); + len += block.getTotalLength(); + } + if (nextKey2 != null) { + Block block = new NextKeyBlock(nextKey2); blocks.add(block); len += block.getTotalLength(); } 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 03cd18d35d80b2b67e131fdde7d51655ce6a8c05..e6b37071d5c4e5eba52a09581e51b7e3e58ca339 100644 --- a/router/java/src/net/i2p/router/crypto/ratchet/NextSessionKey.java +++ b/router/java/src/net/i2p/router/crypto/ratchet/NextSessionKey.java @@ -40,7 +40,7 @@ class NextSessionKey extends PublicKey { public String toString() { StringBuilder buf = new StringBuilder(64); buf.append("[NextSessionKey: "); - buf.append(toBase64()); + buf.append(super.toString()); buf.append(" ID: ").append(_id); buf.append(" reverse? ").append(_isReverse); buf.append(" request? ").append(_isRequest); diff --git a/router/java/src/net/i2p/router/crypto/ratchet/RatchetEntry.java b/router/java/src/net/i2p/router/crypto/ratchet/RatchetEntry.java index 5403575ab243bbb7f3af1e99fef6529c155aee7a..76c356de82968a3103b80b6fd56db7a7a199a8ed 100644 --- a/router/java/src/net/i2p/router/crypto/ratchet/RatchetEntry.java +++ b/router/java/src/net/i2p/router/crypto/ratchet/RatchetEntry.java @@ -6,7 +6,7 @@ import net.i2p.data.SessionKey; /** * Simple object with outbound tag, key, and nonce, - * and an optional next key. + * and an optional next keys. * The object returned from SKM.consumeNextAvailableTag() to the engine encrypt. * * @since 0.9.44 @@ -16,21 +16,23 @@ class RatchetEntry { public final SessionKeyAndNonce key; public final int keyID; public final int pn; - public final NextSessionKey nextKey; + public final NextSessionKey nextForwardKey; + public final NextSessionKey nextReverseKey; public final List<Integer> acksToSend; /** outbound - calculated key */ public RatchetEntry(RatchetSessionTag tag, SessionKeyAndNonce key, int keyID, int pn) { - this(tag, key, keyID, pn, null, null); + this(tag, key, keyID, pn, null, null, null); } public RatchetEntry(RatchetSessionTag tag, SessionKeyAndNonce key, int keyID, int pn, - NextSessionKey nextKey, List<Integer> acksToSend) { + NextSessionKey nextFwdKey, NextSessionKey nextRevKey, List<Integer> acksToSend) { this.tag = tag; this.key = key; this.keyID = keyID; this.pn = pn; - this.nextKey = nextKey; + this.nextForwardKey = nextFwdKey; + this.nextReverseKey = nextRevKey; this.acksToSend = acksToSend; } 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 2330c84db6385a91f414d3cdf94be8b27535ee5f..125a0d4441419b0cd2421d88ffdc1f089d7da7aa 100644 --- a/router/java/src/net/i2p/router/crypto/ratchet/RatchetSKM.java +++ b/router/java/src/net/i2p/router/crypto/ratchet/RatchetSKM.java @@ -22,10 +22,12 @@ 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.SessionKeyManager; import net.i2p.crypto.TagSetHandle; import net.i2p.data.Base64; import net.i2p.data.DataHelper; +import net.i2p.data.PrivateKey; import net.i2p.data.PublicKey; import net.i2p.data.SessionKey; import net.i2p.data.SessionTag; @@ -241,8 +243,8 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener _log.info("Session " + state.hashCode() + " update as Bob. Alice: " + toString(target)); OutboundSession sess = getSession(target); if (sess == null) { - if (_log.shouldDebug()) - _log.debug("Update Bob session but no session found for " + target); + if (_log.shouldWarn()) + _log.warn("Update Bob session but no session found for " + target); // TODO can we recover? return false; } @@ -304,6 +306,19 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener return true; } + /** + * @since 0.9.46 + */ + public void nextKeyReceived(PublicKey target, NextSessionKey key) { + OutboundSession sess = getSession(target); + if (sess == null) { + if (_log.shouldWarn()) + _log.warn("Got NextKey but no session found for " + target); + return; + } + sess.nextKeyReceived(key); + } + /** * @throws UnsupportedOperationException always */ @@ -793,7 +808,8 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener for (RatchetTagSet ts : sets) { int size = ts.size(); total += size; - buf.append("<li><b>ID: ").append(ts.getID()); + buf.append("<li><b>ID: ").append(ts.getID()) + .append(" / ").append(ts.getDebugID()); buf.append(" created:</b> ").append(DataHelper.formatTime(ts.getCreated())) .append(" <b>last use:</b> ").append(DataHelper.formatTime(ts.getDate())); long expires = ts.getExpiration() - now; @@ -835,6 +851,7 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener for (RatchetTagSet ts : sets) { int size = ts.remaining(); buf.append("<li><b>ID: ").append(ts.getID()) + .append(" / ").append(ts.getDebugID()) .append(" created:</b> ").append(DataHelper.formatTime(ts.getCreated())) .append(" <b>last use:</b> ").append(DataHelper.formatTime(ts.getDate())); long expires = ts.getExpiration() - now; @@ -914,6 +931,14 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener */ private int _consecutiveFailures; + // next key + private int _myOBKeyID = -1; + private int _hisOBKeyID = -1; + private int _currentOBTagSetID; + private int _myIBKeyID = -1; + private int _hisIBKeyID = -1; + private int _currentIBTagSetID; + private static final int MAX_FAILS = 2; private static final int MAX_SEND_ACKS = 8; private static final int DEBUG_OB_NSR = 0x10001; @@ -1024,6 +1049,76 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener //state.destroy(); } + /** + * @since 0.9.46 + */ + public void nextKeyReceived(NextSessionKey key) { + boolean isReverse = key.isReverse(); + boolean isRequest = key.isRequest(); + boolean hasKey = key.getData() != null; + int id = key.getID(); + synchronized (_tagSets) { + if (isReverse) { + // this is about my outbound tag set, + // and is an ack of new key sent + if (_hisIBKeyID != id) { + if (_log.shouldWarn()) + _log.warn("Got new key id, ratchet OB " + id); + if (_hisIBKeyID != id + 1) { + if (_log.shouldWarn()) + _log.warn("Got bad new key id OB? " + id); + } + if (hasKey) { + KeyPair nextKeys = _context.keyGenerator().generatePKIKeys(EncType.ECIES_X25519); + PublicKey pub = nextKeys.getPublic(); + PrivateKey priv = nextKeys.getPrivate(); + PrivateKey sharedSecret = ECIESAEADEngine.doDH(priv, key); + // create new OB TS + // find current OB TS, and delete it + } else { + // TODO get it from above + if (_log.shouldWarn()) + _log.warn("Got nextkey w/o key but we don't have it " + id); + } + } else { + if (_log.shouldWarn()) + _log.warn("Got dup new key id for OB " + id); + } + if (isRequest) { + if (_log.shouldWarn()) + _log.warn("invalid req+rev in nextkey"); + // ignore + } + } else { + // this is about my inbound tag set + if (_hisOBKeyID != id) { + if (_log.shouldWarn()) + _log.warn("Got new key id, ratchet IB " + id); + if (_hisOBKeyID != id + 1) { + if (_log.shouldWarn()) + _log.warn("Got bad new key id IB? " + id); + } + if (!hasKey) { + if (_log.shouldWarn()) + _log.warn("Got nextkey w/o key but we don't have it " + id); + } + // find current OB TS, tell him to send ack + // create new IB TS + } else { + if (_log.shouldWarn()) + _log.warn("Got dup new key id for IB " + id); + // find current OB TS, tell him to send ack if nec. + // create new IB TS if nec. + } + if (!isRequest) { + if (_log.shouldWarn()) + _log.warn("invalid fwd w/o req in nextkey"); + // ignore + } + } + } + } + /** * First tag was received for this inbound (ES) tagset. * Find the corresponding outbound (ES) tagset in _unackedTagSets, @@ -1190,7 +1285,8 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener set.setDate(now); SessionKeyAndNonce skn = set.consumeNextKey(); // TODO PN - return new RatchetEntry(tag, skn, set.getID(), 0, set.getNextKey(), getAcksToSend()); + // TODO reverse next key + return new RatchetEntry(tag, skn, set.getID(), 0, set.getNextKey(), null, getAcksToSend()); } 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 bf2716404ddfc4f7ca474dd965ce1225fbcdad59..246981e8eb7d67d1956f31941d0033f43cb5c2dd 100644 --- a/router/java/src/net/i2p/router/crypto/ratchet/RatchetTagSet.java +++ b/router/java/src/net/i2p/router/crypto/ratchet/RatchetTagSet.java @@ -263,6 +263,7 @@ class RatchetTagSet implements TagSetHandle { * @since 0.9.46 */ public synchronized long getExpiration() { + // TODO return shorter if not acked? return _date + _timeout; } @@ -299,8 +300,9 @@ class RatchetTagSet implements TagSetHandle { } /** - * Next Key if applicable - * null if remaining is sufficient + * Next Forward Key if applicable (we're running low). + * Null if remaining is sufficient. + * Once non-null, will be constant for the remaining life of the tagset. * * @return key or null * @since 0.9.46 @@ -308,8 +310,6 @@ class RatchetTagSet implements TagSetHandle { 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; @@ -319,6 +319,18 @@ class RatchetTagSet implements TagSetHandle { return _nextKey; } + /** + * Next Forward KeyPair if applicable (we're running low). + * Null if remaining is sufficient. + * Once non-null, will be constant for the remaining life of the tagset. + * + * @return keys or null + * @since 0.9.46 + */ + public KeyPair getNextKeys() { + return _nextKeys; + } + /** * tags still available * inbound only @@ -502,11 +514,21 @@ class RatchetTagSet implements TagSetHandle { */ public boolean getAcked() { return _acked; } - /** the Key ID */ + /** + * The TagSet ID, starting at 0. + * After that = 1 + my key id + his key id + */ public int getID() { return _id; } + /** + * A unique ID for debugging only + */ + public int getDebugID() { + return _tagSetID; + } + @Override public synchronized String toString() { StringBuilder buf = new StringBuilder(256);