Skip to content
Closed
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
38 changes: 38 additions & 0 deletions spec/std/random_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ RNG_DATA_64 = [148763248732657823u64, 18446744073709551615u64, 0u64,
32456325635673576u64, 2456245614625u64, 32452456246u64, 3956529762u64,
9823674982364u64, 234253464546456u64, 14345435645646u64]

class TestBytesRNG < TestRNG(UInt8)
def random_bytes(n)
result = Bytes.new(n) do
i = @i
@i = (i + 1) % @data.size
@data[i]
end
end

def next_u
fail "next_u should not have been called"
super
end
end

describe "Random" do
it "limited number" do
rand(1).should eq(0)
Expand Down Expand Up @@ -121,6 +136,23 @@ describe "Random" do
Random::DEFAULT.next_bool.should be_a(Bool)
end

it "fills a large buffer with random bytes" do
bytes = Random::DEFAULT.random_bytes(10000)
bytes[9990, 10].should_not eq(Slice(UInt8).new(10))
end

it "generates random bytes" do
rng = TestRNG.new([0xfa19443eu32, 1u32, 0x12345678u32])
rng.random_bytes(9).should eq Bytes[0x3e, 0x44, 0x19, 0xfa, 1, 0, 0, 0, 0x78]
rng.random_bytes(1).should eq Bytes[0x3e]
rng.random_bytes(4).should eq Bytes[1, 0, 0, 0]
rng.random_bytes(3).should eq Bytes[0x78, 0x56, 0x34]
rng.random_bytes(0).should eq Bytes[]

rng = TestRNG.new([12u8, 255u8, 11u8, 5u8, 122u8, 200u8, 192u8])
rng.random_bytes(7).should eq Bytes[12, 255, 11, 5, 122, 200, 192]
end

it "generates by accumulation" do
rng = TestRNG.new([234u8, 153u8, 0u8, 0u8, 127u8, 128u8, 255u8, 255u8])
rng.rand(65536).should eq 60057 # 234*0x100 + 153
Expand Down Expand Up @@ -176,4 +208,10 @@ describe "Random" do
rng.rand(Int8::MIN..Int8::MAX).should eq expected
end
end

it "generates from random bytes" do
rng = TestBytesRNG.new([255u8, 254u8, 234u8, 153u8])
rng.rand(258).should eq 201 # 255*0x100 + 254 [skip]-> (234*0x100 + 153) % 257
rng.rand(Int16::MIN..Int16::MAX).should eq -2
end
end
15 changes: 15 additions & 0 deletions spec/std/secure_random_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,19 @@ describe SecureRandom do
uuid.should match(/\A[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{4}[0-9a-f]{8}\Z/)
end
end

describe "common random operations" do
it "rand" do
x = SecureRandom.rand(123456...654321)
x.should be >= 123456
x.should be < 654321
end

it "shuffle!" do
a = [1, 2, 3]
a.shuffle!(SecureRandom)
b = [1, 2, 3]
3.times { a.includes?(b.shift).should be_true }
end
end
end
51 changes: 35 additions & 16 deletions src/random.cr
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ module Random
# The integers must be uniformly distributed between 0 and the maximal value for the chosen type.
abstract def next_u : UInt

# Generates a slice filled with *n* random bytes.
def random_bytes(n : Int) : Bytes
Copy link
Member

Choose a reason for hiding this comment

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

It would be ideal if this worked the same was as IO#read.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't understand

Copy link
Member

Choose a reason for hiding this comment

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

IO#read takes a slice of a fixed size and fills it. It's designed so that you can reuse the slice efficiently.

This takes a size and returns a slice of that size, which means that the allocation isn't controlled by the caller. I know why you do this for performance when filling it with integers, but I dislike the inconsistency.

Copy link
Member Author

@oprypin oprypin Oct 17, 2016

Choose a reason for hiding this comment

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

@RX14 oh, I've considered doing this (I think avoiding allocations could give even better performance actually), but in the end I think simplicity is more important. The consistency is with SecureRandom.random_bytes.

Anyway, I don't mind changing this, but I'm not sure if it's best. Leaving as is for now.

Copy link
Contributor

Choose a reason for hiding this comment

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

Some buffering would be interesting here too. random_bytes could always get at least.. say.. 32 random bytes from the source. And then successive calls would use this buffer until it is depleted and it needs to be repopulated again. Anyway, this is an improvement for the future. +1 for this pull request.

Copy link
Member Author

Choose a reason for hiding this comment

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

Buffering would solve many problems that were kind of worked around. But... that just seems not... secure. So I avoided it on purpose.

n = n.to_i
size = (n + sizeof(typeof(next_u)) - 1) / sizeof(typeof(next_u)) # (n / sizeof(next_u)).ceil
result = Slice.new(size) { next_u }
result.to_unsafe.as(UInt8*).to_slice(n)
end

# Generates a random `Bool`.
#
# ```
Expand Down Expand Up @@ -188,12 +196,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 @@ -222,16 +225,32 @@ 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))

# 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
{{type}}.new(result)
#
# However, if the *needed_parts* argument is specified, the underlying RNG will be called
# only this number of times.
private def rand_type(type : {{type}}.class,
needed_parts = sizeof({{type}}) / sizeof(typeof(next_u)))
\{% if @type.methods.any? { |meth| meth.name == "random_bytes" } %}
# The RNG type overrode `random_bytes`, so we assume that using it is more optimal.

result = {{utype}}.new(0)
random_bytes(needed_parts).each do |x|
result <<= 8
result |= x
end

{{type}}.new(result)
\{% else %}
# 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

{{type}}.new(result)
\{% end %}
end
{% end %}
{% end %}
Expand Down
28 changes: 20 additions & 8 deletions src/secure_random.cr
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
require "random"
require "base64"

{% if flag?(:linux) %}
Expand All @@ -10,16 +11,23 @@ require "base64"
#
# Examples:
# ```crystal
# SecureRandom.base64 # => "LIa9s/zWzJx49m/9zDX+VQ=="
# SecureRandom.hex # => "c8353864ff9764a39ef74983ec0d4a38"
# SecureRandom.uuid # => "c7ee4add-207f-411a-97b7-0d22788566d6"
# SecureRandom.random_bytes(8) # => Bytes[141, 161, 130, 39, 247, 150, 68, 233]
# SecureRandom.base64 # => "LIa9s/zWzJx49m/9zDX+VQ=="
# SecureRandom.hex # => "c8353864ff9764a39ef74983ec0d4a38"
# SecureRandom.uuid # => "c7ee4add-207f-411a-97b7-0d22788566d6"
#
# SecureRandom.rand(10000) # => 4264
# [1, 2, 3].shuffle(SecureRandom) # => [1, 3, 2]
# ```
#
# The implementation follows the
# [libsodium sysrandom](https://github.com/jedisct1/libsodium/blob/6fad3644b53021fb377ca1207fa6e1ac96d0b131/src/libsodium/randombytes/sysrandom/randombytes_sysrandom.c)
# implementation and uses `getrandom` on Linux (when provided by the kernel),
# then tries to read from `/dev/urandom`.
module SecureRandom
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 SecureRandom should become Random::Secure to be more consistent, now that it's a Random implementation.

extend Random
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 a bit weird that SecureRandom doesn't include Random like Random::MT19937. I guess it's just because it doesn't need a seed?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's a module which can't be instantiated. I was really surprised that because extend is used, the SecureRandom module itself can be used exactly like an instance of a class including Random

extend self

@@initialized = false

# Generates *n* random bytes that are encoded into Base64.
Expand All @@ -29,7 +37,7 @@ module SecureRandom
# ```crystal
# SecureRandom.base64(4) # => "fK1eYg=="
# ```
def self.base64(n : Int = 16) : String
def base64(n : Int = 16) : String
Base64.strict_encode(random_bytes(n))
end

Expand All @@ -42,7 +50,7 @@ module SecureRandom
# SecureRandom.urlsafe_base64(8, true) # => "vvP1kcs841I="
# SecureRandom.urlsafe_base64(16, true) # => "og2aJrELDZWSdJfVGkxNKw=="
# ```
def self.urlsafe_base64(n : Int = 16, padding = false) : String
def urlsafe_base64(n : Int = 16, padding = false) : String
Base64.urlsafe_encode(random_bytes(n), padding)
end

Expand All @@ -54,17 +62,21 @@ module SecureRandom
# SecureRandom.hex # => "05f100a1123f6bdbb427698ab664ff5f"
# SecureRandom.hex(1) # => "1a"
# ```
def self.hex(n : Int = 16) : String
def hex(n : Int = 16) : String
random_bytes(n).hexstring
end

def next_u : UInt8
random_bytes(1)[0]
end

# Generates a slice filled with *n* random bytes.
#
# ```crystal
# SecureRandom.random_bytes # => [145, 255, 191, 133, 132, 139, 53, 136, 93, 238, 2, 37, 138, 244, 3, 216]
# SecureRandom.random_bytes(4) # => [217, 118, 38, 196]
# ```
def self.random_bytes(n : Int = 16) : Slice(UInt8)
def random_bytes(n : Int = 16) : Slice(UInt8)
if n < 0
raise ArgumentError.new "negative size: #{n}"
end
Expand Down Expand Up @@ -155,7 +167,7 @@ module SecureRandom
# ```crystal
# SecureRandom.uuid # => "a4e319dd-a778-4a51-804e-66a07bc63358"
# ```
def self.uuid : String
def uuid : String
bytes = random_bytes(16)
bytes[6] = (bytes[6] & 0x0f) | 0x40
bytes[8] = (bytes[8] & 0x3f) | 0x80
Expand Down