tiny.reticulum.identity.ratchet
Defined in identity.
API (31)
Actions
Public operations.
Capacity.deriveRatchet.fromBytes: Takes 32 bytes of the caller's entropy as a retained private key, following Reticulum@1.5.0 RNS/Identity.py:404-408.Ratchet.generateRatchet.id: Returns the 10-byte name hash of this key's public bytes, which names the key a message was addressed to, following Reticulum@1.5.0 RNS/Identity.py:396-397.Ratchet.publicBytes: Returns the X25519 public bytes a destination publishes for this retained key, following Reticulum@1.5.0 RNS/Identity.py:400-401.Ratchet.toBytesRatchet.zeroRing.activateRing.allRing.deinitRing.initRing.latestRing.latestIdRing.restoreRing.rotate: Puts a fresh key at the front, pushes the rest back one slot, and returns whether it rotated, following Reticulum@1.5.0 RNS/Destination.py:206-209,228-243.
Types and contracts
Public types and contracts.
CapacityCapacity.DeriveErrorLimitsRatchet: One retained X25519 private key.Ring: The caller's storage for a destination's retained keys, held newest first.Ring.CapacityRing.InitErrorRing.LimitsRing.RestoreErrorRing.StorageSeconds
Values and defaults
Public values and defaults.
Ring.claimRing.storage_alignmentRing.work_limitsreference_interval: 1,800 seconds, the rotation interval the reference gives a destination by default, following Reticulum@1.5.0 RNS/Destination.py:90.reference_retained_max: 512, the number of retained keys the reference gives a destination by default, following Reticulum@1.5.0 RNS/Destination.py:85.
Source
Source: lib/reticulum/src/identity/ratchet.zig
zig
const alloc_phase = @import("alloc_phase");const std = @import("std");const reticulum = @import("../root.zig");const capacity = alloc_phase.capacity;const X25519 = std.crypto.dh.X25519;pub const Seconds = u64;/// 512, the number of retained keys the reference gives a destination by/// default, following Reticulum@1.5.0 RNS/Destination.py:85.pub const reference_retained_max: u16 = 512;/// 1,800 seconds, the rotation interval the reference gives a destination by/// default, following Reticulum@1.5.0 RNS/Destination.py:90.pub const reference_interval: Seconds = 30 * 60;/// One retained X25519 private key. A destination publishes the public half in/// its announcements and holds the key afterward, so a message a sender/// addressed to a key it has since retired still opens.pub const Ratchet = struct { bytes: [X25519.secret_length]u8, /// Takes 32 bytes of the caller's entropy as a retained private key, /// following Reticulum@1.5.0 RNS/Identity.py:404-408. pub fn fromBytes(bytes: [X25519.secret_length]u8) Ratchet { return .{ .bytes = bytes }; } pub fn generate(entropy: [X25519.secret_length]u8) Ratchet { return fromBytes(entropy); } pub fn toBytes(self: *const Ratchet) [X25519.secret_length]u8 { return self.bytes; } /// Returns the X25519 public bytes a destination publishes for this /// retained key, following Reticulum@1.5.0 RNS/Identity.py:400-401. pub fn publicBytes(self: *const Ratchet) [X25519.public_length]u8 { return X25519.recoverPublicKey(self.bytes) catch unreachable; } /// Returns the 10-byte name hash of this key's public bytes, which names /// the key a message was addressed to, following Reticulum@1.5.0 /// RNS/Identity.py:396-397. pub fn id(self: *const Ratchet) [reticulum.hash.name_bytes]u8 { const public = self.publicBytes(); return reticulum.hash.name(&public); } pub fn zero(self: *Ratchet) void { std.crypto.secureZero(u8, &self.bytes); }};pub const Limits = struct { retained_max: u16,};pub const Capacity = struct { retained_max: u16, storage_bytes: usize, pub const DeriveError = error{ InvalidLimit, CapacityOverflow, }; pub fn derive(limits: Limits) DeriveError!Capacity { if (limits.retained_max == 0) return error.InvalidLimit; const retained: usize = limits.retained_max; const storage_bytes = capacity.mul(usize, retained, @sizeOf(Ratchet)) catch return error.CapacityOverflow; return .{ .retained_max = limits.retained_max, .storage_bytes = storage_bytes, }; }};const RingLimits = Limits;const RingCapacity = Capacity;/// The caller's storage for a destination's retained keys, held newest first./// The caller fixes how many keys the ring holds, and the storage it hands over/// has to reach the byte count derived from that number. Taking the storage/// zeroes it, and handing it back zeroes it again. Restoring more keys than the/// ring holds returns `error.TooManyRatchets`.pub const Ring = struct { phase: capacity.Phase, capacity: RingCapacity, storage: Storage, count: u16, latest_time: ?Seconds, pub const storage_alignment: usize = @alignOf(Ratchet); pub const Storage = []align(storage_alignment) u8; pub const Limits: type = RingLimits; pub const Capacity: type = RingCapacity; pub const InitError = RingCapacity.DeriveError || error{StorageTooShort}; pub const RestoreError = error{TooManyRatchets}; pub const work_limits: capacity.WorkLimits = .{ .transition_steps_max = 1, .cleanup_steps_per_call_max = 0, .cleanup_calls_at_capacity_max = 0, }; pub const claim: capacity.Declaration = .{ .source = .{ .id = "reticulum.ratchet_ring", .kind = .phase_static, .limit_source = .caller, .storage = .{ .covered = &.{.{ .id = "retained_private_keys", .lifetime = .transferred, .detail = "caller storage for the bounded newest-first private key ring", }}, .excluded = &.{ "ring count and latest wall-clock seconds", "caller-owned restore slices and returned storage", }, }, .capacity = .{ .inputs = &.{capacity.bindInput(RingLimits, "retained_max", "retained_max")}, .type_selectors = &.{capacity.bindType(Ratchet, "ratchet")}, .nodes = &.{ .{ .input = 0 }, .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 0 }, } }, }, .assertions = &.{.{ .scope = .closure_total, .measure = .retained, .relation = .exact, .expression = 1, }}, }, .overload = .{ .kind = .not_applicable, .detail = "rotation replaces the oldest slot inside the exact window", }, .risks = .{ .transitive = .{ .status = .witnessed, .detail = "the corpus checks fixed-value X25519 derivation and key identifiers", }, .foreign = .{ .status = .excluded, .detail = "ring operations cross no foreign or operating-system boundary", }, }, .work = .{ .equation = "rotation copy steps <= retained_max" }, .obligations = &.{ .{ .key = "reticulum_ratchet_ring_capacity", .role = .capacity_model }, .{ .key = "reticulum_ratchet_ring_transitive", .role = .transitive_risk }, .{ .key = "reticulum_ratchet_ring_work", .role = .work_bound }, }, }, .bindings = .{ .owner = @This(), .seal = .{ .family = capacity.selector(@This().activate), .premise = .{ .class = .checked_semantic_fact, .authority = .checker }, }, .teardown = .{ .family = capacity.selector(@This().deinit), .premise = .{ .class = .checked_semantic_fact, .authority = .checker }, }, }, }; pub fn init(storage: Storage, limits: RingLimits) InitError!Ring { const derived = try RingCapacity.derive(limits); if (storage.len < derived.storage_bytes) return error.StorageTooShort; const owned = storage[0..derived.storage_bytes]; std.crypto.secureZero(u8, owned); return .{ .phase = .initialization, .capacity = derived, .storage = owned, .count = 0, .latest_time = null, }; } pub fn activate(self: *Ring) void { std.debug.assert(self.phase == .initialization); std.debug.assert(self.storage.len == self.capacity.storage_bytes); self.phase = .steady; } pub fn deinit(self: *Ring) Storage { std.debug.assert(self.phase == .steady); std.crypto.secureZero(u8, self.storage); self.phase = .teardown; const owned = self.storage; self.* = undefined; return owned; } fn slots(self: *const Ring) []const Ratchet { std.debug.assert(self.phase == .steady); return std.mem.bytesAsSlice(Ratchet, self.storage); } fn slotsMut(self: *Ring) []Ratchet { std.debug.assert(self.phase == .steady); return std.mem.bytesAsSlice(Ratchet, self.storage); } fn provisionedSlotsMut(self: *Ring) []Ratchet { std.debug.assert(self.phase != .teardown); return std.mem.bytesAsSlice(Ratchet, self.storage); } pub fn latest(self: *const Ring) ?*const Ratchet { if (self.count == 0) return null; return &self.slots()[0]; } pub fn latestId(self: *const Ring) ?[reticulum.hash.name_bytes]u8 { const value = self.latest() orelse return null; return value.id(); } pub fn all(self: *const Ring) []const Ratchet { const count: usize = self.count; return self.slots()[0..count]; } pub fn restore( self: *Ring, ratchets: []const Ratchet, latest_time: Seconds, ) RestoreError!void { const retained: usize = self.capacity.retained_max; if (ratchets.len > retained) return error.TooManyRatchets; const owned = self.provisionedSlotsMut(); @memmove(owned[0..ratchets.len], ratchets); std.crypto.secureZero(u8, std.mem.sliceAsBytes(owned[ratchets.len..])); self.count = @intCast(ratchets.len); self.latest_time = if (ratchets.len == 0) null else latest_time; } /// Puts a fresh key at the front, pushes the rest back one slot, and /// returns whether it rotated, following Reticulum@1.5.0 /// RNS/Destination.py:206-209,228-243. A ring holding no key yet rotates, /// and so does one whose newest key is older than the interval the caller /// passed. Once the ring is full the oldest key falls off the back. pub fn rotate(self: *Ring, now: Seconds, interval: Seconds, fresh: Ratchet) bool { const should_rotate = if (self.count == 0) true else if (self.latest_time) |latest_time| now > (std.math.add(Seconds, latest_time, interval) catch std.math.maxInt(Seconds)) else true; if (!should_rotate) return false; const owned = self.slotsMut(); const retained: usize = self.capacity.retained_max; var index = @min(@as(usize, self.count), retained - 1); while (index > 0) : (index -= 1) owned[index] = owned[index - 1]; owned[0] = fresh; if (self.count < self.capacity.retained_max) self.count += 1; self.latest_time = now; return true; }};comptime { alloc_phase.capacity.requireProvisionedExactOwnerShape(Ring);}Source: lib/reticulum/src/identity/root.zig:55
zig
pub const ratchet = ratchet_module;Audit
| Definitions | 32 |
|---|---|
| Public names | 57 |
| Members | 12 |
| Version | 26.7.0 |
| Revision | daab053ee433 |