A Python port of the Invisible Internet Project (I2P)
at main 236 lines 8.6 kB view raw
1"""I2NP message wire format serialization/deserialization. 2 3Provides functional encode/decode for I2NP message headers (short and standard) 4and individual message type payloads (DatabaseStore, DatabaseLookup, 5DatabaseSearchReply, DeliveryStatus). 6 7All multi-byte fields are big-endian (network byte order) per the I2P spec. 8""" 9 10import hashlib 11import hmac as _hmac 12import struct 13 14# I2NP message type constants (from I2P specification) 15MSG_TYPE_DATABASE_STORE = 1 16MSG_TYPE_DATABASE_LOOKUP = 2 17MSG_TYPE_DATABASE_SEARCH_REPLY = 3 18MSG_TYPE_DELIVERY_STATUS = 10 19MSG_TYPE_GARLIC = 11 20MSG_TYPE_TUNNEL_DATA = 18 21MSG_TYPE_TUNNEL_GATEWAY = 19 22MSG_TYPE_DATA = 20 23MSG_TYPE_TUNNEL_BUILD = 21 24MSG_TYPE_TUNNEL_BUILD_REPLY = 22 25MSG_TYPE_VARIABLE_TUNNEL_BUILD = 23 26MSG_TYPE_VARIABLE_TUNNEL_BUILD_REPLY = 24 27 28 29# --------------------------------------------------------------------------- 30# Short header (used inside NTCP2 I2NP blocks) 31# Format: type(1) + msg_id(4 BE) + expiration_seconds(4 BE) + size(2 BE) + payload 32# Total header: 11 bytes 33# --------------------------------------------------------------------------- 34 35def encode_i2np_short(msg_type: int, msg_id: int, expiration: int, payload: bytes) -> bytes: 36 """Encode I2NP message with short header (for NTCP2 blocks). 37 38 Format: type(1) + msg_id(4 BE) + expiration_seconds(4 BE signed) + size(2 BE) + payload 39 """ 40 header = struct.pack("!BIiH", msg_type, msg_id, expiration, len(payload)) 41 return header + payload 42 43 44def decode_i2np_short(data: bytes) -> tuple[int, int, int, bytes]: 45 """Decode I2NP message with short header. 46 47 Returns: (msg_type, msg_id, expiration_seconds, payload) 48 """ 49 msg_type, msg_id, expiration, size = struct.unpack_from("!BIiH", data) 50 payload = data[11:11 + size] 51 return msg_type, msg_id, expiration, payload 52 53 54# --------------------------------------------------------------------------- 55# Standard header (used in tunnel messages) 56# Format: type(1) + msg_id(4 BE) + expiration_ms(8 BE) + size(2 BE) + checksum(1) + payload 57# Checksum = first byte of SHA-256(payload) 58# Total header: 16 bytes 59# --------------------------------------------------------------------------- 60 61def encode_i2np_standard(msg_type: int, msg_id: int, expiration_ms: int, payload: bytes) -> bytes: 62 """Encode I2NP message with standard header (for tunnel messages). 63 64 Format: type(1) + msg_id(4 BE) + expiration_ms(8 BE) + size(2 BE) + checksum(1) + payload 65 Checksum = first byte of SHA-256(payload). 66 """ 67 checksum = hashlib.sha256(payload).digest()[0] 68 header = struct.pack("!BIQHB", msg_type, msg_id, expiration_ms, len(payload), checksum) 69 return header + payload 70 71 72def decode_i2np_standard(data: bytes) -> tuple[int, int, int, bytes]: 73 """Decode I2NP message with standard header. 74 75 Returns: (msg_type, msg_id, expiration_ms, payload) 76 Raises ValueError if checksum doesn't match. 77 """ 78 msg_type, msg_id, expiration_ms, size, checksum = struct.unpack_from("!BIQHB", data) 79 payload = data[16:16 + size] 80 expected_checksum = hashlib.sha256(payload).digest()[0] 81 if not _hmac.compare_digest(bytes([checksum]), bytes([expected_checksum])): 82 raise ValueError(f"Checksum mismatch: got {checksum}, expected {expected_checksum}") 83 return msg_type, msg_id, expiration_ms, payload 84 85 86# --------------------------------------------------------------------------- 87# DatabaseStore (type 1) 88# Payload: key(32) + type_byte(1, 0=RI 1=LS) + reply_token(4) + 89# [if token!=0: reply_gateway(32) + reply_tunnel(4)] + data 90# --------------------------------------------------------------------------- 91 92def encode_database_store(key: bytes, store_type: int, data: bytes, 93 reply_token: int = 0, reply_gateway: bytes = b"", 94 reply_tunnel_id: int = 0) -> bytes: 95 """Encode DatabaseStore payload. 96 97 key: 32-byte hash of the item being stored 98 store_type: 0 = RouterInfo, 1 = LeaseSet 99 data: the RouterInfo or LeaseSet bytes 100 reply_token: if non-zero, request delivery acknowledgment 101 reply_gateway: 32-byte hash (required if reply_token != 0) 102 reply_tunnel_id: 4-byte tunnel ID (required if reply_token != 0) 103 """ 104 parts = [key, struct.pack("!BI", store_type, reply_token)] 105 if reply_token != 0: 106 parts.append(reply_gateway) 107 parts.append(struct.pack("!I", reply_tunnel_id)) 108 parts.append(data) 109 return b"".join(parts) 110 111 112def decode_database_store(payload: bytes) -> dict: 113 """Decode DatabaseStore payload. 114 115 Returns dict with: key, store_type, reply_token, 116 reply_gateway (if token!=0), reply_tunnel_id (if token!=0), data. 117 """ 118 key = payload[:32] 119 store_type = payload[32] 120 reply_token = struct.unpack("!I", payload[33:37])[0] 121 offset = 37 122 result = { 123 "key": key, 124 "store_type": store_type, 125 "reply_token": reply_token, 126 } 127 if reply_token != 0: 128 result["reply_gateway"] = payload[offset:offset + 32] 129 offset += 32 130 result["reply_tunnel_id"] = struct.unpack("!I", payload[offset:offset + 4])[0] 131 offset += 4 132 result["data"] = payload[offset:] 133 return result 134 135 136# --------------------------------------------------------------------------- 137# DatabaseLookup (type 2) 138# Payload: key(32) + from_hash(32) + flags(1) + [reply_tunnel_id(4) if flag bit 0] 139# + size(1) + exclude_list(size * 32) 140# --------------------------------------------------------------------------- 141 142def encode_database_lookup(key: bytes, from_hash: bytes, flags: int = 0, 143 reply_tunnel_id: int = 0, 144 exclude_list: list[bytes] | None = None) -> bytes: 145 """Encode DatabaseLookup payload.""" 146 if exclude_list is None: 147 exclude_list = [] 148 parts = [key, from_hash, struct.pack("!B", flags)] 149 if flags & 0x01: 150 parts.append(struct.pack("!I", reply_tunnel_id)) 151 parts.append(struct.pack("!B", len(exclude_list))) 152 for ex in exclude_list: 153 parts.append(ex) 154 return b"".join(parts) 155 156 157def decode_database_lookup(payload: bytes) -> dict: 158 """Decode DatabaseLookup payload. 159 160 Returns dict with: key, from_hash, flags, reply_tunnel_id, exclude_list. 161 """ 162 key = payload[:32] 163 from_hash = payload[32:64] 164 flags = payload[64] 165 offset = 65 166 reply_tunnel_id = 0 167 if flags & 0x01: 168 reply_tunnel_id = struct.unpack("!I", payload[offset:offset + 4])[0] 169 offset += 4 170 num_excludes = payload[offset] 171 offset += 1 172 exclude_list = [] 173 for _ in range(num_excludes): 174 exclude_list.append(payload[offset:offset + 32]) 175 offset += 32 176 return { 177 "key": key, 178 "from_hash": from_hash, 179 "flags": flags, 180 "reply_tunnel_id": reply_tunnel_id, 181 "exclude_list": exclude_list, 182 } 183 184 185# --------------------------------------------------------------------------- 186# DatabaseSearchReply (type 3) 187# Payload: key(32) + num_peers(1) + [peer_hashes(32 each)] + from_hash(32) 188# --------------------------------------------------------------------------- 189 190def encode_database_search_reply(key: bytes, peer_hashes: list[bytes], 191 from_hash: bytes) -> bytes: 192 """Encode DatabaseSearchReply payload.""" 193 parts = [key, struct.pack("!B", len(peer_hashes))] 194 for ph in peer_hashes: 195 parts.append(ph) 196 parts.append(from_hash) 197 return b"".join(parts) 198 199 200def decode_database_search_reply(payload: bytes) -> dict: 201 """Decode DatabaseSearchReply payload. 202 203 Returns dict with: key, peer_hashes, from_hash. 204 """ 205 key = payload[:32] 206 num_peers = payload[32] 207 offset = 33 208 peer_hashes = [] 209 for _ in range(num_peers): 210 peer_hashes.append(payload[offset:offset + 32]) 211 offset += 32 212 from_hash = payload[offset:offset + 32] 213 return { 214 "key": key, 215 "peer_hashes": peer_hashes, 216 "from_hash": from_hash, 217 } 218 219 220# --------------------------------------------------------------------------- 221# DeliveryStatus (type 10) 222# Payload: msg_id(4 BE) + timestamp(8 BE) 223# --------------------------------------------------------------------------- 224 225def encode_delivery_status(msg_id: int, timestamp: int) -> bytes: 226 """Encode DeliveryStatus payload: msg_id(4 BE) + timestamp(8 BE).""" 227 return struct.pack("!IQ", msg_id, timestamp) 228 229 230def decode_delivery_status(payload: bytes) -> dict: 231 """Decode DeliveryStatus payload. 232 233 Returns dict with: msg_id, timestamp. 234 """ 235 msg_id, timestamp = struct.unpack("!IQ", payload[:12]) 236 return {"msg_id": msg_id, "timestamp": timestamp}