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 }