tiny.reticulum.identity.cipher
Defined in identity.
API (8)
Actions
Public operations.
decrypt: Opens a ciphertext intooutand reports which retained key it took, so a receiver opens what arrived without being told which of its keys the sender chose, following Reticulum@1.5.0 RNS/Identity.py:849-907.encrypt: Seals a payload for a recipient identity and writes the result intoout, returning the bytes written, so a sender uses a key that only the holder of that identity can rebuild.encryptedLength
Types and contracts
Public types and contracts.
Values and defaults
Public values and defaults.
Source
Source: lib/reticulum/src/identity/cipher.zig
zig
const std = @import("std");const reticulum = @import("../root.zig");const crypto = reticulum.crypto;const identity = reticulum.identity;const X25519 = std.crypto.dh.X25519;pub const ephemeral_bytes: u8 = X25519.public_length;pub const derived_bytes: u8 = 64;pub const EncryptError = crypto.token.TokenError || error{InvalidKey};pub const DecryptError = error{ Truncated, RatchetRequired, InvalidToken, OutputTooSmall,};pub const Decrypted = struct { plaintext: []u8, ratchet_id: ?[reticulum.hash.name_bytes]u8,};pub fn encryptedLength(plaintext_length: usize) EncryptError!usize { if (plaintext_length > crypto.token.max_plaintext_length) { return error.PlaintextTooLong; } const token_bytes = try crypto.token.encryptedLength(@intCast(plaintext_length)); return @as(usize, ephemeral_bytes) + token_bytes;}/// Seals a payload for a recipient identity and writes the result into `out`,/// returning the bytes written, so a sender uses a key that only the holder of/// that identity can rebuild. What goes on the wire is the sender's 32-byte/// ephemeral public key followed by a token, following Reticulum@1.5.0/// RNS/Identity.py:804-836. The key both ends arrive at comes from the X25519/// product of the caller's ephemeral secret and either the recipient's rotating/// public key, when the caller passes one, or the recipient's identity key. The/// recipient's identity hash salts that product. The call then expands that/// product into the 64 token bytes through HKDF. The shared secret and the/// derived key are erased before the call returns. The call returns/// `error.PlaintextTooLong` past 65,471 bytes, `error.OutputTooSmall` when/// `out` is shorter than the result, and `error.InvalidKey` when the ephemeral/// secret or the target key forms no valid X25519 point.pub fn encrypt( recipient: *const identity.Public, ratchet_public: ?*const [X25519.public_length]u8, ephemeral_private: *const [X25519.secret_length]u8, iv: [crypto.token.iv_length]u8, plaintext: []const u8, out: []u8,) EncryptError![]u8 { const result_length = try encryptedLength(plaintext.len); if (out.len < result_length) return error.OutputTooSmall; const ephemeral_secret = ephemeral_private.*; const ephemeral_public = X25519.recoverPublicKey(ephemeral_secret) catch return error.InvalidKey; const recipient_bytes = recipient.toBytes(); const target = if (ratchet_public) |public| public.* else recipient_bytes[0..X25519.public_length].*; var shared = X25519.scalarmult(ephemeral_secret, target) catch return error.InvalidKey; defer std.crypto.secureZero(u8, &shared); var derived: [derived_bytes]u8 = undefined; defer std.crypto.secureZero(u8, &derived); const salt = recipient.hash(); _ = crypto.hkdf.derive(derived_bytes, &shared, &salt, null, &derived) catch unreachable; const token = crypto.token.Token.init(&derived) catch unreachable; const body = try token.encrypt(iv, plaintext, out[ephemeral_bytes..result_length]); std.debug.assert(body.len + ephemeral_bytes == result_length); out[0..ephemeral_bytes].* = ephemeral_public; return out[0..result_length];}fn tokenOutputLength(token: []const u8) ?usize { const minimum = crypto.token.overhead + crypto.cbc.block_length; if (token.len < minimum) return null; if (token.len > crypto.token.max_encrypted_length) return null; const padded_length = token.len - crypto.token.overhead; if (padded_length % crypto.cbc.block_length != 0) return null; return padded_length;}fn decryptWithKey( key: *const [X25519.secret_length]u8, peer_public: [X25519.public_length]u8, salt: [reticulum.hash.truncated_bytes]u8, token_bytes: []const u8, out: []u8,) ?[]u8 { var shared = X25519.scalarmult(key.*, peer_public) catch return null; defer std.crypto.secureZero(u8, &shared); var derived: [derived_bytes]u8 = undefined; defer std.crypto.secureZero(u8, &derived); _ = crypto.hkdf.derive(derived_bytes, &shared, &salt, null, &derived) catch return null; const token = crypto.token.Token.init(&derived) catch return null; return token.decrypt(token_bytes, out) catch null;}fn decryptWithRatchet( ratchet: *const identity.Ratchet, peer_public: [X25519.public_length]u8, salt: [reticulum.hash.truncated_bytes]u8, token_bytes: []const u8, out: []u8,) ?[]u8 { var private_bytes = ratchet.toBytes(); defer std.crypto.secureZero(u8, &private_bytes); return decryptWithKey(&private_bytes, peer_public, salt, token_bytes, out);}/// Opens a ciphertext into `out` and reports which retained key it took, so a/// receiver opens what arrived without being told which of its keys the sender/// chose, following Reticulum@1.5.0 RNS/Identity.py:849-907. The call tries/// each retained key in the order the caller gave and then falls back to the/// identity key. Enforcing rotating keys turns that fallback off, so a/// ciphertext no retained key opened returns `error.RatchetRequired`. The call/// returns `error.Truncated` when the ciphertext is as short as the ephemeral/// key or shorter, `error.OutputTooSmall` when `out` is shorter than the/// plaintext, and `error.InvalidToken` when no key opened it. Every retained/// private key the call copies and every key it derives is erased before it/// returns.pub fn decrypt( private: *const identity.Private, ratchets: []const identity.Ratchet, enforce_ratchets: bool, ciphertext: []const u8, out: []u8,) DecryptError!Decrypted { if (ciphertext.len <= ephemeral_bytes) return error.Truncated; const peer_public = ciphertext[0..ephemeral_bytes].*; const token_bytes = ciphertext[ephemeral_bytes..]; if (tokenOutputLength(token_bytes)) |needed| { if (out.len < needed) return error.OutputTooSmall; } const salt = private.hash(); for (ratchets) |*ratchet| { if (decryptWithRatchet(ratchet, peer_public, salt, token_bytes, out)) |plaintext| { return .{ .plaintext = plaintext, .ratchet_id = ratchet.id() }; } } if (enforce_ratchets) return error.RatchetRequired; var private_bytes = private.toBytes(); defer std.crypto.secureZero(u8, &private_bytes); const key = private_bytes[0..X25519.secret_length]; const plaintext = decryptWithKey(key, peer_public, salt, token_bytes, out) orelse return error.InvalidToken; return .{ .plaintext = plaintext, .ratchet_id = null };}Source: lib/reticulum/src/identity/root.zig:54
zig
pub const cipher = @import("cipher.zig");Complete caller list for identity.cipher.decrypt
8 direct callers.
tiny.reticulum.destination.cipher.decrypt[function] atlib/reticulum/src/destination/cipher.zig:127lib.reticulum.src.identity.test.checkCipherVector[function] — private source atlib/reticulum/src/identity/test.zig:119in nearest public ownerlib.reticulum.src.identity.testlib.reticulum.src.identity.test.test_identity_decrypt_falls_back_after_a_wrong_retained_key[function] — test source atlib/reticulum/src/identity/test.zig:210in nearest public ownerlib.reticulum.src.identity.testlib.reticulum.src.identity.test.test_identity_decrypt_rejects_a_32-byte_ciphertext_as_truncated[function] — test source atlib/reticulum/src/identity/test.zig:181in nearest public ownerlib.reticulum.src.identity.testlib.reticulum.src.identity.test.test_identity_decrypt_rejects_one_flipped_ciphertext_byte[function] — test source atlib/reticulum/src/identity/test.zig:193in nearest public ownerlib.reticulum.src.identity.testlib.reticulum.src.identity.test.test_identity_decrypt_requires_a_matching_retained_key_when_enforcement_is_active[function] — test source atlib/reticulum/src/identity/test.zig:170in nearest public ownerlib.reticulum.src.identity.testlib.reticulum.src.properties.cipher.ArbitraryCiphertext.property[function] — private source atlib/reticulum/src/properties/cipher.zig:65in nearest public ownerlib.reticulum.src.properties.cipherlib.reticulum.src.properties.cipher.roundTrip[function] — private source atlib/reticulum/src/properties/cipher.zig:8in nearest public ownerlib.reticulum.src.properties.cipher
Audit
| Definitions | 9 |
|---|---|
| Public names | 9 |
| Members | 6 |
| Version | 26.7.0 |
| Revision | daab053ee433 |