-
-
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
Merge SecureRandom into Random and Random::Secure #4894
Merge SecureRandom into Random and Random::Secure #4894
Conversation
Looks like the formatter failed. |
d1e9d82
to
af8393f
Compare
Indeed. I pushed again. |
I'm not sure if it makes much sense to split this change into two commits. Future readers will just get confused by a commit that adds a bunch of stuff and a commit that deletes a bunch of stuff, when really it's mostly being moved. |
Having 2 commits may help reviewing each patch, avoiding remove/renamings noise. Yet, I agree we should "squash and merge" into a single one if we merge this. |
# Random.new.random_bytes(slice) | ||
# slice # => [217, 118, 38, 196] | ||
# ``` | ||
def random_bytes(buf : Bytes) : Nil |
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.
The one method I added is this one: a generic random_bytes(buf)
that depends on next_u
size, to consume as few numbers as possibles from the PRNG.
I'm not very confident in the filling remaining bytes part (when buf.size
isn't a multiple of sizeof(next_u)
), and would like some review.
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 don't think, it is endianness really matters for 'random_bytes'.
And with this assumption, your 'tail filling' is perfectly correct.
Note, some PRNG can have more efficient implementation of 'random_bytes'.
Chacha20 generates 64 bytes at once, for example.
May be it is better to check 'respond_to?(:random_bytes)' first?
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 see: method should be called differently. random_bytes_native
?
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.
This code is in the base Random
class. PRNGs are free to override random_bytes
, the only bad thing is that they'd have to reimplement the buffer filling logic.
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.
@oprypin , ok, I see.
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.
@ysbaddaden The logic seems good to me :-)
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.
@asterite, but were you reviewing the old code that these comments are on, or the new code?
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.
@oprypin I only see one code, the same I see when I check out the code:
def random_bytes(buf : Bytes) : Nil
ptr = buf.to_unsafe
finish = buf.to_unsafe + buf.size
while ptr < finish
random = next_u
rand_ptr = pointerof(random).as(UInt8*)
if IO::ByteFormat::SystemEndian != IO::ByteFormat::LittleEndian
rand_ptr.to_slice(sizeof(typeof(next_u))).reverse!
end
rand_ptr.copy_to(ptr, {finish - ptr, sizeof(typeof(next_u))}.min)
ptr += sizeof(typeof(next_u))
end
end
Linux 32-bit crashed again on Travis? |
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 don't mind this being rebased instead of squashed. I like having an explanatory commit log.
src/random.cr
Outdated
# ``` | ||
def random_bytes(buf : Bytes) : Nil | ||
n = buf.size / sizeof(typeof(next_u)) | ||
remaining = buf.size - n * sizeof(typeof(next_u)) |
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.
Couldn't this be replaced by divmod?
src/random.cr
Outdated
remaining.times do |i| | ||
bits = i * 8 | ||
mask = typeof(next_u).new(0xff << bits) | ||
buf[-i - 1] = UInt8.new((bytes & mask) >> bits) |
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.
The -i - 1
is cryptic. It needs a note at least.
I'd prefer to go for the simple method of creating a base = n * sizeof(typeof(next_u))
and using base + i
to index.
src/random.cr
Outdated
# | ||
# It is recommended to use the secure `Random::System` as a source or another | ||
# cryptographically quality PRNG such as `Random::ISAAC` or ChaCha20. | ||
def uuid : String |
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'd like UUID to be moved into it's own struct with methods before 1.0 so hopefully this method should disappear and be replaced by a constructor on that. Doesn't help with the hex/base64 methods though. I don't think that using Random::DEFAULT.hex
will be a world-ending issue in Crystal anyway (not that it's usually the correct thing to do) because PCG32 has fairly strong guarantees.
@@ -1,5 +1,3 @@ | |||
require "secure_random" |
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.
oops, my bad.
src/random.cr
Outdated
remaining = buf.size - n * sizeof(typeof(next_u)) | ||
|
||
slice = buf.to_unsafe.as(typeof(next_u)*).to_slice(n) | ||
slice.each_index { |i| slice[i] = next_u } |
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.
This gives different results depending on endianness. May be worth using IO::ByteFormat::LittleEndian
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.
Also seems like the last few bytes are big-endian while the rest are (on most systems) little endian.
I'm suggesting a different implementation in ysbaddaden#2
spec/std/random_spec.cr
Outdated
bytes = Bytes.new(2000) | ||
TestRNG.new(RNG_DATA_32).random_bytes(bytes) | ||
bytes.size.should eq 2000 | ||
bytes[1990, 10].should_not eq(Bytes.new(10)) |
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.
These specs don't really check anything.
Needs something like https://github.com/crystal-lang/crystal/pull/3434/files#diff-0e9f78d10caafce0823cedec53f1c6f0
I'm suggesting an implementation in ysbaddaden#3
Is this Pull request breaks seed code on #4675? |
Probably it will break, if will be merged before. |
Thanks for accepting my changes! As a note, if these commits are squashed during merge, authorship info will be lost. So better apply whatever level of squashing is wanted manually. |
I like this, but why not call it |
I kept the If others agree, I'll change the naming, I shall change the specs to expect a fixed result (for |
If someone could review @oprypin's rewrite of the generic |
random = next_u | ||
rand_ptr = pointerof(random).as(UInt8*) | ||
if IO::ByteFormat::SystemEndian != IO::ByteFormat::LittleEndian | ||
rand_ptr.to_slice(sizeof(typeof(next_u))).reverse! |
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.
Would it be possible to save result of typeof(next_u)
call into a variable? In this way we wouldn't need to call it 3 more times.
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.
These are not actual calls, it's a fully compile-time construct. Using a variable could prevent some compiler optimizations.
This is pure speculation though.
while ptr < finish | ||
random = next_u | ||
rand_ptr = pointerof(random).as(UInt8*) | ||
if IO::ByteFormat::SystemEndian != IO::ByteFormat::LittleEndian |
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.
Is it needed for anything except tests?
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.
This is needed to produce the same results for the same seed, on different systems.
Note that this code is NOT only used for Random::System
I just suggest using |
I think providing the system RNG is pointless if it's not secure (i.e. we can do better in userspace), so we should call it |
@ysbaddaden, it seems like the rename to |
SecureRandom has been dropped in favor to Random::Secure (renamed from Random::System) which is now the secure source for random numbers suitable for cryptography. Moves SecureRandom#random_bytes(Bytes) into Random::Secure, and implements a generic Random#random_bytes(Bytes) to fill a Bytes with random numbers from any PRNG. Moves SecureRandom methods such as #base64 and #uuid into Random, so any PRNG implementation may use these methods; they should only be used with a secure PRNG thought (except maybe in test suites). This change allows to swap PRNG at will. For example swap Random::Secure for Random::ISAAC (seeded from Random::Secure), or use a cryptographically secure but slow PRNG in production, and an insecure but fast and determinable PRNG in test suites, for example.
e5394d7
to
79d0358
Compare
All done. Waiting for CI to run. |
As per #4882 (comment) keeping
SecureRandom
is a source of duplication and of confusion. Despite the explicit naming, there are contradictory understandings as what should be the source for secure random numbers in different contexts:SecureRandom
,Random::System
orCrystal::System::Random
?In order to avoid this pitfall, I propose to clarify that:
Random::Secure
is the source for cryptographically secure random numbers (renamingRendom::System
, see discussion below);Crystal::System::Random
is an internal abstraction to feedRandom::System
(i.e. nothing else must use it).SecureRandom
shall... be removed, and its usage replaced withRandom::System
throughout stdlib.Following these, this patch:
#base64
,#urlsafe_base64
and#uuid
methods fromSecureRandom
intoRandom
;Random#random_bytes
methods;Random::System
asRandom::Secure
;#random_bytes(buf)
method fromSecureRandom
intoRandom::Secure
;SecureRandom
andCrystal::System::Random
usages in stdlib to their equivalent calls onRandom::Secure
.A benefit is that all PRNG implementations may benefit from
#urlsafe_base64
or#uuid
for example, but these methods should only be used with a cryptographically secure PRNG only (e.g.Random::Secure
,Random::ISAAC
, ChaCha20, ...). This is stated obviously in the documentation ofRandom
and each concerned method.That being said, using an insecure but very fast and deterministic PRNG may be suitable in test suites, which is now possible, just by switching the PRNG. Switching from the system source, using a cryptographically secure PRNG (correctly seeded from the system source) is now also possible.