SSU2: Implement split()

Use socket address in PacketBuilder2
Decrypt session confirmed in-place
Check for RI in session confirmed
Copy session confirmed to PeerState2 for retx
RTT calculation fixes
State transitions
Javadoc fixes
WIP, untested
This commit is contained in:
zzz
2022-03-01 13:52:16 -05:00
parent 5ef93f11a9
commit 0f26baf114
5 changed files with 109 additions and 85 deletions

View File

@@ -14,6 +14,7 @@ import com.southernstorm.noise.protocol.CipherState;
import com.southernstorm.noise.protocol.CipherStatePair;
import com.southernstorm.noise.protocol.HandshakeState;
import net.i2p.crypto.HKDF;
import net.i2p.data.Base64;
import net.i2p.data.DataFormatException;
import net.i2p.data.DataHelper;
@@ -50,6 +51,7 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa
private byte[] _rcvHeaderEncryptKey2;
private byte[] _sessCrForReTX;
private long _timeReceived;
private PeerState2 _pstate;
// testing
private static final boolean ENFORCE_TOKEN = false;
@@ -57,8 +59,6 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa
/**
* @param localPort Must be our external port, otherwise the signature of the
* SessionCreated message will be bad if the external port != the internal port.
* @param packet with all header encryption removed,
* either a SessionRequest OR a TokenRequest.
*/
@@ -351,6 +351,7 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa
_currentState != InboundState.IB_STATE_TOKEN_REQUEST_RECEIVED)
throw new IllegalStateException("Bad state for Retry Sent: " + _currentState);
_currentState = InboundState.IB_STATE_RETRY_SENT;
_lastSend = _context.clock().now();
}
/**
@@ -391,7 +392,7 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa
if (_log.shouldDebug())
_log.debug("State after sess req: " + _handshakeState);
_timeReceived = 0;
processPayload(data, off + LONG_HEADER_SIZE, len - (SHORT_HEADER_SIZE + KEY_LEN + MAC_LEN + MAC_LEN), true);
processPayload(data, off + LONG_HEADER_SIZE, len - (LONG_HEADER_SIZE + KEY_LEN + MAC_LEN), true);
if (_timeReceived == 0)
throw new GeneralSecurityException("No DateTime block in Session Request");
long skew = _establishBegin - _timeReceived;
@@ -399,20 +400,17 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa
throw new GeneralSecurityException("Skew exceeded in Session Request: " + skew);
_sendHeaderEncryptKey2 = SSU2Util.hkdf(_context, _handshakeState.getChainingKey(), "SessCreateHeader");
_currentState = InboundState.IB_STATE_REQUEST_RECEIVED;
if (_createdSentCount == 1) {
_rtt = (int) ( _context.clock().now() - _lastSend );
}
_rtt = (int) ( _context.clock().now() - _lastSend );
packetReceived();
}
/**
* Receive the last message in the handshake, and create the PeerState.
*
*
*
* @return the new PeerState2, may also be retrieved from getPeerState()
*/
public synchronized void receiveSessionConfirmed(UDPPacket packet) throws GeneralSecurityException {
public synchronized PeerState2 receiveSessionConfirmed(UDPPacket packet) throws GeneralSecurityException {
if (_currentState != InboundState.IB_STATE_CREATED_SENT)
throw new GeneralSecurityException("Bad state for Session Confirmed: " + _currentState);
DatagramPacket pkt = packet.getPacket();
@@ -429,9 +427,9 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa
if (_log.shouldDebug())
_log.debug("State after mixHash 3: " + _handshakeState);
byte[] payload = new byte[len - 80]; // 16 hdr, 32 static key, 16 MAC, 16 MAC
// decrypt in-place
try {
_handshakeState.readMessage(data, off + SHORT_HEADER_SIZE, len - SHORT_HEADER_SIZE, payload, 0);
_handshakeState.readMessage(data, off + SHORT_HEADER_SIZE, len - SHORT_HEADER_SIZE, data, off + SHORT_HEADER_SIZE);
} catch (GeneralSecurityException gse) {
if (_log.shouldDebug())
_log.debug("Session Confirmed error, State at failure: " + _handshakeState + '\n' + net.i2p.util.HexDump.dump(data, off, len), gse);
@@ -439,27 +437,53 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa
}
if (_log.shouldDebug())
_log.debug("State after sess conf: " + _handshakeState);
processPayload(payload, 0, payload.length, false);
processPayload(data, off + SHORT_HEADER_SIZE, len - (SHORT_HEADER_SIZE + KEY_LEN + MAC_LEN + MAC_LEN), false);
_sessCrForReTX = null;
// TODO split, calculate keys
if (_receivedConfirmedIdentity == null)
throw new GeneralSecurityException("No RI in Session Confirmed");
// TODO fix state
if ( (_currentState == InboundState.IB_STATE_UNKNOWN) ||
(_currentState == InboundState.IB_STATE_REQUEST_RECEIVED) ||
(_currentState == InboundState.IB_STATE_CREATED_SENT) ) {
if (confirmedFullyReceived())
_currentState = InboundState.IB_STATE_CONFIRMED_COMPLETELY;
else
_currentState = InboundState.IB_STATE_CONFIRMED_PARTIALLY;
}
if (_createdSentCount == 1) {
// split()
// The CipherStates are from d_ab/d_ba,
// not from k_ab/k_ba, so there's no use for
// HandshakeState.split()
byte[] ckd = _handshakeState.getChainingKey();
byte[] k_ab = new byte[32];
byte[] k_ba = new byte[32];
HKDF hkdf = new HKDF(_context);
hkdf.calculate(ckd, ZEROLEN, k_ab, k_ba, 0);
// generate keys
byte[] d_ab = new byte[32];
byte[] h_ab = new byte[32];
byte[] d_ba = new byte[32];
byte[] h_ba = new byte[32];
hkdf.calculate(k_ab, ZEROLEN, INFO_DATA, d_ab, h_ab, 0);
hkdf.calculate(k_ba, ZEROLEN, INFO_DATA, d_ba, h_ba, 0);
ChaChaPolyCipherState sender = new ChaChaPolyCipherState();
sender.initializeKey(d_ba, 0);
ChaChaPolyCipherState rcvr = new ChaChaPolyCipherState();
sender.initializeKey(d_ab, 0);
if (_log.shouldDebug())
_log.debug("Generated Chain key: " + Base64.encode(ckd) +
"\nGenerated split key for A->B: " + Base64.encode(k_ab) +
"\nGenerated split key for B->A: " + Base64.encode(k_ba) +
"\nGenerated encrypt key for A->B: " + Base64.encode(d_ab) +
"\nGenerated encrypt key for B->A: " + Base64.encode(d_ba) +
"\nIntro key for Alice: " + Base64.encode(_sendHeaderEncryptKey1) +
"\nIntro key for Bob: " + Base64.encode(_rcvHeaderEncryptKey1) +
"\nGenerated header key 2 for A->B: " + Base64.encode(h_ab) +
"\nGenerated header key 2 for B->A: " + Base64.encode(h_ba));
_handshakeState.destroy();
if (_createdSentCount == 1)
_rtt = (int) ( _context.clock().now() - _lastSend );
}
_pstate = new PeerState2(_context, _transport, _aliceSocketAddress,
_receivedConfirmedIdentity.calculateHash(),
true, _rtt, sender, rcvr,
_sendConnID, _rcvConnID,
_sendHeaderEncryptKey1, h_ba, h_ab);
_currentState = InboundState.IB_STATE_CONFIRMED_COMPLETELY;
packetReceived();
return _pstate;
}
/**
@@ -507,11 +531,11 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa
}
/**
* @return null we have not received the session confirmed
* @return null if we have not received the session confirmed
*/
public synchronized PeerState2 getPeerState() {
// TODO
return null;
_currentState = InboundState.IB_STATE_COMPLETE;
return _pstate;
}
@Override

View File

@@ -51,7 +51,7 @@ class OutboundEstablishState {
// general status
protected final long _establishBegin;
//private long _lastReceive;
private long _lastSend;
protected long _lastSend;
private long _nextSend;
protected RemoteHostId _remoteHostId;
private final RemoteHostId _claimedAddress;

View File

@@ -12,6 +12,7 @@ import com.southernstorm.noise.protocol.CipherState;
import com.southernstorm.noise.protocol.CipherStatePair;
import com.southernstorm.noise.protocol.HandshakeState;
import net.i2p.crypto.HKDF;
import net.i2p.data.Base64;
import net.i2p.data.DataFormatException;
import net.i2p.data.DataHelper;
@@ -51,6 +52,7 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
private byte[] _sessReqForReTX;
private byte[] _sessConfForReTX;
private long _timeReceived;
private PeerState2 _pstate;
private static final boolean SET_TOKEN = false;
private static final long MAX_SKEW = 2*60*1000L;
@@ -381,8 +383,10 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
/**
* note that we just sent the SessionConfirmed packets
* and save them for retransmission
*
* @return the new PeerState2, may also be retrieved from getPeerState()
*/
public synchronized void confirmedPacketsSent(UDPPacket[] packets) {
public synchronized PeerState2 confirmedPacketsSent(UDPPacket[] packets) {
if (_sessConfForReTX == null) {
// store pkt for retx
// only one supported right now
@@ -395,9 +399,49 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
if (_rcvHeaderEncryptKey2 == null)
_rcvHeaderEncryptKey2 = SSU2Util.hkdf(_context, _handshakeState.getChainingKey(), "SessCreateHeader");
// TODO split(), create PeerState2
// split()
// The CipherStates are from d_ab/d_ba,
// not from k_ab/k_ba, so there's no use for
// HandshakeState.split()
byte[] ckd = _handshakeState.getChainingKey();
byte[] k_ab = new byte[32];
byte[] k_ba = new byte[32];
HKDF hkdf = new HKDF(_context);
hkdf.calculate(ckd, ZEROLEN, k_ab, k_ba, 0);
// generate keys
byte[] d_ab = new byte[32];
byte[] h_ab = new byte[32];
byte[] d_ba = new byte[32];
byte[] h_ba = new byte[32];
hkdf.calculate(k_ab, ZEROLEN, INFO_DATA, d_ab, h_ab, 0);
hkdf.calculate(k_ba, ZEROLEN, INFO_DATA, d_ba, h_ba, 0);
ChaChaPolyCipherState sender = new ChaChaPolyCipherState();
sender.initializeKey(d_ab, 0);
ChaChaPolyCipherState rcvr = new ChaChaPolyCipherState();
sender.initializeKey(d_ba, 0);
if (_log.shouldDebug())
_log.debug("Generated Chain key: " + Base64.encode(ckd) +
"\nGenerated split key for A->B: " + Base64.encode(k_ab) +
"\nGenerated split key for B->A: " + Base64.encode(k_ba) +
"\nGenerated encrypt key for A->B: " + Base64.encode(d_ab) +
"\nGenerated encrypt key for B->A: " + Base64.encode(d_ba) +
"\nIntro key for Alice: " + Base64.encode(_sendHeaderEncryptKey1) +
"\nIntro key for Bob: " + Base64.encode(_rcvHeaderEncryptKey1) +
"\nGenerated header key 2 for A->B: " + Base64.encode(h_ab) +
"\nGenerated header key 2 for B->A: " + Base64.encode(h_ba));
_handshakeState.destroy();
if (_requestSentCount == 1)
_rtt = (int) ( _context.clock().now() - _lastSend );
_pstate = new PeerState2(_context, _transport, _bobSocketAddress,
_remotePeer.calculateHash(),
false, _rtt, sender, rcvr,
_sendConnID, _rcvConnID,
_sendHeaderEncryptKey1, h_ab, h_ba);
_currentState = OutboundState.OB_STATE_CONFIRMED_COMPLETELY;
_pstate.confirmedPacketsSent(_sessConfForReTX);
}
confirmedPacketsSent();
return _pstate;
}
/**
@@ -429,12 +473,11 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
}
/**
* @return null we have not sent the session confirmed
* @return null if we have not sent the session confirmed
*/
public synchronized PeerState2 getPeerState() {
// TODO
// set confirmed pkt data
return null;
_currentState = OutboundState.OB_STATE_CONFIRMED_COMPLETELY;
return _pstate;
}
@Override

View File

@@ -286,27 +286,11 @@ class PacketBuilder2 {
UDPPacket packet = buildLongPacketHeader(state.getSendConnID(), n, TOKEN_REQUEST_FLAG_BYTE,
state.getRcvConnID(), 0);
DatagramPacket pkt = packet.getPacket();
byte toIP[] = state.getSentIP();
if (!_transport.isValid(toIP)) {
packet.release();
return null;
}
InetAddress to;
try {
to = InetAddress.getByAddress(toIP);
} catch (UnknownHostException uhe) {
if (_log.shouldLog(Log.ERROR))
_log.error("How did we think this was a valid IP? " + state.getRemoteHostId());
packet.release();
return null;
}
pkt.setLength(LONG_HEADER_SIZE);
byte[] introKey = state.getSendHeaderEncryptKey1();
encryptTokenRequest(packet, introKey, n, introKey, introKey);
state.requestSent();
setTo(packet, to, state.getSentPort());
pkt.setSocketAddress(state.getSentAddress());
packet.setMessageType(TYPE_SREQ);
packet.setPriority(PRIORITY_HIGH);
state.tokenRequestSent(pkt);
@@ -324,27 +308,11 @@ class PacketBuilder2 {
UDPPacket packet = buildLongPacketHeader(state.getSendConnID(), n, SESSION_REQUEST_FLAG_BYTE,
state.getRcvConnID(), state.getToken());
DatagramPacket pkt = packet.getPacket();
byte toIP[] = state.getSentIP();
if (!_transport.isValid(toIP)) {
packet.release();
return null;
}
InetAddress to;
try {
to = InetAddress.getByAddress(toIP);
} catch (UnknownHostException uhe) {
if (_log.shouldLog(Log.ERROR))
_log.error("How did we think this was a valid IP? " + state.getRemoteHostId());
packet.release();
return null;
}
pkt.setLength(LONG_HEADER_SIZE);
byte[] introKey = state.getSendHeaderEncryptKey1();
encryptSessionRequest(packet, state.getHandshakeState(), introKey, introKey, state.needIntroduction());
state.requestSent();
setTo(packet, to, state.getSentPort());
pkt.setSocketAddress(state.getSentAddress());
packet.setMessageType(TYPE_SREQ);
packet.setPriority(PRIORITY_HIGH);
state.requestSent(pkt);
@@ -475,23 +443,12 @@ class PacketBuilder2 {
private UDPPacket buildSessionConfirmedPacket(OutboundEstablishState2 state, int numFragments, byte ourInfo[], int len, boolean gzip) {
UDPPacket packet = buildShortPacketHeader(state.getSendConnID(), 0, SESSION_CONFIRMED_FLAG_BYTE);
DatagramPacket pkt = packet.getPacket();
InetAddress to;
try {
to = InetAddress.getByAddress(state.getSentIP());
} catch (UnknownHostException uhe) {
if (_log.shouldLog(Log.ERROR))
_log.error("How did we think this was a valid IP? " + state.getRemoteHostId());
packet.release();
return null;
}
pkt.setLength(SHORT_HEADER_SIZE);
SSU2Payload.RIBlock block = new SSU2Payload.RIBlock(ourInfo, 0, len,
false, gzip, 0, numFragments);
encryptSessionConfirmed(packet, state.getHandshakeState(), state.getMTU(),
state.getSendHeaderEncryptKey1(), state.getSendHeaderEncryptKey2(), block, state.getNextToken());
setTo(packet, to, state.getSentPort());
pkt.setSocketAddress(state.getSentAddress());
packet.setMessageType(TYPE_CONF);
packet.setPriority(PRIORITY_HIGH);
return packet;

View File

@@ -73,7 +73,7 @@ final class SSU2Header {
* Decrypt bytes 0-7 in header.
* Packet is unmodified.
*
* @param packet must be 8 bytes min
* @param pkt must be 8 bytes min
* @return the destination connection ID
* @throws IndexOutOfBoundsException if too short
*/