forked from I2P_Developers/i2p.i2p
Crypto: Ratchet and Muxed SKMs and Engines (WIP)
This commit is contained in:
@@ -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
|
||||
* <= 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 <= 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
****/
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
212
router/java/src/net/i2p/router/crypto/ratchet/MuxedSKM.java
Normal file
212
router/java/src/net/i2p/router/crypto/ratchet/MuxedSKM.java
Normal 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);
|
||||
}
|
||||
}
|
||||
863
router/java/src/net/i2p/router/crypto/ratchet/RatchetSKM.java
Normal file
863
router/java/src/net/i2p/router/crypto/ratchet/RatchetSKM.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user