diff --git a/history.txt b/history.txt index e2cf6dcd5..0d8517cb3 100644 --- a/history.txt +++ b/history.txt @@ -1,6 +1,16 @@ +2020-12-11 zzz + * Router (proposal 156): + - Change router ECIES SKM to use N pattern + - Allow encrypted messages to ECIES routers + - Allow ECIES routers to become floodfill + - Add XDH factory to VM comm system for tests + 2020-12-06 zzz * Console, webapps: Move web resources to wars - * i2psnark: Initial support for web seeds (WIP) + * i2psnark: + - Add support for web seeds + - Preserve file attribute strings in metainfo + * Streaming: Add Retry-After header to throttle response * Util: Change DoH to RFC 8484 protocol * 2020-12-01 0.9.48 released diff --git a/router/java/src/net/i2p/data/i2np/DatabaseLookupMessage.java b/router/java/src/net/i2p/data/i2np/DatabaseLookupMessage.java index 9fafeb812..139ffe139 100644 --- a/router/java/src/net/i2p/data/i2np/DatabaseLookupMessage.java +++ b/router/java/src/net/i2p/data/i2np/DatabaseLookupMessage.java @@ -47,7 +47,7 @@ public class DatabaseLookupMessage extends FastI2NPMessageImpl { private PublicKey _ratchetPubKey; private Type _type; - public static final boolean USE_ECIES_FF = false; + public static final boolean USE_ECIES_FF = true; //private static volatile long _currentLookupPeriod = 0; //private static volatile int _currentLookupCount = 0; diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index 282c18b42..0725033fa 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 = 2; + public final static long BUILD = 3; /** for example "-test" */ public final static String EXTRA = ""; 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 400f0c494..7e291e644 100644 --- a/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java +++ b/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java @@ -64,9 +64,11 @@ public final class ECIESAEADEngine { private static final int BHLEN = RatchetPayload.BLOCK_HEADER_SIZE; // 3 private static final int DATETIME_SIZE = BHLEN + 4; // 7 private static final int NS_OVERHEAD = KEYLEN + KEYLEN + MACLEN + MACLEN; // 96 + private static final int NS_N_OVERHEAD = KEYLEN + MACLEN; // 48 private static final int NSR_OVERHEAD = TAGLEN + KEYLEN + MACLEN + MACLEN; // 72 private static final int ES_OVERHEAD = TAGLEN + MACLEN; // 24 private static final int MIN_NS_SIZE = NS_OVERHEAD + DATETIME_SIZE; // 103 + private static final int MIN_NS_N_SIZE = NS_N_OVERHEAD + DATETIME_SIZE; // 55 private static final int MIN_NSR_SIZE = NSR_OVERHEAD; // 72 private static final int MIN_ES_SIZE = ES_OVERHEAD; // 24 private static final int MIN_ENCRYPTED_SIZE = MIN_ES_SIZE; @@ -312,8 +314,12 @@ public final class ECIESAEADEngine { private CloveSet x_decryptSlow(byte data[], PrivateKey targetPrivateKey, RatchetSKM keyManager) throws DataFormatException { CloveSet decrypted; - if (data.length >= MIN_NS_SIZE) { - decrypted = decryptNewSession(data, targetPrivateKey, keyManager); + boolean isRouter = keyManager.getDestination() == null; + if (data.length >= MIN_NS_SIZE || (isRouter && data.length >= MIN_NS_N_SIZE)) { + if (isRouter) + decrypted = decryptNewSession_N(data, targetPrivateKey, keyManager); + else + decrypted = decryptNewSession(data, targetPrivateKey, keyManager); if (decrypted != null) { _context.statManager().updateFrequency("crypto.eciesAEAD.decryptNewSession"); } else { @@ -471,6 +477,113 @@ public final class ECIESAEADEngine { return rv; } + /** + * scenario 1A: New Session Message Anonymous + * + *
+     *  - 32 byte ephemeral key (NOT Elligator2)
+     *  - payload (7 bytes minimum for DateTime block)
+     *  - 16 byte MAC
+     * 
+ * + * @param data 55 bytes minimum + * @return null if decryption fails + * @since 0.9.49 + */ + private CloveSet decryptNewSession_N(byte data[], PrivateKey targetPrivateKey, RatchetSKM keyManager) + throws DataFormatException { + // fast MSB check for key < 2^255 + if ((data[KEYLEN - 1] & 0x80) != 0) { + if (_log.shouldDebug()) + _log.debug("Bad PK N"); + return null; + } + + HandshakeState state; + try { + state = new HandshakeState(HandshakeState.PATTERN_ID_N, HandshakeState.RESPONDER, _context.commSystem().getXDHFactory()); + } catch (GeneralSecurityException gse) { + throw new IllegalStateException("bad proto", gse); + } + state.getLocalKeyPair().setKeys(targetPrivateKey.getData(), 0, + targetPrivateKey.toPublic().getData(), 0); + state.start(); + if (_log.shouldDebug()) + _log.debug("State before decrypt new session: " + state); + + int payloadlen = data.length - (KEYLEN + MACLEN); + byte[] payload = new byte[payloadlen]; + try { + state.readMessage(data, 0, data.length, payload, 0); + } catch (GeneralSecurityException gse) { + // we'll get this a lot from very old routers + // logged at INFO in caller + if (_log.shouldDebug()) + _log.debug("Decrypt fail N, state at failure: " + state, gse); + state.destroy(); + return null; + } + // bloom filter here based on ephemeral key + byte[] xx = new byte[KEYLEN]; + System.arraycopy(data, 0, xx, 0, KEYLEN); + PublicKey pk = new PublicKey(EncType.ECIES_X25519, xx); + if (keyManager.isDuplicate(pk)) { + if (_log.shouldWarn()) + _log.warn("Dup eph. key in IB N: " + pk); + state.destroy(); + return NO_CLOVES; + } + + // payload + if (payloadlen == 0) { + // disallowed, datetime block required + if (_log.shouldWarn()) + _log.warn("Zero length payload in N"); + state.destroy(); + return NO_CLOVES; + } + PLCallback pc = new PLCallback(); + try { + int blocks = RatchetPayload.processPayload(_context, pc, payload, 0, payload.length, true); + if (_log.shouldDebug()) + _log.debug("Processed " + blocks + " blocks in IB N"); + } catch (DataFormatException e) { + state.destroy(); + throw e; + } catch (Exception e) { + state.destroy(); + throw new DataFormatException("N payload error", e); + } + + if (pc.datetime == 0) { + // disallowed, datetime block required + if (_log.shouldWarn()) + _log.warn("No datetime block in IB N"); + state.destroy(); + return NO_CLOVES; + } + + if (pc.cloveSet.isEmpty()) { + // this is legal ? + if (_log.shouldDebug()) + _log.debug("No garlic block in N payload"); + state.destroy(); + return NO_CLOVES; + } + + if (_log.shouldDebug()) { + _log.debug("N decrypt success"); + //_log.debug("State after decrypt new session: " + state); + } + state.destroy(); + + int num = pc.cloveSet.size(); + GarlicClove[] arr = new GarlicClove[num]; + // msg id and expiration not checked in GarlicMessageReceiver + CloveSet rv = new CloveSet(pc.cloveSet.toArray(arr), Certificate.NULL_CERT, 0, pc.datetime); + return rv; + } + /** * scenario 2: New Session Reply Message * @@ -763,8 +876,10 @@ public final class ECIESAEADEngine { } if (priv == null) { if (_log.shouldDebug()) - _log.debug("Encrypting as NS zero-key to " + target); - return encryptNewSession(cloves, target, null, null, null, null); + _log.debug("Encrypting as N to " + target); + // N-in-IK, unused + //return encryptNewSession(cloves, target, null, null, null, null); + return encryptNewSession(cloves, target); } RatchetEntry re = keyManager.consumeNextAvailableTag(target); if (re == null) { @@ -870,6 +985,49 @@ public final class ECIESAEADEngine { } + /** + * scenario 1A: New Session Message from an anonymous source. + * + *
+     *  - 32 byte ephemeral key (NOT Elligator2)
+     *  - payload
+     *  - 16 byte MAC
+     * 
+ * + * @return encrypted data or null on failure + * @since 0.9.49 + */ + private byte[] encryptNewSession(CloveSet cloves, PublicKey target) { + HandshakeState state; + try { + state = new HandshakeState(HandshakeState.PATTERN_ID_N, HandshakeState.INITIATOR, _context.commSystem().getXDHFactory()); + } catch (GeneralSecurityException gse) { + throw new IllegalStateException("bad proto", gse); + } + state.getRemotePublicKey().setPublicKey(target.getData(), 0); + state.start(); + if (_log.shouldDebug()) + _log.debug("State before encrypt new session: " + state); + + byte[] payload = createPayload(cloves, cloves.getExpiration(), NS_N_OVERHEAD); + + byte[] enc = new byte[KEYLEN + payload.length + MACLEN]; + try { + state.writeMessage(enc, 0, payload, 0, payload.length); + } catch (GeneralSecurityException gse) { + if (_log.shouldWarn()) + _log.warn("Encrypt fail N", gse); + state.destroy(); + return null; + } + if (_log.shouldDebug()) + _log.debug("Encrypted N: " + enc.length + " bytes, state: " + state); + + state.destroy(); + return enc; + } + + /** * scenario 2: New Session Reply Message * @@ -1367,7 +1525,7 @@ public final class ECIESAEADEngine { RouterContext ctx = new RouterContext(null, props); ctx.initAll(); ECIESAEADEngine e = new ECIESAEADEngine(ctx); - RatchetSKM rskm = new RatchetSKM(ctx); + RatchetSKM rskm = new RatchetSKM(ctx, new Destination()); net.i2p.crypto.KeyPair kp = ctx.keyGenerator().generatePKIKeys(EncType.ECIES_X25519); PublicKey pubKey = kp.getPublic(); PrivateKey privKey = kp.getPrivate(); @@ -1383,11 +1541,11 @@ public final class ECIESAEADEngine { clove.setData(msg); clove.setCertificate(Certificate.NULL_CERT); clove.setCloveId(0); - clove.setExpiration(new java.util.Date(System.currentTimeMillis() + 10000)); + clove.setExpiration(System.currentTimeMillis() + 10000); clove.setInstructions(net.i2p.data.i2np.DeliveryInstructions.LOCAL); GarlicClove[] arr = new GarlicClove[1]; arr[0] = clove; - CloveSet cs = new CloveSet(arr, Certificate.NULL_CERT, clove.getCloveId(), clove.getExpiration().getTime()); + CloveSet cs = new CloveSet(arr, Certificate.NULL_CERT, clove.getCloveId(), clove.getExpiration()); // IK test byte[] encrypted = e.encrypt(cs, pubKey, dest, privKey2, rskm, null); @@ -1409,6 +1567,7 @@ public final class ECIESAEADEngine { } // N test + rskm = new RatchetSKM(ctx); encrypted = e.encrypt(cs, pubKey); System.out.println("N Encrypted:\n" + net.i2p.util.HexDump.dump(encrypted)); diff --git a/router/java/src/net/i2p/router/dummy/VMCommSystem.java b/router/java/src/net/i2p/router/dummy/VMCommSystem.java index 9b804645e..6f2df3792 100644 --- a/router/java/src/net/i2p/router/dummy/VMCommSystem.java +++ b/router/java/src/net/i2p/router/dummy/VMCommSystem.java @@ -16,6 +16,7 @@ import net.i2p.router.CommSystemFacade; import net.i2p.router.JobImpl; import net.i2p.router.OutNetMessage; import net.i2p.router.RouterContext; +import net.i2p.router.transport.crypto.X25519KeyFactory; import net.i2p.util.Log; /** @@ -29,6 +30,8 @@ import net.i2p.util.Log; public class VMCommSystem extends CommSystemFacade { private final Log _log; private final RouterContext _context; + private final X25519KeyFactory _xdhThread; + /** * Mapping from Hash to VMCommSystem for all routers hooked together */ @@ -47,7 +50,16 @@ public class VMCommSystem extends CommSystemFacade { _context.statManager().createRateStat("transport.sendMessageLarge", "How many messages over 4KB are sent?", "Transport", new long[] { 60*1000l, 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); _context.statManager().createRateStat("transport.receiveMessageLarge", "How many messages over 4KB are received?", "Transport", new long[] { 60*1000l, 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); _context.statManager().createRequiredRateStat("transport.sendProcessingTime", "Time to process and send a message (ms)", "Transport", new long[] { 60*1000l, 10*60*1000l, 60*60*1000l, 24*60*60*1000l }); + // we do NOT start the thread, all keys will be generated inline + _xdhThread = new X25519KeyFactory(context); } + + /** + * Factory for making X25519 key pairs. + * @since 0.9.49 so some tests don't NPE + */ + @Override + public X25519KeyFactory getXDHFactory() { return _xdhThread; } public int countActivePeers() { return _commSystemFacades.size() - 1; } diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillMonitorJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillMonitorJob.java index 6c8ee1439..34a9b2661 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillMonitorJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillMonitorJob.java @@ -145,9 +145,6 @@ class FloodfillMonitorJob extends JobImpl { RouterIdentity ident = ri.getIdentity(); if (ident.getSigningPublicKey().getType() == SigType.DSA_SHA1) return false; - // temp until router ratchet SKM implemented - if (ident.getPublicKey().getType() != EncType.ELGAMAL_2048) - return false; char bw = ri.getBandwidthTier().charAt(0); // Only if class N, O, P, X