lib/reticulum/src/properties/cipher.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const hypothesis = @import("hypothesis");
3 const reticulum = @import("reticulum");
4
5 const identity = reticulum.identity;
6 const seed: u64 = 0x5234_4349_5048_4552;
7
8 fn roundTrip(
9 private: *const identity.Private,
10 ratchet: ?*const identity.Ratchet,
11 ephemeral_private: *const [32]u8,
12 iv: [16]u8,
13 plaintext: []const u8,
14 ) !void {
15 var public = private.public();
16 defer public.zero();
17 var ratchet_public: [32]u8 = undefined;
18 const selected: ?*const [32]u8 = if (ratchet) |value| blk: {
19 ratchet_public = value.publicBytes();
20 break :blk &ratchet_public;
21 } else null;
22 var ciphertext_storage: [480]u8 = undefined;
23 const ciphertext = try identity.cipher.encrypt(
24 &public,
25 selected,
26 ephemeral_private,
27 iv,
28 plaintext,
29 &ciphertext_storage,
30 );
31 const retained: []const identity.Ratchet = if (ratchet) |value| value[0..1] else &.{};
32 var plaintext_storage: [384]u8 = undefined;
33 const decrypted = try identity.cipher.decrypt(
34 private,
35 retained,
36 false,
37 ciphertext,
38 &plaintext_storage,
39 );
40 try std.testing.expectEqualSlices(u8, plaintext, decrypted.plaintext);
41 const expected_id: ?[10]u8 = if (ratchet) |value| value.id() else null;
42 try std.testing.expectEqual(expected_id, decrypted.ratchet_id);
43 }
44
45 const CipherRoundTrip = struct {
46 pub fn property(data: *hypothesis.ConjectureData, _: std.mem.Allocator) !void {
47 const private_input = try data.drawBytes(identity.key_bytes, identity.key_bytes);
48 const ephemeral_input = try data.drawBytes(32, 32);
49 const iv_input = try data.drawBytes(16, 16);
50 const ratchet_input = try data.drawBytes(32, 32);
51 const plaintext_length: usize = @intCast(try data.drawInteger(0, 383, 0));
52 const plaintext = try data.drawBytes(plaintext_length, plaintext_length);
53 var private = identity.Private.fromBytes(private_input[0..identity.key_bytes].*);
54 defer private.zero();
55 var ratchet = identity.Ratchet.fromBytes(ratchet_input[0..32].*);
56 defer ratchet.zero();
57 const ephemeral_private = ephemeral_input[0..32].*;
58 const iv = iv_input[0..16].*;
59 try roundTrip(&private, null, &ephemeral_private, iv, plaintext);
60 try roundTrip(&private, &ratchet, &ephemeral_private, iv, plaintext);
61 }
62 };
63
64 const ArbitraryCiphertext = struct {
65 pub fn property(data: *hypothesis.ConjectureData, _: std.mem.Allocator) !void {
66 const private_input = try data.drawBytes(identity.key_bytes, identity.key_bytes);
67 const ratchet_input = try data.drawBytes(32, 32);
68 const ciphertext = try data.drawBytes(0, 500);
69 const enforce = try data.drawBoolean();
70 var private = identity.Private.fromBytes(private_input[0..identity.key_bytes].*);
71 defer private.zero();
72 var ratchet = identity.Ratchet.fromBytes(ratchet_input[0..32].*);
73 defer ratchet.zero();
74 var plaintext: [500]u8 = undefined;
75 _ = identity.cipher.decrypt(
76 &private,
77 (&ratchet)[0..1],
78 enforce,
79 ciphertext,
80 &plaintext,
81 ) catch return;
82 }
83 };
84
85 test "property: Reticulum@1.5.0 RNS/Identity.py:804-907 cipher round trips" {
86 const settings = hypothesis.Settings.quick()
87 .withSeed(seed)
88 .withDatabase("zig-out/hypothesis-failures/reticulum");
89 try hypothesis.checkNamed(CipherRoundTrip, "reticulum-cipher-round-trip", settings);
90 }
91
92 test "property: Reticulum identity decrypt is total on arbitrary bytes" {
93 const settings = hypothesis.Settings.quick()
94 .withSeed(seed ^ 0xffff_ffff_ffff_ffff)
95 .withDatabase("zig-out/hypothesis-failures/reticulum");
96 try hypothesis.checkNamed(ArbitraryCiphertext, "reticulum-cipher-total", settings);
97 }