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