forked from I2P_Developers/i2p.i2p
Ratchet: Track pending sessions
Transition from NSR to ES HandshakeState.clone() fix for multiple NSRs Add tagset ID numbers Debug logging
This commit is contained in:
12
history.txt
12
history.txt
@ -1,3 +1,15 @@
|
||||
2019-11-02 zzz
|
||||
* Router: NSR/ES fixes for proposal 144
|
||||
|
||||
2019-10-31 zzz
|
||||
* Router: Updates for proposal 144
|
||||
|
||||
2019-10-27 zzz
|
||||
* NetDB: Don't send encrypted lookup reply to ratchet dest
|
||||
* OCMOSJ:
|
||||
- Bundle unwrapped ack with LS for ratchet dest
|
||||
- Keep bundling LS until acked
|
||||
|
||||
2019-10-25 zzz
|
||||
* Router (proposal 144):
|
||||
- Set client SKM based on configured encryption
|
||||
|
@ -44,6 +44,7 @@ public class HandshakeState implements Destroyable, Cloneable {
|
||||
private int action;
|
||||
private final int requirements;
|
||||
private int patternIndex;
|
||||
private boolean wasCloned;
|
||||
|
||||
/**
|
||||
* Enumerated value that indicates that the handshake object
|
||||
@ -230,13 +231,32 @@ public class HandshakeState implements Destroyable, Cloneable {
|
||||
* @since 0.9.44
|
||||
*/
|
||||
protected HandshakeState(HandshakeState o) throws CloneNotSupportedException {
|
||||
// everything is shallow copied except for symmetric state
|
||||
// everything is shallow copied except for symmetric state and keys
|
||||
// so destroy() doesn't zero them out later
|
||||
symmetric = o.symmetric.clone();
|
||||
isInitiator = o.isInitiator;
|
||||
localKeyPair = o.localKeyPair;
|
||||
if (o.localKeyPair != null)
|
||||
localKeyPair = o.localKeyPair.clone();
|
||||
if (o.localEphemeral != null) {
|
||||
if (isInitiator) {
|
||||
// always save Alice's local keys
|
||||
localEphemeral = o.localEphemeral.clone();
|
||||
} else {
|
||||
if (o.wasCloned) {
|
||||
// new keys after first time for Bob
|
||||
localEphemeral = o.localEphemeral.clone();
|
||||
localEphemeral.generateKeyPair();
|
||||
} else {
|
||||
// first time for Bob, use the eph. keys previously generated
|
||||
localEphemeral = o.localEphemeral;
|
||||
remotePublicKey = o.remotePublicKey;
|
||||
remoteEphemeral = o.remoteEphemeral;
|
||||
o.wasCloned = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (o.remotePublicKey != null)
|
||||
remotePublicKey = o.remotePublicKey.clone();
|
||||
if (o.remoteEphemeral != null)
|
||||
remoteEphemeral = o.remoteEphemeral.clone();
|
||||
action = o.action;
|
||||
if (action == SPLIT || action == COMPLETE)
|
||||
throw new CloneNotSupportedException("clone after NSR");
|
||||
|
@ -18,7 +18,7 @@ public class RouterVersion {
|
||||
/** deprecated */
|
||||
public final static String ID = "Monotone";
|
||||
public final static String VERSION = CoreVersion.VERSION;
|
||||
public final static long BUILD = 2;
|
||||
public final static long BUILD = 3;
|
||||
|
||||
/** for example "-test" */
|
||||
public final static String EXTRA = "";
|
||||
|
@ -145,21 +145,14 @@ public final class ECIESAEADEngine {
|
||||
CloveSet 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) {
|
||||
if (shouldDebug)
|
||||
_log.debug("Decrypting ES with tag: " + st + ": key: " + key.toBase64() + ": " + data.length + " bytes");
|
||||
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;
|
||||
}
|
||||
if (shouldDebug)
|
||||
_log.debug("Decrypting NSR with tag: " + st + ": key: " + key.toBase64() + ": " + data.length + " bytes");
|
||||
decrypted = decryptNewSessionReply(tag, data, state, keyManager);
|
||||
} else {
|
||||
decrypted = null;
|
||||
@ -179,7 +172,6 @@ public final class ECIESAEADEngine {
|
||||
if (shouldDebug) _log.debug("IB Tag " + st + " not found, trying NS decrypt");
|
||||
decrypted = decryptNewSession(data, targetPrivateKey, keyManager);
|
||||
if (decrypted != null) {
|
||||
if (shouldDebug) _log.debug("NS decrypt success");
|
||||
_context.statManager().updateFrequency("crypto.eciesAEAD.decryptNewSession");
|
||||
} else {
|
||||
_context.statManager().updateFrequency("crypto.eciesAEAD.decryptFailed");
|
||||
@ -248,14 +240,13 @@ public final class ECIESAEADEngine {
|
||||
|
||||
byte[] bobPK = new byte[KEYLEN];
|
||||
state.getRemotePublicKey().getPublicKey(bobPK, 0);
|
||||
if (_log.shouldDebug())
|
||||
_log.debug("NS decrypt success from PK " + Base64.encode(bobPK));
|
||||
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
|
||||
@ -308,11 +299,20 @@ public final class ECIESAEADEngine {
|
||||
*
|
||||
* @param tag 8 bytes, same as first 8 bytes of data
|
||||
* @param data 56 bytes minimum
|
||||
* @param state must have already been cloned
|
||||
* @param state will be cloned here
|
||||
* @return null if decryption fails
|
||||
*/
|
||||
private CloveSet decryptNewSessionReply(byte[] tag, byte[] data, HandshakeState state, RatchetSKM keyManager)
|
||||
private CloveSet decryptNewSessionReply(byte[] tag, byte[] data, HandshakeState oldState, RatchetSKM keyManager)
|
||||
throws DataFormatException {
|
||||
HandshakeState state;
|
||||
try {
|
||||
state = oldState.clone();
|
||||
} catch (CloneNotSupportedException e) {
|
||||
if (_log.shouldWarn())
|
||||
_log.warn("ECIES decrypt fail: clone()", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
// part 1 - handshake
|
||||
byte[] yy = new byte[KEYLEN];
|
||||
System.arraycopy(data, TAGLEN, yy, 0, KEYLEN);
|
||||
@ -337,13 +337,8 @@ public final class ECIESAEADEngine {
|
||||
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
|
||||
@ -373,10 +368,22 @@ public final class ECIESAEADEngine {
|
||||
} catch (Exception e) {
|
||||
throw new DataFormatException("NSR payload error", e);
|
||||
}
|
||||
long now = _context.clock().now();
|
||||
RatchetTagSet tagset_ab = new RatchetTagSet(_hkdf, new SessionKey(ck), new SessionKey(k_ab), now, 0);
|
||||
RatchetTagSet tagset_ba = new RatchetTagSet(_hkdf, keyManager, new SessionKey(ck), new SessionKey(k_ba), now, 0, 5, 5);
|
||||
|
||||
byte[] bobPK = new byte[KEYLEN];
|
||||
state.getRemotePublicKey().getPublicKey(bobPK, 0);
|
||||
if (_log.shouldDebug())
|
||||
_log.debug("NSR decrypt success from PK " + Base64.encode(bobPK));
|
||||
if (Arrays.equals(bobPK, NULLPK)) {
|
||||
// TODO
|
||||
if (_log.shouldWarn())
|
||||
_log.warn("NSR reply to zero static key NS");
|
||||
return null;
|
||||
}
|
||||
|
||||
// tell the SKM
|
||||
PublicKey bob = new PublicKey(EncType.ECIES_X25519, bobPK);
|
||||
keyManager.updateSession(bob, oldState, state);
|
||||
|
||||
if (pc.cloveSet.isEmpty()) {
|
||||
if (_log.shouldWarn())
|
||||
_log.warn("No garlic block in NSR payload");
|
||||
@ -531,10 +538,9 @@ public final class ECIESAEADEngine {
|
||||
_log.warn("ECIES encrypt fail: clone()", e);
|
||||
return null;
|
||||
}
|
||||
// register state with skm
|
||||
if (_log.shouldDebug())
|
||||
_log.debug("Encrypting as NSR to " + target + " with tag " + re.tag);
|
||||
return encryptNewSessionReply(cloves, state, re.tag, keyManager);
|
||||
return encryptNewSessionReply(cloves, target, state, re.tag, keyManager);
|
||||
}
|
||||
if (_log.shouldDebug())
|
||||
_log.debug("Encrypting as ES to " + target + " with key " + re.key + " and tag " + re.tag);
|
||||
@ -616,7 +622,7 @@ public final class ECIESAEADEngine {
|
||||
* @param state must have already been cloned
|
||||
* @return encrypted data or null on failure
|
||||
*/
|
||||
private byte[] encryptNewSessionReply(CloveSet cloves, HandshakeState state,
|
||||
private byte[] encryptNewSessionReply(CloveSet cloves, PublicKey target, HandshakeState state,
|
||||
RatchetSessionTag currentTag, RatchetSKM keyManager) {
|
||||
byte[] tag = currentTag.getData();
|
||||
state.mixHash(tag, 0, TAGLEN);
|
||||
@ -648,12 +654,7 @@ public final class ECIESAEADEngine {
|
||||
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();
|
||||
|
||||
@ -668,10 +669,8 @@ public final class ECIESAEADEngine {
|
||||
_log.warn("Encrypt fail NSR part 2", gse);
|
||||
return null;
|
||||
}
|
||||
long now = _context.clock().now();
|
||||
RatchetTagSet tagset_ab = new RatchetTagSet(_hkdf, new SessionKey(ck), new SessionKey(k_ab), now, 0);
|
||||
RatchetTagSet tagset_ba = new RatchetTagSet(_hkdf, keyManager, new SessionKey(ck), new SessionKey(k_ba), now, 0, 5, 5);
|
||||
// tell the SKM
|
||||
keyManager.updateSession(target, null, state);
|
||||
|
||||
return enc;
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import net.i2p.crypto.EncType;
|
||||
import net.i2p.crypto.HKDF;
|
||||
import net.i2p.crypto.SessionKeyManager;
|
||||
import net.i2p.crypto.TagSetHandle;
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.data.PublicKey;
|
||||
import net.i2p.data.SessionKey;
|
||||
@ -42,6 +43,7 @@ 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 ConcurrentHashMap<PublicKey, OutboundSession> _outboundSessions;
|
||||
private final HashMap<PublicKey, List<OutboundSession>> _pendingOutboundSessions;
|
||||
/** Map allowing us to go from a SessionTag to the containing RatchetTagSet */
|
||||
private final ConcurrentHashMap<RatchetSessionTag, RatchetTagSet> _inboundTagSets;
|
||||
protected final I2PAppContext _context;
|
||||
@ -89,6 +91,7 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
|
||||
_log = context.logManager().getLog(RatchetSKM.class);
|
||||
_context = context;
|
||||
_outboundSessions = new ConcurrentHashMap<PublicKey, OutboundSession>(64);
|
||||
_pendingOutboundSessions = new HashMap<PublicKey, List<OutboundSession>>(64);
|
||||
_inboundTagSets = new ConcurrentHashMap<RatchetSessionTag, RatchetTagSet>(128);
|
||||
_hkdf = new HKDF(context);
|
||||
// start the precalc of Elg2 keys if it wasn't already started
|
||||
@ -176,12 +179,100 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
|
||||
} else {
|
||||
// we are Alice, NS sent
|
||||
OutboundSession sess = new OutboundSession(target, null, state);
|
||||
synchronized (_pendingOutboundSessions) {
|
||||
List<OutboundSession> pending = _pendingOutboundSessions.get(target);
|
||||
if (pending != null) {
|
||||
pending.add(sess);
|
||||
if (_log.shouldInfo())
|
||||
_log.info("New OB session as Alice. Bob: " + toString(target));
|
||||
_log.info("Another new OB session as Alice, total now: " + pending.size() +
|
||||
". Bob: " + toString(target));
|
||||
} else {
|
||||
pending = new ArrayList<OutboundSession>(4);
|
||||
pending.add(sess);
|
||||
_pendingOutboundSessions.put(target, pending);
|
||||
if (_log.shouldInfo())
|
||||
_log.info("First new OB session as Alice. Bob: " + toString(target));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inbound or outbound. Checks state.getRole() to determine.
|
||||
* For outbound (NSR rcvd by Alice), sets session to transition to ES mode outbound.
|
||||
* For inbound (NSR sent by Bob), sets up inbound ES tagset.
|
||||
*
|
||||
* @param oldState null for inbound, pre-clone for outbound
|
||||
*
|
||||
*/
|
||||
boolean updateSession(PublicKey target, HandshakeState oldState, HandshakeState state) {
|
||||
EncType type = target.getType();
|
||||
if (type != EncType.ECIES_X25519)
|
||||
throw new IllegalArgumentException("Bad public key type " + type);
|
||||
boolean isInbound = state.getRole() == HandshakeState.RESPONDER;
|
||||
if (isInbound) {
|
||||
// we are Bob, NSR sent
|
||||
OutboundSession sess = getSession(target);
|
||||
if (sess == null) {
|
||||
if (_log.shouldDebug())
|
||||
_log.debug("Update session but no session found for " + target);
|
||||
// TODO can we recover?
|
||||
return false;
|
||||
}
|
||||
sess.updateSession(state);
|
||||
if (_log.shouldInfo())
|
||||
_log.info("Session update as Bob. Alice: " + toString(target));
|
||||
} else {
|
||||
// we are Alice, NSR received
|
||||
synchronized (_pendingOutboundSessions) {
|
||||
List<OutboundSession> pending = _pendingOutboundSessions.get(target);
|
||||
if (pending == null) {
|
||||
if (_log.shouldDebug())
|
||||
_log.debug("Update session but no sessions found for " + target);
|
||||
// TODO can we recover?
|
||||
return false;
|
||||
}
|
||||
boolean found = false;
|
||||
for (OutboundSession sess : pending) {
|
||||
for (RatchetTagSet ts : sess.getTagSets()) {
|
||||
if (ts.getHandshakeState().equals(oldState)) {
|
||||
if (!found) {
|
||||
found = true;
|
||||
sess.updateSession(state);
|
||||
boolean ok = addSession(sess);
|
||||
if (_log.shouldDebug()) {
|
||||
if (ok)
|
||||
_log.debug("Update session from NSR to ES for " + target);
|
||||
else
|
||||
_log.debug("Session already updated from NSR to ES for " + target);
|
||||
}
|
||||
} else {
|
||||
if (_log.shouldDebug())
|
||||
_log.debug("Dup tagset " + ts + " for " + target);
|
||||
}
|
||||
} else {
|
||||
// TODO
|
||||
// remove old tags
|
||||
if (_log.shouldDebug())
|
||||
_log.debug("Remove tagset " + ts + " for " + target);
|
||||
}
|
||||
}
|
||||
}
|
||||
_pendingOutboundSessions.remove(target);
|
||||
if (!found) {
|
||||
if (_log.shouldDebug())
|
||||
_log.debug("Update session but no session found (out of " + pending.size() + ") for " + target);
|
||||
// TODO can we recover?
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (_log.shouldInfo())
|
||||
_log.info("Session update as Alice. Bob: " + toString(target));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws UnsupportedOperationException always
|
||||
*/
|
||||
@ -665,24 +756,69 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
|
||||
SessionKey rk = new SessionKey(ck);
|
||||
SessionKey tk = new SessionKey(tagsetkey);
|
||||
if (isInbound) {
|
||||
// We are Bob
|
||||
// This is an INBOUND NS, we make an OUTBOUND tagset for the NSR
|
||||
RatchetTagSet tagset = new RatchetTagSet(_hkdf, state,
|
||||
rk, tk,
|
||||
_established, 0);
|
||||
_established, _sentTagSetID.getAndIncrement());
|
||||
_tagSets.add(tagset);
|
||||
if (_log.shouldDebug())
|
||||
_log.debug("New OB Session, rk = " + rk + " tk = " + tk + " 1st tagset: " + tagset);
|
||||
} else {
|
||||
// We are Alice
|
||||
// This is an OUTBOUND NS, we make an INBOUND tagset for the NSR
|
||||
RatchetTagSet tagset = new RatchetTagSet(_hkdf, RatchetSKM.this, state,
|
||||
rk, tk,
|
||||
_established, 0, 5, 5);
|
||||
_established, _rcvTagSetID.getAndIncrement(), 5, 5);
|
||||
_unackedTagSets.add(tagset);
|
||||
if (_log.shouldDebug())
|
||||
_log.debug("New IB Session, rk = " + rk + " tk = " + tk + " 1st tagset: " + tagset);
|
||||
}
|
||||
}
|
||||
|
||||
void updateSession(HandshakeState state) {
|
||||
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 rk = new SessionKey(ck);
|
||||
long now = _context.clock().now();
|
||||
boolean isInbound = state.getRole() == HandshakeState.RESPONDER;
|
||||
if (isInbound) {
|
||||
// We are Bob
|
||||
// This is an OUTBOUND NSR, we make an INBOUND tagset for ES
|
||||
RatchetTagSet tagset_ab = new RatchetTagSet(_hkdf, RatchetSKM.this, rk, new SessionKey(k_ab),
|
||||
now, _rcvTagSetID.getAndIncrement(), 5, 5);
|
||||
// and a pending outbound one
|
||||
RatchetTagSet tagset_ba = new RatchetTagSet(_hkdf, rk, new SessionKey(k_ba),
|
||||
now, _sentTagSetID.getAndIncrement());
|
||||
if (_log.shouldDebug()) {
|
||||
_log.debug("Update IB Session, rk = " + rk + " tk = " + Base64.encode(k_ab) + " ES tagset: " + tagset_ab);
|
||||
_log.debug("Pending OB Session, rk = " + rk + " tk = " + Base64.encode(k_ba) + " ES tagset: " + tagset_ba);
|
||||
}
|
||||
synchronized (_tagSets) {
|
||||
_unackedTagSets.add(tagset_ba);
|
||||
}
|
||||
} else {
|
||||
// We are Alice
|
||||
// This is an INBOUND NSR, we make an OUTBOUND tagset for ES
|
||||
RatchetTagSet tagset_ab = new RatchetTagSet(_hkdf, rk, new SessionKey(k_ab),
|
||||
now, _sentTagSetID.getAndIncrement());
|
||||
// and an inbound one
|
||||
RatchetTagSet tagset_ba = new RatchetTagSet(_hkdf, RatchetSKM.this, rk, new SessionKey(k_ba),
|
||||
now, _rcvTagSetID.getAndIncrement(), 5, 5);
|
||||
if (_log.shouldDebug()) {
|
||||
_log.debug("Update OB Session, rk = " + rk + " tk = " + Base64.encode(k_ab) + " ES tagset: " + tagset_ab);
|
||||
_log.debug("Update IB Session, rk = " + rk + " tk = " + Base64.encode(k_ba) + " ES tagset: " + tagset_ba);
|
||||
}
|
||||
synchronized (_tagSets) {
|
||||
_tagSets.add(tagset_ab);
|
||||
_unackedTagSets.clear();
|
||||
}
|
||||
}
|
||||
//state.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list of RatchetTagSet objects
|
||||
* This is used only by renderStatusHTML().
|
||||
|
@ -153,7 +153,7 @@ class RatchetTagSet implements TagSetHandle {
|
||||
}
|
||||
|
||||
/**
|
||||
* For inbound NSR only, else null.
|
||||
* For inbound/outbound NSR only, else null.
|
||||
* MUST be cloned before processing NSR.
|
||||
*/
|
||||
public HandshakeState getHandshakeState() {
|
||||
@ -339,12 +339,17 @@ class RatchetTagSet implements TagSetHandle {
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder buf = new StringBuilder(256);
|
||||
if (_state != null)
|
||||
buf.append("NSR ");
|
||||
else
|
||||
buf.append("ES ");
|
||||
buf.append("TagSet #").append(_id).append(" created: ").append(new Date(_date));
|
||||
int sz = size();
|
||||
buf.append(" Size: ").append(sz);
|
||||
buf.append('/').append(getOriginalSize());
|
||||
buf.append(" Acked? ").append(_acked);
|
||||
if (_sessionTags != null) {
|
||||
buf.append(" Inbound");
|
||||
for (int i = 0; i < sz; i++) {
|
||||
int n = _sessionTags.keyAt(i);
|
||||
RatchetSessionTag tag = _sessionTags.valueAt(i);
|
||||
@ -357,6 +362,8 @@ class RatchetTagSet implements TagSetHandle {
|
||||
buf.append("\tdeferred");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
buf.append(" Outbound");
|
||||
}
|
||||
return buf.toString();
|
||||
}
|
||||
|
Reference in New Issue
Block a user