A Python port of the Invisible Internet Project (I2P)
1"""GarlicCloveBuilder — construct individual garlic cloves.
2
3Ported from net.i2p.router.message components.
4
5Builds CloveConfig objects for data delivery, ACK requests,
6and LeaseSet delivery — the three clove types in a typical
7outbound garlic message.
8"""
9
10from __future__ import annotations
11
12import os
13import struct
14import time
15
16from i2p_data.garlic_config import CloveConfig, GarlicConfig
17from i2p_data.i2np import DeliveryStatusMessage, DataMessage, DatabaseStoreMessage
18
19
20class GarlicCloveBuilder:
21 """Build individual garlic cloves for different delivery purposes."""
22
23 @staticmethod
24 def build_data_clove(
25 dest_hash: bytes,
26 payload: bytes,
27 expiration: int | None = None,
28 ) -> CloveConfig:
29 """Build a DESTINATION-delivery clove wrapping payload in a DataMessage."""
30 if expiration is None:
31 expiration = int(time.time() * 1000) + 60_000
32
33 # Wrap payload in I2NP DataMessage body format
34 data_msg = DataMessage(payload)
35 msg_bytes = data_msg.to_bytes()
36
37 return CloveConfig.for_destination(
38 dest_hash=dest_hash,
39 message_data=msg_bytes,
40 clove_id=int.from_bytes(os.urandom(4), "big"),
41 expiration=expiration,
42 )
43
44 @staticmethod
45 def build_ack_clove(
46 reply_token: int,
47 our_ib_gateway: bytes,
48 our_ib_tunnel_id: int,
49 expiration: int | None = None,
50 ) -> CloveConfig:
51 """Build a TUNNEL-delivery ACK clove (DeliveryStatusMessage).
52
53 The ACK is delivered via our inbound tunnel so the reply
54 confirms the message reached the destination.
55 """
56 if expiration is None:
57 expiration = int(time.time() * 1000) + 60_000
58
59 # DeliveryStatusMessage with the reply token as msg_id
60 ack_msg = DeliveryStatusMessage(reply_token, int(time.time() * 1000))
61 msg_bytes = ack_msg.to_bytes()
62
63 return CloveConfig.for_tunnel(
64 router_hash=our_ib_gateway,
65 tunnel_id=our_ib_tunnel_id,
66 message_data=msg_bytes,
67 clove_id=int.from_bytes(os.urandom(4), "big"),
68 expiration=expiration,
69 )
70
71 @staticmethod
72 def build_leaseset_clove(
73 our_lease_set_bytes: bytes,
74 expiration: int | None = None,
75 ) -> CloveConfig:
76 """Build a LOCAL-delivery clove carrying our LeaseSet.
77
78 The receiver stores our LeaseSet locally so they can
79 route replies back to us.
80 """
81 if expiration is None:
82 expiration = int(time.time() * 1000) + 60_000
83
84 # Wrap in DatabaseStoreMessage (type 1)
85 key = b"\x00" * 32 # placeholder — receiver extracts from LS
86 db_store = DatabaseStoreMessage(key, 1, 0, our_lease_set_bytes)
87 msg_bytes = db_store.to_bytes()
88
89 return CloveConfig.for_local(
90 message_data=msg_bytes,
91 clove_id=int.from_bytes(os.urandom(4), "big"),
92 expiration=expiration,
93 )
94
95 @staticmethod
96 def create_garlic_config(
97 dest_pub_key: bytes,
98 data_clove: CloveConfig,
99 ack_clove: CloveConfig | None = None,
100 ls_clove: CloveConfig | None = None,
101 msg_id: int | None = None,
102 expiration: int | None = None,
103 ) -> GarlicConfig:
104 """Assemble cloves into a GarlicConfig in standard order.
105
106 Order: ACK (if present), LeaseSet (if present), Data.
107 """
108 if msg_id is None:
109 msg_id = int.from_bytes(os.urandom(4), "big")
110 if expiration is None:
111 expiration = int(time.time() * 1000) + 60_000
112
113 config = GarlicConfig(
114 recipient_public_key=dest_pub_key,
115 message_id=msg_id,
116 expiration=expiration,
117 )
118
119 # Standard order: ACK first, then LS, then data
120 if ack_clove is not None:
121 config.add_clove(ack_clove)
122 if ls_clove is not None:
123 config.add_clove(ls_clove)
124 config.add_clove(data_clove)
125
126 return config