lib/reticulum/src/interface/ifac.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const reticulum = @import("../root.zig");
3
4 /// The 64 bytes that authenticate every frame on one carrier, so a caller holds
5 /// one per closed carrier and passes it to both directions, following
6 /// Reticulum@1.5.0 RNS/Reticulum.py:999-1002. The same bytes both sign the code
7 /// and key the mask.
8 pub const Key = reticulum.identity.KeyBytes;
9 pub const key_bytes: usize = reticulum.identity.key_bytes;
10 pub const min_size_bytes: u7 = 1;
11 pub const max_size_bytes: u7 = 64;
12
13 /// How many bytes of the signature a frame carries as its code, from one byte
14 /// through a whole 64-byte Ed25519 signature, following Reticulum@1.5.0
15 /// RNS/Reticulum.py:152 and Reticulum@1.5.0 RNS/Transport.py:1247. A byte
16 /// outside that range gives null.
17 pub const Size = enum(u7) {
18 bytes_1 = 1,
19 bytes_2 = 2,
20 bytes_3 = 3,
21 bytes_4 = 4,
22 bytes_5 = 5,
23 bytes_6 = 6,
24 bytes_7 = 7,
25 bytes_8 = 8,
26 bytes_9 = 9,
27 bytes_10 = 10,
28 bytes_11 = 11,
29 bytes_12 = 12,
30 bytes_13 = 13,
31 bytes_14 = 14,
32 bytes_15 = 15,
33 bytes_16 = 16,
34 bytes_17 = 17,
35 bytes_18 = 18,
36 bytes_19 = 19,
37 bytes_20 = 20,
38 bytes_21 = 21,
39 bytes_22 = 22,
40 bytes_23 = 23,
41 bytes_24 = 24,
42 bytes_25 = 25,
43 bytes_26 = 26,
44 bytes_27 = 27,
45 bytes_28 = 28,
46 bytes_29 = 29,
47 bytes_30 = 30,
48 bytes_31 = 31,
49 bytes_32 = 32,
50 bytes_33 = 33,
51 bytes_34 = 34,
52 bytes_35 = 35,
53 bytes_36 = 36,
54 bytes_37 = 37,
55 bytes_38 = 38,
56 bytes_39 = 39,
57 bytes_40 = 40,
58 bytes_41 = 41,
59 bytes_42 = 42,
60 bytes_43 = 43,
61 bytes_44 = 44,
62 bytes_45 = 45,
63 bytes_46 = 46,
64 bytes_47 = 47,
65 bytes_48 = 48,
66 bytes_49 = 49,
67 bytes_50 = 50,
68 bytes_51 = 51,
69 bytes_52 = 52,
70 bytes_53 = 53,
71 bytes_54 = 54,
72 bytes_55 = 55,
73 bytes_56 = 56,
74 bytes_57 = 57,
75 bytes_58 = 58,
76 bytes_59 = 59,
77 bytes_60 = 60,
78 bytes_61 = 61,
79 bytes_62 = 62,
80 bytes_63 = 63,
81 bytes_64 = 64,
82
83 pub fn fromByte(value: u8) ?Size {
84 if (value < min_size_bytes) return null;
85 if (value > max_size_bytes) return null;
86 return std.enums.fromInt(Size, @as(u7, @intCast(value)));
87 }
88
89 pub fn byte(self: Size) u7 {
90 return @backingInt(self);
91 }
92 };
93
94 /// 16 bytes, the code size the reference uses unless an operator sets another,
95 /// and the size a caller gets when it states none, following Reticulum@1.5.0
96 /// RNS/Interfaces/Interface.py:96.
97 pub const default_size = Size.bytes_16;
98
99 /// The 32 fixed bytes that salt the derivation of an interface key, for a
100 /// caller reproducing the derivation outside this package, following
101 /// Reticulum@1.5.0 RNS/Reticulum.py:153.
102 pub const salt: [32]u8 = .{
103 0xad, 0xf5, 0x4d, 0x88, 0x2c, 0x9a, 0x9b, 0x80,
104 0x77, 0x1e, 0xb4, 0x99, 0x5d, 0x70, 0x2d, 0x4a,
105 0x3e, 0x73, 0x33, 0x91, 0xb2, 0xa0, 0xf5, 0x3f,
106 0x41, 0x6d, 0x9f, 0x90, 0x7e, 0x55, 0xcf, 0xf8,
107 };
108
109 /// 564 bytes, the longest frame this scheme produces, a 500-byte packet plus a
110 /// 64-byte code, so a caller sizes the buffer it writes an authenticated frame
111 /// into, following Reticulum@1.5.0 RNS/Transport.py:1247-1250.
112 pub const max_frame_bytes: u16 = reticulum.wire.mtu + max_size_bytes;
113
114 pub const DeriveError = error{NoNetwork};
115 pub const ApplyError = error{
116 OutputTooSmall,
117 OverlappingBuffers,
118 PacketTooLarge,
119 PacketTooShort,
120 };
121 pub const StripError = error{
122 InvalidCode,
123 MissingFlag,
124 OutputTooSmall,
125 OverlappingBuffers,
126 PacketTooLarge,
127 Truncated,
128 };
129
130 /// Returns the 64-byte interface key for a network name and a network key, so
131 /// every node on a network derives the same key from that name, following
132 /// Reticulum@1.5.0 RNS/Reticulum.py:989-1002. The key derives from the SHA-256
133 /// digest of each value the caller passed, joined in that order, hashed again,
134 /// then stretched under the fixed salt. The call returns `error.NoNetwork` when
135 /// the caller omits both the name and the key.
136 pub fn derive(netname: ?[]const u8, netkey: ?[]const u8) DeriveError!Key {
137 if (netname == null and netkey == null) return error.NoNetwork;
138 var origin: [reticulum.hash.full_bytes * 2]u8 = undefined;
139 var origin_length: usize = 0;
140 if (netname) |value| {
141 const digest = reticulum.hash.full(value);
142 @memcpy(origin[origin_length..][0..digest.len], &digest);
143 origin_length += digest.len;
144 }
145 if (netkey) |value| {
146 const digest = reticulum.hash.full(value);
147 @memcpy(origin[origin_length..][0..digest.len], &digest);
148 origin_length += digest.len;
149 }
150 std.debug.assert(origin_length >= reticulum.hash.full_bytes);
151 std.debug.assert(origin_length <= origin.len);
152 const origin_hash = reticulum.hash.full(origin[0..origin_length]);
153 var key: Key = undefined;
154 _ = reticulum.crypto.hkdf.derive(key_bytes, &origin_hash, &salt, null, &key) catch
155 unreachable;
156 return key;
157 }
158
159 /// Writes the authenticated frame into `out` and returns it, so a node
160 /// authenticates every frame it puts on a closed carrier, following
161 /// Reticulum@1.5.0 RNS/Transport.py:1244-1276. The code is the tail of an
162 /// Ed25519 signature over the packet, taken under the interface key, and it
163 /// sits right after the frame's first two bytes. Every other byte is combined
164 /// with a mask stretched from the interface key and seeded with that code. The
165 /// frame's first byte goes out with its top bit set, so a receiver knows the
166 /// frame carries a code. The signing copy of the key is erased before the call
167 /// returns. The call returns `error.PacketTooShort` under two bytes,
168 /// `error.PacketTooLarge` past 500 bytes, `error.OutputTooSmall` when `out` is
169 /// shorter than the frame, and `error.OverlappingBuffers` when `out` overlaps
170 /// the packet.
171 pub fn apply(
172 key: *const Key,
173 size: Size,
174 raw: []const u8,
175 out: []u8,
176 ) ApplyError![]u8 {
177 if (raw.len < 2) return error.PacketTooShort;
178 if (raw.len > reticulum.wire.mtu) return error.PacketTooLarge;
179 const code_length: usize = size.byte();
180 std.debug.assert(code_length >= min_size_bytes);
181 std.debug.assert(code_length <= max_size_bytes);
182 const frame_length = std.math.add(usize, raw.len, code_length) catch
183 return error.PacketTooLarge;
184 std.debug.assert(frame_length >= raw.len);
185 std.debug.assert(frame_length <= max_frame_bytes);
186 if (out.len < frame_length) return error.OutputTooSmall;
187 std.debug.assert(out.len >= frame_length);
188 const frame = out[0..frame_length];
189 if (overlaps(raw, frame)) return error.OverlappingBuffers;
190
191 var signer = reticulum.identity.Private.fromBytes(key.*);
192 defer signer.zero();
193 const signature = signer.sign(raw);
194 const code = signature[signature.len - code_length ..];
195 var mask: [max_frame_bytes]u8 = undefined;
196 _ = reticulum.crypto.hkdf.derive(
197 @intCast(frame_length),
198 code,
199 key,
200 null,
201 &mask,
202 ) catch unreachable;
203
204 frame[0] = ((raw[0] | 0x80) ^ mask[0]) | 0x80;
205 frame[1] = raw[1] ^ mask[1];
206 @memcpy(frame[2..][0..code_length], code);
207 var raw_index: usize = 2;
208 while (raw_index < raw.len) : (raw_index += 1) {
209 const frame_index = raw_index + code_length;
210 frame[frame_index] = raw[raw_index] ^ mask[frame_index];
211 }
212 std.debug.assert(raw_index == raw.len);
213 return frame;
214 }
215
216 /// Writes the packet back into `out` and returns it, once the code checks out,
217 /// so a node checks every frame arriving on a closed carrier and recovers the
218 /// packet inside it, following Reticulum@1.5.0 RNS/Transport.py:1648-1687. The
219 /// code the frame carries seeds the same mask, which is undone to recover the
220 /// packet, and the first byte comes back with its top bit cleared. The
221 /// signature is recomputed over the recovered packet and compared against the
222 /// carried code across a fixed 64 bytes, so the comparison takes the same time
223 /// whatever the bytes are. The signing copy of the key is erased before the
224 /// call returns. The call returns `error.Truncated` for an empty frame or a
225 /// frame of at most two bytes plus the code, `error.MissingFlag` when the first
226 /// byte's top bit is clear, `error.PacketTooLarge` past 500 bytes plus the
227 /// code, `error.OutputTooSmall`, `error.OverlappingBuffers`, and
228 /// `error.InvalidCode` when the recomputed signature disagrees.
229 pub fn strip(
230 key: *const Key,
231 size: Size,
232 masked: []const u8,
233 out: []u8,
234 ) StripError![]u8 {
235 if (masked.len == 0) return error.Truncated;
236 if (!hasFlag(masked[0])) return error.MissingFlag;
237 const code_length: usize = size.byte();
238 std.debug.assert(code_length >= min_size_bytes);
239 std.debug.assert(code_length <= max_size_bytes);
240 const prefix_length = 2 + code_length;
241 if (masked.len <= prefix_length) return error.Truncated;
242 if (masked.len > reticulum.wire.mtu + code_length) return error.PacketTooLarge;
243 const raw_length = masked.len - code_length;
244 std.debug.assert(raw_length > 2);
245 std.debug.assert(raw_length <= reticulum.wire.mtu);
246 if (out.len < raw_length) return error.OutputTooSmall;
247 std.debug.assert(out.len >= raw_length);
248 const raw = out[0..raw_length];
249 if (overlaps(masked, raw)) return error.OverlappingBuffers;
250
251 var actual_code: [key_bytes]u8 = @splat(0);
252 const actual_tail = actual_code[actual_code.len - code_length ..];
253 @memcpy(actual_tail, masked[2..][0..code_length]);
254 var mask: [max_frame_bytes]u8 = undefined;
255 _ = reticulum.crypto.hkdf.derive(
256 @intCast(masked.len),
257 actual_tail,
258 key,
259 null,
260 &mask,
261 ) catch unreachable;
262
263 raw[0] = (masked[0] ^ mask[0]) & 0x7f;
264 raw[1] = masked[1] ^ mask[1];
265 var masked_index: usize = prefix_length;
266 while (masked_index < masked.len) : (masked_index += 1) {
267 const raw_index = masked_index - code_length;
268 raw[raw_index] = masked[masked_index] ^ mask[masked_index];
269 }
270 std.debug.assert(masked_index == masked.len);
271
272 var signer = reticulum.identity.Private.fromBytes(key.*);
273 defer signer.zero();
274 const signature = signer.sign(raw);
275 var expected_code: [key_bytes]u8 = @splat(0);
276 const expected_tail = signature[signature.len - code_length ..];
277 @memcpy(expected_code[expected_code.len - code_length ..], expected_tail);
278 if (!equalCode(&actual_code, &expected_code)) {
279 return error.InvalidCode;
280 }
281 return raw;
282 }
283
284 /// Returns whether a frame's first byte has its top bit set, which says the
285 /// frame carries an access code, so a node sorts arriving frames by whether
286 /// they carry a code before trying to check one, following Reticulum@1.5.0
287 /// RNS/Transport.py:1693-1694.
288 pub fn hasFlag(first_byte: u8) bool {
289 return first_byte & 0x80 == 0x80;
290 }
291
292 fn overlaps(input: []const u8, output: []u8) bool {
293 if (input.len == 0) return false;
294 const input_start = @intFromPtr(input.ptr);
295 const output_start = @intFromPtr(output.ptr);
296 const input_end = input_start + input.len;
297 const output_end = output_start + output.len;
298 return input_start < output_end and output_start < input_end;
299 }
300
301 fn equalCode(actual: *const [key_bytes]u8, expected: *const [key_bytes]u8) bool {
302 var difference: u8 = 0;
303 for (actual, expected) |actual_byte, expected_byte| {
304 difference |= actual_byte ^ expected_byte;
305 }
306 return difference == 0;
307 }
308
309 comptime {
310 std.debug.assert(@backingInt(Size.bytes_1) == min_size_bytes);
311 std.debug.assert(@backingInt(Size.bytes_64) == max_size_bytes);
312 }