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 }