-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
ysbaddaden
merged 5 commits into
crystal-lang:master
from
ysbaddaden:extract-sys-random-namespace
May 30, 2017
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
0538f66
Extract Sys::Random from SecureRandom
ysbaddaden 0c6135b
Use arc4random_buf to fill random bytes on OpenBSD
ysbaddaden 704a05b
Add Random::System
ysbaddaden 971e4fb
Add skip_file macros to crystal/system implementations
ysbaddaden fde3b75
Add :unix compilation flag
ysbaddaden File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.