forked from I2P_Developers/i2p.i2p
Crypto: Base classes for ECIES-Ratchet (proposal 144)
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
340
router/java/src/net/i2p/router/crypto/ratchet/Elligator2.java
Normal file
340
router/java/src/net/i2p/router/crypto/ratchet/Elligator2.java
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
****/
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user