From 63e202f8f0926f137896aa01e3545f3e8bf3bb5e Mon Sep 17 00:00:00 2001 From: zzz <zzz@i2pmail.org> Date: Tue, 22 Feb 2022 10:27:42 -0500 Subject: [PATCH] SSU: Start of SSU2 support WIP, not hooked in --- .../i2p/router/transport/udp/SSU2Header.java | 252 ++++++++++++++++++ .../i2p/router/transport/udp/SSU2Util.java | 99 +++++++ 2 files changed, 351 insertions(+) create mode 100644 router/java/src/net/i2p/router/transport/udp/SSU2Header.java create mode 100644 router/java/src/net/i2p/router/transport/udp/SSU2Util.java diff --git a/router/java/src/net/i2p/router/transport/udp/SSU2Header.java b/router/java/src/net/i2p/router/transport/udp/SSU2Header.java new file mode 100644 index 0000000000..fe48009ca8 --- /dev/null +++ b/router/java/src/net/i2p/router/transport/udp/SSU2Header.java @@ -0,0 +1,252 @@ +package net.i2p.router.transport.udp; + +import java.net.DatagramPacket; + +import net.i2p.crypto.ChaCha20; +import net.i2p.data.Base64; +import net.i2p.data.DataHelper; +import static net.i2p.router.transport.udp.SSU2Util.*; + +/** + * Encrypt/decrypt headers + * + * @since 0.9.54 + */ +final class SSU2Header { + + /** 8 bytes of zeros */ + public static final byte[] HEADER_PROT_DATA = new byte[HEADER_PROT_DATA_LEN]; + /** 12 bytes of zeros */ + public static final byte[] CHACHA_IV_0 = new byte[CHACHA_IV_LEN]; + + private SSU2Header() {} + + /** + * Session Request and Session Created only. 64 bytes. + * Packet is unmodified. + * + * @param packet must be 56 bytes min + * @return 64 byte header, null if data too short + */ + public static Header trialDecryptHandshakeHeader(UDPPacket packet, byte[] key1, byte[] key2) { + DatagramPacket pkt = packet.getPacket(); + if (pkt.getLength() < MIN_HANDSHAKE_DATA_LEN) + return null; + Header header = new Header(SESSION_HEADER_SIZE); + decryptHandshakeHeader(pkt, key1, key2, header); + return header; + } + + /** + * Retry, Token Request, Peer Test only. 32 bytes. + * Packet is unmodified. + * + * @param packet must be 56 bytes min + * @return 32 byte header, null if data too short + */ + public static Header trialDecryptLongHeader(UDPPacket packet, byte[] key1, byte[] key2) { + DatagramPacket pkt = packet.getPacket(); + if (pkt.getLength() < MIN_LONG_DATA_LEN) + return null; + Header header = new Header(LONG_HEADER_SIZE); + decryptLongHeader(pkt, key1, key2, header); + return header; + } + + /** + * Session Confirmed and data phase. 16 bytes. + * Packet is unmodified. + * + * @param packet must be 40 bytes min + * @return 16 byte header, null if data too short, must be 40 bytes min + */ + public static Header trialDecryptShortHeader(UDPPacket packet, byte[] key1, byte[] key2) { + DatagramPacket pkt = packet.getPacket(); + if (pkt.getLength() < MIN_DATA_LEN) + return null; + Header header = new Header(SHORT_HEADER_SIZE); + decryptShortHeader(pkt, key1, key2, header); + return header; + } + + /** + * Decrypt bytes 0-7 in header. + * Packet is unmodified. + * + * @param packet must be 8 bytes min + * @return the destination connection ID + * @throws IndexOutOfBoundsException if too short + */ + public static long decryptDestConnID(DatagramPacket pkt, byte[] key1) { + byte data[] = pkt.getData(); + int off = pkt.getOffset(); + int len = pkt.getLength(); + byte[] xor = new byte[HEADER_PROT_DATA_LEN]; + + ChaCha20.decrypt(key1, data, off + len - HEADER_PROT_SAMPLE_1_OFFSET, HEADER_PROT_DATA, 0, xor, 0, HEADER_PROT_DATA_LEN); + for (int i = 0; i < HEADER_PROT_DATA_LEN; i++) { + xor[i] ^= data[i + off + HEADER_PROT_1_OFFSET]; + } + return DataHelper.fromLong8(xor, 0); + } + + /** + * Copy the header back to the packet. Cannot be undone. + */ + public static void acceptTrialDecrypt(UDPPacket packet, Header header) { + DatagramPacket pkt = packet.getPacket(); + int off = pkt.getOffset(); + byte data[] = pkt.getData(); + System.arraycopy(header.data, 0, data, off, header.data.length); + } + + + /** + * Decrypt bytes 0-63 from pkt to header + * First 64 bytes + * Packet is unmodified. + */ + private static void decryptHandshakeHeader(DatagramPacket pkt, byte[] key1, byte[] key2, Header header) { + byte data[] = pkt.getData(); + int off = pkt.getOffset(); + decryptShortHeader(pkt, key1, key2, header); + ChaCha20.decrypt(key2, CHACHA_IV_0, data, off + SHORT_HEADER_SIZE, header.data, SHORT_HEADER_SIZE, KEY_LEN + LONG_HEADER_SIZE - SHORT_HEADER_SIZE); + } + + /** + * Decrypt bytes 0-31 from pkt to header. + * First 32 bytes + * Packet is unmodified. + */ + private static void decryptLongHeader(DatagramPacket pkt, byte[] key1, byte[] key2, Header header) { + byte data[] = pkt.getData(); + int off = pkt.getOffset(); + decryptShortHeader(pkt, key1, key2, header); + ChaCha20.decrypt(key2, CHACHA_IV_0, data, off + SHORT_HEADER_SIZE, header.data, SHORT_HEADER_SIZE, LONG_HEADER_SIZE - SHORT_HEADER_SIZE); + } + + /** + * Decrypt bytes 0-15 to header. + * Packet is unmodified. + * + * First 8 bytes uses key1 and the next-to-last 12 bytes as the IV. + * Next 8 bytes uses key2 and the last 12 bytes as the IV. + */ + private static void decryptShortHeader(DatagramPacket pkt, byte[] key1, byte[] key2, Header header) { + byte data[] = pkt.getData(); + int off = pkt.getOffset(); + int len = pkt.getLength(); + byte[] xor = new byte[HEADER_PROT_DATA_LEN]; + + ChaCha20.decrypt(key1, data, off + len - HEADER_PROT_SAMPLE_1_OFFSET, HEADER_PROT_DATA, 0, xor, 0, HEADER_PROT_DATA_LEN); + for (int i = 0; i < HEADER_PROT_DATA_LEN; i++) { + header.data[i + HEADER_PROT_1_OFFSET] = (byte) (data[i + off + HEADER_PROT_1_OFFSET] ^ xor[i]); + } + + ChaCha20.decrypt(key2, data, off + len - HEADER_PROT_SAMPLE_2_OFFSET, HEADER_PROT_DATA, 0, xor, 0, HEADER_PROT_DATA_LEN); + for (int i = 0; i < HEADER_PROT_DATA_LEN; i++) { + header.data[i + HEADER_PROT_2_OFFSET] = (byte) (data[i + off + HEADER_PROT_2_OFFSET] ^ xor[i]); + } + } + + /** + * A temporary structure returned from trial decrypt, + * with methods to access the fields. + */ + public static class Header { + public final byte[] data; + public Header(int len) { data = new byte[len]; } + + /** all headers */ + public long getDestConnID() { return DataHelper.fromLong8(data, 0); } + /** all headers */ + public long getPacketNumber() { return DataHelper.fromLong(data, PKT_NUM_OFFSET, PKT_NUM_LEN); } + /** all headers */ + public int getType() { return data[TYPE_OFFSET] & 0xff; } + + /** short headers only */ + public int getShortHeaderFlags() { return (int) DataHelper.fromLong(data, SHORT_HEADER_FLAGS_OFFSET, SHORT_HEADER_FLAGS_LEN); } + + /** long headers only */ + public int getVersion() { return data[VERSION_OFFSET] & 0xff; } + /** long headers only */ + public int getNetID() { return data[NETID_OFFSET] & 0xff; } + /** long headers only */ + public int getHandshakeHeaderFlags() { return data[LONG_HEADER_FLAGS_OFFSET] & 0xff; } + /** long headers only */ + public long getSrcConnID() { return DataHelper.fromLong8(data, SRC_CONN_ID_OFFSET); } + /** long headers only */ + public long getToken() { return DataHelper.fromLong8(data, TOKEN_OFFSET); } + + /** handshake headers only */ + public byte[] getEphemeralKey() { + byte[] rv = new byte[KEY_LEN]; + System.arraycopy(data, LONG_HEADER_SIZE, rv, 0, KEY_LEN); + return rv; + } + + @Override + public String toString() { + if (data.length >= SESSION_HEADER_SIZE) { + return "Handshake header destID " + getDestConnID() + " pkt num " + getPacketNumber() + " type " + getType() + + " srcID " + getSrcConnID() + " token " + getToken() + " key " + Base64.encode(getEphemeralKey()); + } + if (data.length >= LONG_HEADER_SIZE) { + return "Long header destID " + getDestConnID() + " pkt num " + getPacketNumber() + " type " + getType() + + " srcID " + getSrcConnID() + " token " + getToken(); + } + return "Short header destID " + getDestConnID() + " pkt num " + getPacketNumber() + " type " + getType(); + } + } + + ////////// Encryption /////////// + + /** + * First 64 bytes + */ + public static void encryptHandshakeHeader(UDPPacket packet, byte[] key1, byte[] key2) { + DatagramPacket pkt = packet.getPacket(); + byte data[] = pkt.getData(); + int off = pkt.getOffset(); + encryptShortHeader(packet, key1, key2); + ChaCha20.encrypt(key2, CHACHA_IV_0, data, off + SHORT_HEADER_SIZE, data, off + SHORT_HEADER_SIZE, KEY_LEN + LONG_HEADER_SIZE - SHORT_HEADER_SIZE); + } + + /** + * First 32 bytes + */ + public static void encryptLongHeader(UDPPacket packet, byte[] key1, byte[] key2) { + DatagramPacket pkt = packet.getPacket(); + byte data[] = pkt.getData(); + int off = pkt.getOffset(); + encryptShortHeader(packet, key1, key2); + ChaCha20.encrypt(key2, CHACHA_IV_0, data, off + SHORT_HEADER_SIZE, data, off + SHORT_HEADER_SIZE, LONG_HEADER_SIZE - SHORT_HEADER_SIZE); + } + + /** + * First 16 bytes. + * + * First 8 bytes uses key1 and the next-to-last 12 bytes as the IV. + * Next 8 bytes uses key2 and the last 12 bytes as the IV. + */ + public static void encryptShortHeader(UDPPacket packet, byte[] key1, byte[] key2) { + DatagramPacket pkt = packet.getPacket(); + byte data[] = pkt.getData(); + int off = pkt.getOffset(); + int len = pkt.getLength(); + byte[] xor = new byte[HEADER_PROT_DATA_LEN]; + + ChaCha20.encrypt(key1, data, off + len - HEADER_PROT_SAMPLE_1_OFFSET, HEADER_PROT_DATA, 0, xor, 0, HEADER_PROT_DATA_LEN); + for (int i = 0; i < HEADER_PROT_DATA_LEN; i++) { + data[i + off + HEADER_PROT_1_OFFSET] ^= xor[i]; + } + + ChaCha20.encrypt(key2, data, off + len - HEADER_PROT_SAMPLE_2_OFFSET, HEADER_PROT_DATA, 0, xor, 0, HEADER_PROT_DATA_LEN); + for (int i = 0; i < HEADER_PROT_DATA_LEN; i++) { + data[i + off + HEADER_PROT_2_OFFSET] ^= xor[i]; + } + } + + + +} diff --git a/router/java/src/net/i2p/router/transport/udp/SSU2Util.java b/router/java/src/net/i2p/router/transport/udp/SSU2Util.java new file mode 100644 index 0000000000..aa43708151 --- /dev/null +++ b/router/java/src/net/i2p/router/transport/udp/SSU2Util.java @@ -0,0 +1,99 @@ +package net.i2p.router.transport.udp; + +import net.i2p.I2PAppContext; +import net.i2p.crypto.EncType; +import net.i2p.crypto.HKDF; + +/** + * SSU2 Utils and constants + * + * @since 0.9.54 + */ +final class SSU2Util { + public static final int PROTOCOL_VERSION = 2; + + // lengths + /** 32 */ + public static final int KEY_LEN = EncType.ECIES_X25519.getPubkeyLen(); + public static final int MAC_LEN = 16; + public static final int CHACHA_IV_LEN = 12; + public static final int INTRO_KEY_LEN = 32; + public static final int SHORT_HEADER_SIZE = 16; + public static final int LONG_HEADER_SIZE = 32; + /** 64 */ + public static final int SESSION_HEADER_SIZE = LONG_HEADER_SIZE + KEY_LEN; + + // header fields + public static final int DEST_CONN_ID_OFFSET = 0; + public static final int PKT_NUM_OFFSET = 8; + public static final int PKT_NUM_LEN = 4; + public static final int TYPE_OFFSET = 12; + public static final int VERSION_OFFSET = 13; + public static final int SHORT_HEADER_FLAGS_OFFSET = 13; + public static final int SHORT_HEADER_FLAGS_LEN = 3; + public static final int NETID_OFFSET = 14; + public static final int LONG_HEADER_FLAGS_OFFSET = 15; + public static final int SRC_CONN_ID_OFFSET = 16; + public static final int TOKEN_OFFSET = 24; + + // header protection + public static final int HEADER_PROT_SAMPLE_LEN = 12; + public static final int TOTAL_PROT_SAMPLE_LEN = 2 * HEADER_PROT_SAMPLE_LEN; + public static final int HEADER_PROT_SAMPLE_1_OFFSET = 2 * HEADER_PROT_SAMPLE_LEN; + public static final int HEADER_PROT_SAMPLE_2_OFFSET = HEADER_PROT_SAMPLE_LEN; + public static final int HEADER_PROT_DATA_LEN = 8; + public static final int HEADER_PROT_1_OFFSET = DEST_CONN_ID_OFFSET; + public static final int HEADER_PROT_2_OFFSET = PKT_NUM_OFFSET; + + public static final int PADDING_MAX = 64; + + /** 40 */ + public static final int MIN_DATA_LEN = SHORT_HEADER_SIZE + TOTAL_PROT_SAMPLE_LEN; + /** 56 */ + public static final int MIN_LONG_DATA_LEN = LONG_HEADER_SIZE + TOTAL_PROT_SAMPLE_LEN; + /** 88 */ + public static final int MIN_HANDSHAKE_DATA_LEN = SESSION_HEADER_SIZE + TOTAL_PROT_SAMPLE_LEN; + + + /** 3 byte block header + 9 byte I2NP header = 12 */ + public static final int FULL_I2NP_HEADER_SIZE = SSU2Payload.BLOCK_HEADER_SIZE + 9; + + /** 3 byte block header + 9 byte I2NP header = 12 */ + public static final int FIRST_FRAGMENT_HEADER_SIZE = SSU2Payload.BLOCK_HEADER_SIZE + 9; + + /** 3 byte block header + 4 byte msg ID + 1 byte fragment info = 8 */ + public static final int FOLLOWON_FRAGMENT_HEADER_SIZE = SSU2Payload.BLOCK_HEADER_SIZE + 5; + + /** 16 byte block header + 2 + 12 = 30 */ + public static final int DATA_HEADER_SIZE = SHORT_HEADER_SIZE + 2 + FULL_I2NP_HEADER_SIZE; + + /** + * The message types, 0-10, as bytes + */ + public static final byte SESSION_REQUEST_FLAG_BYTE = UDPPacket.PAYLOAD_TYPE_SESSION_REQUEST; + public static final byte SESSION_CREATED_FLAG_BYTE = UDPPacket.PAYLOAD_TYPE_SESSION_CREATED; + public static final byte SESSION_CONFIRMED_FLAG_BYTE = UDPPacket.PAYLOAD_TYPE_SESSION_CONFIRMED; + public static final byte DATA_FLAG_BYTE = UDPPacket.PAYLOAD_TYPE_DATA; + public static final byte PEER_TEST_FLAG_BYTE = UDPPacket.PAYLOAD_TYPE_TEST; + public static final byte RETRY_FLAG_BYTE = 9; + public static final byte TOKEN_REQUEST_FLAG_BYTE = 10; + + public static final String INFO_CREATED = "SessCreateHeader"; + public static final String INFO_CONFIRMED = "SessionConfirmed"; + public static final String INFO_DATA = "HKDFSSU2DataKeys"; + + public static final byte[] ZEROLEN = new byte[0]; + public static final byte[] ZEROKEY = new byte[KEY_LEN]; + + private SSU2Util() {} + + /** + * 32 byte output, ZEROLEN data + */ + public static byte[] hkdf(I2PAppContext ctx, byte[] key, String info) { + HKDF hkdf = new HKDF(ctx); + byte[] rv = new byte[32]; + hkdf.calculate(key, ZEROLEN, info, rv); + return rv; + } +} -- GitLab