diff --git a/router/java/src/net/i2p/router/crypto/ratchet/Elg2KeyFactory.java b/router/java/src/net/i2p/router/crypto/ratchet/Elg2KeyFactory.java new file mode 100644 index 000000000..b2f336f85 --- /dev/null +++ b/router/java/src/net/i2p/router/crypto/ratchet/Elg2KeyFactory.java @@ -0,0 +1,178 @@ +package net.i2p.router.crypto.ratchet; + +import java.util.concurrent.LinkedBlockingQueue; + +import net.i2p.I2PAppContext; +import net.i2p.crypto.EncType; +import net.i2p.crypto.KeyFactory; +import net.i2p.crypto.KeyPair; +import net.i2p.data.PrivateKey; +import net.i2p.data.PublicKey; +import net.i2p.util.I2PThread; +import net.i2p.util.Log; +import net.i2p.util.SystemVersion; + +/** + * Elligator2 for X25519 keys. + * + * Try to keep DH pairs at the ready. + * It's important to do this in a separate thread, because if we run out, + * the pairs are generated in the NTCP Pumper thread, + * and it can fall behind. + * + * @since 0.9.44 from X25519KeyFactory + */ +public class Elg2KeyFactory extends I2PThread implements KeyFactory { + + private final I2PAppContext _context; + private final Log _log; + private final int _minSize; + private final int _maxSize; + private final int _calcDelay; + private final LinkedBlockingQueue _keys; + private volatile boolean _isRunning; + private long _checkDelay = 10 * 1000; + + private final static String PROP_DH_PRECALC_MIN = "crypto.edh.precalc.min"; + private final static String PROP_DH_PRECALC_MAX = "crypto.edh.precalc.max"; + private final static String PROP_DH_PRECALC_DELAY = "crypto.edh.precalc.delay"; + private final static int DEFAULT_DH_PRECALC_MIN = 10; + private final static int DEFAULT_DH_PRECALC_MAX = 30; + private final static int DEFAULT_DH_PRECALC_DELAY = 25; + + public Elg2KeyFactory(I2PAppContext ctx) { + super("EDH Precalc"); + _context = ctx; + _log = ctx.logManager().getLog(Elg2KeyFactory.class); + ctx.statManager().createRateStat("crypto.EDHGenerateTime", "How long it takes to create x and X", "Encryption", new long[] { 60*60*1000 }); + ctx.statManager().createRateStat("crypto.EDHUsed", "Need a DH from the queue", "Encryption", new long[] { 60*60*1000 }); + ctx.statManager().createRateStat("crypto.EDHReused", "Unused DH requeued", "Encryption", new long[] { 60*60*1000 }); + ctx.statManager().createRateStat("crypto.EDHEmpty", "DH queue empty", "Encryption", new long[] { 60*60*1000 }); + + // add to the defaults for every 128MB of RAM, up to 512MB + long maxMemory = SystemVersion.getMaxMemory(); + int factor = (int) Math.max(1l, Math.min(4l, 1 + (maxMemory / (128*1024*1024l)))); + int defaultMin = DEFAULT_DH_PRECALC_MIN * factor; + int defaultMax = DEFAULT_DH_PRECALC_MAX * factor; + _minSize = ctx.getProperty(PROP_DH_PRECALC_MIN, defaultMin); + _maxSize = ctx.getProperty(PROP_DH_PRECALC_MAX, defaultMax); + _calcDelay = ctx.getProperty(PROP_DH_PRECALC_DELAY, DEFAULT_DH_PRECALC_DELAY); + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("EDH Precalc (minimum: " + _minSize + " max: " + _maxSize + ", delay: " + + _calcDelay + ")"); + _keys = new LinkedBlockingQueue(_maxSize); + if (!SystemVersion.isWindows()) + setPriority(Thread.NORM_PRIORITY - 1); + } + + /** + * Note that this stops the singleton precalc thread. + * You don't want to do this if there are multiple routers in the JVM. + * Fix this if you care. See Router.shutdown(). + */ + public void shutdown() { + _isRunning = false; + this.interrupt(); + _keys.clear(); + } + + public void run() { + try { + run2(); + } catch (IllegalStateException ise) { + if (_isRunning) + throw ise; + // else ignore, thread can be slow to shutdown on Android, + // PRNG gets stopped first and throws ISE + } + } + + private void run2() { + _isRunning = true; + while (_isRunning) { + int startSize = getSize(); + // Adjust delay + if (startSize <= (_minSize * 2 / 3) && _checkDelay > 1000) + _checkDelay -= 1000; + else if (startSize > (_minSize * 3 / 2) && _checkDelay < 60*1000) + _checkDelay += 1000; + if (startSize < _minSize) { + // fill all the way up, do the check here so we don't + // throw away one when full in addValues() + while (getSize() < _maxSize && _isRunning) { + long curStart = System.currentTimeMillis(); + if (!addKeys(precalc())) + break; + long curCalc = System.currentTimeMillis() - curStart; + // for some relief... + if (!interrupted()) { + try { + Thread.sleep(Math.min(200, Math.max(10, _calcDelay + (curCalc * 3)))); + } catch (InterruptedException ie) {} + } + } + } + if (!_isRunning) + break; + try { + Thread.sleep(_checkDelay); + } catch (InterruptedException ie) { // nop + } + } + } + + /** + * Pulls a prebuilt keypair from the queue, + * or if not available, construct a new one. + */ + public Elg2KeyPair getKeys() { + _context.statManager().addRateData("crypto.EDHUsed", 1); + Elg2KeyPair rv = _keys.poll(); + if (rv == null) { + _context.statManager().addRateData("crypto.EDHEmpty", 1); + rv = precalc(); + // stop sleeping, wake up, make some more + this.interrupt(); + } + return rv; + } + + private Elg2KeyPair precalc() { + long start = System.currentTimeMillis(); + KeyPair rv; + byte[] enc; + int i = 0; + do { + rv = _context.keyGenerator().generatePKIKeys(EncType.ECIES_X25519); + enc = Elligator2.encode(rv.getPublic(), _context.random().nextBoolean()); + i++; + } while (enc == null); + long diff = System.currentTimeMillis() - start; + _context.statManager().addRateData("crypto.EDHGenerateTime", diff); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Took " + i + " tries and " + diff + "ms to generate local DH value"); + return new Elg2KeyPair(rv.getPublic(), rv.getPrivate(), enc); + } + + /** + * Return an unused DH key builder + * to be put back onto the queue for reuse. + */ + public void returnUnused(Elg2KeyPair kp) { +/* + _context.statManager().addRateData("crypto.EDHReused", 1); + _keys.offer(kp); +*/ + } + + /** @return true if successful, false if full */ + private final boolean addKeys(Elg2KeyPair kp) { + return _keys.offer(kp); + } + + private final int getSize() { + return _keys.size(); + } + +} diff --git a/router/java/src/net/i2p/router/crypto/ratchet/Elg2KeyPair.java b/router/java/src/net/i2p/router/crypto/ratchet/Elg2KeyPair.java new file mode 100644 index 000000000..1c05e4c83 --- /dev/null +++ b/router/java/src/net/i2p/router/crypto/ratchet/Elg2KeyPair.java @@ -0,0 +1,24 @@ +package net.i2p.router.crypto.ratchet; + +import net.i2p.crypto.KeyPair; +import net.i2p.data.PrivateKey; +import net.i2p.data.PublicKey; + +/** + * X25519 keys, with the public key Elligator2 encoding pre-calculated + * + * @since 0.9.44 + */ +public class Elg2KeyPair extends KeyPair { + + private final byte[] encoded; + + public Elg2KeyPair(PublicKey publicKey, PrivateKey privateKey, byte[] enc) { + super(publicKey, privateKey); + encoded = enc; + } + + public byte[] getEncoded() { + return encoded; + } +} diff --git a/router/java/src/net/i2p/router/crypto/ratchet/Elligator2.java b/router/java/src/net/i2p/router/crypto/ratchet/Elligator2.java new file mode 100644 index 000000000..3742c9bf4 --- /dev/null +++ b/router/java/src/net/i2p/router/crypto/ratchet/Elligator2.java @@ -0,0 +1,340 @@ +package net.i2p.router.crypto.ratchet; + +import java.math.BigInteger; +import java.util.concurrent.atomic.AtomicBoolean; + +import net.i2p.I2PAppContext; +import net.i2p.crypto.EncType; +import net.i2p.crypto.SigUtil; +import net.i2p.crypto.KeyPair; +import net.i2p.crypto.eddsa.math.bigint.BigIntegerLittleEndianEncoding; +import net.i2p.crypto.eddsa.math.Curve; +import net.i2p.crypto.eddsa.math.Field; +import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec; +import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable; +import net.i2p.data.DataHelper; +import net.i2p.data.PrivateKey; +import net.i2p.data.PublicKey; +import net.i2p.router.transport.crypto.X25519KeyFactory; +import net.i2p.util.HexDump; +import net.i2p.util.NativeBigInteger; + +/** + * Elligator2 for X25519 keys. + * + * Ported from the Jan. 13, 2016 C version at https://github.com/Kleshni/Elligator-2 + * Note: That code was completely rewritten May 8, 2017 and is now much more complex. + * No apparent license. + * + * @since 0.9.44 + */ +class Elligator2 { + + private final I2PAppContext _context; + + private static final BigInteger p, divide_plus_p_3_8, divide_minus_p_1_2, divide_minus_p_1_4, square_root_negative_1; + private static final long Aint = 486662; + private static final BigInteger A = new BigInteger(Long.toString(Aint)); + private static final BigInteger negative_A; + private static final BigInteger u, inverted_u; + private static final BigInteger TWO = new NativeBigInteger("2"); + + private static final int POINT_LENGTH = 32; + private static final int REPRESENTATIVE_LENGTH = 32; + + private static final EdDSANamedCurveSpec SPEC = EdDSANamedCurveTable.getByName("ed25519-sha-512"); + private static final Curve CURVE = SPEC.getCurve(); + private static final Field FIELD = CURVE.getField(); + private static final BigIntegerLittleEndianEncoding ENCODING = new BigIntegerLittleEndianEncoding(); + + private static final boolean DISABLE = false; + + static { + ENCODING.setField(FIELD); + + // p = 2 ^ 255 - 19 + p = TWO.pow(255).subtract(new BigInteger("19")); + + // divide_plus_p_3_8 = (p + 3) / 8 + divide_plus_p_3_8 = p.add(new BigInteger("3")).divide(new BigInteger("8")); + + // divide_minus_p_1_2 = (p - 1) / 2 + divide_minus_p_1_2 = p.subtract(BigInteger.ONE).divide(TWO); + + // divide_minus_p_1_4 = (p - 1) / 4 + divide_minus_p_1_4 = divide_minus_p_1_2.divide(TWO); + + // square_root_negative_1 = 2 ^ divide_minus_p_1_4 (mod p) + square_root_negative_1 = TWO.modPow(divide_minus_p_1_4, p); + + // negative_A = -A (mod p) + negative_A = p.subtract(A); + + // u = 2 + u = TWO; + + // inverted_u = 1 / u (mod p) + inverted_u = u.modInverse(p); + } + + public Elligator2(I2PAppContext ctx) { + _context = ctx; + } + + /** + * From javascript version documentation: + * + * The algorithm can return two different values for a single x coordinate if it's not 0. + * Which one to return is determined by y coordinate. + * Since Curve25519 doesn't use y due to optimizations, you should specify a Boolean value + * as the second argument of the function. + * It should be unpredictable, because it's recoverable from the representative. + * + * @return "representative", little endian or null on failure + */ + public byte[] encode(PublicKey point) { + return encode(point, _context.random().nextBoolean()); + } + + /** + * From javascript version documentation: + * + * The algorithm can return two different values for a single x coordinate if it's not 0. + * Which one to return is determined by y coordinate. + * Since Curve25519 doesn't use y due to optimizations, you should specify a Boolean value + * as the second argument of the function. + * It should be unpredictable, because it's recoverable from the representative. + * + * @return "representative", little endian or null on failure + */ + public static byte[] encode(PublicKey point, boolean alternative) { + if (DISABLE) + return point.getData(); + + // x + BigInteger x = ENCODING.toBigInteger(point.getData()); + + // If x = 0 + if (x.signum() == 0) { + alternative = false; + } + + // negative_plus_x_A = -(x + A) (mod p) + BigInteger negative_plus_x_A = x.add(A).negate(); + + // negative_multiply3_u_x_plus_x_A = -ux(x + A) (mod p) + BigInteger negative_multiply3_u_x_plus_x_A = u.multiply(x); + negative_multiply3_u_x_plus_x_A = negative_multiply3_u_x_plus_x_A.mod(p); + negative_multiply3_u_x_plus_x_A = negative_multiply3_u_x_plus_x_A.multiply(negative_plus_x_A); + negative_multiply3_u_x_plus_x_A = negative_multiply3_u_x_plus_x_A.mod(p); + + // If -ux(x + A) is not a square modulo p + if (legendre(negative_multiply3_u_x_plus_x_A, p) == -1) { + return null; + } + + BigInteger r; + if (alternative) { + // r := -(x + A) / x (mod p) + r = x.modInverse(p); + r = r.multiply(negative_plus_x_A); + } else { + // r := -x / (x + A) (mod p) + r = negative_plus_x_A.modInverse(p); + r = r.multiply(x); + } + r = r.mod(p); + + // r := square_root(r / u) (mod p) + r = r.multiply(inverted_u); + r = r.mod(p); + r = square_root(r); + + // little endian + byte[] rv = ENCODING.encode(r); + return rv; + } + + /** + * From javascript version documentation: + * + * Returns an array with the point and the second argument of the corresponding call to the `encode` function. + * It's also able to return null if the representative is invalid (there are only 10 invalid representatives). + * + * @param representative the encoded data, 32 bytes + * @return x or null on failure + */ + public static PublicKey decode(byte[] representative) { + return decode(null, representative); + } + + /** + * From javascript version documentation: + * + * Returns an array with the point and the second argument of the corresponding call to the `encode` function. + * It's also able to return null if the representative is invalid (there are only 10 invalid representatives). + * + * @param alternative out parameter, or null if you don't care + * @param representative the encoded data, 32 bytes + * @return x or null on failure + */ + public static PublicKey decode(AtomicBoolean alternative, byte[] representative) { + if (representative.length != 32) + throw new IllegalArgumentException("must be 32 bytes"); + if (DISABLE) + return new PublicKey(EncType.ECIES_X25519, representative); + + // r + BigInteger r = ENCODING.toBigInteger(representative); + + // If r >= (p - 1) / 2 + if (r.compareTo(divide_minus_p_1_2) >= 0) { + return null; + } + + // v = -A / (1 + ur ^ 2) (mod p) + BigInteger v = r.multiply(r); + v = v.mod(p); + v = v.multiply(u); + v = v.add(BigInteger.ONE); + v = v.mod(p); + v = v.modInverse(p); + v = v.multiply(negative_A); + v = v.mod(p); + + // plus_v_A = v + A (mod p) + BigInteger plus_v_A = v.add(A); + + // t = x ^ 3 + Ax ^ 2 + Bx (mod p) + BigInteger t = v.multiply(v); + t = t.mod(p); + t = t.multiply(plus_v_A); + t = t.add(v); + t = t.mod(p); + + // e = Legendre symbol (t / p) + int e = legendre(t, p); + + BigInteger x; + if (e == 1) { + x = v; + } else { + x = p.subtract(v); + x = x.subtract(A); + x = x.mod(p); + } + + if (alternative != null) + alternative.set(e == 1); + + byte[] dec = ENCODING.encode(x); + return new PublicKey(EncType.ECIES_X25519, dec); + } + + private static BigInteger square_root(BigInteger x) { + // t = x ^ ((p - 1) / 4) (mod p) + if (!(x instanceof NativeBigInteger)) + x = new NativeBigInteger(x); + + BigInteger t = x.modPow(divide_minus_p_1_4, p); + + // result := x ^ ((p + 3) / 8) (mod p) + BigInteger result = x.modPow(divide_plus_p_3_8, p); + + // If t = -1 (mod p) + t = t.add(BigInteger.ONE); + if (t.compareTo(p) == 0) { + // result := result * square_root(-1) (mod p) + result = result.multiply(square_root_negative_1); + result = result.mod(p); + } + + // If result > (p - 1) / 2 + if (result.compareTo(divide_minus_p_1_2) > 0) { + // result := -result (mod p) + result = p.subtract(result); + } + return result; + } + + /** + * https://gmplib.org/manual/Number-Theoretic-Functions.html + * https://en.wikipedia.org/wiki/Legendre_symbol + * + * @return -1/0/1 + */ + private static int legendre(BigInteger a, BigInteger p) { + if (a.mod(p).signum() == 0) + return 0; + if (!(a instanceof NativeBigInteger)) + a = new NativeBigInteger(a); + BigInteger pm1d2 = p.subtract(BigInteger.ONE).divide(TWO); + BigInteger mp = a.modPow(pm1d2, p); + // mp is either 1 or (p - 1) (0x7ffff...fffec) + //System.out.println("Legendre value: " + mp.toString(16)); + int cmp = mp.compareTo(BigInteger.ONE); + if (cmp == 0) + return 1; + return -1; + } + +/**** + private static final byte[] TEST1 = new byte[] { + 0x33, (byte) 0x95, 0x19, 0x64, 0x00, 0x3c, (byte) 0x94, 0x08, + 0x78, 0x06, 0x3c, (byte) 0xcf, (byte) 0xd0, 0x34, (byte) 0x8a, (byte) 0xf4, + 0x21, 0x50, (byte) 0xca, 0x16, (byte) 0xd2, 0x64, 0x6f, 0x2c, + 0x58, 0x56, (byte) 0xe8, 0x33, (byte) 0x83, 0x77, (byte) 0xd8, (byte) 0x80 + }; + + private static final byte[] TEST2 = new byte[] { + (byte) 0xe7, 0x35, 0x07, (byte) 0xd3, (byte) 0x8b, (byte) 0xae, 0x63, (byte) 0x99, + 0x2b, 0x3f, 0x57, (byte) 0xaa, (byte) 0xc4, (byte) 0x8c, 0x0a, (byte) 0xbc, + 0x14, 0x50, (byte) 0x95, (byte) 0x89, 0x28, (byte) 0x84, 0x57, (byte) 0x99, + 0x5a, 0x2b, 0x4c, (byte) 0xa3, 0x49, 0x0a, (byte) 0xa2, 0x07 + }; + + public static void main(String[] args) { + System.out.println("Test encode:\n" + HexDump.dump(TEST1)); + PublicKey test = new PublicKey(EncType.ECIES_X25519, TEST1); + byte[] repr = encode(test, false); + System.out.println("encoded with false:\n" + HexDump.dump(repr)); + //00000000 28 20 b6 b2 41 e0 f6 8a 6c 4a 7f ee 3d 97 82 28 |( ..A...lJ..=..(| + //00000010 ef 3a e4 55 33 cd 41 0a a9 1a 41 53 31 d8 61 2d |.:.U3.A...AS1.a-| + repr = encode(test, true); + System.out.println("encoded with true:\n" + HexDump.dump(repr)); + //00000000 3c fb 87 c4 6c 0b 45 75 ca 81 75 e0 ed 1c 0a e9 |<...l.Eu..u.....| + //00000010 da e7 9d b7 8d f8 69 97 c4 84 7b 9f 20 b2 77 18 |......i...{. .w.| + + System.out.println("Test decode:\n" + HexDump.dump(TEST2)); + PublicKey pk = decode(null, TEST2); + System.out.println("decoded:\n" + HexDump.dump(pk.getData())); + //00000000 1e 8a ff fe d6 bf 53 fe 27 1a d5 72 47 32 62 de |......S.'..rG2b.| + //00000010 d8 fa ec 68 e5 e6 7e f4 5e bb 82 ee ba 52 60 4f |...h..~.^....R`O| + + I2PAppContext ctx = I2PAppContext.getGlobalContext(); + X25519KeyFactory xkf = new X25519KeyFactory(ctx); + for (int i = 0; i < 10; i++) { + PublicKey pub; + byte[] enc; + int j = 0; + do { + System.out.println("Trying encode " + ++j); + KeyPair kp = xkf.getKeys(); + pub = kp.getPublic(); + enc = encode(pub, ctx.random().nextBoolean()); + } while (enc == null); + PublicKey pub2 = decode(null, enc); + if (pub2 == null) { + System.out.println("Decode FAIL"); + continue; + } + boolean ok = pub.equals(pub2); + System.out.println(ok ? "PASS" : "FAIL"); + if (!ok) { + System.out.println("orig: " + pub.toBase64()); + System.out.println("calc: " + pub2.toBase64()); + } + } + } +****/ +} diff --git a/router/java/src/net/i2p/router/crypto/ratchet/RatchetEntry.java b/router/java/src/net/i2p/router/crypto/ratchet/RatchetEntry.java new file mode 100644 index 000000000..d0c26d3c4 --- /dev/null +++ b/router/java/src/net/i2p/router/crypto/ratchet/RatchetEntry.java @@ -0,0 +1,18 @@ +package net.i2p.router.crypto.ratchet; + +import net.i2p.data.SessionTag; + +/** + * + * @since 0.9.44 + */ +class RatchetEntry { + public final RatchetSessionTag tag; + public final SessionKeyAndNonce key; + + /** outbound - calculated key */ + public RatchetEntry(RatchetSessionTag tag, SessionKeyAndNonce key) { + this.tag = tag; + this.key = key; + } +} diff --git a/router/java/src/net/i2p/router/crypto/ratchet/RatchetPayload.java b/router/java/src/net/i2p/router/crypto/ratchet/RatchetPayload.java new file mode 100644 index 000000000..8ddb61f1f --- /dev/null +++ b/router/java/src/net/i2p/router/crypto/ratchet/RatchetPayload.java @@ -0,0 +1,334 @@ +package net.i2p.router.crypto.ratchet; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import net.i2p.I2PAppContext; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.i2np.GarlicMessage; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.i2np.I2NPMessageException; +import net.i2p.data.i2np.I2NPMessageImpl; +import net.i2p.router.RouterContext; + +/** + * + * Ratchet payload generation and parsing + * + * @since 0.9.44 adapted from NTCP2Payload + */ +class RatchetPayload { + + public static final int BLOCK_HEADER_SIZE = 3; + + private static final int BLOCK_DATETIME = 0; + private static final int BLOCK_SESSIONID = 1; + private static final int BLOCK_GARLIC = 3; + private static final int BLOCK_TERMINATION = 4; + private static final int BLOCK_OPTIONS = 5; + private static final int BLOCK_MSGNUM = 6; + private static final int BLOCK_NEXTKEY = 7; + private static final int BLOCK_ACKKEY = 8; + private static final int BLOCK_REPLYDI = 9; + private static final int BLOCK_PADDING = 254; + + /** + * For all callbacks, recommend throwing exceptions only from the handshake. + * Exceptions will get thrown out of processPayload() and prevent + * processing of succeeding blocks. + */ + public interface PayloadCallback { + public void gotDateTime(long time) throws DataFormatException; + + public void gotGarlic(byte[] data, int off, int len) throws DataFormatException; + + /** + * @param isHandshake true only for message 3 part 2 + */ + public void gotOptions(byte[] options, boolean isHandshake) throws DataFormatException; + + /** + * @param lastReceived in theory could wrap around to negative, but very unlikely + */ + public void gotTermination(int reason, long lastReceived); + + /** + * For stats. + * @param paddingLength the number of padding bytes, not including the 3-byte block header + * @param frameLength the total size of the frame, including all blocks and block headers + */ + public void gotPadding(int paddingLength, int frameLength); + + public void gotUnknown(int type, int len); + } + + /** + * Incoming payload. Calls the callback for each received block. + * + * @return number of blocks processed + * @throws IOException on major errors + * @throws DataFormatException on parsing of individual blocks + * @throws I2NPMessageException on parsing of I2NP block + */ + public static int processPayload(I2PAppContext ctx, PayloadCallback cb, + byte[] payload, int off, int length, boolean isHandshake) + throws IOException, DataFormatException, I2NPMessageException { + int blocks = 0; + boolean gotPadding = false; + boolean gotTermination = false; + int i = off; + final int end = off + length; + while (i < end) { + int type = payload[i++] & 0xff; + if (gotPadding) + throw new IOException("Illegal block after padding: " + type); + if (gotTermination && type != BLOCK_PADDING) + throw new IOException("Illegal block after termination: " + type); + int len = (int) DataHelper.fromLong(payload, i, 2); + i += 2; + if (i + len > end) { + throw new IOException("Block " + blocks + " type " + type + " length " + len + + " at offset " + (i - 3 - off) + " runs over frame of size " + length + + '\n' + net.i2p.util.HexDump.dump(payload, off, length)); + } + switch (type) { + // don't modify i inside switch + + case BLOCK_DATETIME: + if (len != 4) + throw new IOException("Bad length for DATETIME: " + len); + long time = DataHelper.fromLong(payload, i, 4) * 1000; + cb.gotDateTime(time); + break; + + case BLOCK_OPTIONS: + byte[] options = new byte[len]; + System.arraycopy(payload, i, options, 0, len); + cb.gotOptions(options, isHandshake); + break; + + case BLOCK_GARLIC: + cb.gotGarlic(payload, i, len); + break; + + case BLOCK_TERMINATION: + if (isHandshake) + throw new IOException("Illegal block in handshake: " + type); + if (len < 9) + throw new IOException("Bad length for TERMINATION: " + len); + long last = fromLong8(payload, i); + int rsn = payload[i + 8] & 0xff; + cb.gotTermination(rsn, last); + gotTermination = true; + break; + + case BLOCK_PADDING: + gotPadding = true; + cb.gotPadding(len, length); + break; + + default: + if (isHandshake) + throw new IOException("Illegal block in handshake: " + type); + cb.gotUnknown(type, len); + break; + + } + i += len; + blocks++; + } + if (isHandshake && blocks == 0) + throw new IOException("No blocks in handshake"); + return blocks; + } + + /** + * @param payload writes to it starting at off + * @return the new offset + */ + public static int writePayload(byte[] payload, int off, List blocks) { + for (Block block : blocks) { + off = block.write(payload, off); + } + return off; + } + + /** + * Base class for blocks to be transmitted. + * Not used for receive; we use callbacks instead. + */ + public static abstract class Block { + private final int type; + + public Block(int ttype) { + type = ttype; + } + + /** @return new offset */ + public int write(byte[] tgt, int off) { + tgt[off++] = (byte) type; + // we do it this way so we don't call getDataLength(), + // which may be inefficient + // off is where the length goes + int rv = writeData(tgt, off + 2); + DataHelper.toLong(tgt, off, 2, rv - (off + 2)); + return rv; + } + + /** + * @return the size of the block, including the 3 byte header (type and size) + */ + public int getTotalLength() { + return BLOCK_HEADER_SIZE + getDataLength(); + } + + /** + * @return the size of the block, NOT including the 3 byte header (type and size) + */ + public abstract int getDataLength(); + + /** @return new offset */ + public abstract int writeData(byte[] tgt, int off); + + @Override + public String toString() { + return "Payload block type " + type + " length " + getDataLength(); + } + } + + public static class GarlicBlock extends Block { + private byte[] d; + + public GarlicBlock(byte[] data) { + super(BLOCK_GARLIC); + d = data; + } + + public int getDataLength() { + return d.length; + } + + public int writeData(byte[] tgt, int off) { + System.arraycopy(d, 0, tgt, off, d.length); + return off + d.length; + } + } + + public static class PaddingBlock extends Block { + private final int sz; + private final I2PAppContext ctx; + + /** with zero-filled data */ + public PaddingBlock(int size) { + this(null, size); + } + + /** with random data */ + public PaddingBlock(I2PAppContext context, int size) { + super(BLOCK_PADDING); + sz = size; + ctx = context; + } + + public int getDataLength() { + return sz; + } + + public int writeData(byte[] tgt, int off) { + if (ctx != null) + ctx.random().nextBytes(tgt, off, sz); + else + Arrays.fill(tgt, off, off + sz, (byte) 0); + return off + sz; + } + } + + public static class DateTimeBlock extends Block { + private final long now; + + public DateTimeBlock(long time) { + super(BLOCK_DATETIME); + now = time; + } + + public int getDataLength() { + return 4; + } + + public int writeData(byte[] tgt, int off) { + DataHelper.toLong(tgt, off, 4, now / 1000); + return off + 4; + } + } + + public static class OptionsBlock extends Block { + private final byte[] opts; + + public OptionsBlock(byte[] options) { + super(BLOCK_OPTIONS); + opts = options; + } + + public int getDataLength() { + return opts.length; + } + + public int writeData(byte[] tgt, int off) { + System.arraycopy(opts, 0, tgt, off, opts.length); + return off + opts.length; + } + } + + public static class TerminationBlock extends Block { + private final byte rsn; + private final long rcvd; + + public TerminationBlock(int reason, long lastReceived) { + super(BLOCK_TERMINATION); + rsn = (byte) reason; + rcvd = lastReceived; + } + + public int getDataLength() { + return 9; + } + + public int writeData(byte[] tgt, int off) { + toLong8(tgt, off, rcvd); + tgt[off + 8] = rsn; + return off + 9; + } + } + + /** + * Big endian. + * Same as DataHelper.fromLong(src, offset, 8) but allows negative result + * + * @throws ArrayIndexOutOfBoundsException + */ + static long fromLong8(byte src[], int offset) { + long rv = 0; + int limit = offset + 8; + for (int i = offset; i < limit; i++) { + rv <<= 8; + rv |= src[i] & 0xFF; + } + return rv; + } + + /** + * Big endian. + * Same as DataHelper.toLong(target, offset, 8, value) but allows negative value + * + * @throws ArrayIndexOutOfBoundsException + */ + static void toLong8(byte target[], int offset, long value) { + for (int i = offset + 7; i >= offset; i--) { + target[i] = (byte) value; + value >>= 8; + } + } +} diff --git a/router/java/src/net/i2p/router/crypto/ratchet/RatchetSessionTag.java b/router/java/src/net/i2p/router/crypto/ratchet/RatchetSessionTag.java new file mode 100644 index 000000000..936cb15ac --- /dev/null +++ b/router/java/src/net/i2p/router/crypto/ratchet/RatchetSessionTag.java @@ -0,0 +1,67 @@ +package net.i2p.router.crypto.ratchet; + +import java.util.Arrays; + +import net.i2p.data.Base64; + +/** + * 8 bytes, usually of random data. + * Does not extend SessionTag or DataStructure to save space + * + * @since 0.9.44 + */ +public class RatchetSessionTag { + public final static int LENGTH = 8; + + private final long _data; + + public RatchetSessionTag(byte val[]) { + if (val.length != LENGTH) + throw new IllegalArgumentException(); + _data = RatchetPayload.fromLong8(val, 0); + } + + public byte[] getData() { + byte[] rv = new byte[LENGTH]; + RatchetPayload.toLong8(rv, 0, _data); + return rv; + } + + public int length() { + return LENGTH; + } + + public String toBase64() { + return Base64.encode(getData()); + } + + /** + * We assume the data has enough randomness in it, so use the first 4 bytes for speed. + * If this is not the case, override in the extending class. + */ + @Override + public int hashCode() { + return (int) _data; + } + + /** + * Warning - this returns true for two different classes with the same size + * and same data, e.g. SessionKey and SessionTag, but you wouldn't + * put them in the same Set, would you? + */ + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if ((obj == null) || !(obj instanceof RatchetSessionTag)) return false; + return _data == ((RatchetSessionTag) obj)._data; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(64); + buf.append("[RatchetSessionTag: "); + buf.append(toBase64()); + buf.append(']'); + return buf.toString(); + } +} diff --git a/router/java/src/net/i2p/router/crypto/ratchet/SessionKeyAndNonce.java b/router/java/src/net/i2p/router/crypto/ratchet/SessionKeyAndNonce.java new file mode 100644 index 000000000..11da18be7 --- /dev/null +++ b/router/java/src/net/i2p/router/crypto/ratchet/SessionKeyAndNonce.java @@ -0,0 +1,59 @@ +package net.i2p.router.crypto.ratchet; + +import com.southernstorm.noise.protocol.HandshakeState; + +import net.i2p.data.SessionKey; + +/** + * A session key is 32 bytes of data. + * Nonce should be 65535 or less. + * + * @since 0.9.44 + */ +class SessionKeyAndNonce extends SessionKey { + private final int _nonce; + private final HandshakeState _state; + + /** + * For Existing Session + */ + public SessionKeyAndNonce(byte data[], int nonce) { + super(data); + _nonce = nonce; + _state = null; + } + + /** + * For New Session Replies + */ + public SessionKeyAndNonce(HandshakeState state) { + super(); + _nonce = 0; + _state = state; + } + + /** + * For ES, else 0 + */ + public int getNonce() { + return _nonce; + } + + /** + * For inbound NSR only, else null. + * MUST be cloned before processing NSR. + */ + public HandshakeState getHandshakeState() { + return _state; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(64); + buf.append("[SessionKeyAndNonce: "); + buf.append(toBase64()); + buf.append(" nonce: ").append(_nonce); + buf.append(']'); + return buf.toString(); + } +} diff --git a/router/java/src/net/i2p/router/crypto/ratchet/package.html b/router/java/src/net/i2p/router/crypto/ratchet/package.html new file mode 100644 index 000000000..abafe0ded --- /dev/null +++ b/router/java/src/net/i2p/router/crypto/ratchet/package.html @@ -0,0 +1,9 @@ + + +

+Implementation of ECIES-X25519-AEAD-Ratchet (proposal 144). +Since 0.9.44. +Subject to change, not a public API, not for external use. +

+ +