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 }