Tunnels: Continue work on new build messages (proposal #157)

WIP, still disabled, proposal not complete

- Use ChaCha20 to encrypt/decrypt records
- Add OTBRM methods for plaintext record
- Add OTBRM checks for correct plaintext slot number
- Add BRR checks to prevent use of nonexistent AES key/IV
- Set plaintext reply at OBEP in BuildHandler
- Allow OTBRM in InboundMessageDistributor
- Remove timing measurements in BuildMessageProcessor.decrypt()
- Add test to BuildMessageTestStandalone for outbound build
- Add check for all replies to BuildMessageTestStandalone
- Log tweaks
This commit is contained in:
zzz
2021-06-13 10:31:02 -04:00
parent 3fbfb689af
commit 010d1a9953
9 changed files with 264 additions and 82 deletions

View File

@@ -246,6 +246,8 @@ public class BuildRequestRecord {
* Not to be used for short ECIES records; use the ChaChaReplyKey instead.
*/
public SessionKey readReplyKey() {
if (_isEC && _data.length == LENGTH_EC_SHORT)
throw new IllegalStateException();
byte key[] = new byte[SessionKey.KEYSIZE_BYTES];
int off = _isEC ? OFF_REPLY_KEY_EC : OFF_REPLY_KEY;
System.arraycopy(_data, off, key, 0, SessionKey.KEYSIZE_BYTES);
@@ -258,6 +260,8 @@ public class BuildRequestRecord {
* @return 16 bytes
*/
public byte[] readReplyIV() {
if (_isEC && _data.length == LENGTH_EC_SHORT)
throw new IllegalStateException();
byte iv[] = new byte[IV_SIZE];
int off = _isEC ? OFF_REPLY_IV_EC : OFF_REPLY_IV;
System.arraycopy(_data, off, iv, 0, IV_SIZE);

View File

@@ -86,7 +86,7 @@ public class BuildResponseRecord {
* @param options 116 bytes max when serialized
* @return a 236-byte response record
* @throws IllegalArgumentException if options too big or on encryption failure
* @since 0.9.451
* @since 0.9.51
*/
public static ShortEncryptedBuildRecord createShort(I2PAppContext ctx, int status, SessionKey replyKey,
byte replyAD[], Properties options) {

View File

@@ -18,7 +18,7 @@ public class InboundTunnelBuildMessage extends TunnelBuildMessage {
public static final int SHORT_RECORD_SIZE = ShortTunnelBuildMessage.SHORT_RECORD_SIZE;
public static final int MAX_PLAINTEXT_RECORD_SIZE = OutboundTunnelBuildReplyMessage.MAX_PLAINTEXT_RECORD_SIZE;
private int _plaintextSlot;
private int _plaintextSlot = -1;
private byte[] _plaintextRecord;
/** zero record count, will be set with readMessage() */
@@ -32,10 +32,11 @@ public class InboundTunnelBuildMessage extends TunnelBuildMessage {
/**
* @param record must be ShortEncryptedBuildRecord or null
* @throws IllegalArgumentException on bad slot or record length.
*/
@Override
public void setRecord(int index, EncryptedBuildRecord record) {
if (record != null && record.length() != SHORT_RECORD_SIZE)
if (record != null && (record.length() != SHORT_RECORD_SIZE || index == _plaintextSlot))
throw new IllegalArgumentException();
super.setRecord(index, record);
}
@@ -45,7 +46,8 @@ public class InboundTunnelBuildMessage extends TunnelBuildMessage {
* @throws IllegalArgumentException on bad slot or data length.
*/
public void setPlaintextRecord(int slot, byte[] data) {
if (slot < 0 || slot >= RECORD_COUNT || data.length == 0 || data.length > MAX_PLAINTEXT_RECORD_SIZE)
if (slot < 0 || slot >= RECORD_COUNT || data.length == 0 || data.length > MAX_PLAINTEXT_RECORD_SIZE ||
(_records != null && _records[slot] != null))
throw new IllegalArgumentException();
_plaintextSlot = slot;
_plaintextRecord = data;

View File

@@ -1,6 +1,11 @@
package net.i2p.data.i2np;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Properties;
import net.i2p.I2PAppContext;
import net.i2p.data.DataFormatException;
import net.i2p.data.DataHelper;
/**
@@ -18,7 +23,7 @@ public class OutboundTunnelBuildReplyMessage extends TunnelBuildReplyMessage {
public static final int SHORT_RECORD_SIZE = ShortTunnelBuildMessage.SHORT_RECORD_SIZE;
public static final int MAX_PLAINTEXT_RECORD_SIZE = 172;
private int _plaintextSlot;
private int _plaintextSlot = -1;
private byte[] _plaintextRecord;
/** zero record count, will be set with readMessage() */
@@ -32,20 +37,58 @@ public class OutboundTunnelBuildReplyMessage extends TunnelBuildReplyMessage {
/**
* @param record must be ShortEncryptedBuildRecord or null
* @throws IllegalArgumentException on bad slot or record length.
*/
@Override
public void setRecord(int index, EncryptedBuildRecord record) {
if (record != null && record.length() != SHORT_RECORD_SIZE)
if (record != null && (record.length() != SHORT_RECORD_SIZE || index == _plaintextSlot))
throw new IllegalArgumentException();
super.setRecord(index, record);
}
/**
* Set the slot and data for the plaintext record.
* Empty properties will be used.
*
* @param reply 0-255
* @throws IllegalArgumentException on bad slot or data length.
* @since 0.9.51
*/
public void setPlaintextRecord(int slot, int reply) {
// 00 00 reply
byte[] data = new byte[3];
data[2] = (byte) reply;
setPlaintextRecord(slot, data);
}
/**
* Set the slot and data for the plaintext record.
*
* @param reply 0-255
* @param props may be null
* @throws IllegalArgumentException on bad slot or data length.
* @since 0.9.51
*/
public void setPlaintextRecord(int slot, int reply, Properties props) throws DataFormatException {
if (props == null || props.isEmpty()) {
setPlaintextRecord(slot, reply);
return;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
DataHelper.writeProperties(baos, props);
} catch (IOException ioe) {}
baos.write((byte) reply);
setPlaintextRecord(slot, baos.toByteArray());
}
/**
* Set the slot and data for the plaintext record.
* @throws IllegalArgumentException on bad slot or data length.
*/
public void setPlaintextRecord(int slot, byte[] data) {
if (slot < 0 || slot >= RECORD_COUNT || data.length == 0 || data.length > MAX_PLAINTEXT_RECORD_SIZE)
if (slot < 0 || slot >= RECORD_COUNT || data.length == 0 || data.length > MAX_PLAINTEXT_RECORD_SIZE ||
(_records != null && _records[slot] != null))
throw new IllegalArgumentException();
_plaintextSlot = slot;
_plaintextRecord = data;
@@ -66,6 +109,24 @@ public class OutboundTunnelBuildReplyMessage extends TunnelBuildReplyMessage {
return _plaintextRecord;
}
/**
* Get the data for the plaintext record.
* @since 0.9.51
*/
public int getPlaintextReply() {
return _plaintextRecord[_plaintextRecord.length - 1] & 0xff;
}
/**
* Get the data for the plaintext record.
* @since 0.9.51
*/
public Properties getPlaintextOptions() throws DataFormatException {
Properties props = new Properties();
DataHelper.fromProperties(_plaintextRecord, 0, props);
return props;
}
@Override
protected int calculateWrittenLength() { return 4 + _plaintextRecord.length + ((RECORD_COUNT - 1) * SHORT_RECORD_SIZE); }

View File

@@ -13,6 +13,7 @@ import net.i2p.data.i2np.DeliveryInstructions;
import net.i2p.data.i2np.DeliveryStatusMessage;
import net.i2p.data.i2np.GarlicMessage;
import net.i2p.data.i2np.I2NPMessage;
import net.i2p.data.i2np.OutboundTunnelBuildReplyMessage;
import net.i2p.data.i2np.TunnelBuildReplyMessage;
import net.i2p.data.i2np.VariableTunnelBuildReplyMessage;
import net.i2p.router.ClientMessage;
@@ -131,6 +132,7 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
case DeliveryStatusMessage.MESSAGE_TYPE:
case GarlicMessage.MESSAGE_TYPE:
case OutboundTunnelBuildReplyMessage.MESSAGE_TYPE:
case TunnelBuildReplyMessage.MESSAGE_TYPE:
case VariableTunnelBuildReplyMessage.MESSAGE_TYPE:
// these are safe, handled below
@@ -161,6 +163,7 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
case DatabaseSearchReplyMessage.MESSAGE_TYPE:
case DeliveryStatusMessage.MESSAGE_TYPE:
case GarlicMessage.MESSAGE_TYPE:
case OutboundTunnelBuildReplyMessage.MESSAGE_TYPE:
case TunnelBuildReplyMessage.MESSAGE_TYPE:
case VariableTunnelBuildReplyMessage.MESSAGE_TYPE:
// these are safe, handled below

View File

@@ -1049,7 +1049,7 @@ class BuildHandler implements Runnable {
TunnelBuildReplyMessage replyMsg;
if (state.msg.getType() == ShortTunnelBuildMessage.MESSAGE_TYPE) {
OutboundTunnelBuildReplyMessage otbrm = new OutboundTunnelBuildReplyMessage(_context, records);
otbrm.setPlaintextRecord(ourSlot, null); // TODO
otbrm.setPlaintextRecord(ourSlot, response);
replyMsg = otbrm;
} else if (records == TunnelBuildMessage.MAX_RECORD_COUNT) {
replyMsg = new TunnelBuildReplyMessage(_context);

View File

@@ -1,5 +1,6 @@
package net.i2p.router.tunnel.pool;
import net.i2p.crypto.ChaCha20;
import net.i2p.crypto.EncType;
import net.i2p.data.Base64;
import net.i2p.data.DataFormatException;
@@ -9,6 +10,8 @@ import net.i2p.data.PrivateKey;
import net.i2p.data.SessionKey;
import net.i2p.data.i2np.BuildRequestRecord;
import net.i2p.data.i2np.EncryptedBuildRecord;
import net.i2p.data.i2np.ShortEncryptedBuildRecord;
import net.i2p.data.i2np.ShortTunnelBuildMessage;
import net.i2p.data.i2np.TunnelBuildMessage;
import net.i2p.router.RouterContext;
import net.i2p.router.RouterThrottleImpl;
@@ -92,39 +95,39 @@ class BuildMessageProcessor {
public BuildRequestRecord decrypt(TunnelBuildMessage msg, Hash ourHash, PrivateKey privKey) {
BuildRequestRecord rv = null;
int ourHop = -1;
long beforeActualDecrypt = 0;
long afterActualDecrypt = 0;
byte[] ourHashData = ourHash.getData();
long beforeLoop = System.currentTimeMillis();
boolean isShort = msg.getType() == ShortTunnelBuildMessage.MESSAGE_TYPE;
for (int i = 0; i < msg.getRecordCount(); i++) {
EncryptedBuildRecord rec = msg.getRecord(i);
boolean eq = DataHelper.eq(ourHashData, 0, rec.getData(), 0, BuildRequestRecord.PEER_SIZE);
if (eq) {
beforeActualDecrypt = System.currentTimeMillis();
try {
rv = new BuildRequestRecord(ctx, privKey, rec);
afterActualDecrypt = System.currentTimeMillis();
// i2pd bug
boolean isBad = SessionKey.INVALID_KEY.equals(rv.readReplyKey());
if (isBad) {
if (log.shouldLog(Log.WARN))
log.warn(msg.getUniqueId() + ": Bad reply key: " + rv);
ctx.statManager().addRateData("tunnel.buildRequestBadReplyKey", 1);
return null;
}
if (isShort) {
// Bloom filter TBD
} else {
// i2pd bug
boolean isBad = SessionKey.INVALID_KEY.equals(rv.readReplyKey());
if (isBad) {
if (log.shouldLog(Log.WARN))
log.warn(msg.getUniqueId() + ": Bad reply key: " + rv);
ctx.statManager().addRateData("tunnel.buildRequestBadReplyKey", 1);
return null;
}
// The spec says to feed the 32-byte AES-256 reply key into the Bloom filter.
// But we were using the first 32 bytes of the encrypted reply.
// Fixed in 0.9.24
boolean isEC = ctx.keyManager().getPrivateKey().getType() == EncType.ECIES_X25519;
int off = isEC ? BuildRequestRecord.OFF_REPLY_KEY_EC : BuildRequestRecord.OFF_REPLY_KEY;
boolean isDup = _filter.add(rv.getData(), off, 32);
if (isDup) {
if (log.shouldLog(Log.WARN))
log.warn(msg.getUniqueId() + ": Dup record: " + rv);
ctx.statManager().addRateData("tunnel.buildRequestDup", 1);
return null;
// The spec says to feed the 32-byte AES-256 reply key into the Bloom filter.
// But we were using the first 32 bytes of the encrypted reply.
// Fixed in 0.9.24
boolean isEC = ctx.keyManager().getPrivateKey().getType() == EncType.ECIES_X25519;
int off = isEC ? BuildRequestRecord.OFF_REPLY_KEY_EC : BuildRequestRecord.OFF_REPLY_KEY;
boolean isDup = _filter.add(rv.getData(), off, 32);
if (isDup) {
if (log.shouldLog(Log.WARN))
log.warn(msg.getUniqueId() + ": Dup record: " + rv);
ctx.statManager().addRateData("tunnel.buildRequestDup", 1);
return null;
}
}
if (log.shouldLog(Log.DEBUG))
@@ -150,28 +153,36 @@ class BuildMessageProcessor {
return null;
}
long beforeEncrypt = System.currentTimeMillis();
SessionKey replyKey = rv.readReplyKey();
byte iv[] = rv.readReplyIV();
for (int i = 0; i < msg.getRecordCount(); i++) {
if (i != ourHop) {
EncryptedBuildRecord data = msg.getRecord(i);
//if (log.shouldLog(Log.DEBUG))
// log.debug("Encrypting record " + i + "/? with replyKey " + replyKey.toBase64() + "/" + Base64.encode(iv));
// encrypt in-place, corrupts SDS
byte[] bytes = data.getData();
ctx.aes().encrypt(bytes, 0, bytes, 0, replyKey, iv, 0, EncryptedBuildRecord.LENGTH);
if (isShort) {
byte[] replyKey = rv.getChaChaReplyKey().getData();
byte iv[] = new byte[12];
for (int i = 0; i < msg.getRecordCount(); i++) {
if (i != ourHop) {
EncryptedBuildRecord data = msg.getRecord(i);
//if (log.shouldLog(Log.DEBUG))
// log.debug("Encrypting record " + i + "/? with replyKey " + replyKey.toBase64() + "/" + Base64.encode(iv));
// encrypt in-place, corrupts SDS
byte[] bytes = data.getData();
// slot number, little endian
iv[0] = (byte) i;
ChaCha20.encrypt(replyKey, iv, bytes, 0, bytes, 0, ShortEncryptedBuildRecord.LENGTH);
}
}
} else {
SessionKey replyKey = rv.readReplyKey();
byte iv[] = rv.readReplyIV();
for (int i = 0; i < msg.getRecordCount(); i++) {
if (i != ourHop) {
EncryptedBuildRecord data = msg.getRecord(i);
//if (log.shouldLog(Log.DEBUG))
// log.debug("Encrypting record " + i + "/? with replyKey " + replyKey.toBase64() + "/" + Base64.encode(iv));
// encrypt in-place, corrupts SDS
byte[] bytes = data.getData();
ctx.aes().encrypt(bytes, 0, bytes, 0, replyKey, iv, 0, EncryptedBuildRecord.LENGTH);
}
}
}
long afterEncrypt = System.currentTimeMillis();
msg.setRecord(ourHop, null);
if (afterEncrypt-beforeLoop > 1000) {
if (log.shouldLog(Log.WARN))
log.warn("Slow decryption, total=" + (afterEncrypt-beforeLoop)
+ " looping=" + (beforeEncrypt-beforeLoop)
+ " decrypt=" + (afterActualDecrypt-beforeActualDecrypt)
+ " encrypt=" + (afterEncrypt-beforeEncrypt));
}
return rv;
}
}

View File

@@ -3,6 +3,7 @@ package net.i2p.router.tunnel.pool;
import java.util.List;
import net.i2p.I2PAppContext;
import net.i2p.crypto.ChaCha20;
import net.i2p.data.Base64;
import net.i2p.data.DataHelper;
import net.i2p.data.Hash;
@@ -10,6 +11,7 @@ import net.i2p.data.SessionKey;
import net.i2p.data.i2np.BuildResponseRecord;
import net.i2p.data.i2np.EncryptedBuildRecord;
import net.i2p.data.i2np.OutboundTunnelBuildReplyMessage;
import net.i2p.data.i2np.ShortEncryptedBuildRecord;
import net.i2p.data.i2np.TunnelBuildReplyMessage;
import net.i2p.router.tunnel.TunnelCreatorConfig;
import net.i2p.util.Log;
@@ -50,6 +52,7 @@ class BuildReplyHandler {
log.error("Corrupted build reply, expected " + recordOrder.size() + " records, got " + reply.getRecordCount());
return null;
}
boolean isShort = reply.getType() == OutboundTunnelBuildReplyMessage.MESSAGE_TYPE;
int rv[] = new int[reply.getRecordCount()];
for (int i = 0; i < rv.length; i++) {
int hop = recordOrder.get(i).intValue();
@@ -86,6 +89,10 @@ class BuildReplyHandler {
rv[i] = ok;
}
}
if (isShort) {
OutboundTunnelBuildReplyMessage otbrm = (OutboundTunnelBuildReplyMessage) reply;
rv[otbrm.getPlaintextSlot()] = otbrm.getPlaintextReply();
}
return rv;
}
@@ -100,6 +107,24 @@ class BuildReplyHandler {
*/
private int decryptRecord(TunnelBuildReplyMessage reply, TunnelCreatorConfig cfg, int recordNum, int hop) {
EncryptedBuildRecord rec = reply.getRecord(recordNum);
boolean isShort = reply.getType() == OutboundTunnelBuildReplyMessage.MESSAGE_TYPE;
if (rec == null) {
if (!isShort) {
if (log.shouldWarn())
log.warn("Missing record " + recordNum);
return -1;
}
OutboundTunnelBuildReplyMessage otbrm = (OutboundTunnelBuildReplyMessage) reply;
if (otbrm.getPlaintextSlot() != recordNum) {
if (log.shouldWarn())
log.warn("Plaintext slot mismatch expected " + recordNum + " got " + otbrm.getPlaintextSlot());
return -1;
}
int rv = otbrm.getPlaintextReply();
if (log.shouldLog(Log.DEBUG))
log.debug(reply.getUniqueId() + ": Received: " + rv + " for plaintext record " + recordNum + "/" + hop);
return rv;
}
byte[] data = rec.getData();
int start = cfg.getLength() - 1;
if (cfg.isInbound())
@@ -110,18 +135,32 @@ class BuildReplyHandler {
if (isEC)
end++;
// do we need to adjust this for the endpoint?
for (int j = start; j >= end; j--) {
SessionKey replyKey = cfg.getAESReplyKey(j);
byte replyIV[] = cfg.getAESReplyIV(j);
if (log.shouldLog(Log.DEBUG)) {
log.debug(reply.getUniqueId() + ": Decrypting record " + recordNum + "/" + hop + "/" + j + " with replyKey "
+ replyKey.toBase64() + "/" + Base64.encode(replyIV) + ": " + cfg);
log.debug(reply.getUniqueId() + ": before decrypt: " + Base64.encode(data));
log.debug(reply.getUniqueId() + ": Full reply rec: sz=" + data.length + " data=" + Base64.encode(data, 0, TunnelBuildReplyMessage.RECORD_SIZE));
if (isShort) {
byte iv[] = new byte[12];
for (int j = start; j >= end; j--) {
byte[] replyKey = cfg.getChaChaReplyKey(j).getData();
if (log.shouldDebug()) {
log.debug(reply.getUniqueId() + ": Decrypting ChaCha record " + recordNum + "/" + hop + "/" + j + " with replyKey "
+ Base64.encode(replyKey) + " : " + cfg);
}
// slot number, little endian
iv[0] = (byte) recordNum;
ChaCha20.encrypt(replyKey, iv, data, 0, data, 0, ShortEncryptedBuildRecord.LENGTH);
}
} else {
for (int j = start; j >= end; j--) {
SessionKey replyKey = cfg.getAESReplyKey(j);
byte replyIV[] = cfg.getAESReplyIV(j);
if (log.shouldDebug()) {
log.debug(reply.getUniqueId() + ": Decrypting AES record " + recordNum + "/" + hop + "/" + j + " with replyKey "
+ replyKey.toBase64() + "/" + Base64.encode(replyIV) + ": " + cfg);
//log.debug(reply.getUniqueId() + ": before decrypt: " + Base64.encode(data));
//log.debug(reply.getUniqueId() + ": Full reply rec: sz=" + data.length + " data=" + Base64.encode(data));
}
ctx.aes().decrypt(data, 0, data, 0, replyKey, replyIV, 0, data.length);
//if (log.shouldLog(Log.DEBUG))
// log.debug(reply.getUniqueId() + ": after decrypt: " + Base64.encode(data));
}
ctx.aes().decrypt(data, 0, data, 0, replyKey, replyIV, 0, TunnelBuildReplyMessage.RECORD_SIZE);
if (log.shouldLog(Log.DEBUG))
log.debug(reply.getUniqueId() + ": after decrypt: " + Base64.encode(data));
}
// ok, all of the layered encryption is stripped, so lets verify it
// (formatted per BuildResponseRecord.create)