diff --git a/core/crypto/crypto.odin b/core/crypto/crypto.odin index ddcc5d367..35e88c5ed 100644 --- a/core/crypto/crypto.odin +++ b/core/crypto/crypto.odin @@ -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) +} diff --git a/core/crypto/rand_generic.odin b/core/crypto/rand_generic.odin new file mode 100644 index 000000000..98890b5b1 --- /dev/null +++ b/core/crypto/rand_generic.odin @@ -0,0 +1,7 @@ +package crypto + +when ODIN_OS != "linux" { + _rand_bytes :: proc (dst: []byte) { + unimplemented("crypto: rand_bytes not supported on this OS") + } +} diff --git a/core/crypto/rand_linux.odin b/core/crypto/rand_linux.odin new file mode 100644 index 000000000..4d1183757 --- /dev/null +++ b/core/crypto/rand_linux.odin @@ -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:] + } +} diff --git a/tests/core/crypto/test_core_crypto.odin b/tests/core/crypto/test_core_crypto.odin index 731833096..2ad00be66 100644 --- a/tests/core/crypto/test_core_crypto.odin +++ b/tests/core/crypto/test_core_crypto.odin @@ -120,6 +120,7 @@ main :: proc() { test_poly1305(&t) test_chacha20poly1305(&t) test_x25519(&t) + test_rand_bytes(&t) bench_modern(&t) diff --git a/tests/core/crypto/test_core_crypto_modern.odin b/tests/core/crypto/test_core_crypto_modern.odin index b3d9e47fd..71adad137 100644 --- a/tests/core/crypto/test_core_crypto_modern.odin +++ b/tests/core/crypto/test_core_crypto_modern.odin @@ -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:")