Crypto: Ratchet and Muxed SKMs and Engines (WIP)

This commit is contained in:
zzz
2019-10-24 14:28:39 +00:00
parent 6a47319b66
commit 7b28640e91
4 changed files with 1959 additions and 0 deletions

View File

@@ -0,0 +1,834 @@
package net.i2p.router.crypto.ratchet;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.southernstorm.noise.crypto.x25519.Curve25519;
import com.southernstorm.noise.protocol.ChaChaPolyCipherState;
import com.southernstorm.noise.protocol.CipherState;
import com.southernstorm.noise.protocol.CipherStatePair;
import com.southernstorm.noise.protocol.DHState;
import com.southernstorm.noise.protocol.HandshakeState;
import net.i2p.I2PAppContext;
import net.i2p.crypto.EncType;
import net.i2p.crypto.HKDF;
import net.i2p.crypto.SessionKeyManager;
import net.i2p.data.Base64;
import net.i2p.data.DataFormatException;
import net.i2p.data.DataHelper;
import net.i2p.data.Hash;
import net.i2p.data.PrivateKey;
import net.i2p.data.PublicKey;
import net.i2p.data.SessionKey;
import net.i2p.data.SessionTag;
import static net.i2p.router.crypto.ratchet.RatchetPayload.*;
import net.i2p.util.Log;
import net.i2p.util.SimpleByteCache;
/**
* Handles the actual ECIES+AEAD encryption and decryption scenarios using the
* supplied keys and data.
*
* No, this does not extend ElGamalAESEngine or AEADEngine or CryptixAEADEngine.
*
* @since 0.9.44
*/
public final class ECIESAEADEngine {
private final I2PAppContext _context;
private final Log _log;
private final HKDF _hkdf;
private final Elg2KeyFactory _edhThread;
private boolean _isRunning;
private static final byte[] ZEROLEN = new byte[0];
private static final int TAGLEN = 8;
private static final int MACLEN = 16;
private static final int KEYLEN = 32;
private static final int BHLEN = RatchetPayload.BLOCK_HEADER_SIZE; // 3
private static final int DATETIME_SIZE = BHLEN + 4; // 7
private static final int MIN_NS_SIZE = KEYLEN + KEYLEN + MACLEN + DATETIME_SIZE + MACLEN; // 103
private static final int MIN_NSR_SIZE = TAGLEN + KEYLEN + MACLEN; // 56
private static final int MIN_ES_SIZE = TAGLEN + MACLEN; // 24
private static final int MIN_ENCRYPTED_SIZE = MIN_ES_SIZE;
private static final byte[] NULLPK = new byte[KEYLEN];
private static final int MAXPAD = 16;
private static final String INFO_0 = "SessionReplyTags";
private static final String INFO_6 = "AttachPayloadKDF";
/**
* Caller MUST call startup() to get threaded generation.
* Will still work without, will just generate inline.
*
* startup() is called from RatchetSKM constructor so it's deferred until we need it.
*/
public ECIESAEADEngine(I2PAppContext ctx) {
_context = ctx;
_log = _context.logManager().getLog(ECIESAEADEngine.class);
_hkdf = new HKDF(ctx);
_edhThread = new Elg2KeyFactory(ctx);
_context.statManager().createFrequencyStat("crypto.eciesAEAD.encryptNewSession",
"how frequently we encrypt to a new ECIES/AEAD+SessionTag session?",
"Encryption", new long[] { 60*60*1000l});
_context.statManager().createFrequencyStat("crypto.eciesAEAD.encryptExistingSession",
"how frequently we encrypt to an existing ECIES/AEAD+SessionTag session?",
"Encryption", new long[] { 60*60*1000l});
_context.statManager().createFrequencyStat("crypto.eciesAEAD.decryptNewSession",
"how frequently we decrypt with a new ECIES/AEAD+SessionTag session?",
"Encryption", new long[] { 60*60*1000l});
_context.statManager().createFrequencyStat("crypto.eciesAEAD.decryptExistingSession",
"how frequently we decrypt with an existing ECIES/AEAD+SessionTag session?",
"Encryption", new long[] { 60*60*1000l});
_context.statManager().createFrequencyStat("crypto.eciesAEAD.decryptFailed",
"how frequently we fail to decrypt with ECIES/AEAD+SessionTag?",
"Encryption", new long[] { 60*60*1000l});
}
/**
* May be called multiple times
*/
public synchronized void startup() {
if (!_isRunning) {
_edhThread.start();
_isRunning = true;
}
}
/**
* Cannot be restarted
*/
public synchronized void shutdown() {
_isRunning = false;
_edhThread.shutdown();
}
//// start decrypt ////
/**
* Decrypt the message using the given private key
* and using tags from the specified key manager.
* This works according to the
* ECIES+AEAD algorithm in the data structure spec.
*
* Warning - use the correct SessionKeyManager. Clients should instantiate their own.
* Clients using I2PAppContext.sessionKeyManager() may be correlated with the router,
* unless you are careful to use different keys.
*
* @return decrypted data or null on failure
*/
public byte[] decrypt(byte data[], PrivateKey targetPrivateKey, RatchetSKM keyManager) throws DataFormatException {
if (targetPrivateKey.getType() != EncType.ECIES_X25519)
throw new IllegalArgumentException();
if (data == null) {
if (_log.shouldLog(Log.ERROR)) _log.error("Null data being decrypted?");
return null;
}
if (data.length < MIN_ENCRYPTED_SIZE) {
if (_log.shouldLog(Log.ERROR))
_log.error("Data is less than the minimum size (" + data.length + " < " + MIN_ENCRYPTED_SIZE + ")");
return null;
}
byte tag[] = new byte[TAGLEN];
System.arraycopy(data, 0, tag, 0, TAGLEN);
RatchetSessionTag st = new RatchetSessionTag(tag);
SessionKeyAndNonce key = keyManager.consumeTag(st);
byte decrypted[];
final boolean shouldDebug = _log.shouldDebug();
if (key != null) {
//if (_log.shouldLog(Log.DEBUG)) _log.debug("Key is known for tag " + st);
if (shouldDebug)
_log.debug("Decrypting existing session encrypted with tag: " + st.toString() + ": key: " + key.toBase64() + ": " + data.length + " bytes " /* + Base64.encode(data, 0, 64) */ );
HandshakeState state = key.getHandshakeState();
if (state != null) {
decrypted = decryptExistingSession(tag, data, key, targetPrivateKey);
} else if (data.length >= MIN_NSR_SIZE) {
try {
state = state.clone();
} catch (CloneNotSupportedException e) {
if (_log.shouldWarn())
_log.warn("ECIES decrypt fail: clone()", e);
return null;
}
decrypted = decryptNewSessionReply(tag, data, state);
} else {
decrypted = null;
if (_log.shouldWarn())
_log.warn("ECIES decrypt fail, tag found but no state and too small for NSR: " + data.length + " bytes");
}
if (decrypted != null) {
///
_context.statManager().updateFrequency("crypto.eciesAEAD.decryptExistingSession");
} else {
_context.statManager().updateFrequency("crypto.eciesAEAD.decryptFailed");
if (_log.shouldWarn()) {
_log.warn("ECIES decrypt fail: known tag [" + st + "], failed decrypt");
}
}
} else if (data.length >= MIN_NS_SIZE) {
if (shouldDebug) _log.debug("IB Tag " + st + " not found, trying NS decrypt");
decrypted = decryptNewSession(data, targetPrivateKey);
if (decrypted != null) {
if (shouldDebug) _log.debug("NS decrypt success");
_context.statManager().updateFrequency("crypto.eciesAEAD.decryptNewSession");
} else {
_context.statManager().updateFrequency("crypto.eciesAEAD.decryptFailed");
if (_log.shouldWarn())
_log.warn("ECIES decrypt fail as new session");
}
} else {
decrypted = null;
if (_log.shouldWarn())
_log.warn("ECIES decrypt fail, tag not found and too small for NS: " + data.length + " bytes");
}
return decrypted;
}
/**
* scenario 1: New Session Message
*
* Begin with 80 bytes, ECIES encrypted, containing:
* <pre>
* - 32 byte Elligator2 key
* - 32 byte static key
* - 16 byte MAC
* </pre>
* And then the data:
* <pre>
* - payload (7 bytes minimum for DateTime block)
* - 16 byte MAC
* </pre>
*
* @param data 96 bytes minimum
* @return null if decryption fails
*/
private byte[] decryptNewSession(byte data[], PrivateKey targetPrivateKey)
throws DataFormatException {
HandshakeState state;
try {
state = new HandshakeState(HandshakeState.PATTERN_ID_IK, HandshakeState.RESPONDER, _edhThread);
} catch (GeneralSecurityException gse) {
throw new IllegalStateException("bad proto", gse);
}
state.getLocalKeyPair().setPublicKey(targetPrivateKey.toPublic().getData(), 0);
state.getLocalKeyPair().setPrivateKey(targetPrivateKey.getData(), 0);
state.start();
// Elg2
byte[] tmp = new byte[KEYLEN];
System.arraycopy(data, 0, tmp, 0, KEYLEN);
PublicKey pk = Elligator2.decode(tmp);
if (pk == null) {
if (_log.shouldWarn())
_log.warn("Elg2 decode fail NS");
return null;
}
System.arraycopy(pk.getData(), 0, data, 0, KEYLEN);
int payloadlen = data.length - (KEYLEN + KEYLEN + MACLEN + MACLEN);
byte[] payload = new byte[payloadlen];
try {
state.readMessage(data, 0, data.length, payload, 0);
} catch (GeneralSecurityException gse) {
if (_log.shouldWarn())
_log.warn("Decrypt fail NS", gse);
return null;
}
byte[] bobPK = new byte[KEYLEN];
state.getRemotePublicKey().getPublicKey(bobPK, 0);
if (Arrays.equals(bobPK, NULLPK)) {
// TODO
if (_log.shouldWarn())
_log.warn("Zero static key in IB NS");
return null;
} else {
if (_log.shouldDebug())
_log.debug("Received NS from PK " + Base64.encode(bobPK));
}
// payload
if (payloadlen == 0) {
if (_log.shouldWarn())
_log.warn("Zero length payload in NS");
return null;
}
PLCallback pc = new PLCallback();
try {
int blocks = RatchetPayload.processPayload(_context, pc, payload, 0, payload.length, true);
if (_log.shouldDebug())
_log.debug("Processed " + blocks + " blocks in IB NS");
} catch (DataFormatException e) {
throw e;
} catch (Exception e) {
throw new DataFormatException("Msg 1 payload error", e);
}
if (pc.cloveSet == null) {
if (_log.shouldWarn())
_log.warn("No garlic block in NS payload");
}
return pc.cloveSet;
}
/**
* scenario 2: New Session Reply Message
*
* Begin with 56 bytes, containing:
* <pre>
* - 8 byte SessionTag
* - 32 byte Elligator2 key
* - 16 byte MAC
* </pre>
* And then the data:
* <pre>
* - payload
* - 16 byte MAC
* </pre>
*
* @param tag 8 bytes, same as first 8 bytes of data
* @param data 56 bytes minimum
* @param state must have already been cloned
* @return null if decryption fails
*/
private byte[] decryptNewSessionReply(byte[] tag, byte[] data, HandshakeState state)
throws DataFormatException {
// part 1 - handshake
byte[] yy = new byte[KEYLEN];
System.arraycopy(data, TAGLEN, yy, 0, KEYLEN);
PublicKey k = Elligator2.decode(yy);
if (k == null) {
if (_log.shouldWarn())
_log.warn("Elg2 decode fail NSR");
return null;
}
System.arraycopy(k.getData(), 0, data, TAGLEN, KEYLEN);
state.mixHash(tag, 0, TAGLEN);
try {
state.readMessage(data, 8, 48, ZEROLEN, 0);
} catch (GeneralSecurityException gse) {
if (_log.shouldWarn())
_log.warn("Decrypt fail NSR part 1", gse);
return null;
}
// split()
byte[] ck = state.getChainingKey();
byte[] k_ab = new byte[32];
byte[] k_ba = new byte[32];
_hkdf.calculate(ck, ZEROLEN, k_ab, k_ba, 0);
SessionKey tk = new SessionKey(ck);
byte[] temp_key = doHMAC(tk, ZEROLEN);
// unused
tk = new SessionKey(temp_key);
CipherStatePair ckp = state.split();
CipherState rcvr = ckp.getReceiver();
CipherState sender = ckp.getSender();
byte[] hash = state.getHandshakeHash();
// part 2 - payload
byte[] encpayloadkey = new byte[32];
_hkdf.calculate(k_ba, ZEROLEN, INFO_6, encpayloadkey);
byte[] payload = new byte[data.length - (TAGLEN + KEYLEN + MACLEN + MACLEN)];
try {
rcvr.decryptWithAd(hash, data, TAGLEN + KEYLEN + MACLEN, payload, 0, payload.length + MACLEN);
} catch (GeneralSecurityException gse) {
if (_log.shouldWarn())
_log.warn("Decrypt fail NSR part 2", gse);
return null;
}
if (payload.length == 0) {
if (_log.shouldWarn())
_log.warn("Zero length payload in NSR");
return null;
}
PLCallback pc = new PLCallback();
try {
int blocks = RatchetPayload.processPayload(_context, pc, payload, 0, payload.length, false);
if (_log.shouldDebug())
_log.debug("Processed " + blocks + " blocks in IB NSR");
} catch (DataFormatException e) {
throw e;
} catch (Exception e) {
throw new DataFormatException("NSR payload error", e);
}
RatchetTagSet tagset_ab = new RatchetTagSet(_hkdf, new SessionKey(ck), new SessionKey(k_ab), 0, 0);
RatchetTagSet tagset_ba = new RatchetTagSet(_hkdf, null, new SessionKey(ck), new SessionKey(k_ba), 0, 0, 5, 5);
if (pc.cloveSet == null) {
if (_log.shouldWarn())
_log.warn("No garlic block in NSR payload");
}
return pc.cloveSet;
}
/**
* scenario 3: Existing Session Message
*
* <pre>
* - 8 byte SessionTag
* - payload
* - 16 byte MAC
* </pre>
*
* If anything doesn't match up in decryption, it returns null
*
* @param tag 8 bytes for ad, same as first 8 bytes of data
* @param data 24 bytes minimum, first 8 bytes will be skipped
*
* @return decrypted data or null on failure
*
*/
private byte[] decryptExistingSession(byte[] tag, byte[] data, SessionKeyAndNonce key, PrivateKey targetPrivateKey)
throws DataFormatException {
// TODO decrypt in place?
byte decrypted[] = decryptAEADBlock(tag, data, TAGLEN, data.length - TAGLEN, key, key.getNonce());
if (decrypted == null) {
if (_log.shouldWarn())
_log.warn("Decrypt of ES failed");
return null;
}
if (decrypted.length == 0) {
if (_log.shouldWarn())
_log.warn("Zero length payload in ES");
return null;
}
PLCallback pc = new PLCallback();
try {
int blocks = RatchetPayload.processPayload(_context, pc, decrypted, 0, decrypted.length, false);
if (_log.shouldDebug())
_log.debug("Processed " + blocks + " blocks in IB ES");
} catch (DataFormatException e) {
throw e;
} catch (Exception e) {
throw new DataFormatException("ES payload error", e);
}
if (pc.cloveSet == null) {
if (_log.shouldWarn())
_log.warn("No garlic block in ES payload");
}
return pc.cloveSet;
}
/**
* No AD
*
* @return decrypted data or null on failure
*/
private byte[] decryptAEADBlock(byte encrypted[], int offset, int len, SessionKey key,
long n) throws DataFormatException {
// TODO decrypt in place?
return decryptAEADBlock(null, encrypted, offset, len, key, n);
}
/*
* With optional AD
*
* @param ad may be null
* @return decrypted data or null on failure
*/
private byte[] decryptAEADBlock(byte[] ad, byte encrypted[], int offset, int encryptedLen, SessionKey key,
long n) throws DataFormatException {
// TODO decrypt in place?
byte decrypted[] = new byte[encryptedLen - MACLEN];
ChaChaPolyCipherState chacha = new ChaChaPolyCipherState();
chacha.initializeKey(key.getData(), 0);
chacha.setNonce(n);
try {
chacha.decryptWithAd(ad, encrypted, offset, decrypted, 0, encryptedLen);
} catch (GeneralSecurityException e) {
if (_log.shouldWarn())
_log.warn("Unable to decrypt AEAD block", e);
return null;
}
////
return decrypted;
}
//// end decrypt, start encrypt ////
/**
* Encrypt the data to the target using the given key and deliver the specified tags
* No new session key
* This is the one called from GarlicMessageBuilder and is the primary entry point.
*
* Re: padded size: The AEAD block adds at least 39 bytes of overhead to the data, and
* that is included in the minimum size calculation.
*
* In the router, we always use garlic messages. A garlic message with a single
* clove and zero data is about 84 bytes, so that's 123 bytes minimum. So any paddingSize
* &lt;= 128 is a no-op as every message will be at least 128 bytes
* (Streaming, if used, adds more overhead).
*
* Outside the router, with a client using its own message format, the minimum size
* is 48, so any paddingSize &lt;= 48 is a no-op.
*
* Not included in the minimum is a 32-byte session tag for an existing session,
* or a 514-byte ECIES block and several 32-byte session tags for a new session.
* So the returned encrypted data will be at least 32 bytes larger than paddedSize.
*
* @param target public key to which the data should be encrypted.
* @param priv local private key to encrypt with, from the leaseset
* @param expiration only used for new session messages
* @return encrypted data or null on failure
*
*/
public byte[] encrypt(byte data[], PublicKey target, PrivateKey priv,
RatchetSKM keyManager, long expiration) {
if (target.getType() != EncType.ECIES_X25519)
throw new IllegalArgumentException();
if (Arrays.equals(target.getData(), NULLPK)) {
// TODO
if (_log.shouldWarn())
_log.warn("Zero static key target");
return null;
}
RatchetEntry re = keyManager.consumeNextAvailableTag(target);
if (re == null) {
if (_log.shouldDebug())
_log.debug("Encrypting as NS to " + target);
return encryptNewSession(data, target, priv, keyManager, expiration);
}
////
byte[] tagsetkey = new byte[32];
/*
byte[] ck = state.getChainingKey();
_hkdf.calculate(ck, ZEROLEN, INFO_0, tagsetkey);
RatchetTagSet tagset = new RatchetTagSet(_context, new SessionKey(ck), new SessionKey(tagsetkey), 0, 0);
*/
HandshakeState state = re.key.getHandshakeState();
if (state != null) {
try {
state = state.clone();
} catch (CloneNotSupportedException e) {
if (_log.shouldWarn())
_log.warn("ECIES encrypt fail: clone()", e);
return null;
}
// register state with skm
return encryptNewSessionReply(data, state, re.tag);
}
byte rv[] = encryptExistingSession(data, target, re.key, re.tag);
return rv;
}
/**
* scenario 1: New Session Message
*
* Begin with 80 bytes, ECIES encrypted, containing:
* <pre>
* - 32 byte Elligator2 key
* - 32 byte static key
* - 16 byte MAC
* </pre>
* And then the data:
* <pre>
* - payload
* - 16 byte MAC
* </pre>
*
* @return encrypted data or null on failure
*/
private byte[] encryptNewSession(byte data[], PublicKey target, PrivateKey priv,
RatchetSKM keyManager, long expiration) {
HandshakeState state;
try {
state = new HandshakeState(HandshakeState.PATTERN_ID_IK, HandshakeState.INITIATOR, _edhThread);
} catch (GeneralSecurityException gse) {
throw new IllegalStateException("bad proto", gse);
}
state.getRemotePublicKey().setPublicKey(target.getData(), 0);
state.getLocalKeyPair().setPublicKey(priv.toPublic().getData(), 0);
state.getLocalKeyPair().setPrivateKey(priv.getData(), 0);
state.start();
int padlen = 1 + _context.random().nextInt(MAXPAD);
byte[] payload = new byte[BHLEN + padlen + BHLEN + 4 + BHLEN + data.length];
List<Block> blocks = new ArrayList<Block>(4);
Block block = new DateTimeBlock(expiration);
blocks.add(block);
block = new GarlicBlock(data);
blocks.add(block);
block = new PaddingBlock(_context, padlen);
blocks.add(block);
int payloadlen = createPayload(payload, 0, blocks);
if (payloadlen != payload.length)
throw new IllegalStateException("payload size mismatch");
byte[] enc = new byte[KEYLEN + KEYLEN + MACLEN + payloadlen + MACLEN];
try {
state.writeMessage(enc, 0, payload, 0, payloadlen);
} catch (GeneralSecurityException gse) {
if (_log.shouldWarn())
_log.warn("Encrypt fail NS", gse);
return null;
}
// overwrite eph. key with encoded key
DHState eph = state.getLocalEphemeralKeyPair();
if (eph == null || !eph.hasEncodedPublicKey()) {
if (_log.shouldWarn())
_log.warn("Bad NS state");
return null;
}
eph.getEncodedPublicKey(enc, 0);
// save for tagset HKDF
byte[] ck = state.getChainingKey();
// register state with skm
// keyManager.tagsDelivered(state);
// todo
return enc;
}
/**
* scenario 2: New Session Reply Message
*
* Begin with 56 bytes, containing:
* <pre>
* - 8 byte SessionTag
* - 32 byte Elligator2 key
* - 16 byte MAC
* </pre>
* And then the data:
* <pre>
* - payload
* - 16 byte MAC
* </pre>
*
* @param state must have already been cloned
* @return encrypted data or null on failure
*/
private byte[] encryptNewSessionReply(byte data[], HandshakeState state, RatchetSessionTag currentTag) {
byte[] tag = currentTag.getData();
state.mixHash(tag, 0, TAGLEN);
int padlen = 1 + _context.random().nextInt(MAXPAD);
byte[] payload = new byte[BHLEN + padlen + BHLEN + data.length];
List<Block> blocks = new ArrayList<Block>(2);
Block block = new GarlicBlock(data);
blocks.add(block);
block = new PaddingBlock(_context, padlen);
blocks.add(block);
int payloadlen = createPayload(payload, 0, blocks);
if (payloadlen != payload.length)
throw new IllegalStateException("payload size mismatch");
// part 1 - tag and empty payload
byte[] enc = new byte[TAGLEN + KEYLEN + MACLEN + payloadlen + MACLEN];
System.arraycopy(tag, 0, enc, 0, TAGLEN);
try {
state.writeMessage(enc, TAGLEN, ZEROLEN, 0, 0);
} catch (GeneralSecurityException gse) {
if (_log.shouldWarn())
_log.warn("Encrypt fail NSR part 1", gse);
return null;
}
// overwrite eph. key with encoded key
DHState eph = state.getLocalEphemeralKeyPair();
if (eph == null || !eph.hasEncodedPublicKey()) {
if (_log.shouldWarn())
_log.warn("Bad NSR state");
return null;
}
eph.getEncodedPublicKey(enc, TAGLEN);
// split()
byte[] ck = state.getChainingKey();
byte[] k_ab = new byte[32];
byte[] k_ba = new byte[32];
_hkdf.calculate(ck, ZEROLEN, k_ab, k_ba, 0);
SessionKey tk = new SessionKey(ck);
byte[] temp_key = doHMAC(tk, ZEROLEN);
// unused
tk = new SessionKey(temp_key);
CipherStatePair ckp = state.split();
CipherState rcvr = ckp.getReceiver();
CipherState sender = ckp.getSender();
byte[] hash = state.getHandshakeHash();
// part 2 - payload
byte[] encpayloadkey = new byte[32];
_hkdf.calculate(k_ba, ZEROLEN, INFO_6, encpayloadkey);
try {
sender.encryptWithAd(tag, payload, 0, enc, TAGLEN + KEYLEN + MACLEN, payload.length);
} catch (GeneralSecurityException gse) {
if (_log.shouldWarn())
_log.warn("Encrypt fail NSR part 2", gse);
return null;
}
RatchetTagSet tagset_ab = new RatchetTagSet(_hkdf, new SessionKey(ck), new SessionKey(k_ab), 0, 0);
/// lsnr
RatchetTagSet tagset_ba = new RatchetTagSet(_hkdf, null, new SessionKey(ck), new SessionKey(k_ba), 0, 0, 5, 5);
return enc;
}
/**
* scenario 3: Existing Session Message
*
* <pre>
* - 8 byte SessionTag
* - payload
* - 16 byte MAC
* </pre>
*
* @param target unused, this is AEAD encrypt only using the session key and tag
* @return encrypted data or null on failure
*/
private byte[] encryptExistingSession(byte data[], PublicKey target, SessionKeyAndNonce key,
RatchetSessionTag currentTag) {
byte rawTag[] = currentTag.getData();
int padlen = 1 + _context.random().nextInt(MAXPAD);
byte[] payload = new byte[BHLEN + padlen + BHLEN + data.length];
List<Block> blocks = new ArrayList<Block>(2);
Block block = new GarlicBlock(data);
blocks.add(block);
block = new PaddingBlock(_context, padlen);
blocks.add(block);
int payloadlen = createPayload(payload, 0, blocks);
if (payloadlen != payload.length)
throw new IllegalStateException("payload size mismatch");
byte encr[] = encryptAEADBlock(rawTag, payload, key, key.getNonce());
System.arraycopy(rawTag, 0, encr, 0, TAGLEN);
return encr;
}
/**
* No ad
*/
final byte[] encryptAEADBlock(byte data[], SessionKey key, long n) {
return encryptAEADBlock(null, data, key, n);
}
/**
*
* @param ad may be null
* @return space will be left at beginning for ad (tag)
*/
private final byte[] encryptAEADBlock(byte[] ad, byte data[], SessionKey key, long n) {
ChaChaPolyCipherState chacha = new ChaChaPolyCipherState();
chacha.initializeKey(key.getData(), 0);
chacha.setNonce(n);
int adsz = ad != null ? ad.length : 0;
byte enc[] = new byte[adsz + data.length + MACLEN];
try {
chacha.encryptWithAd(ad, data, 0, enc, adsz, data.length);
} catch (GeneralSecurityException e) {
if (_log.shouldWarn())
_log.warn("Unable to encrypt AEAD block", e);
return null;
}
return enc;
}
private static final PrivateKey doDH(PrivateKey privkey, PublicKey pubkey) {
byte[] dh = new byte[KEYLEN];
Curve25519.eval(dh, 0, privkey.getData(), pubkey.getData());
return new PrivateKey(EncType.ECIES_X25519, dh);
}
/////////////////////////////////////////////////////////
// payload stuff
/////////////////////////////////////////////////////////
private void processPayload(byte[] payload, int length, boolean isHandshake) throws Exception {
}
private class PLCallback implements RatchetPayload.PayloadCallback {
public byte[] cloveSet;
public long datetime;
public void gotDateTime(long time) {
if (_log.shouldDebug())
_log.debug("Got DATE block: " + DataHelper.formatTime(time));
if (datetime != 0)
throw new IllegalArgumentException("Multiple DATETIME blocks");
datetime = time;
}
public void gotOptions(byte[] options, boolean isHandshake) {
if (_log.shouldDebug())
_log.debug("Got OPTIONS block length " + options.length);
}
public void gotGarlic(byte[] data, int off, int len) {
if (_log.shouldDebug())
_log.debug("Got GARLIC block length " + len);
if (cloveSet != null)
throw new IllegalArgumentException("Multiple GARLIC blocks");
cloveSet = new byte[len];
System.arraycopy(data, off, cloveSet, 0, len);
}
public void gotTermination(int reason, long count) {
if (_log.shouldDebug())
_log.debug("Got TERMINATION block, reason: " + reason + " count: " + count);
}
public void gotUnknown(int type, int len) {
if (_log.shouldDebug())
_log.debug("Got UNKNOWN block, type: " + type + " len: " + len);
}
public void gotPadding(int paddingLength, int frameLength) {
if (_log.shouldDebug())
_log.debug("Got PADDING block, len: " + paddingLength + " in frame len: " + frameLength);
}
}
/**
* @return the new offset
*/
private int createPayload(byte[] payload, int off, List<Block> blocks) {
return RatchetPayload.writePayload(payload, off, blocks);
}
private byte[] doHMAC(SessionKey key, byte data[]) {
byte[] rv = new byte[32];
_context.hmac256().calculate(key, data, 0, data.length, rv, 0);
return rv;
}
/****
public static void main(String args[]) {
I2PAppContext ctx = new I2PAppContext();
ECIESAEADEngine e = new ECIESAEADEngine(ctx);
Object kp[] = ctx.keyGenerator().generatePKIKeypair();
PublicKey pubKey = (PublicKey)kp[0];
PrivateKey privKey = (PrivateKey)kp[1];
SessionKey sessionKey = ctx.keyGenerator().generateSessionKey();
for (int i = 0; i < 10; i++) {
try {
Set tags = new HashSet(5);
if (i == 0) {
for (int j = 0; j < 5; j++)
tags.add(new SessionTag(true));
}
byte encrypted[] = e.encrypt("blah".getBytes(), pubKey, sessionKey, tags, 1024);
byte decrypted[] = e.decrypt(encrypted, privKey);
if ("blah".equals(new String(decrypted))) {
System.out.println("equal on " + i);
} else {
System.out.println("NOT equal on " + i + ": " + new String(decrypted));
break;
}
ctx.sessionKeyManager().tagsDelivered(pubKey, sessionKey, tags);
} catch (Exception ee) {
ee.printStackTrace();
break;
}
}
}
****/
}

View File

@@ -0,0 +1,50 @@
package net.i2p.router.crypto.ratchet;
import java.util.List;
import net.i2p.crypto.EncType;
import net.i2p.data.DataFormatException;
import net.i2p.data.PrivateKey;
import net.i2p.router.RouterContext;
import net.i2p.util.Log;
/**
* Handles the actual decryption using the
* supplied keys and data.
*
* @since 0.9.44
*/
public final class MuxedEngine {
private final RouterContext _context;
private final Log _log;
public MuxedEngine(RouterContext ctx) {
_context = ctx;
_log = _context.logManager().getLog(MuxedEngine.class);
}
/**
* Decrypt the message with the given private keys
*
* @return decrypted data or null on failure
*/
public byte[] decrypt(byte data[], PrivateKey elgKey, PrivateKey ecKey, MuxedSKM keyManager) throws DataFormatException {
if (elgKey.getType() != EncType.ELGAMAL_2048 ||
ecKey.getType() != EncType.ECIES_X25519)
throw new IllegalArgumentException();
byte[] rv = null;
boolean tryElg = false;
// See proposal 144
if (data.length >= 128) {
int mod = data.length % 16;
if (mod == 0 || mod == 2)
tryElg = true;
}
// Always try ElG first, for now
if (tryElg)
rv = _context.elGamalAESEngine().decrypt(data, elgKey, keyManager.getElgSKM());
if (rv == null)
rv = _context.eciesEngine().decrypt(data, ecKey, keyManager.getECSKM());
return rv;
}
}

View File

@@ -0,0 +1,212 @@
package net.i2p.router.crypto.ratchet;
import java.io.IOException;
import java.io.Writer;
import java.util.Set;
import net.i2p.I2PAppContext;
import net.i2p.crypto.EncType;
import net.i2p.crypto.TagSetHandle;
import net.i2p.crypto.SessionKeyManager;
import net.i2p.data.PublicKey;
import net.i2p.data.SessionKey;
import net.i2p.data.SessionTag;
import net.i2p.router.crypto.TransientSessionKeyManager;
/**
* Both.
*
* @since 0.9.44
*/
public class MuxedSKM extends SessionKeyManager {
private final TransientSessionKeyManager _elg;
private final RatchetSKM _ec;
public MuxedSKM(TransientSessionKeyManager elg, RatchetSKM ec) {
_elg = elg;
_ec = ec;
}
public TransientSessionKeyManager getElgSKM() { return _elg; }
public RatchetSKM getECSKM() { return _ec; }
@Override
public SessionKey getCurrentKey(PublicKey target) {
EncType type = target.getType();
if (type == EncType.ELGAMAL_2048)
return _elg.getCurrentKey(target);
if (type == EncType.ECIES_X25519)
return _ec.getCurrentKey(target);
return null;
}
@Override
public SessionKey getCurrentOrNewKey(PublicKey target) {
EncType type = target.getType();
if (type == EncType.ELGAMAL_2048)
return _elg.getCurrentOrNewKey(target);
if (type == EncType.ECIES_X25519)
return _ec.getCurrentOrNewKey(target);
return null;
}
@Override
public void createSession(PublicKey target, SessionKey key) {
EncType type = target.getType();
if (type == EncType.ELGAMAL_2048)
_elg.createSession(target, key);
else if (type == EncType.ECIES_X25519)
_ec.createSession(target, key);
else
throw new IllegalArgumentException();
}
@Override
public SessionKey createSession(PublicKey target) {
EncType type = target.getType();
if (type == EncType.ELGAMAL_2048)
return _elg.createSession(target);
if (type == EncType.ECIES_X25519)
return _ec.createSession(target);
return null;
}
/**
* ElG only
*/
@Override
public SessionTag consumeNextAvailableTag(PublicKey target, SessionKey key) {
EncType type = target.getType();
if (type == EncType.ELGAMAL_2048)
return _elg.consumeNextAvailableTag(target, key);
return null;
}
/**
* EC only
*/
public RatchetEntry consumeNextAvailableTag(PublicKey target) {
EncType type = target.getType();
if (type == EncType.ECIES_X25519)
return _ec.consumeNextAvailableTag(target);
return null;
}
@Override
public int getTagsToSend() { return 0; };
@Override
public int getLowThreshold() { return 0; };
/**
* ElG only
*/
@Override
public boolean shouldSendTags(PublicKey target, SessionKey key) {
EncType type = target.getType();
if (type == EncType.ELGAMAL_2048)
return _elg.shouldSendTags(target, key);
return false;
}
/**
* ElG only
*/
@Override
public boolean shouldSendTags(PublicKey target, SessionKey key, int lowThreshold) {
EncType type = target.getType();
if (type == EncType.ELGAMAL_2048)
return _elg.shouldSendTags(target, key, lowThreshold);
return false;
}
@Override
public int getAvailableTags(PublicKey target, SessionKey key) {
EncType type = target.getType();
if (type == EncType.ELGAMAL_2048)
return _elg.getAvailableTags(target, key);
if (type == EncType.ECIES_X25519)
return _ec.getAvailableTags(target, key);
return 0;
}
@Override
public long getAvailableTimeLeft(PublicKey target, SessionKey key) {
EncType type = target.getType();
if (type == EncType.ELGAMAL_2048)
return _elg.getAvailableTimeLeft(target, key);
if (type == EncType.ECIES_X25519)
return _ec.getAvailableTimeLeft(target, key);
return 0;
}
@Override
public TagSetHandle tagsDelivered(PublicKey target, SessionKey key, Set<SessionTag> sessionTags) {
EncType type = target.getType();
if (type == EncType.ELGAMAL_2048)
return _elg.tagsDelivered(target, key, sessionTags);
if (type == EncType.ECIES_X25519)
return _ec.tagsDelivered(target, key, sessionTags);
return null;
}
/**
* ElG only
*/
@Override
public void tagsReceived(SessionKey key, Set<SessionTag> sessionTags) {
_elg.tagsReceived(key, sessionTags);
}
/**
* ElG only
*/
@Override
public void tagsReceived(SessionKey key, Set<SessionTag> sessionTags, long expire) {
_elg.tagsReceived(key, sessionTags, expire);
}
@Override
public SessionKey consumeTag(SessionTag tag) {
SessionKey rv = _elg.consumeTag(tag);
if (rv == null) {
byte[] stag = new byte[8];
System.arraycopy(tag.getData(), 0, stag, 0, 8);
RatchetSessionTag rstag = new RatchetSessionTag(stag);
rv = _ec.consumeTag(rstag);
}
return rv;
}
@Override
public void shutdown() {
_elg.shutdown();
_ec.shutdown();
}
@Override
public void renderStatusHTML(Writer out) throws IOException {
_elg.renderStatusHTML(out);
_ec.renderStatusHTML(out);
}
@Override
public void failTags(PublicKey target, SessionKey key, TagSetHandle ts) {
EncType type = target.getType();
if (type == EncType.ELGAMAL_2048)
_elg.failTags(target, key, ts);
else if (type == EncType.ECIES_X25519)
_ec.failTags(target, key, ts);
}
@Override
public void tagsAcked(PublicKey target, SessionKey key, TagSetHandle ts) {
EncType type = target.getType();
if (type == EncType.ELGAMAL_2048)
_elg.tagsAcked(target, key, ts);
else if (type == EncType.ECIES_X25519)
_ec.tagsAcked(target, key, ts);
}
}

View File

@@ -0,0 +1,863 @@
package net.i2p.router.crypto.ratchet;
import java.io.IOException;
import java.io.Serializable;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import com.southernstorm.noise.protocol.HandshakeState;
import net.i2p.I2PAppContext;
import net.i2p.crypto.EncType;
import net.i2p.crypto.HKDF;
import net.i2p.crypto.SessionKeyManager;
import net.i2p.crypto.TagSetHandle;
import net.i2p.data.DataHelper;
import net.i2p.data.PublicKey;
import net.i2p.data.SessionKey;
import net.i2p.data.SessionTag;
import net.i2p.router.RouterContext;
import net.i2p.util.Log;
import net.i2p.util.SimpleTimer;
/**
*
*
*
* @since 0.9.44
*/
public class RatchetSKM extends SessionKeyManager implements SessionTagListener {
private final Log _log;
/** Map allowing us to go from the targeted PublicKey to the OutboundSession used */
private final Map<PublicKey, OutboundSession> _outboundSessions;
/** Map allowing us to go from a SessionTag to the containing RatchetTagSet */
private final ConcurrentHashMap<RatchetSessionTag, RatchetTagSet> _inboundTagSets;
protected final I2PAppContext _context;
private volatile boolean _alive;
/** for debugging */
private final AtomicInteger _rcvTagSetID = new AtomicInteger();
private final AtomicInteger _sentTagSetID = new AtomicInteger();
private final HKDF _hkdf;
/**
* Let outbound session tags sit around for this long before expiring them.
* Inbound tag expiration is set by SESSION_LIFETIME_MAX_MS
*/
private final static long SESSION_TAG_DURATION_MS = 12 * 60 * 1000;
/**
* Keep unused inbound session tags around for this long (a few minutes longer than
* session tags are used on the outbound side so that no reasonable network lag
* can cause failed decrypts)
*
* This is also the max idle time for an outbound session.
*/
private final static long SESSION_LIFETIME_MAX_MS = SESSION_TAG_DURATION_MS + 3 * 60 * 1000;
/**
* Time to send more if we are this close to expiration
*/
private static final long SESSION_TAG_EXPIRATION_WINDOW = 90 * 1000;
private static final int MIN_RCV_WINDOW = 20;
private static final int MAX_RCV_WINDOW = 50;
/**
* The session key manager should only be constructed and accessed through the
* application context. This constructor should only be used by the
* appropriate application context itself.
*
*/
public RatchetSKM(RouterContext context) {
super(context);
_log = context.logManager().getLog(RatchetSKM.class);
_context = context;
_outboundSessions = new HashMap<PublicKey, OutboundSession>(64);
_inboundTagSets = new ConcurrentHashMap<RatchetSessionTag, RatchetTagSet>(128);
_hkdf = new HKDF(context);
// start the precalc of Elg2 keys if it wasn't already started
context.eciesEngine().startup();
_alive = true;
_context.simpleTimer2().addEvent(new CleanupEvent(), 60*1000);
}
@Override
public void shutdown() {
_alive = false;
_inboundTagSets.clear();
synchronized (_outboundSessions) {
_outboundSessions.clear();
}
}
private class CleanupEvent implements SimpleTimer.TimedEvent {
public void timeReached() {
if (!_alive)
return;
// TODO
_context.simpleTimer2().addEvent(this, 60*1000);
}
}
/** RatchetTagSet */
private Set<RatchetTagSet> getRatchetTagSets() {
synchronized (_inboundTagSets) {
return new HashSet<RatchetTagSet>(_inboundTagSets.values());
}
}
/** OutboundSession - used only by HTML */
private Set<OutboundSession> getOutboundSessions() {
synchronized (_outboundSessions) {
return new HashSet<OutboundSession>(_outboundSessions.values());
}
}
/**
* Retrieve the session key currently associated with encryption to the target,
* or null if a new session key should be generated.
*
* Warning - don't generate a new session if this returns null, it's racy, use getCurrentOrNewKey()
*/
@Override
public SessionKey getCurrentKey(PublicKey target) {
OutboundSession sess = getSession(target);
if (sess == null) return null;
long now = _context.clock().now();
if (sess.getLastUsedDate() < now - SESSION_LIFETIME_MAX_MS) {
if (_log.shouldInfo())
_log.info("Expiring old session key established on "
+ new Date(sess.getEstablishedDate())
+ " but not used for "
+ (now-sess.getLastUsedDate())
+ "ms with target " + toString(target));
return null;
}
return sess.getCurrentKey();
}
/**
* Retrieve the session key currently associated with encryption to the target.
* Generates a new session and session key if not previously exising.
*
* @return non-null
*/
@Override
public SessionKey getCurrentOrNewKey(PublicKey target) {
synchronized (_outboundSessions) {
OutboundSession sess = _outboundSessions.get(target);
if (sess != null) {
long now = _context.clock().now();
if (sess.getLastUsedDate() < now - SESSION_LIFETIME_MAX_MS)
sess = null;
}
if (sess == null) {
SessionKey key = _context.keyGenerator().generateSessionKey();
createAndReturnSession(target, key);
return key;
}
return sess.getCurrentKey();
}
}
/**
* Associate a new session key with the specified target. Metrics to determine
* when to expire that key begin with this call.
*
* Racy if called after getCurrentKey() to check for a current session;
* use getCurrentOrNewKey() in that case.
*/
@Override
public void createSession(PublicKey target, SessionKey key) {
createAndReturnSession(target, key);
}
/**
* Same as above but for internal use, returns OutboundSession so we don't have
* to do a subsequent getSession()
*
*/
private OutboundSession createAndReturnSession(PublicKey target, SessionKey key) {
EncType type = target.getType();
if (type != EncType.ECIES_X25519)
throw new IllegalArgumentException("Bad public key type " + type);
if (_log.shouldInfo())
_log.info("New OB session, sesskey: " + key + " target: " + toString(target));
OutboundSession sess = new OutboundSession(_context, _log, target, key);
addSession(sess);
return sess;
}
/**
* @throws UnsupportedOperationException always
*/
@Override
public SessionTag consumeNextAvailableTag(PublicKey target, SessionKey key) {
throw new UnsupportedOperationException();
}
/**
* Outbound.
*
* Retrieve the next available session tag and key for sending a message to the target.
*
* If this returns null, no session is set up yet, and a New Session message should be sent.
*
* If this returns non-null, the tag in the RatchetEntry will be non-null.
*
* If the SessionKeyAndNonce contains a HandshakeState, then the session setup is in progress,
* and a New Session Reply message should be sent.
* Otherwise, an Existing Session message should be sent.
*
*/
public RatchetEntry consumeNextAvailableTag(PublicKey target) {
OutboundSession sess = getSession(target);
if (sess == null) {
if (_log.shouldDebug())
_log.debug("No OB session to " + toString(target));
return null;
}
return sess.consumeNext();
}
/**
* How many to send, IF we need to.
* @return the configured value (not adjusted for current available)
*/
@Override
public int getTagsToSend() { return 0; };
/**
* @return the configured value
*/
@Override
public int getLowThreshold() { return 999999; };
/**
* @return false always
*/
@Override
public boolean shouldSendTags(PublicKey target, SessionKey key, int lowThreshold) {
return false;
}
/**
* Determine (approximately) how many available session tags for the current target
* have been confirmed and are available
*
*/
@Override
public int getAvailableTags(PublicKey target, SessionKey key) {
OutboundSession sess = getSession(target);
if (sess == null) { return 0; }
if (sess.getCurrentKey().equals(key)) {
return sess.availableTags();
}
return 0;
}
/**
* Determine how long the available tags will be available for before expiring, in
* milliseconds
*/
@Override
public long getAvailableTimeLeft(PublicKey target, SessionKey key) {
OutboundSession sess = getSession(target);
if (sess == null) { return 0; }
if (sess.getCurrentKey().equals(key)) {
long end = sess.getLastExpirationDate();
if (end <= 0)
return 0;
else
return end - _context.clock().now();
}
return 0;
}
/**
* Take note of the fact that the given sessionTags associated with the key for
* encryption to the target have been sent. Whether to use the tags immediately
* (i.e. assume they will be received) or to wait until an ack, is implementation dependent.
*
*
* @param sessionTags ignored, must be null
* @return the TagSetHandle. Caller MUST subsequently call failTags() or tagsAcked()
* with this handle. May be null.
*/
@Override
public TagSetHandle tagsDelivered(PublicKey target, SessionKey key, Set<SessionTag> sessionTags) {
// TODO
if (!(key instanceof SessionKeyAndNonce)) {
if (_log.shouldWarn())
_log.warn("Bad SK type");
//TODO
return null;
}
SessionKeyAndNonce sk = (SessionKeyAndNonce) key;
// if this is ever null, this is racy and needs synch
OutboundSession sess = getSession(target);
if (sess == null) {
if (_log.shouldWarn())
_log.warn("No session for delivered RatchetTagSet to target: " + toString(target));
sess = createAndReturnSession(target, key);
} else {
sess.setCurrentKey(key);
}
///////////
RatchetTagSet set = new RatchetTagSet(_hkdf, key, key, _context.clock().now(), _sentTagSetID.incrementAndGet());
sess.addTags(set);
if (_log.shouldDebug())
_log.debug("Tags delivered: " + set +
" target: " + toString(target) /** + ": " + sessionTags */ );
return set;
}
/**
* Mark all of the tags delivered to the target up to this point as invalid, since the peer
* has failed to respond when they should have. This call essentially lets the system recover
* from corrupted tag sets and crashes
*
* @deprecated unused and rather drastic
*/
@Override
@Deprecated
public void failTags(PublicKey target) {
removeSession(target);
}
/**
* Mark these tags as invalid, since the peer
* has failed to ack them in time.
*/
@Override
public void failTags(PublicKey target, SessionKey key, TagSetHandle ts) {
OutboundSession sess = getSession(target);
if (sess == null) {
if (_log.shouldWarn())
_log.warn("No session for failed RatchetTagSet: " + ts);
return;
}
if(!key.equals(sess.getCurrentKey())) {
if (_log.shouldWarn())
_log.warn("Wrong session key (wanted " + sess.getCurrentKey() + ") for failed RatchetTagSet: " + ts);
return;
}
if (_log.shouldWarn())
_log.warn("TagSet failed: " + ts);
sess.failTags((RatchetTagSet)ts);
}
/**
* Mark these tags as acked, start to use them (if we haven't already)
* If the set was previously failed, it will be added back in.
*/
@Override
public void tagsAcked(PublicKey target, SessionKey key, TagSetHandle ts) {
OutboundSession sess = getSession(target);
if (sess == null) {
if (_log.shouldWarn())
_log.warn("No session for acked RatchetTagSet: " + ts);
return;
}
if(!key.equals(sess.getCurrentKey())) {
if (_log.shouldWarn())
_log.warn("Wrong session key (wanted " + sess.getCurrentKey() + ") for acked RatchetTagSet: " + ts);
return;
}
if (_log.shouldDebug())
_log.debug("TagSet acked: " + ts);
sess.ackTags((RatchetTagSet)ts);
}
/**
* @throws UnsupportedOperationException always
*/
@Override
public void tagsReceived(SessionKey key, Set<SessionTag> sessionTags) {
throw new UnsupportedOperationException();
}
/**
* @throws UnsupportedOperationException always
*/
@Override
public void tagsReceived(SessionKey key, Set<SessionTag> sessionTags, long expire) {
throw new UnsupportedOperationException();
}
/**
* remove a bunch of arbitrarily selected tags, then drop all of
* the associated tag sets. this is very time consuming - iterating
* across the entire _inboundTagSets map, but it should be very rare,
* and the stats we can gather can hopefully reduce the frequency of
* using too many session tags in the future
*
*/
private void clearExcess(int overage) {}
/**
* @throws UnsupportedOperationException always
*/
@Override
public SessionKey consumeTag(SessionTag tag) {
throw new UnsupportedOperationException();
}
/**
* Determine if we have received a session key associated with the given session tag,
* and if so, discard it and return the decryption
* key it was received with (via tagsReceived(...)). returns null if no session key
* matches
*
* If the return value has null data, it will have a non-null HandshakeState.
*
* @return a SessionKeyAndNonce or null
*/
public SessionKeyAndNonce consumeTag(RatchetSessionTag tag) {
RatchetTagSet tagSet;
SessionKeyAndNonce key;
tagSet = _inboundTagSets.remove(tag);
if (tagSet == null) {
if (_log.shouldDebug())
_log.debug("IB tag not found: " + tag.toBase64());
return null;
}
HandshakeState state = tagSet.getHandshakeState();
if (state != null) {
key = new SessionKeyAndNonce(state);
if (_log.shouldDebug())
_log.debug("IB NSR Tag consumed: " + tag + " from: " + tagSet);
} else {
key = tagSet.consume(tag);
if (_log.shouldDebug())
_log.debug("IB ES Tag consumed: " + tag + " from: " + tagSet);
}
return key;
}
private OutboundSession getSession(PublicKey target) {
synchronized (_outboundSessions) {
return _outboundSessions.get(target);
}
}
private void addSession(OutboundSession sess) {
synchronized (_outboundSessions) {
_outboundSessions.put(sess.getTarget(), sess);
}
}
private void removeSession(PublicKey target) {
if (target == null) return;
OutboundSession session = null;
synchronized (_outboundSessions) {
session = _outboundSessions.remove(target);
}
if ( (session != null) && (_log.shouldWarn()) )
_log.warn("Removing session tags with " + session.availableTags() + " available for "
+ (session.getLastExpirationDate()-_context.clock().now())
+ "ms more", new Exception("Removed by"));
}
/**
* Aggressively expire inbound tag sets and outbound sessions
*
* @return number of tag sets expired (bogus as it overcounts inbound)
*/
private int aggressiveExpire() {
return 0;
}
/// begin SessionTagListener ///
/**
* Map the tag to this tagset.
*
* @return true if added, false if dup
*/
public boolean addTag(RatchetSessionTag tag, RatchetTagSet ts) {
return _inboundTagSets.putIfAbsent(tag, ts) == null;
}
/**
* Remove the tag associated with this tagset.
*/
public void expireTag(RatchetSessionTag tag, RatchetTagSet ts) {
_inboundTagSets.remove(tag, ts);
}
/// end SessionTagListener ///
/**
* Return a map of session key to a set of inbound RatchetTagSets for that SessionKey
*/
private Map<SessionKey, Set<RatchetTagSet>> getRatchetTagSetsBySessionKey() {
Set<RatchetTagSet> inbound = getRatchetTagSets();
Map<SessionKey, Set<RatchetTagSet>> inboundSets = new HashMap<SessionKey, Set<RatchetTagSet>>(inbound.size());
// Build a map of the inbound tag sets, grouped by SessionKey
for (RatchetTagSet ts : inbound) {
Set<RatchetTagSet> sets = inboundSets.get(ts.getAssociatedKey());
if (sets == null) {
sets = new HashSet<RatchetTagSet>(4);
inboundSets.put(ts.getAssociatedKey(), sets);
}
sets.add(ts);
}
return inboundSets;
}
@Override
public void renderStatusHTML(Writer out) throws IOException {
StringBuilder buf = new StringBuilder(1024);
buf.append("<h3 class=\"debug_inboundsessions\">Ratchet Inbound sessions</h3>" +
"<table>");
Map<SessionKey, Set<RatchetTagSet>> inboundSets = getRatchetTagSetsBySessionKey();
int total = 0;
int totalSets = 0;
long now = _context.clock().now();
Set<RatchetTagSet> sets = new TreeSet<RatchetTagSet>(new RatchetTagSetComparator());
for (Map.Entry<SessionKey, Set<RatchetTagSet>> e : inboundSets.entrySet()) {
SessionKey skey = e.getKey();
sets.clear();
sets.addAll(e.getValue());
totalSets += sets.size();
buf.append("<tr><td><b>Session key:</b> ").append(skey.toBase64()).append("</td>" +
"<td><b>Sets:</b> ").append(sets.size()).append("</td></tr>" +
"<tr class=\"expiry\"><td colspan=\"2\"><ul>");
for (RatchetTagSet ts : sets) {
int size = ts.getTags().size();
total += size;
buf.append("<li><b>ID: ").append(ts.getID());
long expires = ts.getDate() - now;
if (expires > 0)
buf.append(" expires in:</b> ").append(DataHelper.formatDuration2(expires)).append(" with ");
else
buf.append(" expired:</b> ").append(DataHelper.formatDuration2(0 - expires)).append(" ago with ");
buf.append(size).append('/').append(ts.getOriginalSize()).append(" tags remaining</li>");
}
buf.append("</ul></td></tr>\n");
out.write(buf.toString());
buf.setLength(0);
}
buf.append("<tr><th colspan=\"2\">Total inbound tags: ").append(total).append(" (")
.append(DataHelper.formatSize2(32*total)).append("B); sets: ").append(totalSets)
.append("; sessions: ").append(inboundSets.size())
.append("</th></tr>\n" +
"</table>" +
"<h3 class=\"debug_outboundsessions\">Ratchet Outbound sessions</h3>" +
"<table>");
total = 0;
totalSets = 0;
Set<OutboundSession> outbound = getOutboundSessions();
for (Iterator<OutboundSession> iter = outbound.iterator(); iter.hasNext();) {
OutboundSession sess = iter.next();
sets.clear();
sets.addAll(sess.getTagSets());
totalSets += sets.size();
buf.append("<tr class=\"debug_outboundtarget\"><td><div class=\"debug_targetinfo\"><b>Target public key:</b> ").append(toString(sess.getTarget())).append("<br>" +
"<b>Established:</b> ").append(DataHelper.formatDuration2(now - sess.getEstablishedDate())).append(" ago<br>" +
"<b>Ack Received?</b> ").append(sess.getAckReceived()).append("<br>" +
"<b>Last Used:</b> ").append(DataHelper.formatDuration2(now - sess.getLastUsedDate())).append(" ago<br>" +
"<b>Session key:</b> ").append(sess.getCurrentKey().toBase64()).append("</div></td>" +
"<td><b># Sets:</b> ").append(sess.getTagSets().size()).append("</td></tr>" +
"<tr><td colspan=\"2\"><ul>");
for (Iterator<RatchetTagSet> siter = sets.iterator(); siter.hasNext();) {
RatchetTagSet ts = siter.next();
int size = ts.getTags().size();
total += size;
buf.append("<li><b>ID: ").append(ts.getID())
.append(" Sent:</b> ").append(DataHelper.formatDuration2(now - ts.getDate())).append(" ago with ");
buf.append(size).append('/').append(ts.getOriginalSize()).append(" tags remaining; acked? ").append(ts.getAcked()).append("</li>");
}
buf.append("</ul></td></tr>\n");
out.write(buf.toString());
buf.setLength(0);
}
buf.append("<tr><th colspan=\"2\">Total outbound tags: ").append(total).append(" (")
.append(DataHelper.formatSize2(32*total)).append("B); sets: ").append(totalSets)
.append("; sessions: ").append(outbound.size())
.append("</th></tr>\n</table>");
out.write(buf.toString());
}
/**
* For debugging
*/
private static String toString(PublicKey target) {
if (target == null)
return "null";
return target.toBase64().substring(0, 20) + "...";
}
/**
* Just for the HTML method above so we can see what's going on easier
* Earliest first
*/
private static class RatchetTagSetComparator implements Comparator<RatchetTagSet>, Serializable {
public int compare(RatchetTagSet l, RatchetTagSet r) {
int rv = (int) (l.getDate() - r.getDate());
if (rv != 0)
return rv;
return l.hashCode() - r.hashCode();
}
}
/**
* The state for a crypto session to a single public key
*/
private static class OutboundSession {
private final I2PAppContext _context;
private final Log _log;
private final PublicKey _target;
private SessionKey _currentKey;
private final long _established;
private long _lastUsed;
/**
* Before the first ack, all tagsets go here. These are never expired, we rely
* on the callers to call failTags() or ackTags() to remove them from this list.
* Actually we now do a failsafe expire.
* Synch on _tagSets to access this.
* No particular order.
*/
private final Set<RatchetTagSet> _unackedTagSets;
/**
* As tagsets are acked, they go here.
* After the first ack, new tagsets go here (i.e. presumed acked)
* In order, earliest first.
*/
private final List<RatchetTagSet> _tagSets;
/**
* Set to true after first tagset is acked.
* Upon repeated failures, we may revert back to false.
* This prevents us getting "stuck" forever, using tags that weren't acked
* to deliver the next set of tags.
*/
private volatile boolean _acked;
/**
* Fail count
* Synch on _tagSets to access this.
*/
private int _consecutiveFailures;
private static final int MAX_FAILS = 2;
public OutboundSession(I2PAppContext ctx, Log log, PublicKey target, SessionKey key) {
_context = ctx;
_log = log;
_target = target;
_currentKey = key;
_established = ctx.clock().now();
_lastUsed = _established;
_unackedTagSets = new HashSet<RatchetTagSet>(4);
_tagSets = new ArrayList<RatchetTagSet>(6);
}
/**
* @return list of RatchetTagSet objects
* This is used only by renderStatusHTML().
* It includes both acked and unacked RatchetTagSets.
*/
List<RatchetTagSet> getTagSets() {
List<RatchetTagSet> rv;
synchronized (_tagSets) {
rv = new ArrayList<RatchetTagSet>(_unackedTagSets);
rv.addAll(_tagSets);
}
return rv;
}
/**
* got an ack for these tags
* For tagsets delivered after the session was acked, this is a nop
* because the tagset was originally placed directly on the acked list.
* If the set was previously failed, it will be added back in.
*/
void ackTags(RatchetTagSet set) {
synchronized (_tagSets) {
if (_unackedTagSets.remove(set)) {
// we could perhaps use it even if not previuosly in unacked,
// i.e. it was expired already, but _tagSets is a list not a set...
_tagSets.add(set);
} else if (!_tagSets.contains(set)) {
// add back (sucess after fail)
_tagSets.add(set);
if (_log.shouldWarn())
_log.warn("Ack of unknown (previously failed?) tagset: " + set);
} else if (set.getAcked()) {
if (_log.shouldWarn())
_log.warn("Dup ack of tagset: " + set);
}
_acked = true;
_consecutiveFailures = 0;
}
set.setAcked();
}
/** didn't get an ack for these tags */
void failTags(RatchetTagSet set) {
synchronized (_tagSets) {
_unackedTagSets.remove(set);
_tagSets.remove(set);
}
}
public PublicKey getTarget() {
return _target;
}
public SessionKey getCurrentKey() {
return _currentKey;
}
public void setCurrentKey(SessionKey key) {
_lastUsed = _context.clock().now();
if (_currentKey != null) {
if (!_currentKey.equals(key)) {
synchronized (_tagSets) {
if (_log.shouldWarn()) {
int dropped = 0;
for (RatchetTagSet set : _tagSets) {
dropped += set.getTags().size();
}
_log.warn("Rekeyed from " + _currentKey + " to " + key
+ ": dropping " + dropped + " session tags", new Exception());
}
_acked = false;
_tagSets.clear();
}
}
}
_currentKey = key;
}
public long getEstablishedDate() {
return _established;
}
public long getLastUsedDate() {
return _lastUsed;
}
/**
* Expire old tags, returning the number of tag sets removed
*/
public int expireTags() {
long now = _context.clock().now();
int removed = 0;
synchronized (_tagSets) {
for (Iterator<RatchetTagSet> iter = _tagSets.iterator(); iter.hasNext(); ) {
RatchetTagSet set = iter.next();
if (set.getDate() + SESSION_TAG_DURATION_MS <= now) {
iter.remove();
removed++;
}
}
// failsafe, sometimes these are sticking around, not sure why, so clean them periodically
if ((now & 0x0f) == 0) {
for (Iterator<RatchetTagSet> iter = _unackedTagSets.iterator(); iter.hasNext(); ) {
RatchetTagSet set = iter.next();
if (set.getDate() + SESSION_TAG_DURATION_MS <= now) {
iter.remove();
removed++;
}
}
}
}
return removed;
}
public RatchetEntry consumeNext() {
long now = _context.clock().now();
_lastUsed = now;
synchronized (_tagSets) {
while (!_tagSets.isEmpty()) {
RatchetTagSet set = _tagSets.get(0);
synchronized(set) {
if (set.getDate() + SESSION_TAG_DURATION_MS > now) {
RatchetSessionTag tag = set.consumeNext();
if (tag != null) {
SessionKeyAndNonce skn = set.consumeNextKey();
return new RatchetEntry(tag, skn);
} else if (_log.shouldInfo()) {
_log.info("Removing empty " + set);
}
} else {
if (_log.shouldInfo())
_log.info("Expired " + set);
}
}
_tagSets.remove(0);
}
}
return null;
}
/** @return the total number of tags in acked RatchetTagSets */
public int availableTags() {
int tags = 0;
long now = _context.clock().now();
synchronized (_tagSets) {
for (int i = 0; i < _tagSets.size(); i++) {
RatchetTagSet set = _tagSets.get(i);
if (!set.getAcked())
continue;
if (set.getDate() + SESSION_TAG_DURATION_MS > now) {
/////////// just add fixed number?
int sz = set.getTags().size();
tags += sz;
}
}
}
return tags;
}
/**
* Get the furthest away tag set expiration date - after which all of the
* tags will have expired
*
*/
public long getLastExpirationDate() {
long last = 0;
synchronized (_tagSets) {
for (RatchetTagSet set : _tagSets) {
if ( (set.getDate() > last) && (!set.getTags().isEmpty()) )
last = set.getDate();
}
}
if (last > 0)
return last + SESSION_TAG_DURATION_MS;
return -1;
}
/**
* Put the RatchetTagSet on the unacked list.
*/
public void addTags(RatchetTagSet set) {
_lastUsed = _context.clock().now();
synchronized (_tagSets) {
_unackedTagSets.add(set);
}
}
public boolean getAckReceived() {
return _acked;
}
}
}