lib/sys/src/random.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const builtin = @import("builtin");
  3 const capabilities = @import("capabilities.zig");
  4 const linux = @import("linux.zig");
  5 
  6 pub const required_capabilities = switch (builtin.os.tag) {
  7     .linux => capabilities.noLibc(&.{ .process, .random }),
  8     else => capabilities.host(&.{ .process, .random }),
  9 };
 10 
 11 pub const SecureError = error{Unavailable};
 12 
 13 const csprng_u16_reseed_draws: u32 = 1 << 16;
 14 
 15 const SystemEntropy = struct {
 16     fn fill(_: *const SystemEntropy, buffer: []u8) SecureError!void {
 17         return secureBytes(buffer);
 18     }
 19 };
 20 
 21 const CsprngU16Stream = struct {
 22     generator: std.Random.DefaultCsprng = undefined,
 23     process_epoch: u64 = 0,
 24     draws: u32 = 0,
 25     initialized: bool = false,
 26 
 27     fn next(self: *CsprngU16Stream, epoch: u64, entropy: anytype) SecureError!u16 {
 28         std.debug.assert(self.draws <= csprng_u16_reseed_draws);
 29         if (!self.initialized or self.process_epoch != epoch or
 30             self.draws == csprng_u16_reseed_draws)
 31         {
 32             try self.reseed(epoch, entropy);
 33         }
 34         var bytes: [2]u8 = undefined;
 35         self.generator.fill(&bytes);
 36         self.draws += 1;
 37         std.debug.assert(self.draws <= csprng_u16_reseed_draws);
 38         return std.mem.readInt(u16, &bytes, .native);
 39     }
 40 
 41     fn reseed(self: *CsprngU16Stream, epoch: u64, entropy: anytype) SecureError!void {
 42         var seed: [std.Random.DefaultCsprng.secret_seed_length]u8 = undefined;
 43         defer std.crypto.secureZero(u8, &seed);
 44         try entropy.fill(&seed);
 45         self.generator = .init(seed);
 46         self.process_epoch = epoch;
 47         self.draws = 0;
 48         self.initialized = true;
 49     }
 50 };
 51 
 52 const TestEntropy = struct {
 53     successes: u8 = 0,
 54     failures_remaining: u8 = 0,
 55 
 56     fn fill(self: *TestEntropy, buffer: []u8) SecureError!void {
 57         if (self.failures_remaining != 0) {
 58             self.failures_remaining -= 1;
 59             return error.Unavailable;
 60         }
 61         self.successes += 1;
 62         for (buffer, 0..) |*byte, index| {
 63             byte.* = self.successes +% @as(u8, @truncate(index));
 64         }
 65     }
 66 };
 67 
 68 threadlocal var csprng_u16_stream: CsprngU16Stream = .{};
 69 const system_entropy: SystemEntropy = .{};
 70 
 71 pub fn secureBytes(buffer: []u8) SecureError!void {
 72     switch (builtin.os.tag) {
 73         .linux => return linuxSecureBytes(buffer),
 74         .freestanding, .wasi => return error.Unavailable,
 75         else => std.Io.Threaded.global_single_threaded.io().randomSecure(buffer) catch
 76             return error.Unavailable,
 77     }
 78 }
 79 
 80 pub fn seedU64(fallback: u64) u64 {
 81     var seed: u64 = undefined;
 82     secureBytes(std.mem.asBytes(&seed)) catch return fallback;
 83     return seed;
 84 }
 85 
 86 pub fn csprngU16() SecureError!u16 {
 87     const epoch = processEpoch() orelse return freshSecureU16();
 88     return csprng_u16_stream.next(epoch, &system_entropy);
 89 }
 90 
 91 fn freshSecureU16() SecureError!u16 {
 92     var bytes: [2]u8 = undefined;
 93     try secureBytes(&bytes);
 94     return std.mem.readInt(u16, &bytes, .native);
 95 }
 96 
 97 fn processEpoch() ?u64 {
 98     return switch (comptime builtin.os.tag) {
 99         .linux => @intCast(linux.processId()),
100         .windows, .wasi, .freestanding => 0,
101         else => posixProcessEpoch(),
102     };
103 }
104 
105 fn posixProcessEpoch() ?u64 {
106     if (comptime !@hasDecl(std.posix.system, "getpid")) return null;
107     if (comptime @TypeOf(std.posix.system.getpid) == void) return null;
108     return @intCast(std.posix.system.getpid());
109 }
110 
111 fn linuxSecureBytes(buffer: []u8) SecureError!void {
112     var filled: usize = 0;
113     while (filled < buffer.len) {
114         const rc = linux.getRandom(buffer[filled..]);
115         switch (linux.errno(rc)) {
116             .success => {
117                 if (rc == 0) return error.Unavailable;
118                 filled += rc;
119             },
120             .intr => {},
121             else => return error.Unavailable,
122         }
123     }
124 }
125 
126 test "seedU64 is callable" {
127     _ = seedU64(0);
128 }
129 
130 test "CSPRNG u16 stream reseeds across epochs and its draw bound" {
131     var entropy: TestEntropy = .{};
132     var stream: CsprngU16Stream = .{};
133     _ = try stream.next(41, &entropy);
134     _ = try stream.next(41, &entropy);
135     try std.testing.expectEqual(@as(u8, 1), entropy.successes);
136     try std.testing.expectEqual(@as(u32, 2), stream.draws);
137 
138     stream.draws = csprng_u16_reseed_draws;
139     _ = try stream.next(41, &entropy);
140     try std.testing.expectEqual(@as(u8, 2), entropy.successes);
141     try std.testing.expectEqual(@as(u32, 1), stream.draws);
142 
143     _ = try stream.next(42, &entropy);
144     try std.testing.expectEqual(@as(u8, 3), entropy.successes);
145     try std.testing.expectEqual(@as(u64, 42), stream.process_epoch);
146 }
147 
148 test "CSPRNG u16 stream retries failed initialization" {
149     var entropy: TestEntropy = .{ .failures_remaining = 1 };
150     var stream: CsprngU16Stream = .{};
151     try std.testing.expectError(error.Unavailable, stream.next(7, &entropy));
152     try std.testing.expect(!stream.initialized);
153     _ = try stream.next(7, &entropy);
154     try std.testing.expect(stream.initialized);
155     try std.testing.expectEqual(@as(u32, 1), stream.draws);
156 }
157 
158 test "CSPRNG u16 stream reseeds after fork" {
159     if (comptime builtin.os.tag != .linux) return error.SkipZigTest;
160     _ = try csprngU16();
161     const parent_epoch = csprng_u16_stream.process_epoch;
162     const fork_result = std.os.linux.fork();
163     switch (std.posix.errno(fork_result)) {
164         .SUCCESS => {},
165         .AGAIN, .NOMEM => return error.SkipZigTest,
166         else => |err| return std.posix.unexpectedErrno(err),
167     }
168     if (fork_result == 0) {
169         _ = csprngU16() catch std.os.linux.exit(2);
170         const child_epoch: u64 = @intCast(std.os.linux.getpid());
171         if (csprng_u16_stream.process_epoch != child_epoch) std.os.linux.exit(3);
172         if (csprng_u16_stream.process_epoch == parent_epoch) std.os.linux.exit(4);
173         std.os.linux.exit(0);
174     }
175 
176     var status: i32 = undefined;
177     const waited = std.os.linux.waitpid(@intCast(fork_result), &status, 0);
178     switch (std.posix.errno(waited)) {
179         .SUCCESS => {},
180         else => |err| return std.posix.unexpectedErrno(err),
181     }
182     try std.testing.expectEqual(fork_result, waited);
183     try std.testing.expectEqual(@as(i32, 0), status);
184 }