Skip to content
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

add system random to stdlib: std/sysrand #16459

Merged
merged 69 commits into from
Feb 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
ca1887a
close #10307(add testcase for #10307)
ringabout Nov 4, 2020
f1d6740
Merge
ringabout Dec 24, 2020
469d159
add secrets to stdlib
ringabout Dec 24, 2020
5856729
rename
ringabout Dec 24, 2020
335427b
fix
ringabout Dec 24, 2020
ff1153d
fix
ringabout Dec 24, 2020
cfa81bf
Update tests/magics/t10307.nim
ringabout Dec 24, 2020
39b6aab
update secrets
ringabout Jan 23, 2021
6cfe937
fix
ringabout Jan 24, 2021
0f9fdc6
fix
ringabout Jan 24, 2021
170c61d
add openbsd
ringabout Jan 24, 2021
e60ac45
openbsd
ringabout Jan 24, 2021
2766827
add macosx
ringabout Jan 24, 2021
d52702c
better
ringabout Jan 24, 2021
faa30cb
Merge remote-tracking branch 'upstream/devel' into didodido
ringabout Jan 24, 2021
66758ca
fix
ringabout Jan 24, 2021
410dfc8
fix
ringabout Jan 24, 2021
cf0eea5
better interface
ringabout Jan 24, 2021
430a9cf
fix
ringabout Jan 24, 2021
94d90e8
add js support
ringabout Jan 24, 2021
a93d360
add notes
ringabout Jan 24, 2021
153c442
add freebsd
ringabout Jan 24, 2021
88e86f0
better
ringabout Jan 24, 2021
a67e2a8
better
ringabout Jan 24, 2021
1e1d01c
fix
ringabout Jan 24, 2021
af90ab7
add tests
ringabout Jan 25, 2021
918ddb1
minor clean
ringabout Jan 25, 2021
4ee409b
slight better
ringabout Jan 25, 2021
a856a33
better
ringabout Jan 25, 2021
9da336b
fix
ringabout Jan 25, 2021
67600a1
better
ringabout Jan 25, 2021
66560b0
rename
ringabout Jan 25, 2021
3a9affe
better type
ringabout Jan 25, 2021
4c638a2
add comments
ringabout Jan 25, 2021
b351779
fix header
ringabout Jan 25, 2021
08971f8
fix
ringabout Jan 25, 2021
05c5d16
fix name
ringabout Jan 25, 2021
53a9aca
fix header
ringabout Jan 25, 2021
2dd08fd
a bit better
ringabout Jan 26, 2021
84889b9
fix
ringabout Jan 26, 2021
f2b02db
freebsd fallback to dev/urandom
ringabout Jan 26, 2021
9972b31
restore
ringabout Jan 26, 2021
5f1aa74
better
ringabout Jan 26, 2021
5e258e7
better
ringabout Jan 26, 2021
6a34803
better
ringabout Jan 26, 2021
f0b5fda
add nodejs support
ringabout Jan 26, 2021
90ed94f
done
ringabout Jan 26, 2021
edf1293
better style
ringabout Jan 26, 2021
e376ea4
fix
ringabout Jan 26, 2021
4c86fa5
better
ringabout Jan 26, 2021
7bffe1e
fix
ringabout Jan 26, 2021
dd61e99
typo
ringabout Jan 26, 2021
1fc359a
resolve comments
ringabout Jan 27, 2021
bf592f6
better
ringabout Jan 27, 2021
2445403
better comments
ringabout Jan 27, 2021
8fffc5d
fix errors
ringabout Jan 27, 2021
99f6a1a
fix
ringabout Jan 27, 2021
1b88b84
better js
ringabout Jan 27, 2021
ec67a1a
reduce size
ringabout Jan 27, 2021
9836c5b
better
ringabout Jan 27, 2021
8ee923e
better impl
ringabout Jan 27, 2021
c1cec17
small
ringabout Jan 27, 2021
2f716fc
better
ringabout Jan 27, 2021
cb383a7
Merge remote-tracking branch 'upstream/devel' into didodido
ringabout Jan 27, 2021
4693e8f
finish
ringabout Jan 28, 2021
dbda6d3
Merge branch 'devel' into didodido
ringabout Feb 1, 2021
4cc557d
Merge remote-tracking branch 'upstream/devel' into didodido
ringabout Feb 5, 2021
12edec2
fix
ringabout Feb 5, 2021
8d2e7e2
fix comments
ringabout Feb 5, 2021
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
2 changes: 2 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ with other backends. see #9125. Use `-d:nimLegacyJsRound` for previous behavior.

- Removed the optional `longestMatch` parameter of the `critbits._WithPrefix` iterators (it never worked reliably)

- Added `std/sysrand` module to get random numbers from a secure source
provided by the operating system.

- Added optional `options` argument to `copyFile`, `copyFileToDir`, and
`copyFileWithPermissions`. By default, on non-Windows OSes, symlinks are
Expand Down
3 changes: 3 additions & 0 deletions doc/lib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,9 @@ Math libraries
* `random <random.html>`_
Fast and tiny random number generator.

* `std/sysrand <sysrand.html>`_
Cryptographically secure pseudorandom number generator.

* `rationals <rationals.html>`_
This module implements rational numbers and relevant mathematical operations.

Expand Down
1 change: 1 addition & 0 deletions lib/pure/random.nim
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
##
## See also
## ========
## * `std/sysrand module<sysrand.html>`_ for cryptographically secure pseudorandom number generator
## * `math module<math.html>`_ for basic math routines
## * `mersenne module<mersenne.html>`_ for the Mersenne Twister random number
## generator
Expand Down
6 changes: 6 additions & 0 deletions lib/std/private/jsutils.nim
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,20 @@ when defined(js):
ArrayBuffer* = ref object of JsRoot
Float64Array* = ref object of JsRoot
Uint32Array* = ref object of JsRoot
Uint8Array* = ref object of JsRoot
BigUint64Array* = ref object of JsRoot


func newArrayBuffer*(n: int): ArrayBuffer {.importjs: "new ArrayBuffer(#)".}
func newFloat64Array*(buffer: ArrayBuffer): Float64Array {.importjs: "new Float64Array(#)".}
func newUint32Array*(buffer: ArrayBuffer): Uint32Array {.importjs: "new Uint32Array(#)".}
func newBigUint64Array*(buffer: ArrayBuffer): BigUint64Array {.importjs: "new BigUint64Array(#)".}


func newUint8Array*(n: int): Uint8Array {.importjs: "new Uint8Array(#)".}

func `[]`*(arr: Uint32Array, i: int): uint32 {.importjs: "#[#]".}
func `[]`*(arr: Uint8Array, i: int): uint8 {.importjs: "#[#]".}
func `[]`*(arr: BigUint64Array, i: int): JsBigInt {.importjs: "#[#]".}
func `[]=`*(arr: Float64Array, i: int, v: float) {.importjs: "#[#] = #".}

Expand Down
308 changes: 308 additions & 0 deletions lib/std/sysrand.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
#
#
# Nim's Runtime Library
# (c) Copyright 2021 Nim contributors
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#

## `std/sysrand` generates random numbers from a secure source provided by the operating system.
ringabout marked this conversation as resolved.
Show resolved Hide resolved
## It is also called Cryptographically secure pseudorandom number generator.
## It should be unpredictable enough for cryptographic applications,
## though its exact quality depends on the OS implementation.
##
## | Targets | Implementation|
## | :--- | ----: |
## | Windows | `BCryptGenRandom`_ |
## | Linux | `getrandom`_ |
## | MacOSX | `getentropy`_ |
## | IOS | `SecRandomCopyBytes`_ |
## | OpenBSD | `getentropy openbsd`_ |
## | FreeBSD | `getrandom freebsd`_ |
## | JS(Web Browser) | `getRandomValues`_ |
## | Nodejs | `randomFillSync`_ |
## | Other Unix platforms | `/dev/urandom`_ |
##
## .. _BCryptGenRandom: https://docs.microsoft.com/en-us/windows/win32/api/bcrypt/nf-bcrypt-bcryptgenrandom
## .. _getrandom: https://man7.org/linux/man-pages/man2/getrandom.2.html
## .. _getentropy: https://www.unix.com/man-page/mojave/2/getentropy
## .. _SecRandomCopyBytes: https://developer.apple.com/documentation/security/1399291-secrandomcopybytes?language=objc
## .. _getentropy openbsd: https://man.openbsd.org/getentropy.2
## .. _getrandom freebsd: https://www.freebsd.org/cgi/man.cgi?query=getrandom&manpath=FreeBSD+12.0-stable
## .. _getRandomValues: https://www.w3.org/TR/WebCryptoAPI/#Crypto-method-getRandomValues
## .. _randomFillSync: https://nodejs.org/api/crypto.html#crypto_crypto_randomfillsync_buffer_offset_size
## .. _/dev/urandom: https://en.wikipedia.org/wiki//dev/random
##

runnableExamples:
doAssert urandom(0).len == 0
doAssert urandom(113).len == 113
doAssert urandom(1234) != urandom(1234) # unlikely to fail in practice

##
## See also
## ========
## * `random module <random.html>`_
##


when not defined(js):
import std/os

when defined(posix):
ringabout marked this conversation as resolved.
Show resolved Hide resolved
import std/posix

const batchImplOS = defined(freebsd) or defined(openbsd) or (defined(macosx) and not defined(ios))

when batchImplOS:
const batchSize = 256

template batchImpl(result: var int, dest: var openArray[byte], getRandomImpl) =
let size = dest.len
if size == 0:
return

let
chunks = (size - 1) div batchSize
left = size - chunks * batchSize

for i in 0 ..< chunks:
let readBytes = getRandomImpl(addr dest[result], batchSize)
if readBytes < 0:
return readBytes
inc(result, batchSize)

result = getRandomImpl(addr dest[result], left)

when defined(js):
import std/private/jsutils

when defined(nodejs):
{.emit: "const _nim_nodejs_crypto = require('crypto');".}

proc randomFillSync(p: Uint8Array) {.importjs: "_nim_nodejs_crypto.randomFillSync(#)".}

template urandomImpl(result: var int, dest: var openArray[byte]) =
let size = dest.len
if size == 0:
return

var src = newUint8Array(size)
randomFillSync(src)
for i in 0 ..< size:
dest[i] = src[i]

else:
const batchSize = 256

proc getRandomValues(p: Uint8Array) {.importjs: "window.crypto.getRandomValues(#)".}
# The requested length of `p` must not be more than 65536.
ringabout marked this conversation as resolved.
Show resolved Hide resolved

proc assign(dest: var openArray[byte], src: Uint8Array, base: int, size: int) =
getRandomValues(src)
for j in 0 ..< size:
dest[base + j] = src[j]

template urandomImpl(result: var int, dest: var openArray[byte]) =
let size = dest.len
if size == 0:
return

if size <= batchSize:
var src = newUint8Array(size)
assign(dest, src, 0, size)
return

let
chunks = (size - 1) div batchSize
left = size - chunks * batchSize

var srcArray = newUint8Array(batchSize)
for i in 0 ..< chunks:
assign(dest, srcArray, result, batchSize)
inc(result, batchSize)

var leftArray = newUint8Array(left)
assign(dest, leftArray, result, left)

elif defined(windows):
type
PVOID = pointer
BCRYPT_ALG_HANDLE = PVOID
PUCHAR = ptr cuchar
NTSTATUS = clong
ULONG = culong

const
STATUS_SUCCESS = 0x00000000
BCRYPT_USE_SYSTEM_PREFERRED_RNG = 0x00000002

proc bCryptGenRandom(
hAlgorithm: BCRYPT_ALG_HANDLE,
pbBuffer: PUCHAR,
cbBuffer: ULONG,
dwFlags: ULONG
): NTSTATUS {.stdcall, importc: "BCryptGenRandom", dynlib: "Bcrypt.dll".}


proc randomBytes(pbBuffer: pointer, cbBuffer: Natural): int {.inline.} =
bCryptGenRandom(nil, cast[PUCHAR](pbBuffer), ULONG(cbBuffer),
BCRYPT_USE_SYSTEM_PREFERRED_RNG)

template urandomImpl(result: var int, dest: var openArray[byte]) =
let size = dest.len
if size == 0:
return

result = randomBytes(addr dest[0], size)

elif defined(linux):
let SYS_getrandom {.importc: "SYS_getrandom", header: "<sys/syscall.h>".}: clong
ringabout marked this conversation as resolved.
Show resolved Hide resolved
const syscallHeader = """#include <unistd.h>
#include <sys/syscall.h>"""

proc syscall(
n: clong, buf: pointer, bufLen: cint, flags: cuint
): clong {.importc: "syscall", header: syscallHeader.}
# When reading from the urandom source (GRND_RANDOM is not set),
# getrandom() will block until the entropy pool has been
# initialized (unless the GRND_NONBLOCK flag was specified). If a
# request is made to read a large number of bytes (more than 256),
# getrandom() will block until those bytes have been generated and
# transferred from kernel memory to buf.

template urandomImpl(result: var int, dest: var openArray[byte]) =
let size = dest.len
if size == 0:
return

while result < size:
let readBytes = syscall(SYS_getrandom, addr dest[result], cint(size - result), 0).int
if readBytes == 0:
doAssert false
elif readBytes > 0:
inc(result, readBytes)
else:
if osLastError().int in {EINTR, EAGAIN}:
discard
else:
result = -1
break

elif defined(openbsd):
proc getentropy(p: pointer, size: cint): cint {.importc: "getentropy", header: "<unistd.h>".}
# fills a buffer with high-quality entropy,
# which can be used as input for process-context pseudorandom generators like `arc4random`.
# The maximum buffer size permitted is 256 bytes.

proc getRandomImpl(p: pointer, size: int): int {.inline.} =
result = getentropy(p, cint(size)).int

elif defined(freebsd):
type cssize_t {.importc: "ssize_t", header: "<sys/types.h>".} = int

proc getrandom(p: pointer, size: csize_t, flags: cuint): cssize_t {.importc: "getrandom", header: "<sys/random.h>".}
# Upon successful completion, the number of bytes which were actually read
# is returned. For requests larger than 256 bytes, this can be fewer bytes
# than were requested. Otherwise, -1 is returned and the global variable
# errno is set to indicate the error.

proc getRandomImpl(p: pointer, size: int): int {.inline.} =
result = getrandom(p, csize_t(batchSize), 0)

elif defined(ios):
{.passL: "-framework Security".}

const errSecSuccess = 0 ## No error.

type
SecRandom {.importc: "struct __SecRandom".} = object

SecRandomRef = ptr SecRandom
## An abstract Core Foundation-type object containing information about a random number generator.

proc secRandomCopyBytes(
rnd: SecRandomRef, count: csize_t, bytes: pointer
): cint {.importc: "SecRandomCopyBytes", header: "<Security/SecRandom.h>".}
## https://developer.apple.com/documentation/security/1399291-secrandomcopybytes

template urandomImpl(result: var int, dest: var openArray[byte]) =
let size = dest.len
if size == 0:
return

result = secRandomCopyBytes(nil, csize_t(size), addr dest[0])

elif defined(macosx):
const sysrandomHeader = """#include <Availability.h>
#include <sys/random.h>
"""

proc getentropy(p: pointer, size: csize_t): cint {.importc: "getentropy", header: sysrandomHeader.}
# getentropy() fills a buffer with random data, which can be used as input
# for process-context pseudorandom generators like arc4random(3).
# The maximum buffer size permitted is 256 bytes.

proc getRandomImpl(p: pointer, size: int): int {.inline.} =
result = getentropy(p, csize_t(size)).int

else:
template urandomImpl(result: var int, dest: var openArray[byte]) =
let size = dest.len
if size == 0:
return

# see: https://www.2uo.de/myths-about-urandom/ which justifies using urandom instead of random
let fd = posix.open("/dev/urandom", O_RDONLY)
ringabout marked this conversation as resolved.
Show resolved Hide resolved
defer: discard posix.close(fd)

if fd > 0:
ringabout marked this conversation as resolved.
Show resolved Hide resolved
var stat: Stat
if fstat(fd, stat) != -1 and S_ISCHR(stat.st_mode):
ringabout marked this conversation as resolved.
Show resolved Hide resolved
let
chunks = (size - 1) div batchSize
left = size - chunks * batchSize

for i in 0 ..< chunks:
let readBytes = posix.read(fd, addr dest[result], batchSize)
if readBytes < 0:
return readBytes
ringabout marked this conversation as resolved.
Show resolved Hide resolved
inc(result, batchSize)

result = posix.read(fd, addr dest[result], left)
ringabout marked this conversation as resolved.
Show resolved Hide resolved
else:
result = -1
else:
result = -1

proc urandomInternalImpl(dest: var openArray[byte]): int {.inline.} =
when batchImplOS:
batchImpl(result, dest, getRandomImpl)
else:
urandomImpl(result, dest)

proc urandom*(dest: var openArray[byte]): bool =
## Fills `dest` with random bytes suitable for cryptographic use.
## If succeed, returns `true`.
##
## If `dest` is empty, `urandom` immediately returns success,
## without calling underlying operating system api.
result = true
when defined(js): discard urandomInternalImpl(dest)
else:
let ret = urandomInternalImpl(dest)
when defined(windows):
if ret != STATUS_SUCCESS:
result = false
else:
if ret < 0:
result = false

proc urandom*(size: Natural): seq[byte] {.inline.} =
ringabout marked this conversation as resolved.
Show resolved Hide resolved
## Returns random bytes suitable for cryptographic use.
result = newSeq[byte](size)
when defined(js): discard urandomInternalImpl(result)
else:
if not urandom(result):
raiseOsError(osLastError())
13 changes: 13 additions & 0 deletions tests/stdlib/tsysrand.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
discard """
targets: "c cpp js"
"""

import std/sysrand


doAssert urandom(0).len == 0
doAssert urandom(10).len == 10
doAssert urandom(20).len == 20
doAssert urandom(120).len == 120
doAssert urandom(113).len == 113
doAssert urandom(1234) != urandom(1234) # unlikely to fail in practice