A Python port of the Invisible Internet Project (I2P)
1"""NTCP2 wire-format frame encryption.
2
3Implements the NTCP2 wire format where each frame uses TWO AEAD
4encrypt operations with the CipherState:
5
61. The frame length (2 bytes, big-endian) is encrypted first,
7 producing 18 bytes (2 plaintext + 16 AEAD tag).
82. The frame payload (type + length + data) is encrypted next,
9 producing N + 16 bytes (N plaintext + 16 AEAD tag).
10
11Each operation consumes one nonce from the CipherState, so each
12frame advances the nonce counter by 2.
13"""
14
15import struct
16
17from i2p_crypto.noise import CipherState
18from i2p_transport.ntcp2 import NTCP2Frame
19
20
21class NTCP2WireCodec:
22 """Encrypts and decrypts NTCP2 frames using the two-operation wire format."""
23
24 def encrypt_frame(self, cipher: CipherState, frame: NTCP2Frame) -> bytes:
25 """Encrypt an NTCP2Frame into wire-format bytes.
26
27 Args:
28 cipher: CipherState with key set (post-handshake).
29 frame: The frame to encrypt.
30
31 Returns:
32 Wire bytes: encrypted_length (18) + encrypted_payload (N + 16).
33 """
34 frame_bytes = frame.to_bytes()
35
36 # Operation 1: encrypt the 2-byte frame length
37 length_plaintext = struct.pack("!H", len(frame_bytes))
38 encrypted_length = cipher.encrypt_with_ad(b"", length_plaintext)
39
40 # Operation 2: encrypt the frame bytes
41 encrypted_payload = cipher.encrypt_with_ad(b"", frame_bytes)
42
43 return encrypted_length + encrypted_payload
44
45 def decrypt_frame_length(self, cipher: CipherState, encrypted_length: bytes) -> int:
46 """Decrypt the 18-byte encrypted length field.
47
48 Args:
49 cipher: CipherState with matching key/nonce.
50 encrypted_length: 18 bytes (2 plaintext + 16 AEAD tag).
51
52 Returns:
53 The frame length as an integer.
54 """
55 length_bytes = cipher.decrypt_with_ad(b"", encrypted_length)
56 return struct.unpack("!H", length_bytes)[0]
57
58 def decrypt_frame_payload(self, cipher: CipherState, encrypted_payload: bytes) -> NTCP2Frame:
59 """Decrypt the encrypted payload and parse it as an NTCP2Frame.
60
61 Args:
62 cipher: CipherState with matching key/nonce (after length decrypt).
63 encrypted_payload: The encrypted frame bytes (N + 16 bytes).
64
65 Returns:
66 The decrypted NTCP2Frame.
67 """
68 frame_bytes = cipher.decrypt_with_ad(b"", encrypted_payload)
69 return NTCP2Frame.from_bytes(frame_bytes)
70
71 def encrypt_and_get_wire_bytes(self, cipher: CipherState, frame: NTCP2Frame) -> bytes:
72 """Convenience method: same as encrypt_frame.
73
74 Args:
75 cipher: CipherState with key set.
76 frame: The frame to encrypt.
77
78 Returns:
79 Full wire bytes for transmission.
80 """
81 return self.encrypt_frame(cipher, frame)