SSU: Start of SSU2 support

WIP, not hooked in
This commit is contained in:
zzz
2022-02-22 10:27:42 -05:00
parent 80535875ad
commit 63e202f8f0
2 changed files with 351 additions and 0 deletions

View File

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

View File

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