Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Crystal::System::Random namespace #4450

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions spec/std/random/system_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
require "spec"
require "random/system"

describe "Random::System" do
rng = Random::System.new

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's worth testing one nontrivial case, like generating a 64 bit signed integer. It is not part of Random::System directly but having this called in real code can be better at pointing the compiler at something that accidentally went wrong.

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
2 changes: 2 additions & 0 deletions src/compiler/crystal/semantic/flags.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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) }

Expand Down
27 changes: 27 additions & 0 deletions src/crystal/system/random.cr
Original file line number Diff line number Diff line change
@@ -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 %}
16 changes: 16 additions & 0 deletions src/crystal/system/unix/arc4random.cr
Original file line number Diff line number Diff line change
@@ -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
84 changes: 84 additions & 0 deletions src/crystal/system/unix/getrandom.cr
Original file line number Diff line number Diff line change
@@ -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
32 changes: 32 additions & 0 deletions src/crystal/system/unix/urandom.cr
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions src/lib_c/amd64-unknown-openbsd/c/stdlib.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 2 additions & 9 deletions src/random.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
52 changes: 52 additions & 0 deletions src/random/system.cr
Original file line number Diff line number Diff line change
@@ -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
79 changes: 2 additions & 77 deletions src/secure_random.cr
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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)
Expand Down