Crypto: Base classes for ECIES-Ratchet (proposal 144)

This commit is contained in:
zzz
2019-10-23 12:33:13 +00:00
parent aa3d2f39b1
commit 236354e5a8
8 changed files with 1029 additions and 0 deletions

View File

@@ -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<Elg2KeyPair> _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<Elg2KeyPair>(_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();
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}
}
****/
}

View File

@@ -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;
}
}

View File

@@ -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<Block> 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;
}
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,9 @@
<html>
<body>
<p>
Implementation of ECIES-X25519-AEAD-Ratchet (proposal 144).
Since 0.9.44.
Subject to change, not a public API, not for external use.
</p>
</body>
</html>