diff --git a/spec/std/random_spec.cr b/spec/std/random_spec.cr index 13ac6bac52f8..11a09dc85cf4 100644 --- a/spec/std/random_spec.cr +++ b/spec/std/random_spec.cr @@ -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) @@ -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 @@ -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 diff --git a/spec/std/secure_random_spec.cr b/spec/std/secure_random_spec.cr index 4f4f3b9684be..c7022cfdeee6 100644 --- a/spec/std/secure_random_spec.cr +++ b/spec/std/secure_random_spec.cr @@ -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 diff --git a/src/random.cr b/src/random.cr index 81c1df20650b..b4b632e21366 100644 --- a/src/random.cr +++ b/src/random.cr @@ -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 + 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`. # # ``` @@ -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 @@ -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 %} diff --git a/src/secure_random.cr b/src/secure_random.cr index 5a76c32ce0e9..14a91fff8a82 100644 --- a/src/secure_random.cr +++ b/src/secure_random.cr @@ -1,3 +1,4 @@ +require "random" require "base64" {% if flag?(:linux) %} @@ -10,9 +11,13 @@ 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 @@ -20,6 +25,9 @@ require "base64" # implementation and uses `getrandom` on Linux (when provided by the kernel), # then tries to read from `/dev/urandom`. module SecureRandom + extend Random + extend self + @@initialized = false # Generates *n* random bytes that are encoded into Base64. @@ -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 @@ -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 @@ -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 @@ -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