mirror of
https://github.com/ziglang/zig.git
synced 2025-12-06 14:23:09 +00:00
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.
98 lines
2.6 KiB
Zig
98 lines
2.6 KiB
Zig
//! 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;
|
|
}
|
|
}
|