-
-
Save eu90h/5dfd65a3aa1b276be49c to your computer and use it in GitHub Desktop.
#lang racket/base | |
(require racket/contract/base "unix_rand.rkt" "windows_rand.rkt" (only-in file/sha1 bytes->hex-string)) | |
(provide (contract-out [crypto-random-bytes (-> exact-positive-integer? (or/c bytes? string?))] | |
[crypto-random (-> exact-positive-integer? exact-positive-integer?)])) | |
(define bytes->number (compose (lambda (s) (string->number s 16)) bytes->hex-string)) | |
; (: crypto-random-bytes (-> Positive-Integer Bytes)) | |
; returns n random bytes from the os. | |
(define (crypto-random-bytes n) | |
(case (system-type 'os) | |
[(unix macosx) (crypto-random-unix-bytes n)] | |
[(windows) (crypto-random-windows-bytes n)] | |
[else (raise (make-exn:fail:unsupported | |
"Only UNIX, OS X, and Windows XP or greater are currently supported" | |
(current-continuation-marks)))])) | |
; (: crypto-random (-> Positive-Integer Nonnegative-Integer)) | |
; returns a random n byte nonnegative-integer | |
(define (crypto-random n) | |
(bytes->number (crypto-random-bytes n))) |
#lang racket/base | |
(provide crypto-random-unix-bytes) | |
(define (check-urandom-exists) | |
(unless (file-exists? "/dev/urandom") | |
(raise (make-exn:fail:filesystem | |
"/dev/urandom does not exist" | |
(current-continuation-marks))))) | |
; (: crypto-random-unix-bytes (-> Positive-Integer Bytes)) | |
(define (crypto-random-unix-bytes n) | |
(check-urandom-exists) | |
(define urandom (open-input-file "/dev/urandom")) | |
(read-bytes n urandom)) |
#lang racket/base | |
(provide crypto-random-windows-bytes) | |
(require ffi/com | |
ffi/unsafe | |
ffi/unsafe/define | |
ffi/winapi) | |
(define-ffi-definer define-advapi (and (eq? (system-type) 'windows) (ffi-lib "Advapi32.dll")) | |
#:default-make-fail make-not-available) | |
; supposed to be the same csprng as CryptGenRand, but with less overhead | |
; see Microsoft security dev Michael Howard: http://blogs.msdn.com/b/michael_howard/archive/2005/01/14/353379.aspx | |
; this is for Windows XP and later only, but I doubt that's a problem | |
(define-advapi SystemFunction036 | |
(_fun #:abi winapi _pointer _ulong -> _bool)) | |
(define (check-SystemFunction036-exists) | |
(unless SystemFunction036 | |
(raise (make-exn:fail:unsupported | |
"Unable to load RtlGenRandom (SystemFunction036) from Advapi32.dll" | |
(current-continuation-marks))))) | |
; (: crypto-random-windows-bytes (-> Positive-Integer Bytes)) | |
(define (crypto-random-windows-bytes n) | |
(check-SystemFunction036-exists) | |
(define rand-bytes-buf (make-bytes n)) | |
(if (SystemFunction036 rand-bytes-buf n) | |
rand-bytes-buf | |
(raise (make-exn:fail | |
"SystemFunction036 failed to generate bytes" | |
(current-continuation-marks))))) |
Thank you very much for the comments. A couple of silly mistakes / left-over testing artifacts to clear up I didn't catch, good eye. Haha that default parameter cracks me up seeing it now, I don't know, it's way past my bed time :P
I tried writing a contract and got an error about the ->
, I think the FFI's use of the ->
symbol is messing with something.
I love the de-nesting of conditions into unless
checks, definitely will do that.
The EOF thing was a last minute thought and definitely needs looking into. I have a hunch that the check is useless here.
Thanks for the notes on exceptions, still unfamiliar with that stuff.
OK thanks to @jackfirth, I've significantly restructured the program. Still need to change exception handling, and investigate unix EOF issue however.
Edit: on the subject on EOFs, as long as we're reading from /dev/urandom it does seem that we're guaranteed the number of bytes requested.
Note about the contract: positive?
returns true for positive reals, false for nonpositive reals, and throws an exception for anything else. So if you give secure-random-bytes
a string, for example, you won't get a contract failure - the contract itself will blow up with a positive?: expected real
error. Additionally, positive?
won't guard against non-integer inputs. You probably want exact-positive-integer?
.
Updated, thanks.
reworked error handling in line with @jackfirth's suggestions. Hopefully I did that correctly.
Added crypto-random
which returns an integer as opposed to a byte string
added optional argument to crypto-random-bytes
that allows hex-string output - is this a good idea?
[and is there a better way to do bytes->number conversion?]
More minor thoughts:
- You could make a
(raise-unsupported-error message)
function in a private helper module that does(raise (make-exn:fail:unsupported message (current-continuation-marks)))
to eliminate some duplication and increase readability. - Instead of passing a flag to allow hex string output, it would be easier to have a
crypto-random-hex-string
function. Though that's probably not too useful to provide since it's just(compose bytes->hex-string crypto-random-bytes)
. - For implementing
crypto-random
, it'd be more useful to do it in terms of abytes->number
function. I don't know a better way to make that function than by doingbytes->hex-string
followed bystring->number
. Hand rolling your own in terms ofbytes->list
is a possibility but it would be messy and doesn't really belong with this feature. crypto-random
's inputn
should behave the same way asrandom
's and be an upper bound on the result number, not the number of bytes to use to generate the result number. This is trickier, but it's worth it since it makescrypto-random
a drop-in replacement forrandom
(in the one-argument case).
The exception raising looks good to me. I find it strange there isn't a built in bytes->number
function to use. Maybe a separate PR should add that? Unless there's some good reason it's not provided.
I decided to remove the hex string option. bytes->hex-string makes it trivial for someone to convert the output of crypto-random-bytes into a hex string if they need it, so it doesn't warrant the added complexity.
a bytes->number would definitely be a useful addition to the bytes pkg.
your point about crypto-random's interface is spot-on. I'll have to think of how best to do this.
Some initial thoughts:
#lang racket/base
instead of#lang racket
so that people using#lang racket/base
to speed up startup times who happen to use this library don't trigger an import of the whole of racket and lose that speed. Some of the libraries you pull in to implement this like the FFI may themselves use#lang racket
, making the speed benefits vanish, but it's still a good practice in the case that those libraries are later optimized to depend onracket/base
.unless positive?
checkrandom
function takes an upper bound and returns an integer, butsecure-random
takes a number of bytes to produce and produces bytes. Their names don't imply this difference in return types. I recommend naming itsecure-random-bytes
and implementing asecure-random
function that behaves likerandom
but with input fromsecure-random-bytes
.raise
should raise some descendant of theexn:fail
structure type rather than a string to allow for proper exception handling. There's anexn:fail:unsupported
structure type specifically for features that are unavailable due to platform configurations, ideally you could subtype that with some specificexn:fail:unsupported:secure-random
structure type.dev/urandom
won't returneof-object?
? If so, ditch the check and just return(read-bytes n urandom)
, relying on the contract ofsecure-random
to throw an exception ifeof-object?
is returned. It should never happen, but if it does something should blow up, rather than empty bytes being returned. Maybe an exception type for this specifically would be in order? Might not be worth it.(if test body fail)
expressions into something like(unless test fail) body ...
, then you can usedefine
instead oflet
to get rid of some nesting and make things clearer. Additionally, you can turn the(unless test fail)
expressions into their own functions for readability. Example would be changing this:Into this:
(system-type 'os)
for themselves to see what kind of system racket thinks they have. This is especially useful if this error occurs somewhere the user doesn't have easy access to a repl, like an app running in the cloud on a black box where log output is all you've got for diagnosing a crash.These are just my opinions, do not take them as absolutes. Let me know what you think.