Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: crystal-lang/crystal
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: a8b93366f0d1
Choose a base ref
...
head repository: crystal-lang/crystal
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: eeac6ac3e4c2
Choose a head ref
  • 6 commits
  • 10 files changed
  • 1 contributor

Commits on May 25, 2017

  1. Extract Sys::Random from SecureRandom

    Extracts the platform specific parts out of SecureRandom into a
    simple API in `sys/random` that should be implemented for each
    platform.
    ysbaddaden committed May 25, 2017
    Copy the full SHA
    0538f66 View commit details
  2. Copy the full SHA
    0c6135b View commit details
  3. 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`.
    ysbaddaden committed May 25, 2017
    Copy the full SHA
    704a05b View commit details

Commits on May 30, 2017

  1. Add skip_file macros to crystal/system implementations

    Protects against glob requires of `crystal/system/**` that would
    fail, since it would load conflicting or incompatible
    implementations.
    ysbaddaden committed May 30, 2017
    Copy the full SHA
    971e4fb View commit details
  2. Add :unix compilation flag

    This is automatically set based on the target triple for known unix
    systems. This should eventually simplify skip_file conditions for
    generic UNIX implementations.
    ysbaddaden committed May 30, 2017
    Copy the full SHA
    fde3b75 View commit details
  3. Merge pull request #4450 from ysbaddaden/extract-sys-random-namespace

    Crystal::System::Random namespace
    ysbaddaden authored May 30, 2017
    Copy the full SHA
    eeac6ac View commit details
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

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
@@ -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) }

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
@@ -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
11 changes: 2 additions & 9 deletions src/random.cr
Original file line number Diff line number Diff line change
@@ -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
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.
@@ -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)