forked from I2P_Developers/i2p.i2p
SSU: Start of SSU2 support
WIP, not hooked in
This commit is contained in:
252
router/java/src/net/i2p/router/transport/udp/SSU2Header.java
Normal file
252
router/java/src/net/i2p/router/transport/udp/SSU2Header.java
Normal 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];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
99
router/java/src/net/i2p/router/transport/udp/SSU2Util.java
Normal file
99
router/java/src/net/i2p/router/transport/udp/SSU2Util.java
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user