lib/reticulum/src/destination/cipher.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const reticulum = @import("../root.zig");
3
4 const crypto = reticulum.crypto;
5 const destination = reticulum.destination;
6 const identity = reticulum.identity;
7
8 /// 383 bytes, the most plaintext one packet to a single-identity destination
9 /// carries, which the reference fixes as
10 /// `floor((464 - 48 - 32) / 16) * 16 - 1`, so a caller sizes the payload it can
11 /// send to one identity, following Reticulum@1.5.0 RNS/Packet.py:106.
12 pub const encrypted_mdu: u16 = 383;
13 /// 464 bytes, the most payload a packet to a plain destination carries, so a
14 /// caller sizes the payload it can send to a plain destination, following
15 /// Reticulum@1.5.0 RNS/Packet.py:110 and Reticulum@1.5.0
16 /// RNS/Reticulum.py:148-155.
17 pub const plain_mdu: u16 = reticulum.wire.mtu - reticulum.wire.header_two_bytes - 1;
18
19 pub const SingleEncrypt = struct {
20 public: *const identity.Public,
21 ratchet_public: ?*const [32]u8,
22 ephemeral_private: *const [32]u8,
23 iv: [crypto.token.iv_length]u8,
24 };
25
26 pub const GroupEncrypt = struct {
27 key: *const [64]u8,
28 iv: [crypto.token.iv_length]u8,
29 };
30
31 pub const EncryptKeys = union(destination.Type) {
32 single: SingleEncrypt,
33 group: GroupEncrypt,
34 plain: void,
35 link: void,
36 };
37
38 pub const SingleDecrypt = struct {
39 private: *const identity.Private,
40 ratchets: []const identity.Ratchet,
41 enforce_ratchets: bool,
42 };
43
44 pub const GroupDecrypt = struct {
45 key: *const [64]u8,
46 };
47
48 pub const DecryptKeys = union(destination.Type) {
49 single: SingleDecrypt,
50 group: GroupDecrypt,
51 plain: void,
52 link: void,
53 };
54
55 pub const EncryptError = identity.cipher.EncryptError || error{Unsupported};
56 pub const DecryptError = identity.cipher.DecryptError || error{Unsupported};
57
58 pub const Encrypted = struct {
59 ciphertext: []u8,
60 ratchet_id: ?[reticulum.hash.name_bytes]u8,
61 };
62
63 pub const Decrypted = struct {
64 plaintext: []u8,
65 ratchet_id: ?[reticulum.hash.name_bytes]u8,
66 };
67
68 fn copy(input: []const u8, out: []u8) error{OutputTooSmall}![]u8 {
69 if (out.len < input.len) return error.OutputTooSmall;
70 @memmove(out[0..input.len], input);
71 return out[0..input.len];
72 }
73
74 /// Seals a payload into `out` by the kind of destination the keys name, so a
75 /// caller seals for whichever kind it is sending to without writing the choice
76 /// out itself, and reports the rotating key it used when it used one, following
77 /// Reticulum@1.5.0 RNS/Destination.py:596-617. A plain destination takes a copy
78 /// of the payload, a single-identity destination takes the ephemeral-key
79 /// exchange, and a group takes a sealed token under the shared 64-byte key.
80 /// Sending to a link returns `error.Unsupported`, because a link seals its
81 /// payloads with the key the two ends already agreed on. The call returns
82 /// `error.OutputTooSmall` when `out` is shorter than the result, and the errors
83 /// the single-identity path raises.
84 pub fn encrypt(keys: EncryptKeys, plaintext: []const u8, out: []u8) EncryptError!Encrypted {
85 return switch (keys) {
86 .plain => .{ .ciphertext = try copy(plaintext, out), .ratchet_id = null },
87 .single => |single| blk: {
88 const ciphertext = try identity.cipher.encrypt(
89 single.public,
90 single.ratchet_public,
91 single.ephemeral_private,
92 single.iv,
93 plaintext,
94 out,
95 );
96 const ratchet_id = if (single.ratchet_public) |public|
97 reticulum.hash.name(public)
98 else
99 null;
100 break :blk .{ .ciphertext = ciphertext, .ratchet_id = ratchet_id };
101 },
102 .group => |group| blk: {
103 const token = crypto.token.Token.init(group.key) catch unreachable;
104 const ciphertext = try token.encrypt(group.iv, plaintext, out);
105 break :blk .{ .ciphertext = ciphertext, .ratchet_id = null };
106 },
107 .link => error.Unsupported,
108 };
109 }
110
111 fn decryptGroup(group: GroupDecrypt, ciphertext: []const u8, out: []u8) DecryptError![]u8 {
112 const token = crypto.token.Token.init(group.key) catch unreachable;
113 return token.decrypt(ciphertext, out) catch |err| switch (err) {
114 error.OutputTooSmall => error.OutputTooSmall,
115 else => error.InvalidToken,
116 };
117 }
118
119 /// Opens a payload into `out` by the kind of destination the keys name, so a
120 /// caller opens an arriving payload by the kind it was addressed to, and
121 /// reports the rotating key that opened it when one did, following
122 /// Reticulum@1.5.0 RNS/Destination.py:622-665. A plain destination takes a copy
123 /// of the payload, a single-identity destination walks its retained keys and
124 /// then its identity key, and a group opens a sealed token under the shared
125 /// 64-byte key. A payload addressed to a link returns `error.Unsupported`. A
126 /// group payload that fails its tag check returns `error.InvalidToken`.
127 pub fn decrypt(keys: DecryptKeys, ciphertext: []const u8, out: []u8) DecryptError!Decrypted {
128 return switch (keys) {
129 .plain => .{ .plaintext = try copy(ciphertext, out), .ratchet_id = null },
130 .single => |single| blk: {
131 const result = try identity.cipher.decrypt(
132 single.private,
133 single.ratchets,
134 single.enforce_ratchets,
135 ciphertext,
136 out,
137 );
138 break :blk .{
139 .plaintext = result.plaintext,
140 .ratchet_id = result.ratchet_id,
141 };
142 },
143 .group => |group| .{
144 .plaintext = try decryptGroup(group, ciphertext, out),
145 .ratchet_id = null,
146 },
147 .link => error.Unsupported,
148 };
149 }
150
151 comptime {
152 std.debug.assert(plain_mdu == 464);
153 }