Skip to content

Commit

Permalink
add system random to stdlib: std/sysrand (#16459)
Browse files Browse the repository at this point in the history
  • Loading branch information
ringabout authored Feb 12, 2021
1 parent 81533a0 commit 18c24eb
Show file tree
Hide file tree
Showing 6 changed files with 333 additions and 0 deletions.
2 changes: 2 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ with other backends. see #9125. Use `-d:nimLegacyJsRound` for previous behavior.

- Deprecated `any`. See https://github.com/nim-lang/RFCs/issues/281

- 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.
## 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):
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.

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
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>".}

This comment has been minimized.

Copy link
@euantorano

euantorano Feb 18, 2021

Contributor

OpenBSD advises arc4random for normal code, rather than getentropy: https://man.openbsd.org/arc4random.3

# 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)
defer: discard posix.close(fd)

if fd > 0:
var stat: Stat
if fstat(fd, stat) != -1 and S_ISCHR(stat.st_mode):
let
chunks = (size - 1) div batchSize

This comment has been minimized.

Copy link
@euantorano

euantorano Feb 18, 2021

Contributor

Note: batchSize can be undefined here, as it is on NetBSD:

../../lib/std/sysrand.nim(264, 35) Error: undeclared identifier: 'batchSize'

This comment has been minimized.

Copy link
@ringabout

ringabout Feb 19, 2021

Author Member

@euantorano
It should be fixed by this PR already?
f455e03

This comment has been minimized.

Copy link
@euantorano

euantorano Feb 19, 2021

Contributor

@euantorano
It should be fixed by this PR already?
f455e03

Looks like it is now: http://jenkins.euantorano.co.uk/job/Nim/124/console

left = size - chunks * batchSize

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

result = posix.read(fd, addr dest[result], left)
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.} =
## 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

0 comments on commit 18c24eb

Please sign in to comment.