From 18c24eb4d0694beb971581d59a4570826c5fa01f Mon Sep 17 00:00:00 2001 From: flywind <43030857+xflywind@users.noreply.github.com> Date: Thu, 11 Feb 2021 18:00:01 -0600 Subject: [PATCH] add system random to stdlib: std/sysrand (#16459) --- changelog.md | 2 + doc/lib.rst | 3 + lib/pure/random.nim | 1 + lib/std/private/jsutils.nim | 6 + lib/std/sysrand.nim | 308 ++++++++++++++++++++++++++++++++++++ tests/stdlib/tsysrand.nim | 13 ++ 6 files changed, 333 insertions(+) create mode 100644 lib/std/sysrand.nim create mode 100644 tests/stdlib/tsysrand.nim diff --git a/changelog.md b/changelog.md index 51df5d226ca7..5d801082a153 100644 --- a/changelog.md +++ b/changelog.md @@ -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 diff --git a/doc/lib.rst b/doc/lib.rst index 3055a89a70b0..11b479902cee 100644 --- a/doc/lib.rst +++ b/doc/lib.rst @@ -264,6 +264,9 @@ Math libraries * `random `_ Fast and tiny random number generator. +* `std/sysrand `_ + Cryptographically secure pseudorandom number generator. + * `rationals `_ This module implements rational numbers and relevant mathematical operations. diff --git a/lib/pure/random.nim b/lib/pure/random.nim index f03cae56a0b3..63672b49690d 100644 --- a/lib/pure/random.nim +++ b/lib/pure/random.nim @@ -70,6 +70,7 @@ ## ## See also ## ======== +## * `std/sysrand module`_ for cryptographically secure pseudorandom number generator ## * `math module`_ for basic math routines ## * `mersenne module`_ for the Mersenne Twister random number ## generator diff --git a/lib/std/private/jsutils.nim b/lib/std/private/jsutils.nim index 47a6caa2bbfb..aa2e984ffc5c 100644 --- a/lib/std/private/jsutils.nim +++ b/lib/std/private/jsutils.nim @@ -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: "#[#] = #".} diff --git a/lib/std/sysrand.nim b/lib/std/sysrand.nim new file mode 100644 index 000000000000..9a143adb3973 --- /dev/null +++ b/lib/std/sysrand.nim @@ -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 `_ +## + + +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: "".}: clong + const syscallHeader = """#include +#include """ + + 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: "".} + # 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: "".} = int + + proc getrandom(p: pointer, size: csize_t, flags: cuint): cssize_t {.importc: "getrandom", header: "".} + # 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: "".} + ## 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 +#include +""" + + 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 + 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()) diff --git a/tests/stdlib/tsysrand.nim b/tests/stdlib/tsysrand.nim new file mode 100644 index 000000000000..d248849f8401 --- /dev/null +++ b/tests/stdlib/tsysrand.nim @@ -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