crypto: Add rand_bytes

This adds `rand_bytes(dst: []byte)` which fills the destination buffer
with entropy from the cryptographic random number generator.  This takes
the "simple is best" approach and just directly returns the OS CSPRNG
output instead of doing anything fancy (a la OpenBSD's arc4random).
This commit is contained in:
Yawning Angel
2021-11-11 07:59:45 +00:00
parent 61c581baeb
commit 6bafa21bee
5 changed files with 96 additions and 0 deletions

View File

@@ -39,3 +39,14 @@ compare_byte_ptrs_constant_time :: proc "contextless" (a, b: ^byte, n: int) -> i
// iff v == 0, setting the sign-bit, which gets returned.
return int((u32(v)-1) >> 31)
}
// rand_bytes fills the dst buffer with cryptographic entropy taken from
// the system entropy source. This routine will block if the system entropy
// source is not ready yet. All system entropy source failures are treated
// as catastrophic, resulting in a panic.
rand_bytes :: proc (dst: []byte) {
// zero-fill the buffer first
mem.zero_explicit(raw_data(dst), len(dst))
_rand_bytes(dst)
}

View File

@@ -0,0 +1,7 @@
package crypto
when ODIN_OS != "linux" {
_rand_bytes :: proc (dst: []byte) {
unimplemented("crypto: rand_bytes not supported on this OS")
}
}

View File

@@ -0,0 +1,37 @@
package crypto
import "core:fmt"
import "core:os"
import "core:sys/unix"
_MAX_PER_CALL_BYTES :: 33554431 // 2^25 - 1
_rand_bytes :: proc (dst: []byte) {
dst := dst
l := len(dst)
for l > 0 {
to_read := min(l, _MAX_PER_CALL_BYTES)
ret := unix.sys_getrandom(raw_data(dst), to_read, 0)
if ret < 0 {
switch os.Errno(-ret) {
case os.EINTR:
// Call interupted by a signal handler, just retry the
// request.
continue
case os.ENOSYS:
// The kernel is apparently prehistoric (< 3.17 circa 2014)
// and does not support getrandom.
panic("crypto: getrandom not available in kernel")
case:
// All other failures are things that should NEVER happen
// unless the kernel interface changes (ie: the Linux
// developers break userland).
panic(fmt.tprintf("crypto: getrandom failed: %d", ret))
}
}
l -= ret
dst = dst[ret:]
}
}

View File

@@ -120,6 +120,7 @@ main :: proc() {
test_poly1305(&t)
test_chacha20poly1305(&t)
test_x25519(&t)
test_rand_bytes(&t)
bench_modern(&t)

View File

@@ -4,6 +4,7 @@ import "core:testing"
import "core:fmt"
import "core:mem"
import "core:time"
import "core:crypto"
import "core:crypto/chacha20"
import "core:crypto/chacha20poly1305"
@@ -303,6 +304,45 @@ test_x25519 :: proc(t: ^testing.T) {
// how to work with JSON.
}
@(test)
test_rand_bytes :: proc(t: ^testing.T) {
log(t, "Testing rand_bytes")
if ODIN_OS != "linux" {
log(t, "rand_bytes not supported - skipping")
return
}
allocator := context.allocator
buf := make([]byte, 1 << 25, allocator)
defer delete(buf)
// Testing a CSPRNG for correctness is incredibly involved and
// beyond the scope of an implementation that offloads
// responsibility for correctness to the OS.
//
// Just attempt to randomize a sufficiently large buffer, where
// sufficiently large is:
// * Larger than the maximum getentropy request size (256 bytes).
// * Larger than the maximum getrandom request size (2^25 - 1 bytes).
//
// While theoretically non-deterministic, if this fails, chances
// are the CSPRNG is busted.
seems_ok := false
for i := 0; i < 256; i = i + 1 {
mem.zero_explicit(raw_data(buf), len(buf))
crypto.rand_bytes(buf)
if buf[0] != 0 && buf[len(buf)-1] != 0 {
seems_ok = true
break
}
}
expect(t, seems_ok, "Expected to randomize the head and tail of the buffer within a handful of attempts")
}
@(test)
bench_modern :: proc(t: ^testing.T) {
fmt.println("Starting benchmarks:")