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 };