From dff4bbfd2426ce4943972782d2bcffc89b3fd26d Mon Sep 17 00:00:00 2001 From: Frank Denis <124872+jedisct1@users.noreply.github.com> Date: Tue, 21 Mar 2023 05:54:10 +0100 Subject: [PATCH] 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. --- lib/std/crypto.zig | 11 - lib/std/crypto/ascon.zig | 11 + lib/std/crypto/benchmark.zig | 2 - lib/std/crypto/chacha20.zig | 77 ++++- lib/std/crypto/gimli.zig | 527 ----------------------------------- lib/std/crypto/tlcsprng.zig | 18 +- lib/std/crypto/xoodoo.zig | 141 ---------- lib/std/rand.zig | 5 +- lib/std/rand/Ascon.zig | 47 ++-- lib/std/rand/ChaCha.zig | 97 +++++++ lib/std/rand/Gimli.zig | 34 --- lib/std/rand/Xoodoo.zig | 42 --- lib/std/rand/benchmark.zig | 217 +++++++++++++++ 13 files changed, 443 insertions(+), 786 deletions(-) delete mode 100644 lib/std/crypto/gimli.zig delete mode 100644 lib/std/crypto/xoodoo.zig create mode 100644 lib/std/rand/ChaCha.zig delete mode 100644 lib/std/rand/Gimli.zig delete mode 100644 lib/std/rand/Xoodoo.zig create mode 100644 lib/std/rand/benchmark.zig diff --git a/lib/std/crypto.zig b/lib/std/crypto.zig index 9b995480aa..6f0d1d9b6e 100644 --- a/lib/std/crypto.zig +++ b/lib/std/crypto.zig @@ -17,8 +17,6 @@ pub const aead = struct { pub const Aes256Ocb = @import("crypto/aes_ocb.zig").Aes256Ocb; }; - pub const Gimli = @import("crypto/gimli.zig").Aead; - pub const chacha_poly = struct { pub const ChaCha20Poly1305 = @import("crypto/chacha20.zig").ChaCha20Poly1305; 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 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. /// @@ -87,7 +83,6 @@ pub const ecc = struct { pub const hash = struct { pub const blake2 = @import("crypto/blake2.zig"); 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 Sha1 = @import("crypto/sha1.zig").Sha1; pub const sha2 = @import("crypto/sha2.zig"); @@ -221,8 +216,6 @@ test { _ = aead.aes_ocb.Aes128Ocb; _ = aead.aes_ocb.Aes256Ocb; - _ = aead.Gimli; - _ = aead.chacha_poly.ChaCha20Poly1305; _ = aead.chacha_poly.ChaCha12Poly1305; _ = aead.chacha_poly.ChaCha8Poly1305; @@ -239,8 +232,6 @@ test { _ = core.aes; _ = core.Ascon; - _ = core.Gimli; - _ = core.Xoodoo; _ = core.modes; _ = dh.X25519; @@ -256,7 +247,6 @@ test { _ = hash.blake2; _ = hash.Blake3; - _ = hash.Gimli; _ = hash.Md5; _ = hash.Sha1; _ = hash.sha2; @@ -334,7 +324,6 @@ test "issue #4532: no index out of bounds" { hash.blake2.Blake2b256, hash.blake2.Blake2b384, hash.blake2.Blake2b512, - hash.Gimli, }; inline for (types) |Hasher| { diff --git a/lib/std/crypto/ascon.zig b/lib/std/crypto/ascon.zig index 6de003d436..f37d9acea5 100644 --- a/lib/std/crypto/ascon.zig +++ b/lib/std/crypto/ascon.zig @@ -168,6 +168,17 @@ pub fn State(comptime endian: builtin.Endian) type { 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. inline fn round(state: *Self, rk: u64) void { const x = &state.st; diff --git a/lib/std/crypto/benchmark.zig b/lib/std/crypto/benchmark.zig index ff098c7804..f512c513e7 100644 --- a/lib/std/crypto/benchmark.zig +++ b/lib/std/crypto/benchmark.zig @@ -29,7 +29,6 @@ const hashes = [_]Crypto{ Crypto{ .ty = crypto.hash.sha3.Shake256, .name = "shake-256" }, Crypto{ .ty = crypto.hash.sha3.TurboShake128(null), .name = "turboshake-128" }, 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.Blake2b512, .name = "blake2b" }, 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.XChaCha8Poly1305, .name = "xchacha8Poly1305" }, 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.Aegis256, .name = "aegis-256" }, Crypto{ .ty = crypto.aead.aes_gcm.Aes128Gcm, .name = "aes128-gcm" }, diff --git a/lib/std/crypto/chacha20.zig b/lib/std/crypto/chacha20.zig index 883ee51a62..aa0f148be9 100644 --- a/lib/std/crypto/chacha20.zig +++ b/lib/std/crypto/chacha20.zig @@ -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 { var c: [4]u32 = undefined; 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 { var c: [4]u32 = undefined; for (c, 0..) |_, i| { @@ -387,6 +427,8 @@ fn ChaChaIETF(comptime rounds_nb: usize) type { pub const nonce_length = 12; /// Key length in bytes. 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`. /// 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]); 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; /// Key length in bytes. 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`. /// 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[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 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); } + + /// 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; /// Key length in bytes. 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`. /// 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); 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); + } }; } diff --git a/lib/std/crypto/gimli.zig b/lib/std/crypto/gimli.zig deleted file mode 100644 index 0189f4c359..0000000000 --- a/lib/std/crypto/gimli.zig +++ /dev/null @@ -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); - } -} diff --git a/lib/std/crypto/tlcsprng.zig b/lib/std/crypto/tlcsprng.zig index 5a09e20f07..ac706e5f6a 100644 --- a/lib/std/crypto/tlcsprng.zig +++ b/lib/std/crypto/tlcsprng.zig @@ -41,9 +41,11 @@ const maybe_have_wipe_on_fork = builtin.os.isAtLeast(.linux, .{ }) orelse true; const is_haiku = builtin.os.tag == .haiku; +const Rng = std.rand.DefaultCsprng; + const Context = struct { init_state: enum(u8) { uninitialized = 0, initialized, failed }, - gimli: std.crypto.core.Gimli, + rng: Rng, }; var install_atfork_handler = std.once(struct { @@ -93,7 +95,7 @@ fn tlsCsprngFill(_: *anyopaque, buffer: []u8) void { const S = struct { threadlocal var buf: Context align(mem.page_size) = .{ .init_state = .uninitialized, - .gimli = undefined, + .rng = undefined, }; }; wipe_mem = mem.asBytes(&S.buf); @@ -156,12 +158,7 @@ fn childAtForkHandler() callconv(.C) void { fn fillWithCsprng(buffer: []u8) void { const ctx = @ptrCast(*Context, wipe_mem.ptr); - if (buffer.len != 0) { - ctx.gimli.squeeze(buffer); - } else { - ctx.gimli.permute(); - } - mem.set(u8, ctx.gimli.toSlice()[0..std.crypto.core.Gimli.RATE], 0); + return ctx.rng.fill(buffer); } pub fn defaultRandomSeed(buffer: []u8) void { @@ -169,7 +166,7 @@ pub fn defaultRandomSeed(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 // to override the default seed function. This also makes // `std.crypto.random` available on freestanding targets, provided that @@ -177,7 +174,8 @@ fn initAndFill(buffer: []u8) void { std.options.cryptoRandomSeed(&seed); 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 // in stack overflows instead of invalid random data. diff --git a/lib/std/crypto/xoodoo.zig b/lib/std/crypto/xoodoo.zig deleted file mode 100644 index ea3554a635..0000000000 --- a/lib/std/crypto/xoodoo.zig +++ /dev/null @@ -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); -} diff --git a/lib/std/rand.zig b/lib/std/rand.zig index 9722a47682..4f9cb0db56 100644 --- a/lib/std/rand.zig +++ b/lib/std/rand.zig @@ -18,11 +18,12 @@ const maxInt = std.math.maxInt; pub const DefaultPrng = Xoshiro256; /// Cryptographically secure random numbers. -pub const DefaultCsprng = Ascon; +pub const DefaultCsprng = ChaCha; pub const Ascon = @import("rand/Ascon.zig"); +pub const ChaCha = @import("rand/ChaCha.zig"); + pub const Isaac64 = @import("rand/Isaac64.zig"); -pub const Xoodoo = @import("rand/Xoodoo.zig"); pub const Pcg = @import("rand/Pcg.zig"); pub const Xoroshiro128 = @import("rand/Xoroshiro128.zig"); pub const Xoshiro256 = @import("rand/Xoshiro256.zig"); diff --git a/lib/std/rand/Ascon.zig b/lib/std/rand/Ascon.zig index b6e8ce4899..3600e02905 100644 --- a/lib/std/rand/Ascon.zig +++ b/lib/std/rand/Ascon.zig @@ -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 min = std.math.min; @@ -6,30 +14,38 @@ const mem = std.mem; const Random = std.rand.Random; 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; /// The seed must be uniform, secret and `secret_seed_length` bytes long. pub fn init(secret_seed: [secret_seed_length]u8) Self { - var state = std.crypto.core.Ascon(.Little).initXofA(); - var i: usize = 0; - while (i + rate <= secret_seed.len) : (i += rate) { - 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 }; + var self = Self{ .state = Ascon.initXof() }; + self.addEntropy(&secret_seed); + return self; } +/// 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 { return Random.init(self, fill); } +/// Fills the buffer with random bytes. pub fn fill(self: *Self, buf: []u8) void { var i: usize = 0; while (true) { @@ -40,6 +56,5 @@ pub fn fill(self: *Self, buf: []u8) void { self.state.permuteR(8); i += n; } - self.state.clear(0, rate); - self.state.permuteR(8); + self.state.permuteRatchet(6, rate); } diff --git a/lib/std/rand/ChaCha.zig b/lib/std/rand/ChaCha.zig new file mode 100644 index 0000000000..0992aeb97e --- /dev/null +++ b/lib/std/rand/ChaCha.zig @@ -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; + } +} diff --git a/lib/std/rand/Gimli.zig b/lib/std/rand/Gimli.zig deleted file mode 100644 index 7b7720503e..0000000000 --- a/lib/std/rand/Gimli.zig +++ /dev/null @@ -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); -} diff --git a/lib/std/rand/Xoodoo.zig b/lib/std/rand/Xoodoo.zig deleted file mode 100644 index 3e16f2a864..0000000000 --- a/lib/std/rand/Xoodoo.zig +++ /dev/null @@ -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(); -} diff --git a/lib/std/rand/benchmark.zig b/lib/std/rand/benchmark.zig new file mode 100644 index 0000000000..668b664c27 --- /dev/null +++ b/lib/std/rand/benchmark.zig @@ -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)}); + } + } + } + } +}