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