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/compiler/crystal/semantic/flags.cr b/src/compiler/crystal/semantic/flags.cr index 0481644bc7ba..a2d0fa2a71cd 100644 --- a/src/compiler/crystal/semantic/flags.cr +++ b/src/compiler/crystal/semantic/flags.cr @@ -26,6 +26,8 @@ class Crystal::Program set.add "darwin" if set.any?(&.starts_with?("macosx")) || set.any?(&.starts_with?("darwin")) set.add "freebsd" if set.any?(&.starts_with?("freebsd")) set.add "openbsd" if set.any?(&.starts_with?("openbsd")) + set.add "unix" if set.any? { |flag| %w(cygnus darwin freebsd linux openbsd).includes?(flag) } + set.add "x86_64" if set.any?(&.starts_with?("amd64")) set.add "i686" if set.any? { |flag| %w(i586 i486 i386).includes?(flag) } diff --git a/src/crystal/system/random.cr b/src/crystal/system/random.cr new file mode 100644 index 000000000000..881c26cac462 --- /dev/null +++ b/src/crystal/system/random.cr @@ -0,0 +1,27 @@ +# :nodoc: +module Crystal + # :nodoc: + module System + # :nodoc: + 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 + +{% if flag?(:linux) %} + require "./unix/getrandom" +{% elsif flag?(:openbsd) %} + require "./unix/arc4random" +{% else %} + # TODO: restrict on flag?(:unix) after crystal > 0.22.0 is released + require "./unix/urandom" +{% end %} diff --git a/src/crystal/system/unix/arc4random.cr b/src/crystal/system/unix/arc4random.cr new file mode 100644 index 000000000000..67aa0867560a --- /dev/null +++ b/src/crystal/system/unix/arc4random.cr @@ -0,0 +1,16 @@ +{% skip_file unless flag?(:openbsd) %} + +require "c/stdlib" + +module Crystal::System::Random + # Fills *buffer* with random bytes using arc4random. + # + # NOTE: only secure on OpenBSD and CloudABI + 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 new file mode 100644 index 000000000000..68ae5035a0e9 --- /dev/null +++ b/src/crystal/system/unix/getrandom.cr @@ -0,0 +1,84 @@ +{% skip_file unless flag?(:linux) %} + +require "c/unistd" +require "c/sys/syscall" + +module Crystal::System::Random + @@initialized = false + @@getrandom_available = false + + private def self.init + @@initialized = true + + if sys_getrandom(Bytes.new(16)) >= 0 + @@getrandom_available = true + else + @@urandom = urandom = File.open("/dev/urandom", "r") + urandom.sync = true # don't buffer bytes + end + end + + # Reads n random bytes using the Linux `getrandom(2)` syscall. + def self.random_bytes(buf : Bytes) : Nil + init unless @@initialized + + if @@getrandom_available + getrandom(buf) + elsif urandom = @@urandom + urandom.read_fully(buf) + else + raise "Failed to access secure source to generate random bytes!" + 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 + # interrupted or returning early + chunk_size = 256 + + while buf.size > 0 + if buf.size < chunk_size + chunk_size = buf.size + end + + read_bytes = sys_getrandom(buf[0, chunk_size]) + raise Errno.new("getrandom") if read_bytes == -1 + + buf += read_bytes + end + end + + # Low-level wrapper for the `getrandom(2)` syscall, returns the number of + # bytes read or `-1` if an error occured (or the syscall isn't available) + # and sets `Errno.value`. + # + # We use the kernel syscall instead of the `getrandom` C function so any + # binary compiled for Linux will always use getrandom if the kernel is 3.17+ + # and silently fallback to read from /dev/urandom if not (so it's more + # portable). + private def self.sys_getrandom(buf : Bytes) + loop do + read_bytes = LibC.syscall(LibC::SYS_getrandom, buf, LibC::SizeT.new(buf.size), 0) + if read_bytes < 0 && (Errno.value == Errno::EINTR || Errno.value == Errno::EAGAIN) + Fiber.yield + else + return read_bytes + end + end + end +end diff --git a/src/crystal/system/unix/urandom.cr b/src/crystal/system/unix/urandom.cr new file mode 100644 index 000000000000..d42612dfa0b6 --- /dev/null +++ b/src/crystal/system/unix/urandom.cr @@ -0,0 +1,32 @@ +# TODO: replace with `flag?(:unix) && !flag?(:openbsd) && !flag?(:linux)` after crystal > 0.22.0 is released +{% skip_file if flag?(:openbsd) && flag?(:linux) %} + +module Crystal::System::Random + @@initialized = false + + private def self.init + @@initialized = true + @@urandom = urandom = File.open("/dev/urandom", "r") + urandom.sync = true # don't buffer bytes + end + + def self.random_bytes(buf : Bytes) : Nil + init unless @@initialized + + if urandom = @@urandom + urandom.read_fully(buf) + else + 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 5729203618e5..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,8 @@ 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 fun exit(x0 : Int) : NoReturn 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 diff --git a/src/secure_random.cr b/src/secure_random.cr index 5828c4044cde..d713f1a95a38 100644 --- a/src/secure_random.cr +++ b/src/secure_random.cr @@ -1,9 +1,5 @@ require "base64" - -{% if flag?(:linux) %} - require "c/unistd" - require "c/sys/syscall" -{% end %} +require "crystal/system/random" # The `SecureRandom` module is an interface for creating cryptography secure # random values in different formats. @@ -20,8 +16,6 @@ require "base64" # implementation and uses `getrandom` on Linux (when provided by the kernel), # then tries to read from `/dev/urandom`. module SecureRandom - @@initialized = false - # Generates *n* random bytes that are encoded into base64. # # Check `Base64#strict_encode` for details. @@ -80,78 +74,9 @@ module SecureRandom # slice # => [217, 118, 38, 196] # ``` def self.random_bytes(buf : Bytes) : Nil - init unless @@initialized - - {% if flag?(:linux) %} - if @@getrandom_available - getrandom(buf) - return - end - {% end %} - - if urandom = @@urandom - urandom.read_fully(buf) - return - end - - raise "Failed to access secure source to generate random bytes!" - end - - private def self.init - @@initialized = true - - {% if flag?(:linux) %} - if sys_getrandom(Bytes.new(16)) >= 0 - @@getrandom_available = true - return - end - {% end %} - - @@urandom = urandom = File.open("/dev/urandom", "r") - urandom.sync = true # don't buffer bytes + Crystal::System::Random.random_bytes(buf) end - {% if flag?(:linux) %} - @@getrandom_available = false - - # Reads n random bytes using the Linux `getrandom(2)` syscall. - private def self.getrandom(buf : Bytes) - # getrandom(2) may only read up to 256 bytes at once without being - # interrupted or returning early - chunk_size = 256 - - while buf.size > 0 - if buf.size < chunk_size - chunk_size = buf.size - end - - read_bytes = sys_getrandom(buf[0, chunk_size]) - raise Errno.new("getrandom") if read_bytes == -1 - - buf += read_bytes - end - end - - # Low-level wrapper for the `getrandom(2)` syscall, returns the number of - # bytes read or `-1` if an error occured (or the syscall isn't available) - # and sets `Errno.value`. - # - # We use the kernel syscall instead of the `getrandom` C function so any - # binary compiled for Linux will always use getrandom if the kernel is 3.17+ - # and silently fallback to read from /dev/urandom if not (so it's more - # portable). - private def self.sys_getrandom(buf : Bytes) - loop do - read_bytes = LibC.syscall(LibC::SYS_getrandom, buf, LibC::SizeT.new(buf.size), 0) - if read_bytes < 0 && (Errno.value == Errno::EINTR || Errno.value == Errno::EAGAIN) - Fiber.yield - else - return read_bytes - end - end - end - {% end %} - # Generates a UUID (Universally Unique Identifier). # # It generates a random v4 UUID. Check [RFC 4122 Section 4.4](https://tools.ietf.org/html/rfc4122#section-4.4)