lib/reticulum/src/identity/key.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const reticulum = @import("../root.zig");
3
4 const Ed25519 = std.crypto.sign.Ed25519;
5 const X25519 = std.crypto.dh.X25519;
6
7 /// 64 bytes, the width of a Reticulum key value, which holds an X25519 half
8 /// followed by an Ed25519 half, following Reticulum@1.5.0
9 /// RNS/Identity.py:59-62.
10 pub const key_bytes: usize = 64;
11 /// 32 bytes, the width of each half of a key value, following Reticulum@1.5.0
12 /// RNS/Identity.py:59-62.
13 pub const half_bytes: usize = 32;
14 pub const KeyBytes = [key_bytes]u8;
15
16 /// The 64 private bytes behind one identity, an X25519 secret followed by an
17 /// Ed25519 seed, following Reticulum@1.5.0
18 /// RNS/Identity.py:59-62,624,727,737-754. The public bytes, the identity hash,
19 /// and every signature derive from these. The value holds the bytes inline, so
20 /// a copy is a second copy of the secret, and the caller that wants them gone
21 /// keeps one instance and erases it.
22 pub const Private = struct {
23 bytes: KeyBytes,
24
25 pub fn fromBytes(bytes: KeyBytes) Private {
26 return .{ .bytes = bytes };
27 }
28
29 pub fn zero(self: *Private) void {
30 std.crypto.secureZero(u8, &self.bytes);
31 }
32
33 pub fn toBytes(self: *const Private) KeyBytes {
34 return self.bytes;
35 }
36
37 pub fn publicBytes(self: *const Private) KeyBytes {
38 const encryption = X25519.recoverPublicKey(self.bytes[0..half_bytes].*) catch
39 unreachable;
40 const signing = Ed25519.KeyPair.generateDeterministic(
41 self.bytes[half_bytes..key_bytes].*,
42 ) catch unreachable;
43 var result: KeyBytes = undefined;
44 result[0..half_bytes].* = encryption;
45 result[half_bytes..key_bytes].* = signing.public_key.toBytes();
46 return result;
47 }
48
49 pub fn public(self: *const Private) Public {
50 return Public.fromBytes(self.publicBytes());
51 }
52
53 pub fn hash(self: *const Private) [reticulum.hash.truncated_bytes]u8 {
54 return reticulum.hash.truncated(&self.publicBytes());
55 }
56
57 pub fn sign(self: *const Private, message: []const u8) [64]u8 {
58 const signing = Ed25519.KeyPair.generateDeterministic(
59 self.bytes[half_bytes..key_bytes].*,
60 ) catch unreachable;
61 const signature = signing.sign(message, null) catch unreachable;
62 return signature.toBytes();
63 }
64 };
65
66 /// The 64 public bytes of one identity, an X25519 public key followed by an
67 /// Ed25519 public key, following Reticulum@1.5.0
68 /// RNS/Identity.py:59-62,730-735,766-778. The value holds the bytes inline, so
69 /// a copy is a second copy, and the caller that wants them gone keeps one
70 /// instance and erases it.
71 pub const Public = struct {
72 bytes: KeyBytes,
73
74 pub fn fromBytes(bytes: KeyBytes) Public {
75 return .{ .bytes = bytes };
76 }
77
78 pub fn zero(self: *Public) void {
79 std.crypto.secureZero(u8, &self.bytes);
80 }
81
82 pub fn toBytes(self: *const Public) KeyBytes {
83 return self.bytes;
84 }
85
86 pub fn borrowed(self: *const Public) BorrowedPublic {
87 return BorrowedPublic.fromBytes(&self.bytes);
88 }
89
90 pub fn hash(self: *const Public) [reticulum.hash.truncated_bytes]u8 {
91 return self.borrowed().hash();
92 }
93
94 pub fn validate(self: *const Public, signature: [64]u8, message: []const u8) bool {
95 return self.borrowed().validate(signature, message);
96 }
97 };
98
99 /// A view of 64 public bytes the caller already holds, which hashes and checks
100 /// signatures where the bytes lie. The view copies nothing, so the bytes it
101 /// points at have to outlive it.
102 pub const BorrowedPublic = struct {
103 bytes: *const KeyBytes,
104
105 pub fn fromBytes(bytes: *const KeyBytes) BorrowedPublic {
106 return .{ .bytes = bytes };
107 }
108
109 pub fn toBytes(self: BorrowedPublic) KeyBytes {
110 return self.bytes.*;
111 }
112
113 /// Returns the first 16 bytes of the SHA-256 digest of the 64 public bytes,
114 /// which names the identity inside a destination name, following
115 /// Reticulum@1.5.0 RNS/Identity.py:784-786.
116 pub fn hash(self: BorrowedPublic) [reticulum.hash.truncated_bytes]u8 {
117 return reticulum.hash.truncated(self.bytes[0..key_bytes]);
118 }
119
120 /// Returns whether an Ed25519 signature over a message checks out under the
121 /// identity's signing half, following Reticulum@1.5.0 RNS/Identity.py:925.
122 /// Public bytes that form no valid Ed25519 point give false.
123 pub fn validate(self: BorrowedPublic, signature: [64]u8, message: []const u8) bool {
124 const signing = Ed25519.PublicKey.fromBytes(
125 self.bytes[half_bytes..key_bytes].*,
126 ) catch return false;
127 const encoded = Ed25519.Signature.fromBytes(signature);
128 encoded.verify(message, signing) catch return false;
129 return true;
130 }
131 };