Remove Gimli and Xoodoo from the standard library (#14928)

These are great permutations, and there's nothing wrong with them
from a practical security perspective.

However, both were competing in the NIST lightweight crypto
competition.

Gimli didn't pass the 3rd selection round, and is not much used
in the wild besides Zig and libhydrogen. It will never be
standardized and is unlikely to get more traction in the future.

Xoodyak, that Xoodoo is the permutation of, was a finalist.

It has a lot of advantages and *might* be standardized without NIST.
But this is too early to tell, and too risky to commit to it
in a standard library.

For lightweight crypto, Ascon is the one that we know NIST will
standardize and that we can safely rely on from a usage perspective.

Switch to a traditional ChaCha-based CSPRNG, with an Ascon-based one
as an option for constrained systems.

Add a RNG benchmark by the way.

Gimli and Xoodoo served us well. Their code will be maintained,
but outside the standard library.
This commit is contained in:
Frank Denis 2023-03-21 05:54:10 +01:00 committed by GitHub
parent bc0f246911
commit dff4bbfd24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 443 additions and 786 deletions

View File

@ -17,8 +17,6 @@ pub const aead = struct {
pub const Aes256Ocb = @import("crypto/aes_ocb.zig").Aes256Ocb; pub const Aes256Ocb = @import("crypto/aes_ocb.zig").Aes256Ocb;
}; };
pub const Gimli = @import("crypto/gimli.zig").Aead;
pub const chacha_poly = struct { pub const chacha_poly = struct {
pub const ChaCha20Poly1305 = @import("crypto/chacha20.zig").ChaCha20Poly1305; pub const ChaCha20Poly1305 = @import("crypto/chacha20.zig").ChaCha20Poly1305;
pub const ChaCha12Poly1305 = @import("crypto/chacha20.zig").ChaCha12Poly1305; pub const ChaCha12Poly1305 = @import("crypto/chacha20.zig").ChaCha12Poly1305;
@ -52,8 +50,6 @@ pub const core = struct {
pub const keccak = @import("crypto/keccak_p.zig"); pub const keccak = @import("crypto/keccak_p.zig");
pub const Ascon = @import("crypto/ascon.zig").State; pub const Ascon = @import("crypto/ascon.zig").State;
pub const Gimli = @import("crypto/gimli.zig").State;
pub const Xoodoo = @import("crypto/xoodoo.zig").State;
/// Modes are generic compositions to construct encryption/decryption functions from block ciphers and permutations. /// Modes are generic compositions to construct encryption/decryption functions from block ciphers and permutations.
/// ///
@ -87,7 +83,6 @@ pub const ecc = struct {
pub const hash = struct { pub const hash = struct {
pub const blake2 = @import("crypto/blake2.zig"); pub const blake2 = @import("crypto/blake2.zig");
pub const Blake3 = @import("crypto/blake3.zig").Blake3; pub const Blake3 = @import("crypto/blake3.zig").Blake3;
pub const Gimli = @import("crypto/gimli.zig").Hash;
pub const Md5 = @import("crypto/md5.zig").Md5; pub const Md5 = @import("crypto/md5.zig").Md5;
pub const Sha1 = @import("crypto/sha1.zig").Sha1; pub const Sha1 = @import("crypto/sha1.zig").Sha1;
pub const sha2 = @import("crypto/sha2.zig"); pub const sha2 = @import("crypto/sha2.zig");
@ -221,8 +216,6 @@ test {
_ = aead.aes_ocb.Aes128Ocb; _ = aead.aes_ocb.Aes128Ocb;
_ = aead.aes_ocb.Aes256Ocb; _ = aead.aes_ocb.Aes256Ocb;
_ = aead.Gimli;
_ = aead.chacha_poly.ChaCha20Poly1305; _ = aead.chacha_poly.ChaCha20Poly1305;
_ = aead.chacha_poly.ChaCha12Poly1305; _ = aead.chacha_poly.ChaCha12Poly1305;
_ = aead.chacha_poly.ChaCha8Poly1305; _ = aead.chacha_poly.ChaCha8Poly1305;
@ -239,8 +232,6 @@ test {
_ = core.aes; _ = core.aes;
_ = core.Ascon; _ = core.Ascon;
_ = core.Gimli;
_ = core.Xoodoo;
_ = core.modes; _ = core.modes;
_ = dh.X25519; _ = dh.X25519;
@ -256,7 +247,6 @@ test {
_ = hash.blake2; _ = hash.blake2;
_ = hash.Blake3; _ = hash.Blake3;
_ = hash.Gimli;
_ = hash.Md5; _ = hash.Md5;
_ = hash.Sha1; _ = hash.Sha1;
_ = hash.sha2; _ = hash.sha2;
@ -334,7 +324,6 @@ test "issue #4532: no index out of bounds" {
hash.blake2.Blake2b256, hash.blake2.Blake2b256,
hash.blake2.Blake2b384, hash.blake2.Blake2b384,
hash.blake2.Blake2b512, hash.blake2.Blake2b512,
hash.Gimli,
}; };
inline for (types) |Hasher| { inline for (types) |Hasher| {

View File

@ -168,6 +168,17 @@ pub fn State(comptime endian: builtin.Endian) type {
state.permuteR(12); state.permuteR(12);
} }
/// Apply a permutation to the state and prevent backtracking.
/// The rate is expressed in bytes and must be a multiple of the word size (8).
pub inline fn permuteRatchet(state: *Self, comptime rounds: u4, comptime rate: u6) void {
const capacity = block_bytes - rate;
debug.assert(capacity > 0 and capacity % 8 == 0); // capacity must be a multiple of 64 bits
var mask: [capacity / 8]u64 = undefined;
inline for (&mask, state.st[state.st.len - mask.len ..]) |*m, x| m.* = x;
state.permuteR(rounds);
inline for (mask, state.st[state.st.len - mask.len ..]) |m, *x| x.* ^= m;
}
// Core Ascon permutation. // Core Ascon permutation.
inline fn round(state: *Self, rk: u64) void { inline fn round(state: *Self, rk: u64) void {
const x = &state.st; const x = &state.st;

View File

@ -29,7 +29,6 @@ const hashes = [_]Crypto{
Crypto{ .ty = crypto.hash.sha3.Shake256, .name = "shake-256" }, Crypto{ .ty = crypto.hash.sha3.Shake256, .name = "shake-256" },
Crypto{ .ty = crypto.hash.sha3.TurboShake128(null), .name = "turboshake-128" }, Crypto{ .ty = crypto.hash.sha3.TurboShake128(null), .name = "turboshake-128" },
Crypto{ .ty = crypto.hash.sha3.TurboShake256(null), .name = "turboshake-256" }, Crypto{ .ty = crypto.hash.sha3.TurboShake256(null), .name = "turboshake-256" },
Crypto{ .ty = crypto.hash.Gimli, .name = "gimli-hash" },
Crypto{ .ty = crypto.hash.blake2.Blake2s256, .name = "blake2s" }, Crypto{ .ty = crypto.hash.blake2.Blake2s256, .name = "blake2s" },
Crypto{ .ty = crypto.hash.blake2.Blake2b512, .name = "blake2b" }, Crypto{ .ty = crypto.hash.blake2.Blake2b512, .name = "blake2b" },
Crypto{ .ty = crypto.hash.Blake3, .name = "blake3" }, Crypto{ .ty = crypto.hash.Blake3, .name = "blake3" },
@ -274,7 +273,6 @@ const aeads = [_]Crypto{
Crypto{ .ty = crypto.aead.chacha_poly.XChaCha20Poly1305, .name = "xchacha20Poly1305" }, Crypto{ .ty = crypto.aead.chacha_poly.XChaCha20Poly1305, .name = "xchacha20Poly1305" },
Crypto{ .ty = crypto.aead.chacha_poly.XChaCha8Poly1305, .name = "xchacha8Poly1305" }, Crypto{ .ty = crypto.aead.chacha_poly.XChaCha8Poly1305, .name = "xchacha8Poly1305" },
Crypto{ .ty = crypto.aead.salsa_poly.XSalsa20Poly1305, .name = "xsalsa20Poly1305" }, Crypto{ .ty = crypto.aead.salsa_poly.XSalsa20Poly1305, .name = "xsalsa20Poly1305" },
Crypto{ .ty = crypto.aead.Gimli, .name = "gimli-aead" },
Crypto{ .ty = crypto.aead.aegis.Aegis128L, .name = "aegis-128l" }, Crypto{ .ty = crypto.aead.aegis.Aegis128L, .name = "aegis-128l" },
Crypto{ .ty = crypto.aead.aegis.Aegis256, .name = "aegis-256" }, Crypto{ .ty = crypto.aead.aegis.Aegis256, .name = "aegis-256" },
Crypto{ .ty = crypto.aead.aes_gcm.Aes128Gcm, .name = "aes128-gcm" }, Crypto{ .ty = crypto.aead.aes_gcm.Aes128Gcm, .name = "aes128-gcm" },

View File

@ -195,6 +195,26 @@ fn ChaChaVecImpl(comptime rounds_nb: usize) type {
} }
} }
fn chacha20Stream(out: []u8, key: [8]u32, counter: [4]u32) void {
var ctx = initContext(key, counter);
var x: BlockVec = undefined;
var i: usize = 0;
while (i + 64 <= out.len) : (i += 64) {
chacha20Core(x[0..], ctx);
contextFeedback(&x, ctx);
hashToBytes(out[i..][0..64], x);
ctx[3][0] += 1;
}
if (i < out.len) {
chacha20Core(x[0..], ctx);
contextFeedback(&x, ctx);
var buf: [64]u8 = undefined;
hashToBytes(buf[0..], x);
mem.copy(u8, out[i..], buf[0 .. out.len - i]);
}
}
fn hchacha20(input: [16]u8, key: [32]u8) [32]u8 { fn hchacha20(input: [16]u8, key: [32]u8) [32]u8 {
var c: [4]u32 = undefined; var c: [4]u32 = undefined;
for (c, 0..) |_, i| { for (c, 0..) |_, i| {
@ -336,6 +356,26 @@ fn ChaChaNonVecImpl(comptime rounds_nb: usize) type {
} }
} }
fn chacha20Stream(out: []u8, key: [8]u32, counter: [4]u32) void {
var ctx = initContext(key, counter);
var x: BlockVec = undefined;
var i: usize = 0;
while (i + 64 <= out.len) : (i += 64) {
chacha20Core(x[0..], ctx);
contextFeedback(&x, ctx);
hashToBytes(out[i..][0..64], x);
ctx[12] += 1;
}
if (i < out.len) {
chacha20Core(x[0..], ctx);
contextFeedback(&x, ctx);
var buf: [64]u8 = undefined;
hashToBytes(buf[0..], x);
mem.copy(u8, out[i..], buf[0 .. out.len - i]);
}
}
fn hchacha20(input: [16]u8, key: [32]u8) [32]u8 { fn hchacha20(input: [16]u8, key: [32]u8) [32]u8 {
var c: [4]u32 = undefined; var c: [4]u32 = undefined;
for (c, 0..) |_, i| { for (c, 0..) |_, i| {
@ -387,6 +427,8 @@ fn ChaChaIETF(comptime rounds_nb: usize) type {
pub const nonce_length = 12; pub const nonce_length = 12;
/// Key length in bytes. /// Key length in bytes.
pub const key_length = 32; pub const key_length = 32;
/// Block length in bytes.
pub const block_length = 64;
/// Add the output of the ChaCha20 stream cipher to `in` and stores the result into `out`. /// Add the output of the ChaCha20 stream cipher to `in` and stores the result into `out`.
/// WARNING: This function doesn't provide authenticated encryption. /// WARNING: This function doesn't provide authenticated encryption.
@ -402,6 +444,18 @@ fn ChaChaIETF(comptime rounds_nb: usize) type {
d[3] = mem.readIntLittle(u32, nonce[8..12]); d[3] = mem.readIntLittle(u32, nonce[8..12]);
ChaChaImpl(rounds_nb).chacha20Xor(out, in, keyToWords(key), d); ChaChaImpl(rounds_nb).chacha20Xor(out, in, keyToWords(key), d);
} }
/// Write the output of the ChaCha20 stream cipher into `out`.
pub fn stream(out: []u8, counter: u32, key: [key_length]u8, nonce: [nonce_length]u8) void {
assert(out.len / 64 <= (1 << 32 - 1) - counter);
var d: [4]u32 = undefined;
d[0] = counter;
d[1] = mem.readIntLittle(u32, nonce[0..4]);
d[2] = mem.readIntLittle(u32, nonce[4..8]);
d[3] = mem.readIntLittle(u32, nonce[8..12]);
ChaChaImpl(rounds_nb).chacha20Stream(out, keyToWords(key), d);
}
}; };
} }
@ -411,6 +465,8 @@ fn ChaChaWith64BitNonce(comptime rounds_nb: usize) type {
pub const nonce_length = 8; pub const nonce_length = 8;
/// Key length in bytes. /// Key length in bytes.
pub const key_length = 32; pub const key_length = 32;
/// Block length in bytes.
pub const block_length = 64;
/// Add the output of the ChaCha20 stream cipher to `in` and stores the result into `out`. /// Add the output of the ChaCha20 stream cipher to `in` and stores the result into `out`.
/// WARNING: This function doesn't provide authenticated encryption. /// WARNING: This function doesn't provide authenticated encryption.
@ -427,7 +483,6 @@ fn ChaChaWith64BitNonce(comptime rounds_nb: usize) type {
c[2] = mem.readIntLittle(u32, nonce[0..4]); c[2] = mem.readIntLittle(u32, nonce[0..4]);
c[3] = mem.readIntLittle(u32, nonce[4..8]); c[3] = mem.readIntLittle(u32, nonce[4..8]);
const block_length = (1 << 6);
// The full block size is greater than the address space on a 32bit machine // The full block size is greater than the address space on a 32bit machine
const big_block = if (@sizeOf(usize) > 4) (block_length << 32) else maxInt(usize); const big_block = if (@sizeOf(usize) > 4) (block_length << 32) else maxInt(usize);
@ -448,6 +503,18 @@ fn ChaChaWith64BitNonce(comptime rounds_nb: usize) type {
} }
ChaChaImpl(rounds_nb).chacha20Xor(out[cursor..], in[cursor..], k, c); ChaChaImpl(rounds_nb).chacha20Xor(out[cursor..], in[cursor..], k, c);
} }
/// Write the output of the ChaCha20 stream cipher into `out`.
pub fn stream(out: []u8, counter: u32, key: [key_length]u8, nonce: [nonce_length]u8) void {
assert(out.len / 64 <= (1 << 32 - 1) - counter);
const k = keyToWords(key);
var c: [4]u32 = undefined;
c[0] = @truncate(u32, counter);
c[1] = @truncate(u32, counter >> 32);
c[2] = mem.readIntLittle(u32, nonce[0..4]);
c[3] = mem.readIntLittle(u32, nonce[4..8]);
ChaChaImpl(rounds_nb).chacha20Stream(out, k, c);
}
}; };
} }
@ -457,6 +524,8 @@ fn XChaChaIETF(comptime rounds_nb: usize) type {
pub const nonce_length = 24; pub const nonce_length = 24;
/// Key length in bytes. /// Key length in bytes.
pub const key_length = 32; pub const key_length = 32;
/// Block length in bytes.
pub const block_length = 64;
/// Add the output of the XChaCha20 stream cipher to `in` and stores the result into `out`. /// Add the output of the XChaCha20 stream cipher to `in` and stores the result into `out`.
/// WARNING: This function doesn't provide authenticated encryption. /// WARNING: This function doesn't provide authenticated encryption.
@ -465,6 +534,12 @@ fn XChaChaIETF(comptime rounds_nb: usize) type {
const extended = extend(key, nonce, rounds_nb); const extended = extend(key, nonce, rounds_nb);
ChaChaIETF(rounds_nb).xor(out, in, counter, extended.key, extended.nonce); ChaChaIETF(rounds_nb).xor(out, in, counter, extended.key, extended.nonce);
} }
/// Write the output of the XChaCha20 stream cipher into `out`.
pub fn stream(out: []u8, counter: u32, key: [key_length]u8, nonce: [nonce_length]u8) void {
const extended = extend(key, nonce, rounds_nb);
ChaChaIETF(rounds_nb).xor(out, counter, extended.key, extended.nonce);
}
}; };
} }

View File

@ -1,527 +0,0 @@
//! Gimli is a 384-bit permutation designed to achieve high security with high
//! performance across a broad range of platforms, including 64-bit Intel/AMD
//! server CPUs, 64-bit and 32-bit ARM smartphone CPUs, 32-bit ARM
//! microcontrollers, 8-bit AVR microcontrollers, FPGAs, ASICs without
//! side-channel protection, and ASICs with side-channel protection.
//!
//! https://gimli.cr.yp.to/
//! https://csrc.nist.gov/CSRC/media/Projects/Lightweight-Cryptography/documents/round-1/spec-doc/gimli-spec.pdf
const std = @import("../std.zig");
const builtin = @import("builtin");
const mem = std.mem;
const math = std.math;
const debug = std.debug;
const assert = std.debug.assert;
const testing = std.testing;
const htest = @import("test.zig");
const AuthenticationError = std.crypto.errors.AuthenticationError;
pub const State = struct {
pub const BLOCKBYTES = 48;
pub const RATE = 16;
data: [BLOCKBYTES / 4]u32 align(16),
const Self = @This();
pub fn init(initial_state: [State.BLOCKBYTES]u8) Self {
var data: [BLOCKBYTES / 4]u32 = undefined;
var i: usize = 0;
while (i < State.BLOCKBYTES) : (i += 4) {
data[i / 4] = mem.readIntNative(u32, initial_state[i..][0..4]);
}
return Self{ .data = data };
}
/// TODO follow the span() convention instead of having this and `toSliceConst`
pub fn toSlice(self: *Self) *[BLOCKBYTES]u8 {
return mem.asBytes(&self.data);
}
/// TODO follow the span() convention instead of having this and `toSlice`
pub fn toSliceConst(self: *const Self) *const [BLOCKBYTES]u8 {
return mem.asBytes(&self.data);
}
inline fn endianSwap(self: *Self) void {
for (&self.data) |*w| {
w.* = mem.littleToNative(u32, w.*);
}
}
fn permute_unrolled(self: *Self) void {
self.endianSwap();
const state = &self.data;
comptime var round = @as(u32, 24);
inline while (round > 0) : (round -= 1) {
var column = @as(usize, 0);
while (column < 4) : (column += 1) {
const x = math.rotl(u32, state[column], 24);
const y = math.rotl(u32, state[4 + column], 9);
const z = state[8 + column];
state[8 + column] = ((x ^ (z << 1)) ^ ((y & z) << 2));
state[4 + column] = ((y ^ x) ^ ((x | z) << 1));
state[column] = ((z ^ y) ^ ((x & y) << 3));
}
switch (round & 3) {
0 => {
mem.swap(u32, &state[0], &state[1]);
mem.swap(u32, &state[2], &state[3]);
state[0] ^= round | 0x9e377900;
},
2 => {
mem.swap(u32, &state[0], &state[2]);
mem.swap(u32, &state[1], &state[3]);
},
else => {},
}
}
self.endianSwap();
}
fn permute_small(self: *Self) void {
self.endianSwap();
const state = &self.data;
var round = @as(u32, 24);
while (round > 0) : (round -= 1) {
var column = @as(usize, 0);
while (column < 4) : (column += 1) {
const x = math.rotl(u32, state[column], 24);
const y = math.rotl(u32, state[4 + column], 9);
const z = state[8 + column];
state[8 + column] = ((x ^ (z << 1)) ^ ((y & z) << 2));
state[4 + column] = ((y ^ x) ^ ((x | z) << 1));
state[column] = ((z ^ y) ^ ((x & y) << 3));
}
switch (round & 3) {
0 => {
mem.swap(u32, &state[0], &state[1]);
mem.swap(u32, &state[2], &state[3]);
state[0] ^= round | 0x9e377900;
},
2 => {
mem.swap(u32, &state[0], &state[2]);
mem.swap(u32, &state[1], &state[3]);
},
else => {},
}
}
self.endianSwap();
}
const Lane = @Vector(4, u32);
inline fn shift(x: Lane, comptime n: comptime_int) Lane {
return x << @splat(4, @as(u5, n));
}
fn permute_vectorized(self: *Self) void {
self.endianSwap();
const state = &self.data;
var x = Lane{ state[0], state[1], state[2], state[3] };
var y = Lane{ state[4], state[5], state[6], state[7] };
var z = Lane{ state[8], state[9], state[10], state[11] };
var round = @as(u32, 24);
while (round > 0) : (round -= 1) {
x = math.rotl(Lane, x, 24);
y = math.rotl(Lane, y, 9);
const newz = x ^ shift(z, 1) ^ shift(y & z, 2);
const newy = y ^ x ^ shift(x | z, 1);
const newx = z ^ y ^ shift(x & y, 3);
x = newx;
y = newy;
z = newz;
switch (round & 3) {
0 => {
x = @shuffle(u32, x, undefined, [_]i32{ 1, 0, 3, 2 });
x[0] ^= round | 0x9e377900;
},
2 => {
x = @shuffle(u32, x, undefined, [_]i32{ 2, 3, 0, 1 });
},
else => {},
}
}
comptime var i: usize = 0;
inline while (i < 4) : (i += 1) {
state[0 + i] = x[i];
state[4 + i] = y[i];
state[8 + i] = z[i];
}
self.endianSwap();
}
pub const permute = if (builtin.cpu.arch == .x86_64) impl: {
break :impl permute_vectorized;
} else if (builtin.mode == .ReleaseSmall) impl: {
break :impl permute_small;
} else impl: {
break :impl permute_unrolled;
};
pub fn squeeze(self: *Self, out: []u8) void {
var i = @as(usize, 0);
while (i + RATE <= out.len) : (i += RATE) {
self.permute();
mem.copy(u8, out[i..], self.toSliceConst()[0..RATE]);
}
const leftover = out.len - i;
if (leftover != 0) {
self.permute();
mem.copy(u8, out[i..], self.toSliceConst()[0..leftover]);
}
}
};
test "permute" {
// test vector from gimli-20170627
const tv_input = [3][4]u32{
[4]u32{ 0x00000000, 0x9e3779ba, 0x3c6ef37a, 0xdaa66d46 },
[4]u32{ 0x78dde724, 0x1715611a, 0xb54cdb2e, 0x53845566 },
[4]u32{ 0xf1bbcfc8, 0x8ff34a5a, 0x2e2ac522, 0xcc624026 },
};
var input: [48]u8 = undefined;
var i: usize = 0;
while (i < 12) : (i += 1) {
mem.writeIntLittle(u32, input[i * 4 ..][0..4], tv_input[i / 4][i % 4]);
}
var state = State.init(input);
state.permute();
const tv_output = [3][4]u32{
[4]u32{ 0xba11c85a, 0x91bad119, 0x380ce880, 0xd24c2c68 },
[4]u32{ 0x3eceffea, 0x277a921c, 0x4f73a0bd, 0xda5a9cd8 },
[4]u32{ 0x84b673f0, 0x34e52ff7, 0x9e2bef49, 0xf41bb8d6 },
};
var expected_output: [48]u8 = undefined;
i = 0;
while (i < 12) : (i += 1) {
mem.writeIntLittle(u32, expected_output[i * 4 ..][0..4], tv_output[i / 4][i % 4]);
}
try testing.expectEqualSlices(u8, state.toSliceConst(), expected_output[0..]);
}
pub const Hash = struct {
state: State,
buf_off: usize,
pub const block_length = State.RATE;
pub const digest_length = 32;
pub const Options = struct {};
const Self = @This();
pub fn init(options: Options) Self {
_ = options;
return Self{
.state = State{ .data = [_]u32{0} ** (State.BLOCKBYTES / 4) },
.buf_off = 0,
};
}
/// Also known as 'absorb'
pub fn update(self: *Self, data: []const u8) void {
const buf = self.state.toSlice();
var in = data;
while (in.len > 0) {
const left = State.RATE - self.buf_off;
const ps = math.min(in.len, left);
for (buf[self.buf_off .. self.buf_off + ps], 0..) |*p, i| {
p.* ^= in[i];
}
self.buf_off += ps;
in = in[ps..];
if (self.buf_off == State.RATE) {
self.state.permute();
self.buf_off = 0;
}
}
}
/// Finish the current hashing operation, writing the hash to `out`
///
/// From 4.9 "Application to hashing"
/// By default, Gimli-Hash provides a fixed-length output of 32 bytes
/// (the concatenation of two 16-byte blocks). However, Gimli-Hash can
/// be used as an extendable one-way function (XOF).
pub fn final(self: *Self, out: []u8) void {
const buf = self.state.toSlice();
// XOR 1 into the next byte of the state
buf[self.buf_off] ^= 1;
// XOR 1 into the last byte of the state, position 47.
buf[buf.len - 1] ^= 1;
self.state.squeeze(out);
}
pub const Error = error{};
pub const Writer = std.io.Writer(*Self, Error, write);
fn write(self: *Self, bytes: []const u8) Error!usize {
self.update(bytes);
return bytes.len;
}
pub fn writer(self: *Self) Writer {
return .{ .context = self };
}
};
pub fn hash(out: []u8, in: []const u8, options: Hash.Options) void {
var st = Hash.init(options);
st.update(in);
st.final(out);
}
test "hash" {
// a test vector (30) from NIST KAT submission.
var msg: [58 / 2]u8 = undefined;
_ = try std.fmt.hexToBytes(&msg, "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C");
var md: [32]u8 = undefined;
hash(&md, &msg, .{});
try htest.assertEqual("1C9A03DC6A5DDC5444CFC6F4B154CFF5CF081633B2CEA4D7D0AE7CCFED5AAA44", &md);
}
test "hash test vector 17" {
var msg: [32 / 2]u8 = undefined;
_ = try std.fmt.hexToBytes(&msg, "000102030405060708090A0B0C0D0E0F");
var md: [32]u8 = undefined;
hash(&md, &msg, .{});
try htest.assertEqual("404C130AF1B9023A7908200919F690FFBB756D5176E056FFDE320016A37C7282", &md);
}
test "hash test vector 33" {
var msg: [32]u8 = undefined;
_ = try std.fmt.hexToBytes(&msg, "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F");
var md: [32]u8 = undefined;
hash(&md, &msg, .{});
try htest.assertEqual("A8F4FA28708BDA7EFB4C1914CA4AFA9E475B82D588D36504F87DBB0ED9AB3C4B", &md);
}
pub const Aead = struct {
pub const tag_length = State.RATE;
pub const nonce_length = 16;
pub const key_length = 32;
/// ad: Associated Data
/// npub: public nonce
/// k: private key
fn init(ad: []const u8, npub: [nonce_length]u8, k: [key_length]u8) State {
var state = State{
.data = undefined,
};
const buf = state.toSlice();
// Gimli-Cipher initializes a 48-byte Gimli state to a 16-byte nonce
// followed by a 32-byte key.
assert(npub.len + k.len == State.BLOCKBYTES);
std.mem.copy(u8, buf[0..npub.len], &npub);
std.mem.copy(u8, buf[npub.len .. npub.len + k.len], &k);
// It then applies the Gimli permutation.
state.permute();
{
// Gimli-Cipher then handles each block of associated data, including
// exactly one final non-full block, in the same way as Gimli-Hash.
var data = ad;
while (data.len >= State.RATE) : (data = data[State.RATE..]) {
for (buf[0..State.RATE], 0..) |*p, i| {
p.* ^= data[i];
}
state.permute();
}
for (buf[0..data.len], 0..) |*p, i| {
p.* ^= data[i];
}
// XOR 1 into the next byte of the state
buf[data.len] ^= 1;
// XOR 1 into the last byte of the state, position 47.
buf[buf.len - 1] ^= 1;
state.permute();
}
return state;
}
/// c: ciphertext: output buffer should be of size m.len
/// tag: authentication tag: output MAC
/// m: message
/// ad: Associated Data
/// npub: public nonce
/// k: private key
pub fn encrypt(c: []u8, tag: *[tag_length]u8, m: []const u8, ad: []const u8, npub: [nonce_length]u8, k: [key_length]u8) void {
assert(c.len == m.len);
var state = Aead.init(ad, npub, k);
const buf = state.toSlice();
// Gimli-Cipher then handles each block of plaintext, including
// exactly one final non-full block, in the same way as Gimli-Hash.
// Whenever a plaintext byte is XORed into a state byte, the new state
// byte is output as ciphertext.
var in = m;
var out = c;
while (in.len >= State.RATE) : ({
in = in[State.RATE..];
out = out[State.RATE..];
}) {
for (in[0..State.RATE], 0..) |v, i| {
buf[i] ^= v;
}
mem.copy(u8, out[0..State.RATE], buf[0..State.RATE]);
state.permute();
}
for (in[0..], 0..) |v, i| {
buf[i] ^= v;
out[i] = buf[i];
}
// XOR 1 into the next byte of the state
buf[in.len] ^= 1;
// XOR 1 into the last byte of the state, position 47.
buf[buf.len - 1] ^= 1;
state.permute();
// After the final non-full block of plaintext, the first 16 bytes
// of the state are output as an authentication tag.
std.mem.copy(u8, tag, buf[0..State.RATE]);
}
/// m: message: output buffer should be of size c.len
/// c: ciphertext
/// tag: authentication tag
/// ad: Associated Data
/// npub: public nonce
/// k: private key
/// NOTE: the check of the authentication tag is currently not done in constant time
pub fn decrypt(m: []u8, c: []const u8, tag: [tag_length]u8, ad: []const u8, npub: [nonce_length]u8, k: [key_length]u8) AuthenticationError!void {
assert(c.len == m.len);
var state = Aead.init(ad, npub, k);
const buf = state.toSlice();
var in = c;
var out = m;
while (in.len >= State.RATE) : ({
in = in[State.RATE..];
out = out[State.RATE..];
}) {
const d = in[0..State.RATE].*;
for (d, 0..) |v, i| {
out[i] = buf[i] ^ v;
}
mem.copy(u8, buf[0..State.RATE], d[0..State.RATE]);
state.permute();
}
for (buf[0..in.len], 0..) |*p, i| {
const d = in[i];
out[i] = p.* ^ d;
p.* = d;
}
// XOR 1 into the next byte of the state
buf[in.len] ^= 1;
// XOR 1 into the last byte of the state, position 47.
buf[buf.len - 1] ^= 1;
state.permute();
// After the final non-full block of plaintext, the first 16 bytes
// of the state are the authentication tag.
// TODO: use a constant-time equality check here, see https://github.com/ziglang/zig/issues/1776
if (!mem.eql(u8, buf[0..State.RATE], &tag)) {
@memset(m.ptr, undefined, m.len);
return error.AuthenticationFailed;
}
}
};
test "cipher" {
var key: [32]u8 = undefined;
_ = try std.fmt.hexToBytes(&key, "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F");
var nonce: [16]u8 = undefined;
_ = try std.fmt.hexToBytes(&nonce, "000102030405060708090A0B0C0D0E0F");
{ // test vector (1) from NIST KAT submission.
const ad: [0]u8 = undefined;
const pt: [0]u8 = undefined;
var ct: [pt.len]u8 = undefined;
var tag: [16]u8 = undefined;
Aead.encrypt(&ct, &tag, &pt, &ad, nonce, key);
try htest.assertEqual("", &ct);
try htest.assertEqual("14DA9BB7120BF58B985A8E00FDEBA15B", &tag);
var pt2: [pt.len]u8 = undefined;
try Aead.decrypt(&pt2, &ct, tag, &ad, nonce, key);
try testing.expectEqualSlices(u8, &pt, &pt2);
}
{ // test vector (34) from NIST KAT submission.
const ad: [0]u8 = undefined;
var pt: [2 / 2]u8 = undefined;
_ = try std.fmt.hexToBytes(&pt, "00");
var ct: [pt.len]u8 = undefined;
var tag: [16]u8 = undefined;
Aead.encrypt(&ct, &tag, &pt, &ad, nonce, key);
try htest.assertEqual("7F", &ct);
try htest.assertEqual("80492C317B1CD58A1EDC3A0D3E9876FC", &tag);
var pt2: [pt.len]u8 = undefined;
try Aead.decrypt(&pt2, &ct, tag, &ad, nonce, key);
try testing.expectEqualSlices(u8, &pt, &pt2);
}
{ // test vector (106) from NIST KAT submission.
var ad: [12 / 2]u8 = undefined;
_ = try std.fmt.hexToBytes(&ad, "000102030405");
var pt: [6 / 2]u8 = undefined;
_ = try std.fmt.hexToBytes(&pt, "000102");
var ct: [pt.len]u8 = undefined;
var tag: [16]u8 = undefined;
Aead.encrypt(&ct, &tag, &pt, &ad, nonce, key);
try htest.assertEqual("484D35", &ct);
try htest.assertEqual("030BBEA23B61C00CED60A923BDCF9147", &tag);
var pt2: [pt.len]u8 = undefined;
try Aead.decrypt(&pt2, &ct, tag, &ad, nonce, key);
try testing.expectEqualSlices(u8, &pt, &pt2);
}
{ // test vector (790) from NIST KAT submission.
var ad: [60 / 2]u8 = undefined;
_ = try std.fmt.hexToBytes(&ad, "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D");
var pt: [46 / 2]u8 = undefined;
_ = try std.fmt.hexToBytes(&pt, "000102030405060708090A0B0C0D0E0F10111213141516");
var ct: [pt.len]u8 = undefined;
var tag: [16]u8 = undefined;
Aead.encrypt(&ct, &tag, &pt, &ad, nonce, key);
try htest.assertEqual("6815B4A0ECDAD01596EAD87D9E690697475D234C6A13D1", &ct);
try htest.assertEqual("DFE23F1642508290D68245279558B2FB", &tag);
var pt2: [pt.len]u8 = undefined;
try Aead.decrypt(&pt2, &ct, tag, &ad, nonce, key);
try testing.expectEqualSlices(u8, &pt, &pt2);
}
{ // test vector (1057) from NIST KAT submission.
const ad: [0]u8 = undefined;
var pt: [64 / 2]u8 = undefined;
_ = try std.fmt.hexToBytes(&pt, "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F");
var ct: [pt.len]u8 = undefined;
var tag: [16]u8 = undefined;
Aead.encrypt(&ct, &tag, &pt, &ad, nonce, key);
try htest.assertEqual("7F8A2CF4F52AA4D6B2E74105C30A2777B9D0C8AEFDD555DE35861BD3011F652F", &ct);
try htest.assertEqual("7256456FA935AC34BBF55AE135F33257", &tag);
var pt2: [pt.len]u8 = undefined;
try Aead.decrypt(&pt2, &ct, tag, &ad, nonce, key);
try testing.expectEqualSlices(u8, &pt, &pt2);
}
}

View File

@ -41,9 +41,11 @@ const maybe_have_wipe_on_fork = builtin.os.isAtLeast(.linux, .{
}) orelse true; }) orelse true;
const is_haiku = builtin.os.tag == .haiku; const is_haiku = builtin.os.tag == .haiku;
const Rng = std.rand.DefaultCsprng;
const Context = struct { const Context = struct {
init_state: enum(u8) { uninitialized = 0, initialized, failed }, init_state: enum(u8) { uninitialized = 0, initialized, failed },
gimli: std.crypto.core.Gimli, rng: Rng,
}; };
var install_atfork_handler = std.once(struct { var install_atfork_handler = std.once(struct {
@ -93,7 +95,7 @@ fn tlsCsprngFill(_: *anyopaque, buffer: []u8) void {
const S = struct { const S = struct {
threadlocal var buf: Context align(mem.page_size) = .{ threadlocal var buf: Context align(mem.page_size) = .{
.init_state = .uninitialized, .init_state = .uninitialized,
.gimli = undefined, .rng = undefined,
}; };
}; };
wipe_mem = mem.asBytes(&S.buf); wipe_mem = mem.asBytes(&S.buf);
@ -156,12 +158,7 @@ fn childAtForkHandler() callconv(.C) void {
fn fillWithCsprng(buffer: []u8) void { fn fillWithCsprng(buffer: []u8) void {
const ctx = @ptrCast(*Context, wipe_mem.ptr); const ctx = @ptrCast(*Context, wipe_mem.ptr);
if (buffer.len != 0) { return ctx.rng.fill(buffer);
ctx.gimli.squeeze(buffer);
} else {
ctx.gimli.permute();
}
mem.set(u8, ctx.gimli.toSlice()[0..std.crypto.core.Gimli.RATE], 0);
} }
pub fn defaultRandomSeed(buffer: []u8) void { pub fn defaultRandomSeed(buffer: []u8) void {
@ -169,7 +166,7 @@ pub fn defaultRandomSeed(buffer: []u8) void {
} }
fn initAndFill(buffer: []u8) void { fn initAndFill(buffer: []u8) void {
var seed: [std.crypto.core.Gimli.BLOCKBYTES]u8 = undefined; var seed: [Rng.secret_seed_length]u8 = undefined;
// Because we panic on getrandom() failing, we provide the opportunity // Because we panic on getrandom() failing, we provide the opportunity
// to override the default seed function. This also makes // to override the default seed function. This also makes
// `std.crypto.random` available on freestanding targets, provided that // `std.crypto.random` available on freestanding targets, provided that
@ -177,7 +174,8 @@ fn initAndFill(buffer: []u8) void {
std.options.cryptoRandomSeed(&seed); std.options.cryptoRandomSeed(&seed);
const ctx = @ptrCast(*Context, wipe_mem.ptr); const ctx = @ptrCast(*Context, wipe_mem.ptr);
ctx.gimli = std.crypto.core.Gimli.init(seed); ctx.rng = Rng.init(seed);
std.crypto.utils.secureZero(u8, &seed);
// This is at the end so that accidental recursive dependencies result // This is at the end so that accidental recursive dependencies result
// in stack overflows instead of invalid random data. // in stack overflows instead of invalid random data.

View File

@ -1,141 +0,0 @@
//! Xoodoo is a 384-bit permutation designed to achieve high security with high
//! performance across a broad range of platforms, including 64-bit Intel/AMD
//! server CPUs, 64-bit and 32-bit ARM smartphone CPUs, 32-bit ARM
//! microcontrollers, 8-bit AVR microcontrollers, FPGAs, ASICs without
//! side-channel protection, and ASICs with side-channel protection.
//!
//! Xoodoo is the core function of Xoodyak, a finalist of the NIST lightweight cryptography competition.
//! https://csrc.nist.gov/CSRC/media/Projects/Lightweight-Cryptography/documents/round-1/spec-doc/Xoodyak-spec.pdf
//!
//! It is not meant to be used directly, but as a building block for symmetric cryptography.
const std = @import("../std.zig");
const builtin = @import("builtin");
const mem = std.mem;
const math = std.math;
const testing = std.testing;
/// A Xoodoo state.
pub const State = struct {
/// Number of bytes in the state.
pub const block_bytes = 48;
const rcs = [12]u32{ 0x058, 0x038, 0x3c0, 0x0d0, 0x120, 0x014, 0x060, 0x02c, 0x380, 0x0f0, 0x1a0, 0x012 };
const Lane = @Vector(4, u32);
st: [3]Lane,
/// Initialize a state from a slice of bytes.
pub fn init(initial_state: [block_bytes]u8) State {
var state = State{ .st = undefined };
mem.copy(u8, state.asBytes(), &initial_state);
state.endianSwap();
return state;
}
// A representation of the state as 32-bit words.
fn asWords(self: *State) *[12]u32 {
return @ptrCast(*[12]u32, &self.st);
}
/// A representation of the state as bytes. The byte order is architecture-dependent.
pub fn asBytes(self: *State) *[block_bytes]u8 {
return mem.asBytes(&self.st);
}
/// Byte-swap words storing the bytes of a given range if the architecture is not little-endian.
pub fn endianSwapPartial(self: *State, from: usize, to: usize) void {
for (self.asWords()[from / 4 .. (to + 3) / 4]) |*w| {
w.* = mem.littleToNative(u32, w.*);
}
}
/// Byte-swap the entire state if the architecture is not little-endian.
pub fn endianSwap(self: *State) void {
for (self.asWords()) |*w| {
w.* = mem.littleToNative(u32, w.*);
}
}
/// XOR a byte into the state at a given offset.
pub fn addByte(self: *State, byte: u8, offset: usize) void {
self.endianSwapPartial(offset, offset);
self.asBytes()[offset] ^= byte;
self.endianSwapPartial(offset, offset);
}
/// XOR bytes into the beginning of the state.
pub fn addBytes(self: *State, bytes: []const u8) void {
self.endianSwap();
for (self.asBytes()[0..bytes.len], 0..) |*byte, i| {
byte.* ^= bytes[i];
}
self.endianSwap();
}
/// Extract the first bytes of the state.
pub fn extract(self: *State, out: []u8) void {
self.endianSwap();
mem.copy(u8, out, self.asBytes()[0..out.len]);
self.endianSwap();
}
/// Set the words storing the bytes of a given range to zero.
pub fn clear(self: *State, from: usize, to: usize) void {
mem.set(u32, self.asWords()[from / 4 .. (to + 3) / 4], 0);
}
/// Apply the Xoodoo permutation.
pub fn permute(self: *State) void {
const rot8x32 = comptime if (builtin.target.cpu.arch.endian() == .Big)
[_]i32{ 9, 10, 11, 8, 13, 14, 15, 12, 1, 2, 3, 0, 5, 6, 7, 4 }
else
[_]i32{ 11, 8, 9, 10, 15, 12, 13, 14, 3, 0, 1, 2, 7, 4, 5, 6 };
var a = self.st[0];
var b = self.st[1];
var c = self.st[2];
inline for (rcs) |rc| {
var p = @shuffle(u32, a ^ b ^ c, undefined, [_]i32{ 3, 0, 1, 2 });
var e = math.rotl(Lane, p, 5);
p = math.rotl(Lane, p, 14);
e ^= p;
a ^= e;
b ^= e;
c ^= e;
b = @shuffle(u32, b, undefined, [_]i32{ 3, 0, 1, 2 });
c = math.rotl(Lane, c, 11);
a[0] ^= rc;
a ^= ~b & c;
b ^= ~c & a;
c ^= ~a & b;
b = math.rotl(Lane, b, 1);
c = @bitCast(Lane, @shuffle(u8, @bitCast(@Vector(16, u8), c), undefined, rot8x32));
}
self.st[0] = a;
self.st[1] = b;
self.st[2] = c;
}
};
test "xoodoo" {
const bytes = [_]u8{0x01} ** State.block_bytes;
var st = State.init(bytes);
var out: [State.block_bytes]u8 = undefined;
st.permute();
st.extract(&out);
const expected1 = [_]u8{ 51, 240, 163, 117, 43, 238, 62, 200, 114, 52, 79, 41, 48, 108, 150, 181, 24, 5, 252, 185, 235, 179, 28, 3, 116, 170, 36, 15, 232, 35, 116, 61, 110, 4, 109, 227, 91, 205, 0, 180, 179, 146, 112, 235, 96, 212, 206, 205 };
try testing.expectEqualSlices(u8, &expected1, &out);
st.clear(0, 10);
st.extract(&out);
const expected2 = [_]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 108, 150, 181, 24, 5, 252, 185, 235, 179, 28, 3, 116, 170, 36, 15, 232, 35, 116, 61, 110, 4, 109, 227, 91, 205, 0, 180, 179, 146, 112, 235, 96, 212, 206, 205 };
try testing.expectEqualSlices(u8, &expected2, &out);
st.addByte(1, 5);
st.addByte(2, 5);
st.extract(&out);
const expected3 = [_]u8{ 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 48, 108, 150, 181, 24, 5, 252, 185, 235, 179, 28, 3, 116, 170, 36, 15, 232, 35, 116, 61, 110, 4, 109, 227, 91, 205, 0, 180, 179, 146, 112, 235, 96, 212, 206, 205 };
try testing.expectEqualSlices(u8, &expected3, &out);
st.addBytes(&bytes);
st.extract(&out);
const expected4 = [_]u8{ 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 49, 109, 151, 180, 25, 4, 253, 184, 234, 178, 29, 2, 117, 171, 37, 14, 233, 34, 117, 60, 111, 5, 108, 226, 90, 204, 1, 181, 178, 147, 113, 234, 97, 213, 207, 204 };
try testing.expectEqualSlices(u8, &expected4, &out);
}

View File

@ -18,11 +18,12 @@ const maxInt = std.math.maxInt;
pub const DefaultPrng = Xoshiro256; pub const DefaultPrng = Xoshiro256;
/// Cryptographically secure random numbers. /// Cryptographically secure random numbers.
pub const DefaultCsprng = Ascon; pub const DefaultCsprng = ChaCha;
pub const Ascon = @import("rand/Ascon.zig"); pub const Ascon = @import("rand/Ascon.zig");
pub const ChaCha = @import("rand/ChaCha.zig");
pub const Isaac64 = @import("rand/Isaac64.zig"); pub const Isaac64 = @import("rand/Isaac64.zig");
pub const Xoodoo = @import("rand/Xoodoo.zig");
pub const Pcg = @import("rand/Pcg.zig"); pub const Pcg = @import("rand/Pcg.zig");
pub const Xoroshiro128 = @import("rand/Xoroshiro128.zig"); pub const Xoroshiro128 = @import("rand/Xoroshiro128.zig");
pub const Xoshiro256 = @import("rand/Xoshiro256.zig"); pub const Xoshiro256 = @import("rand/Xoshiro256.zig");

View File

@ -1,4 +1,12 @@
//! CSPRNG based on the Ascon XOFa construction //! CSPRNG based on the Reverie construction, a permutation-based PRNG
//! with forward security, instantiated with the Ascon(128,12,8) permutation.
//!
//! Compared to ChaCha, this PRNG is has a much smaller state, and can be
//! a better choice for constrained environments.
//!
//! References:
//! - A Robust and Sponge-Like PRNG with Improved Efficiency https://eprint.iacr.org/2016/886.pdf
//! - Ascon https://ascon.iaik.tugraz.at/files/asconv12-nist.pdf
const std = @import("std"); const std = @import("std");
const min = std.math.min; const min = std.math.min;
@ -6,30 +14,38 @@ const mem = std.mem;
const Random = std.rand.Random; const Random = std.rand.Random;
const Self = @This(); const Self = @This();
state: std.crypto.core.Ascon(.Little), const Ascon = std.crypto.core.Ascon(.Little);
const rate = 8; state: Ascon,
const rate = 16;
pub const secret_seed_length = 32; pub const secret_seed_length = 32;
/// The seed must be uniform, secret and `secret_seed_length` bytes long. /// The seed must be uniform, secret and `secret_seed_length` bytes long.
pub fn init(secret_seed: [secret_seed_length]u8) Self { pub fn init(secret_seed: [secret_seed_length]u8) Self {
var state = std.crypto.core.Ascon(.Little).initXofA(); var self = Self{ .state = Ascon.initXof() };
var i: usize = 0; self.addEntropy(&secret_seed);
while (i + rate <= secret_seed.len) : (i += rate) { return self;
state.addBytes(secret_seed[i..][0..rate]);
state.permuteR(8);
}
const left = secret_seed.len - i;
if (left > 0) state.addBytes(secret_seed[i..]);
state.addByte(0x80, left);
state.permute();
return Self{ .state = state };
} }
/// Inserts entropy to refresh the internal state.
pub fn addEntropy(self: *Self, bytes: []const u8) void {
comptime std.debug.assert(secret_seed_length % rate == 0);
var i: usize = 0;
while (i + rate < bytes.len) : (i += rate) {
self.state.addBytes(bytes[i..][0..rate]);
self.state.permuteR(8);
}
if (i != bytes.len) self.state.addBytes(bytes[i..]);
self.state.permute();
}
/// Returns a `std.rand.Random` structure backed by the current RNG.
pub fn random(self: *Self) Random { pub fn random(self: *Self) Random {
return Random.init(self, fill); return Random.init(self, fill);
} }
/// Fills the buffer with random bytes.
pub fn fill(self: *Self, buf: []u8) void { pub fn fill(self: *Self, buf: []u8) void {
var i: usize = 0; var i: usize = 0;
while (true) { while (true) {
@ -40,6 +56,5 @@ pub fn fill(self: *Self, buf: []u8) void {
self.state.permuteR(8); self.state.permuteR(8);
i += n; i += n;
} }
self.state.clear(0, rate); self.state.permuteRatchet(6, rate);
self.state.permuteR(8);
} }

97
lib/std/rand/ChaCha.zig Normal file
View File

@ -0,0 +1,97 @@
//! CSPRNG based on the ChaCha8 stream cipher, with forward security.
//!
//! References:
//! - Fast-key-erasure random-number generators https://blog.cr.yp.to/20170723-random.html
const std = @import("std");
const mem = std.mem;
const Random = std.rand.Random;
const Self = @This();
const Cipher = std.crypto.stream.chacha.ChaCha8IETF;
const State = [2 * Cipher.block_length]u8;
state: State,
offset: usize,
const nonce = [_]u8{0} ** Cipher.nonce_length;
pub const secret_seed_length = Cipher.key_length;
/// The seed must be uniform, secret and `secret_seed_length` bytes long.
pub fn init(secret_seed: [secret_seed_length]u8) Self {
var self = Self{ .state = undefined, .offset = 0 };
Cipher.stream(&self.state, 0, secret_seed, nonce);
return self;
}
/// Inserts entropy to refresh the internal state.
pub fn addEntropy(self: *Self, bytes: []const u8) void {
var i: usize = 0;
while (i + Cipher.key_length <= bytes.len) : (i += Cipher.key_length) {
Cipher.xor(
self.state[0..Cipher.key_length],
self.state[0..Cipher.key_length],
0,
bytes[i..][0..Cipher.key_length].*,
nonce,
);
}
if (i < bytes.len) {
var k = [_]u8{0} ** Cipher.key_length;
mem.copy(u8, k[0..], bytes[i..]);
Cipher.xor(
self.state[0..Cipher.key_length],
self.state[0..Cipher.key_length],
0,
k,
nonce,
);
}
self.refill();
}
/// Returns a `std.rand.Random` structure backed by the current RNG.
pub fn random(self: *Self) Random {
return Random.init(self, fill);
}
// Refills the buffer with random bytes, overwriting the previous key.
fn refill(self: *Self) void {
Cipher.stream(&self.state, 0, self.state[0..Cipher.key_length].*, nonce);
self.offset = 0;
}
/// Fills the buffer with random bytes.
pub fn fill(self: *Self, buf_: []u8) void {
const bytes = self.state[Cipher.key_length..];
var buf = buf_;
const avail = bytes.len - self.offset;
if (avail > 0) {
// Bytes from the current block
const n = @min(avail, buf.len);
mem.copy(u8, buf[0..n], bytes[self.offset..][0..n]);
mem.set(u8, bytes[self.offset..][0..n], 0);
buf = buf[n..];
self.offset += n;
}
if (buf.len == 0) return;
self.refill();
// Full blocks
while (buf.len >= bytes.len) {
mem.copy(u8, buf[0..bytes.len], bytes);
buf = buf[bytes.len..];
self.refill();
}
// Remaining bytes
if (buf.len > 0) {
mem.copy(u8, buf, bytes[0..buf.len]);
mem.set(u8, bytes[0..buf.len], 0);
self.offset = buf.len;
}
}

View File

@ -1,34 +0,0 @@
//! CSPRNG
const std = @import("std");
const Random = std.rand.Random;
const mem = std.mem;
const Gimli = @This();
state: std.crypto.core.Gimli,
pub const secret_seed_length = 32;
/// The seed must be uniform, secret and `secret_seed_length` bytes long.
pub fn init(secret_seed: [secret_seed_length]u8) Gimli {
var initial_state: [std.crypto.core.Gimli.BLOCKBYTES]u8 = undefined;
mem.copy(u8, initial_state[0..secret_seed_length], &secret_seed);
mem.set(u8, initial_state[secret_seed_length..], 0);
var self = Gimli{
.state = std.crypto.core.Gimli.init(initial_state),
};
return self;
}
pub fn random(self: *Gimli) Random {
return Random.init(self, fill);
}
pub fn fill(self: *Gimli, buf: []u8) void {
if (buf.len != 0) {
self.state.squeeze(buf);
} else {
self.state.permute();
}
mem.set(u8, self.state.toSlice()[0..std.crypto.core.Gimli.RATE], 0);
}

View File

@ -1,42 +0,0 @@
//! CSPRNG
const std = @import("std");
const Random = std.rand.Random;
const min = std.math.min;
const mem = std.mem;
const Xoodoo = @This();
const State = std.crypto.core.Xoodoo;
state: State,
const rate = 16;
pub const secret_seed_length = 32;
/// The seed must be uniform, secret and `secret_seed_length` bytes long.
pub fn init(secret_seed: [secret_seed_length]u8) Xoodoo {
var initial_state: [State.block_bytes]u8 = undefined;
mem.copy(u8, initial_state[0..secret_seed_length], &secret_seed);
mem.set(u8, initial_state[secret_seed_length..], 0);
var state = State.init(initial_state);
state.permute();
return Xoodoo{ .state = state };
}
pub fn random(self: *Xoodoo) Random {
return Random.init(self, fill);
}
pub fn fill(self: *Xoodoo, buf: []u8) void {
var i: usize = 0;
while (true) {
const left = buf.len - i;
const n = min(left, rate);
self.state.extract(buf[i..][0..n]);
if (left == 0) break;
self.state.permute();
i += n;
}
self.state.clear(0, rate);
self.state.permute();
}

217
lib/std/rand/benchmark.zig Normal file
View File

@ -0,0 +1,217 @@
// zig run -O ReleaseFast --zig-lib-dir ../.. benchmark.zig
const std = @import("std");
const builtin = @import("builtin");
const time = std.time;
const Timer = time.Timer;
const rand = std.rand;
const KiB = 1024;
const MiB = 1024 * KiB;
const GiB = 1024 * MiB;
const Rng = struct {
ty: type,
name: []const u8,
init_u8s: ?[]const u8 = null,
init_u64: ?u64 = null,
};
const prngs = [_]Rng{
Rng{
.ty = rand.Isaac64,
.name = "isaac64",
.init_u64 = 0,
},
Rng{
.ty = rand.Pcg,
.name = "pcg",
.init_u64 = 0,
},
Rng{
.ty = rand.RomuTrio,
.name = "romutrio",
.init_u64 = 0,
},
Rng{
.ty = std.rand.Sfc64,
.name = "sfc64",
.init_u64 = 0,
},
Rng{
.ty = std.rand.Xoroshiro128,
.name = "xoroshiro128",
.init_u64 = 0,
},
Rng{
.ty = std.rand.Xoshiro256,
.name = "xoshiro256",
.init_u64 = 0,
},
};
const csprngs = [_]Rng{
Rng{
.ty = rand.Ascon,
.name = "ascon",
.init_u8s = &[_]u8{0} ** 32,
},
Rng{
.ty = rand.ChaCha,
.name = "chacha",
.init_u8s = &[_]u8{0} ** 32,
},
};
const Result = struct {
throughput: u64,
};
const long_block_size: usize = 8 * 8192;
const short_block_size: usize = 8;
pub fn benchmark(comptime H: anytype, bytes: usize, comptime block_size: usize) !Result {
var rng = blk: {
if (H.init_u8s) |init| {
break :blk H.ty.init(init[0..].*);
}
if (H.init_u64) |init| {
break :blk H.ty.init(init);
}
break :blk H.ty.init();
};
var block: [block_size]u8 = undefined;
var offset: usize = 0;
var timer = try Timer.start();
const start = timer.lap();
while (offset < bytes) : (offset += block.len) {
rng.fill(block[0..]);
}
const end = timer.read();
const elapsed_s = @intToFloat(f64, end - start) / time.ns_per_s;
const throughput = @floatToInt(u64, @intToFloat(f64, bytes) / elapsed_s);
std.debug.assert(rng.random().int(u64) != 0);
return Result{
.throughput = throughput,
};
}
fn usage() void {
std.debug.print(
\\throughput_test [options]
\\
\\Options:
\\ --filter [test-name]
\\ --count [int]
\\ --prngs-only
\\ --csprngs-only
\\ --short-only
\\ --long-only
\\ --help
\\
, .{});
}
fn mode(comptime x: comptime_int) comptime_int {
return if (builtin.mode == .Debug) x / 64 else x;
}
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
var buffer: [1024]u8 = undefined;
var fixed = std.heap.FixedBufferAllocator.init(buffer[0..]);
const args = try std.process.argsAlloc(fixed.allocator());
var filter: ?[]u8 = "";
var count: usize = mode(128 * MiB);
var bench_prngs = true;
var bench_csprngs = true;
var bench_long = true;
var bench_short = true;
var i: usize = 1;
while (i < args.len) : (i += 1) {
if (std.mem.eql(u8, args[i], "--mode")) {
try stdout.print("{}\n", .{builtin.mode});
return;
} else if (std.mem.eql(u8, args[i], "--filter")) {
i += 1;
if (i == args.len) {
usage();
std.os.exit(1);
}
filter = args[i];
} else if (std.mem.eql(u8, args[i], "--count")) {
i += 1;
if (i == args.len) {
usage();
std.os.exit(1);
}
const c = try std.fmt.parseUnsigned(usize, args[i], 10);
count = c * MiB;
} else if (std.mem.eql(u8, args[i], "--csprngs-only")) {
bench_prngs = false;
} else if (std.mem.eql(u8, args[i], "--prngs-only")) {
bench_csprngs = false;
} else if (std.mem.eql(u8, args[i], "--short-only")) {
bench_long = false;
} else if (std.mem.eql(u8, args[i], "--long-only")) {
bench_short = false;
} else if (std.mem.eql(u8, args[i], "--help")) {
usage();
return;
} else {
usage();
std.os.exit(1);
}
}
if (bench_prngs) {
if (bench_long) {
inline for (prngs) |R| {
if (filter == null or std.mem.indexOf(u8, R.name, filter.?) != null) {
try stdout.print("{s} (long outputs)\n", .{R.name});
const result_long = try benchmark(R, count, long_block_size);
try stdout.print(" {:5} MiB/s\n", .{result_long.throughput / (1 * MiB)});
}
}
}
if (bench_short) {
inline for (prngs) |R| {
if (filter == null or std.mem.indexOf(u8, R.name, filter.?) != null) {
try stdout.print("{s} (short outputs)\n", .{R.name});
const result_short = try benchmark(R, count, short_block_size);
try stdout.print(" {:5} MiB/s\n", .{result_short.throughput / (1 * MiB)});
}
}
}
}
if (bench_csprngs) {
if (bench_long) {
inline for (csprngs) |R| {
if (filter == null or std.mem.indexOf(u8, R.name, filter.?) != null) {
try stdout.print("{s} (cryptographic, long outputs)\n", .{R.name});
const result_long = try benchmark(R, count, long_block_size);
try stdout.print(" {:5} MiB/s\n", .{result_long.throughput / (1 * MiB)});
}
}
}
if (bench_short) {
inline for (csprngs) |R| {
if (filter == null or std.mem.indexOf(u8, R.name, filter.?) != null) {
try stdout.print("{s} (cryptographic, short outputs)\n", .{R.name});
const result_short = try benchmark(R, count, short_block_size);
try stdout.print(" {:5} MiB/s\n", .{result_short.throughput / (1 * MiB)});
}
}
}
}
}