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:
zzz
2019-11-02 15:42:16 +00:00
parent 7c4569816f
commit 3ba48fda86
6 changed files with 221 additions and 47 deletions

View File

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

View File

@ -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;
localEphemeral = o.localEphemeral;
remotePublicKey = o.remotePublicKey;
remoteEphemeral = o.remoteEphemeral;
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;
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");

View File

@ -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 = "";

View File

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

View File

@ -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);
if (_log.shouldInfo())
_log.info("New OB session as Alice. Bob: " + toString(target));
synchronized (_pendingOutboundSessions) {
List<OutboundSession> pending = _pendingOutboundSessions.get(target);
if (pending != null) {
pending.add(sess);
if (_log.shouldInfo())
_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().

View File

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