From 704a05b048e736dc7a4f368a47f68a4152e31205 Mon Sep 17 00:00:00 2001 From: Julien Portalier Date: Tue, 23 May 2017 15:54:34 +0200 Subject: [PATCH] Add Random::System Allows to generate random numbers using a secure source provided by the system. It actually uses the same source as SecureRandom. Includes changes by Oleh Prypin (@prypin) to try and read as few bytes are required from `/dev/urandom`. --- spec/std/random/system_spec.cr | 16 +++++++ src/crystal/system/random.cr | 7 +++ src/crystal/system/unix/arc4random.cr | 4 ++ src/crystal/system/unix/getrandom.cr | 14 ++++++ src/crystal/system/unix/urandom.cr | 10 ++++ src/lib_c/amd64-unknown-openbsd/c/stdlib.cr | 1 + src/random.cr | 11 +---- src/random/system.cr | 52 +++++++++++++++++++++ 8 files changed, 106 insertions(+), 9 deletions(-) create mode 100644 spec/std/random/system_spec.cr create mode 100644 src/random/system.cr diff --git a/spec/std/random/system_spec.cr b/spec/std/random/system_spec.cr new file mode 100644 index 000000000000..ee55adeddbf5 --- /dev/null +++ b/spec/std/random/system_spec.cr @@ -0,0 +1,16 @@ +require "spec" +require "random/system" + +describe "Random::System" do + rng = Random::System.new + + it "returns random number from the secure system source" do + rng.next_u.should be_a(Int::Unsigned) + + x = rng.rand(123456...654321) + x.should be >= 123456 + x.should be < 654321 + + rng.rand(Int64::MAX / 2).should be <= (Int64::MAX / 2) + end +end diff --git a/src/crystal/system/random.cr b/src/crystal/system/random.cr index 24fe2e9b95f5..64fc6bbd13e1 100644 --- a/src/crystal/system/random.cr +++ b/src/crystal/system/random.cr @@ -6,6 +6,13 @@ module Crystal module Random # Fills *buffer* with random bytes from a secure source. # def self.random_bytes(buffer : Bytes) : Nil + + # Returns a random unsigned integer from a secure source. Implementations + # may choose the integer size to return based on what the system source + # provides. They may choose to return a single byte (UInt8) in which case + # `::Random` will prefer `#random_bytes` to read as many bytes as required + # at once, avoiding multiple reads or reading too many bytes. + # def self.next_u end end end diff --git a/src/crystal/system/unix/arc4random.cr b/src/crystal/system/unix/arc4random.cr index db8636b7150e..a389bafb7172 100644 --- a/src/crystal/system/unix/arc4random.cr +++ b/src/crystal/system/unix/arc4random.cr @@ -7,4 +7,8 @@ module Crystal::System::Random def self.random_bytes(buffer : Bytes) : Nil LibC.arc4random_buf(buffer.to_unsafe.as(Void*), buffer.size) end + + def self.next_u : UInt32 + LibC.arc4random + end end diff --git a/src/crystal/system/unix/getrandom.cr b/src/crystal/system/unix/getrandom.cr index 68159f4119ee..3ad517840ada 100644 --- a/src/crystal/system/unix/getrandom.cr +++ b/src/crystal/system/unix/getrandom.cr @@ -29,6 +29,20 @@ module Crystal::System::Random end end + def self.next_u : UInt8 + init unless @@initialized + + if @@getrandom_available + buf = uninitialized UInt8[1] + getrandom(buf.to_slice) + buf.to_unsafe.as(UInt8*).value + elsif urandom = @@urandom + urandom.read_byte.not_nil! + else + raise "Failed to access secure source to generate random bytes!" + end + end + # Reads n random bytes using the Linux `getrandom(2)` syscall. private def self.getrandom(buf) # getrandom(2) may only read up to 256 bytes at once without being diff --git a/src/crystal/system/unix/urandom.cr b/src/crystal/system/unix/urandom.cr index fd2d09251790..24b79359c191 100644 --- a/src/crystal/system/unix/urandom.cr +++ b/src/crystal/system/unix/urandom.cr @@ -16,4 +16,14 @@ module Crystal::System::Random raise "Failed to access secure source to generate random bytes!" end end + + def self.next_u : UInt8 + init unless @@initialized + + if urandom = @@urandom + urandom.read_bytes(UInt8) + else + raise "Failed to access secure source to generate random bytes!" + end + end end diff --git a/src/lib_c/amd64-unknown-openbsd/c/stdlib.cr b/src/lib_c/amd64-unknown-openbsd/c/stdlib.cr index 5a1ddf83e627..079cee258a8b 100644 --- a/src/lib_c/amd64-unknown-openbsd/c/stdlib.cr +++ b/src/lib_c/amd64-unknown-openbsd/c/stdlib.cr @@ -7,6 +7,7 @@ lib LibC rem : Int end + fun arc4random : UInt32 fun arc4random_buf(x0 : Void*, x1 : SizeT) : Void fun atof(x0 : Char*) : Double fun div(x0 : Int, x1 : Int) : DivT diff --git a/src/random.cr b/src/random.cr index 2086a1a5ec70..1319ef4c9697 100644 --- a/src/random.cr +++ b/src/random.cr @@ -194,12 +194,7 @@ module Random end loop do - # Build up the number combining multiple outputs from the RNG. - result = {{utype}}.new(next_u) - (needed_parts - 1).times do - result <<= sizeof(typeof(next_u))*8 - result |= {{utype}}.new(next_u) - end + result = rand_type({{utype}}, needed_parts) # For a uniform distribution we may need to throw away some numbers. if result < limit || limit == 0 @@ -228,9 +223,7 @@ module Random end # Generates a random integer in range `{{type}}::MIN..{{type}}::MAX`. - private def rand_type(type : {{type}}.class) : {{type}} - needed_parts = {{size/8}} / sizeof(typeof(next_u)) - + private def rand_type(type : {{type}}.class, needed_parts = sizeof({{type}}) / sizeof(typeof(next_u))) : {{type}} # Build up the number combining multiple outputs from the RNG. result = {{utype}}.new(next_u) (needed_parts - 1).times do diff --git a/src/random/system.cr b/src/random/system.cr new file mode 100644 index 000000000000..1c81cda9def0 --- /dev/null +++ b/src/random/system.cr @@ -0,0 +1,52 @@ +require "crystal/system/random" + +# Generates random numbers from a secure source of the system. +# +# For example `arc4random` is used on OpenBSD, whereas on Linux it uses +# `getrandom` (if the kernel supports it) and fallbacks on reading from +# `/dev/urandom` on UNIX systems. +struct Random::System + include Random + + def initialize + end + + def next_u + Crystal::System::Random.next_u + end + + {% for type in [UInt8, UInt16, UInt32, UInt64] %} + # Generates a random integer of a given type. The number of bytes to + # generate can be limited; by default it will generate as many bytes as + # needed to fill the integer size. + private def rand_type(type : {{type}}.class, needed_parts = nil) : {{type}} + needed_bytes = + if needed_parts + needed_parts * sizeof(typeof(next_u)) + else + sizeof({{type}}) + end + + buf = uninitialized UInt8[sizeof({{type}})] + + if needed_bytes < sizeof({{type}}) + bytes = Slice.new(buf.to_unsafe, needed_bytes) + Crystal::System::Random.random_bytes(bytes) + + bytes.reduce({{type}}.new(0)) do |result, byte| + (result << 8) | byte + end + else + Crystal::System::Random.random_bytes(buf.to_slice) + buf.to_unsafe.as({{type}}*).value + end + end + {% end %} + + {% for type in [Int8, Int16, Int32, Int64] %} + private def rand_type(type : {{type}}.class, needed_bytes = sizeof({{type}})) : {{type}} + result = rand_type({{"U#{type}".id}}, needed_bytes) + {{type}}.new(result) + end + {% end %} +end