From 385be8164480a3f3bb18d16fffac1217f70e2800 Mon Sep 17 00:00:00 2001 From: Mindy Batek Date: Wed, 6 Aug 2025 00:13:47 +0200 Subject: [PATCH 1/8] Provide handles to CSPRNGs through `std.internal.entropy` --- std/internal/entropy.d | 843 ++++++++++++++++++++++++++--------------- std/random.d | 8 +- 2 files changed, 543 insertions(+), 308 deletions(-) diff --git a/std/internal/entropy.d b/std/internal/entropy.d index 92b3323250f..5469136a937 100644 --- a/std/internal/entropy.d +++ b/std/internal/entropy.d @@ -31,6 +31,8 @@ module std.internal.entropy; import std.meta; +import std.sumtype; +import std.typecons; version (OSX) version = Darwin; @@ -41,6 +43,215 @@ else version (TVOS) else version (WatchOS) version = Darwin; +version (Darwin) mixin entropyImpl!( + EntropySource.arc4random, + Implementation.ARC4Random, + Implementation.CharDevURandom, + Implementation.CharDevRandom, +); +else version (DragonFlyBSD) mixin entropyImpl!( + EntropySourceID.getentropy, + Implementation.Getentropy, + Implementation.CharDevURandom, + Implementation.CharDevRandom, +); +else version (FreeBSD) mixin entropyImpl!( + EntropySourceID.getentropy, + Implementation.Getentropy, + Implementation.CharDevURandom, + Implementation.CharDevRandom, +); +else version (linux) mixin entropyImpl!( + EntropySourceID.getrandom, + Implementation.Getrandom, + Implementation.CharDevURandom, + Implementation.CharDevRandom, +); +else version (NetBSD) mixin entropyImpl!( + EntropySourceID.arc4random, + Implementation.ARC4Random, + Implementation.CharDevURandom, + Implementation.CharDevRandom, +); +else version (OpenBSD) mixin entropyImpl!( + EntropySourceID.arc4random, + Implementation.ARC4Random, + Implementation.CharDevURandom, + Implementation.CharDevRandom, +); +else version (Posix) mixin entropyImpl!( + EntropySourceID.charDevURandom, + Implementation.CharDevURandom, + Implementation.CharDevRandom, +); +else version (Windows) mixin entropyImpl!( + EntropySourceID.bcryptGenRandom, + Implementation.BCryptGenRandom, +); +else mixin entropyImpl!( + EntropySourceID.none, +); + +/+ + Building blocks and implementation helpers + +/ +private +{ + /++ + A “Chunks” implementation that works with `void[]`. + +/ + struct VoidChunks + { + void[] _data; + size_t _chunkSize; + + @nogc nothrow pure @safe: + + this(void[] data, size_t chunkSize) + { + _data = data; + _chunkSize = chunkSize; + } + + bool empty() const + { + return _data.length == 0; + } + + inout(void)[] front() inout + { + if (_data.length < _chunkSize) + return _data; + + return _data[0 .. _chunkSize]; + } + + void popFront() + { + if (_data.length <= _chunkSize) + { + _data = null; + return; + } + + _data = _data[_chunkSize .. $]; + } + } + + template isValidSupportedSource(alias SupportedSource) + { + enum isValidSupportedSource = ( + is(SupportedSource == struct) && + is(typeof(SupportedSource.id) == EntropySourceID) && + SupportedSource.id != EntropySourceID.tryAll && + SupportedSource.id != EntropySourceID.none + ); + } + + /++ + `getEntropyImpl()` implementation helper. + To be instantiated and mixed in with platform-specific configuration. + + Params: + defaultSource = Default entropy source of the platform + SupportedSources = Sequence of `SrcFunPair` + representing the supported sources of the platform + +/ + mixin template entropyImpl(EntropySourceID defaultSource, SupportedSources...) + if (allSatisfy!(isValidSupportedSource, SupportedSources)) + { + private: + /// Preconfigured entropy source preset of the platform. + enum defaultEntropySource = defaultSource; + + alias _InnerEntropySourceHandle = SumType!( + Implementation.None, + SupportedSources, + ); + + @nogc nothrow @safe: + + EntropyStatus _openEntropySourceImpl(out EntropySourceHandle.Inner handle) @safe + { + switch (_entropySource) + { + static foreach (Source; SupportedSources) + {{ + case Source.id: + auto source = Source(); + const status = source.open(); + () @trusted { handle = EntropySourceHandle.Inner(source); }(); + return status; + }} + + case EntropySourceID.tryAll: + { + const status = _tryOpenEntropySources(handle); + handle.saveSourceForNextUse(); + return status; + } + + case EntropySourceID.none: + auto none = Implementation.None(); + () @trusted { handle = EntropySourceHandle.Inner(none); }(); + return none.open(); + + default: + return EntropyStatus.unavailablePlatform; + } + } + + EntropyStatus openEntropySourceImpl(out EntropySourceHandle.InnerRefCounted handle) @safe + { + EntropySourceHandle.Inner innerHandle; + const status = _openEntropySourceImpl(innerHandle); + handle = safeRefCounted(innerHandle); + + return status; + } + + EntropyStatus _tryOpenEntropySources(out EntropySourceHandle.Inner handle) @safe + { + static foreach (Source; SupportedSources) + {{ + auto source = Source(); + const status = source.open(); + if (status == EntropyStatus.ok) + { + () @trusted { handle = EntropySourceHandle.Inner(source); }(); + return status; + } + }} + + auto fallback = Implementation.None(); + () @trusted { handle = EntropySourceHandle.Inner(fallback); }(); + return fallback.open(); + } + } + + auto matchCall(string methodName, Args...)(ref scope EntropySourceHandle.Inner source, Args args) @nogc @safe + { + import std.array : join; + import std.string : chomp; + + enum methodCall = (args.length == 0) + ? `matched.` ~ methodName + : `matched.` ~ methodName ~ `(args)`; + enum handler(T) = `(ref scope ` + ~ __traits(fullyQualifiedName, T) + .chomp(".CharDev") /+ quick'n'dirty workaround +/ + ~ ` matched) @safe => ` ~ methodCall; + enum handlers = `AliasSeq!(` ~ [staticMap!(handler, EntropySourceHandle.Inner.Types)].join(",\n") ~ `)`; + + return source.match!(mixin(handlers)); + } + + auto borrowMatchCall(string methodName, Args...)(EntropySourceHandle.InnerRefCounted source, Args args) @nogc @safe + { + return source.borrow!((EntropySourceHandle.Inner borrowed) => matchCall!methodName(borrowed, args)); + } +} + // Self-test: Detect potentially unsuitable default entropy source. @safe unittest { @@ -118,6 +329,56 @@ else version (WatchOS) @nogc nothrow: // Flagship function +/++ + Opens a handle to an applicable system CSPRNG. + +/ +EntropyStatus openEntropySource(out EntropySourceHandle handle) @safe +{ + EntropySourceHandle.InnerRefCounted innerHandle; + const status = openEntropySourceImpl(innerHandle); + handle = EntropySourceHandle(innerHandle); + return status; +} + +// Flagship function +EntropyStatus getEntropy(EntropySourceHandle source, scope void[] buffer) @safe +{ + return source.handle.borrowMatchCall!"getEntropy"(buffer); +} + +string getErrorMessage(EntropySourceHandle source, EntropyStatus status) @safe +{ + static string genericErrorMessage(EntropyStatus status) + { + if (status == EntropyStatus.unavailable || + status == EntropyStatus.unavailableLibrary) + return "getEntropy(): An entropy source was unavailable."; + if (status == EntropyStatus.unavailablePlatform) + return "getEntropy(): The requested entropy source is not supported on this platform."; + if (status == EntropyStatus.readError) + return "getEntropy(): Could not retrieve entropy from the selected source."; + + return "getEntropy(): An unknown error occurred."; + } + + const msg = source.handle.borrowMatchCall!"getErrorMessage"(status); + if (msg is null) + return genericErrorMessage(status); + + return msg; +} + +EntropySourceID id(EntropySourceHandle source) @safe +{ + return source.handle.borrowMatchCall!"id"(); +} + +private EntropySourceID id(EntropySourceHandle.Inner source) @safe +{ + return source.matchCall!"id"(); +} + +// Legacy flagship function /++ Retrieves random data from an applicable system CSPRNG. @@ -140,7 +401,13 @@ else version (WatchOS) +/ EntropyResult getEntropy(scope void[] buffer) @safe { - return getEntropyImpl(buffer); + EntropySourceHandle handle; + const statusOpen = openEntropySource(handle); + if (statusOpen != EntropyStatus.ok) + return EntropyResult(statusOpen, handle.id); + + const statusGet = handle.getEntropy(buffer); + return EntropyResult(statusGet, handle.id); } /// @@ -222,7 +489,7 @@ EntropyResult getEntropy(scope void* buffer, size_t length) @system It might be useful in cases where the default entropy source — as chosen by the maintainer of the used compiler package — is unavailable on a system. - Usually, `EntropySource.tryAll` will be the most reasonable option + Usually, `EntropySourceID.tryAll` will be the most reasonable option in such cases. Params: @@ -233,10 +500,10 @@ EntropyResult getEntropy(scope void* buffer, size_t length) @system --- // Using `forceEntropySource` almost always is a bad idea. // As a rule of thumb, this SHOULD NOT be done. - forceEntropySource(EntropySource.none); + forceEntropySource(EntropySourceID.none); --- +/ -void forceEntropySource(EntropySource source) @safe +void forceEntropySource(EntropySourceID source) @safe { _entropySource = source; } @@ -269,7 +536,7 @@ void forceEntropySource(EntropySource source) @safe The recommended way to check for success is through the `isOK()` helper function. +/ -EntropyResult getEntropy(scope void* buffer, size_t length, EntropySource source) @system +EntropyResult getEntropy(scope void* buffer, size_t length, EntropySourceID source) @system { const sourcePrevious = _entropySource; scope (exit) _entropySource = sourcePrevious; @@ -283,8 +550,8 @@ EntropyResult getEntropy(scope void* buffer, size_t length, EntropySource source { ubyte[4] bytes; - // `EntropySource.none` always fails. - assert(!getEntropy(bytes.ptr, bytes.length, EntropySource.none).isOK); + // `EntropySourceID.none` always fails. + assert(!getEntropy(bytes.ptr, bytes.length, EntropySourceID.none).isOK); } /++ @@ -292,7 +559,7 @@ EntropyResult getEntropy(scope void* buffer, size_t length, EntropySource source (No actual low-level entropy sources are provided on purpose.) +/ -enum EntropySource +enum EntropySourceID { /// Implements a $(I hunting) strategy for finding an entropy source that /// is available at runtime. @@ -340,12 +607,12 @@ enum EntropySource /// enum EntropyStatus { - /// success - ok = 0, - /// catch-all error unknownError = 1, + /// success + ok = 0, + /// An entropy source was unavailable. unavailable, @@ -360,17 +627,17 @@ enum EntropyStatus } /++ - Status report returned by `getEntropy` functions. + Status report returned by legacy `getEntropy` functions. Use the `isOK` helper function to test for success. +/ struct EntropyResult { /// - EntropyStatus status; + EntropyStatus status = EntropyStatus.unknownError; /// - EntropySource source; + EntropySourceID source; /++ Returns: @@ -381,12 +648,12 @@ struct EntropyResult if (status == EntropyStatus.ok) return "getEntropy(): OK."; - if (source == EntropySource.none) + if (source == EntropySourceID.none) { if (status == EntropyStatus.unavailable) return "getEntropy(): Error - No suitable entropy source was available."; } - else if (source == EntropySource.getrandom) + else if (source == EntropySourceID.getrandom) { if (status == EntropyStatus.unavailableLibrary) return "getEntropy(): `dlopen(\"libc\")` failed."; @@ -395,26 +662,26 @@ struct EntropyResult if (status == EntropyStatus.readError) return "getEntropy(): `getrandom()` failed."; } - else if (source == EntropySource.getentropy) + else if (source == EntropySourceID.getentropy) { if (status == EntropyStatus.readError) return "getEntropy(): `getentropy()` failed."; } - else if (source == EntropySource.charDevURandom) + else if (source == EntropySourceID.charDevURandom) { if (status == EntropyStatus.unavailable) return "getEntropy(): `/dev/urandom` is unavailable."; if (status == EntropyStatus.readError) return "getEntropy(): Reading from `/dev/urandom` failed."; } - else if (source == EntropySource.charDevURandom) + else if (source == EntropySourceID.charDevURandom) { if (status == EntropyStatus.unavailable) return "getEntropy(): `/dev/random` is unavailable."; if (status == EntropyStatus.readError) return "getEntropy(): Reading from `/dev/random` failed."; } - else if (source == EntropySource.bcryptGenRandom) + else if (source == EntropySourceID.bcryptGenRandom) { if (status == EntropyStatus.unavailableLibrary) return "getEntropy(): `LoadLibraryA(\"Bcrypt.dll\")` failed."; @@ -508,280 +775,212 @@ pragma(inline, true) void crashOnError(const EntropyResult value) pure @safe assert(false, value.toString()); } -/+ - Building blocks and implementation helpers +/++ + Depending on the underlying implementation, this handle might only be a dummy. + Some implementations build upon handles themselves, + hence this generic wrapper has to provide support for doing so. +/ +struct EntropySourceHandle +{ + private + { + alias Inner = _InnerEntropySourceHandle; + alias InnerRefCounted = SafeRefCounted!( + EntropySourceHandle.Inner, + RefCountedAutoInitialize.no + ); + } + + private + { + InnerRefCounted handle; + } + + private this(InnerRefCounted handle) @nogc nothrow pure @safe + { + this.handle = handle; + } +} + private { - /++ - A “Chunks” implementation that works with `void[]`. - +/ - struct VoidChunks + static EntropySourceID _entropySource = defaultEntropySource; + + void saveSourceForNextUse(EntropySourceHandle.Inner source) @safe { - void[] _data; - size_t _chunkSize; + if (source.id == EntropySourceID.none) + return; - @nogc nothrow pure @safe: + _entropySource = source.id; + } +} - this(void[] data, size_t chunkSize) +private struct Implementation +{ +static: + + version(all) + struct None + { + enum id = EntropySourceID.none; + + @nogc nothrow @safe: + + EntropyStatus open() scope { - _data = data; - _chunkSize = chunkSize; + return EntropyStatus.unavailable; } - bool empty() const + void close() scope { - return _data.length == 0; + // no-op } - inout(void)[] front() inout + EntropyStatus getEntropy(scope void[]) scope { - if (_data.length < _chunkSize) - return _data; - - return _data[0 .. _chunkSize]; + return EntropyStatus.unavailable; } - void popFront() + static string getErrorMessage(EntropyStatus status) { - if (_data.length <= _chunkSize) - { - _data = null; - return; - } + if (status == EntropyStatus.unavailable) + return "getEntropy(): Error - No suitable entropy source was available."; - _data = _data[_chunkSize .. $]; + return null; } } - struct SrcFunPair(EntropySource source, alias func) + version(Posix) + struct CharDev(EntropySourceID sourceID, string path) { - enum src = source; - alias fun = func; - } + import core.stdc.stdio : FILE, fclose, fopen, fread; - template isValidSupportedSource(SupportedSource) - { - enum isValidSupportedSource = ( - is(SupportedSource == SrcFunPair!Args, Args...) && - SupportedSource.src != EntropySource.tryAll && - SupportedSource.src != EntropySource.none - ); - } + private + { + enum string _path = path ~ "\0"; + FILE* _file = null; + } - /++ - `getEntropyImpl()` implementation helper. - To be instantiated and mixed in with platform-specific configuration. + @nogc nothrow @safe: - Params: - defaultSource = Default entropy source of the platform - SupportedSources = Sequence of `SrcFunPair` - representing the supported sources of the platform - +/ - mixin template entropyImpl(EntropySource defaultSource, SupportedSources...) - if (allSatisfy!(isValidSupportedSource, SupportedSources)) - { - private: - /// Preconfigured entropy source preset of the platform. - enum defaultEntropySource = defaultSource; + enum id = sourceID; - EntropyResult getEntropyImpl(scope void[] buffer) @safe + EntropyStatus open() scope @trusted { - switch (_entropySource) - { - static foreach (source; SupportedSources) - { - case source.src: - return source.fun(buffer); - } + _file = fopen(_path.ptr, "r"); - case EntropySource.tryAll: - { - const result = _tryEntropySources(buffer); - result.saveSourceForNextUse(); - return result; - } - - case EntropySource.none: - return getEntropyViaNone(buffer); + if (_file is null) + return EntropyStatus.unavailable; - default: - return EntropyResult(EntropyStatus.unavailablePlatform, _entropySource); - } + return EntropyStatus.ok; } - EntropyResult _tryEntropySources(scope void[] buffer) @safe + void close() scope @trusted { - EntropyResult result; - - static foreach (source; SupportedSources) - { - result = source.fun(buffer); - if (!result.isUnavailable) - return result; - } - - result = EntropyResult( - EntropyStatus.unavailable, - EntropySource.none, - ); + if (_file is null) + return; - return result; + fclose(_file); } - } -} - -version (Darwin) mixin entropyImpl!( - EntropySource.arc4random, - SrcFunPair!(EntropySource.arc4random, getEntropyViaARC4Random), - SrcFunPair!(EntropySource.charDevURandom, getEntropyViaCharDevURandom), - SrcFunPair!(EntropySource.charDevRandom, getEntropyViaCharDevRandom), -); -else version (DragonFlyBSD) mixin entropyImpl!( - EntropySource.getentropy, - SrcFunPair!(EntropySource.getentropy, getEntropyViaGetentropy), - SrcFunPair!(EntropySource.charDevURandom, getEntropyViaCharDevURandom), - SrcFunPair!(EntropySource.charDevRandom, getEntropyViaCharDevRandom), -); -else version (FreeBSD) mixin entropyImpl!( - EntropySource.getentropy, - SrcFunPair!(EntropySource.getentropy, getEntropyViaGetentropy), - SrcFunPair!(EntropySource.charDevURandom, getEntropyViaCharDevURandom), - SrcFunPair!(EntropySource.charDevRandom, getEntropyViaCharDevRandom), -); -else version (linux) mixin entropyImpl!( - EntropySource.getrandom, - SrcFunPair!(EntropySource.getrandom, getEntropyViaGetrandom), - SrcFunPair!(EntropySource.charDevURandom, getEntropyViaCharDevURandom), - SrcFunPair!(EntropySource.charDevRandom, getEntropyViaCharDevRandom), -); -else version (NetBSD) mixin entropyImpl!( - EntropySource.arc4random, - SrcFunPair!(EntropySource.arc4random, getEntropyViaARC4Random), - SrcFunPair!(EntropySource.charDevURandom, getEntropyViaCharDevURandom), - SrcFunPair!(EntropySource.charDevRandom, getEntropyViaCharDevRandom), -); -else version (OpenBSD) mixin entropyImpl!( - EntropySource.arc4random, - SrcFunPair!(EntropySource.arc4random, getEntropyViaARC4Random), - SrcFunPair!(EntropySource.charDevURandom, getEntropyViaCharDevURandom), - SrcFunPair!(EntropySource.charDevRandom, getEntropyViaCharDevRandom), -); -else version (Posix) mixin entropyImpl!( - EntropySource.charDevURandom, - SrcFunPair!(EntropySource.charDevURandom, getEntropyViaCharDevURandom), - SrcFunPair!(EntropySource.charDevRandom, getEntropyViaCharDevRandom), -); -else version (Windows) mixin entropyImpl!( - EntropySource.bcryptGenRandom, - SrcFunPair!(EntropySource.bcryptGenRandom, getEntropyViaBCryptGenRandom), -); -else mixin entropyImpl!( - EntropySource.none, -); -private -{ - static EntropySource _entropySource = defaultEntropySource; + EntropyStatus getEntropy(scope void[] buffer) scope @trusted + { + if (_file is null) + return EntropyStatus.unavailable; - void saveSourceForNextUse(const EntropyResult result) @safe - { - if (!result.isOK) - return; + const bytesRead = fread(buffer.ptr, 1, buffer.length, _file); + if (bytesRead != buffer.length) + return EntropyStatus.readError; - _entropySource = result.source; - } -} + return EntropyStatus.ok; + } -version (all) -{ -private: + static string getErrorMessage(EntropyStatus status) + { + if (status == EntropyStatus.unavailable) + return "getEntropy(): `" ~ path ~ "` is unavailable."; + if (status == EntropyStatus.readError) + return "getEntropy(): Reading from `" ~ path ~ "` failed."; - EntropyResult getEntropyViaNone(scope void[]) @safe - { - return EntropyResult(EntropyStatus.unavailable, EntropySource.none); + return null; + } } -} - -version (Posix) -{ -private: - EntropyResult getEntropyViaCharDevURandom(scope void[] buffer) @trusted - { - const status = getEntropyViaCharDev(buffer, "/dev/urandom".ptr); - return EntropyResult(status, EntropySource.charDevURandom); - } + version(Posix) + alias CharDevURandom = CharDev!( + EntropySourceID.charDevURandom, + "/dev/urandom", + ); - EntropyResult getEntropyViaCharDevRandom(scope void[] buffer) @trusted - { - const status = getEntropyViaCharDev(buffer, "/dev/random".ptr); - return EntropyResult(status, EntropySource.charDevRandom); - } + version(Posix) + alias CharDevRandom = CharDev!( + EntropySourceID.charDevRandom, + "/dev/random", + ); - EntropyStatus getEntropyViaCharDev(scope void[] buffer, const(char)* charDevName) @system + version(linux) + struct Getrandom { - import core.stdc.stdio : fclose, fopen, fread; - - auto charDev = fopen(charDevName, "r"); - if (charDev is null) - return EntropyStatus.unavailable; - scope (exit) - fclose(charDev); + enum id = EntropySourceID.getrandom; - const bytesRead = fread(buffer.ptr, 1, buffer.length, charDev); - if (bytesRead != buffer.length) - return EntropyStatus.readError; - - return EntropyStatus.ok; - } -} + @nogc nothrow @safe: -version (linux) -{ -private: + EntropyStatus open() scope + { + return EntropyStatus.ok; + } - EntropyResult getEntropyViaGetrandom(scope void[] buffer) @trusted - { - const status = syscallGetrandom(buffer, 0); - return EntropyResult(status, EntropySource.getrandom); - } + void close() scope + { + // no-op + } - EntropyStatus syscallGetrandom(scope void[] buffer, uint flags) @system - { - import core.sys.linux.errno : EINTR, ENOSYS, errno; - import core.sys.linux.sys.syscall : SYS_getrandom; - import core.sys.linux.unistd : syscall; + EntropyStatus getEntropy(scope void[] buffer) scope @trusted + { + return syscallGetrandom(buffer, 0); + } - while (buffer.length > 0) + private EntropyStatus syscallGetrandom(scope void[] buffer, uint flags) scope @system { - const got = syscall(SYS_getrandom, buffer.ptr, buffer.length, flags); + import core.sys.linux.errno : EINTR, ENOSYS, errno; + import core.sys.linux.sys.syscall : SYS_getrandom; + import core.sys.linux.unistd : syscall; - if (got == -1) + while (buffer.length > 0) { - switch (errno) + const got = syscall(SYS_getrandom, buffer.ptr, buffer.length, flags); + + if (got == -1) { - case EINTR: - break; // That’s fine. - case ENOSYS: - return EntropyStatus.unavailable; - default: - return EntropyStatus.readError; + switch (errno) + { + case EINTR: + break; // That’s fine. + case ENOSYS: + return EntropyStatus.unavailable; + default: + return EntropyStatus.readError; + } } + + if (got > 0) + buffer = buffer[got .. $]; } - if (got > 0) - buffer = buffer[got .. $]; + return EntropyStatus.ok; } - return EntropyStatus.ok; + static string getErrorMessage(EntropyStatus status) + { + if (status == EntropyStatus.readError) + return "getEntropy(): `syscall(SYS_getrandom, …)` failed."; + + return null; + } } -} -// BSD -private -{ + // BSD version (Darwin) version = SecureARC4Random; version (DragonFlyBSD) @@ -794,25 +993,54 @@ private version = SecureARC4Random; version (SecureARC4Random) + struct ARC4Random { - EntropyResult getEntropyViaARC4Random(scope void[] buffer) @trusted + @nogc nothrow @safe: + + EntropyStatus open() scope + { + return EntropyStatus.ok; + } + + void close() scope + { + // no-op + } + + EntropyStatus getEntropy(scope void[] buffer) scope @trusted { arc4random_buf(buffer.ptr, buffer.length); - return EntropyResult(EntropyStatus.ok, EntropySource.arc4random); + return EntropyStatus.ok; + } + + private static + { + extern(C) void arc4random_buf(scope void* buf, size_t nbytes) @system; } - private extern(C) void arc4random_buf(scope void* buf, size_t nbytes) @system; + static string getErrorMessage(EntropyStatus status) + { + // `arc4random_buf()` will always succeed (or segfault). + return null; + } } version (UseGetentropy) + struct Getentropy { - EntropyResult getEntropyViaGetentropy(scope void[] buffer) @trusted + @nogc nothrow @safe: + + EntropyStatus open() scope { - const status = callGetentropy(buffer); - return EntropyResult(status, EntropySource.getentropy); + return EntropyStatus.ok; } - private EntropyStatus callGetentropy(scope void[] buffer) @system + void close() scope + { + // no-op + } + + EntropyStatus getEntropy(scope void[] buffer) scope @trusted { /+ genentropy(3): @@ -828,91 +1056,98 @@ private return EntropyStatus.ok; } - private extern(C) int getentropy(scope void* buf, size_t buflen) @system; - } -} - -version (Windows) -{ - import core.sys.windows.bcrypt : BCryptGenRandom, BCRYPT_USE_SYSTEM_PREFERRED_RNG; - import core.sys.windows.windef : HMODULE, PUCHAR, ULONG; - import core.sys.windows.ntdef : NT_SUCCESS; - -private: + private static + { + extern(C) int getentropy(scope void* buf, size_t buflen) @system; + } - EntropyResult getEntropyViaBCryptGenRandom(scope void[] buffer) @trusted - { - const loaded = loadBcrypt(); - if (loaded != EntropyStatus.ok) - return EntropyResult(loaded, EntropySource.bcryptGenRandom); + static string getErrorMessage(EntropyStatus status) + { + if (status == EntropyStatus.readError) + return "getEntropy(): `getentropy()` failed."; - const status = callBcryptGenRandom(buffer); - return EntropyResult(status, EntropySource.bcryptGenRandom); + return null; + } } - EntropyStatus callBcryptGenRandom(scope void[] buffer) @system + version (Windows) + struct BCryptGenRandom { - foreach (chunk; VoidChunks(buffer, ULONG.max)) + import core.sys.windows.bcrypt : BCryptGenRandom, BCRYPT_USE_SYSTEM_PREFERRED_RNG; + import core.sys.windows.windef : HMODULE, PUCHAR, ULONG; + import core.sys.windows.ntdef : NT_SUCCESS; + + private { - assert(chunk.length <= ULONG.max, "Bad chunk length."); + HMODULE _hBcrypt = null; + typeof(BCryptGenRandom)* _ptrBCryptGenRandom; + } - const gotRandom = ptrBCryptGenRandom( - null, - cast(PUCHAR) buffer.ptr, - cast(ULONG) buffer.length, - BCRYPT_USE_SYSTEM_PREFERRED_RNG, - ); + @nogc nothrow @safe: - if (!NT_SUCCESS(gotRandom)) - return EntropyStatus.readError; - } + EntropyStatus open() scope @trusted + { + import core.sys.windows.winbase : GetProcAddress, LoadLibraryA; - return EntropyStatus.ok; - } + if (_hBcrypt !is null) + return EntropyStatus.ok; - static - { - HMODULE hBcrypt = null; - typeof(BCryptGenRandom)* ptrBCryptGenRandom; - } + _hBcrypt = LoadLibraryA("bcrypt.dll"); + if (!hBcrypt) + return EntropyStatus.unavailableLibrary; - EntropyStatus loadBcrypt() @system - { - import core.sys.windows.winbase : GetProcAddress, LoadLibraryA; + _ptrBCryptGenRandom = cast(typeof(_ptrBCryptGenRandom)) GetProcAddress(_hBcrypt, "BCryptGenRandom"); + if (!_ptrBCryptGenRandom) + return EntropyStatus.unavailable; - if (hBcrypt !is null) return EntropyStatus.ok; + } - hBcrypt = LoadLibraryA("Bcrypt.dll"); - if (!hBcrypt) - return EntropyStatus.unavailableLibrary; + void close() scope @trusted + { + import core.sys.windows.winbase : FreeLibrary; - ptrBCryptGenRandom = cast(typeof(ptrBCryptGenRandom)) GetProcAddress(hBcrypt, "BCryptGenRandom"); - if (!ptrBCryptGenRandom) - return EntropyStatus.unavailable; + if (hBcrypt is null) + return; - return EntropyStatus.ok; - } + if (!FreeLibrary(hBcrypt)) + return; // Error - // Will free `Bcrypt.dll`. - void freeBcrypt() @system - { - import core.sys.windows.winbase : FreeLibrary; + hBcrypt = null; + ptrBCryptGenRandom = null; + } - if (hBcrypt is null) - return; - if (!FreeLibrary(hBcrypt)) + EntropyStatus getEntropy(scope void[] buffer) scope @trusted { - return; // Error + foreach (chunk; VoidChunks(buffer, ULONG.max)) + { + assert(chunk.length <= ULONG.max, "Bad chunk length."); + + const gotRandom = _ptrBCryptGenRandom( + null, + cast(PUCHAR) buffer.ptr, + cast(ULONG) buffer.length, + BCRYPT_USE_SYSTEM_PREFERRED_RNG, + ); + + if (!NT_SUCCESS(gotRandom)) + return EntropyStatus.readError; + } + + return EntropyStatus.ok; } - hBcrypt = null; - ptrBCryptGenRandom = null; - } + static string getErrorMessage(EntropyStatus status) + { + if (status == EntropyStatus.unavailableLibrary) + return "getEntropy(): `LoadLibraryA(\"bcrypt.dll\")` failed."; + if (status == EntropyStatus.unavailable) + return "getEntropy(): `GetProcAddress(hBcrypt , \"BCryptGenRandom\")` failed."; + if (status == EntropyStatus.readError) + return "getEntropy(): `BCryptGenRandom()` failed."; - static ~this() @system - { - freeBcrypt(); + return null; + } } } diff --git a/std/random.d b/std/random.d index 0176b0ae653..4c6026639d9 100644 --- a/std/random.d +++ b/std/random.d @@ -1819,10 +1819,10 @@ how excellent the source of entropy is. { version (SeedUseGetEntropy) { - import std.internal.entropy : crashOnError, EntropySource, getEntropy; + import std.internal.entropy : crashOnError, EntropySourceID, getEntropy; uint buffer; - const status = (() @trusted => getEntropy(&buffer, buffer.sizeof, EntropySource.tryAll))(); + const status = (() @trusted => getEntropy(&buffer, buffer.sizeof, EntropySourceID.tryAll))(); crashOnError(status); return buffer; } @@ -1877,10 +1877,10 @@ if (isUnsigned!UIntType) { version (SeedUseGetEntropy) { - import std.internal.entropy : crashOnError, EntropySource, getEntropy; + import std.internal.entropy : crashOnError, EntropySourceID, getEntropy; UIntType buffer; - const status = (() @trusted => getEntropy(&buffer, buffer.sizeof, EntropySource.tryAll))(); + const status = (() @trusted => getEntropy(&buffer, buffer.sizeof, EntropySourceID.tryAll))(); crashOnError(status); return buffer; } From bf8b594c8cc562f54aef8a18c2c44b9426e8767b Mon Sep 17 00:00:00 2001 From: Mindy Batek Date: Wed, 6 Aug 2025 00:21:27 +0200 Subject: [PATCH 2/8] Cleanup workaround --- std/internal/entropy.d | 92 +++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/std/internal/entropy.d b/std/internal/entropy.d index 5469136a937..dccca7a471f 100644 --- a/std/internal/entropy.d +++ b/std/internal/entropy.d @@ -237,10 +237,7 @@ private enum methodCall = (args.length == 0) ? `matched.` ~ methodName : `matched.` ~ methodName ~ `(args)`; - enum handler(T) = `(ref scope ` - ~ __traits(fullyQualifiedName, T) - .chomp(".CharDev") /+ quick'n'dirty workaround +/ - ~ ` matched) @safe => ` ~ methodCall; + enum handler(T) = `(ref scope ` ~ __traits(fullyQualifiedName, T) ~ ` matched) @safe => ` ~ methodCall; enum handlers = `AliasSeq!(` ~ [staticMap!(handler, EntropySourceHandle.Inner.Types)].join(",\n") ~ `)`; return source.match!(mixin(handlers)); @@ -851,72 +848,75 @@ static: } version(Posix) - struct CharDev(EntropySourceID sourceID, string path) + template CharDevImpl(EntropySourceID sourceID, string path) { - import core.stdc.stdio : FILE, fclose, fopen, fread; - - private + struct CharDev { - enum string _path = path ~ "\0"; - FILE* _file = null; - } + import core.stdc.stdio : FILE, fclose, fopen, fread; - @nogc nothrow @safe: + private + { + enum string _path = path ~ "\0"; + FILE* _file = null; + } - enum id = sourceID; + @nogc nothrow @safe: - EntropyStatus open() scope @trusted - { - _file = fopen(_path.ptr, "r"); + enum id = sourceID; - if (_file is null) - return EntropyStatus.unavailable; + EntropyStatus open() scope @trusted + { + _file = fopen(_path.ptr, "r"); - return EntropyStatus.ok; - } + if (_file is null) + return EntropyStatus.unavailable; - void close() scope @trusted - { - if (_file is null) - return; + return EntropyStatus.ok; + } - fclose(_file); - } + void close() scope @trusted + { + if (_file is null) + return; - EntropyStatus getEntropy(scope void[] buffer) scope @trusted - { - if (_file is null) - return EntropyStatus.unavailable; + fclose(_file); + } - const bytesRead = fread(buffer.ptr, 1, buffer.length, _file); - if (bytesRead != buffer.length) - return EntropyStatus.readError; + EntropyStatus getEntropy(scope void[] buffer) scope @trusted + { + if (_file is null) + return EntropyStatus.unavailable; - return EntropyStatus.ok; - } + const bytesRead = fread(buffer.ptr, 1, buffer.length, _file); + if (bytesRead != buffer.length) + return EntropyStatus.readError; - static string getErrorMessage(EntropyStatus status) - { - if (status == EntropyStatus.unavailable) - return "getEntropy(): `" ~ path ~ "` is unavailable."; - if (status == EntropyStatus.readError) - return "getEntropy(): Reading from `" ~ path ~ "` failed."; + return EntropyStatus.ok; + } - return null; + static string getErrorMessage(EntropyStatus status) + { + if (status == EntropyStatus.unavailable) + return "getEntropy(): `" ~ path ~ "` is unavailable."; + if (status == EntropyStatus.readError) + return "getEntropy(): Reading from `" ~ path ~ "` failed."; + + return null; + } } } version(Posix) - alias CharDevURandom = CharDev!( + alias CharDevURandom = CharDevImpl!( EntropySourceID.charDevURandom, "/dev/urandom", - ); + ).CharDev; version(Posix) - alias CharDevRandom = CharDev!( + alias CharDevRandom = CharDevImpl!( EntropySourceID.charDevRandom, "/dev/random", - ); + ).CharDev; version(linux) struct Getrandom From c26821bc70ccc5acc1d95b8f97a1ecfbf69d39b0 Mon Sep 17 00:00:00 2001 From: Mindy Batek Date: Sun, 10 Aug 2025 04:19:02 +0200 Subject: [PATCH 3/8] Refactor `std.internal.entropy` --- std/internal/entropy.d | 1565 ++++++++++++++++++---------------------- std/random.d | 10 +- 2 files changed, 708 insertions(+), 867 deletions(-) diff --git a/std/internal/entropy.d b/std/internal/entropy.d index dccca7a471f..ceb2ad4bb93 100644 --- a/std/internal/entropy.d +++ b/std/internal/entropy.d @@ -32,6 +32,7 @@ module std.internal.entropy; import std.meta; import std.sumtype; +import std.traits; import std.typecons; version (OSX) @@ -43,64 +44,37 @@ else version (TVOS) else version (WatchOS) version = Darwin; -version (Darwin) mixin entropyImpl!( - EntropySource.arc4random, - Implementation.ARC4Random, - Implementation.CharDevURandom, - Implementation.CharDevRandom, -); -else version (DragonFlyBSD) mixin entropyImpl!( - EntropySourceID.getentropy, - Implementation.Getentropy, - Implementation.CharDevURandom, - Implementation.CharDevRandom, -); -else version (FreeBSD) mixin entropyImpl!( - EntropySourceID.getentropy, - Implementation.Getentropy, - Implementation.CharDevURandom, - Implementation.CharDevRandom, -); -else version (linux) mixin entropyImpl!( - EntropySourceID.getrandom, - Implementation.Getrandom, - Implementation.CharDevURandom, - Implementation.CharDevRandom, -); -else version (NetBSD) mixin entropyImpl!( - EntropySourceID.arc4random, - Implementation.ARC4Random, - Implementation.CharDevURandom, - Implementation.CharDevRandom, -); -else version (OpenBSD) mixin entropyImpl!( - EntropySourceID.arc4random, - Implementation.ARC4Random, - Implementation.CharDevURandom, - Implementation.CharDevRandom, -); -else version (Posix) mixin entropyImpl!( - EntropySourceID.charDevURandom, - Implementation.CharDevURandom, - Implementation.CharDevRandom, -); -else version (Windows) mixin entropyImpl!( - EntropySourceID.bcryptGenRandom, - Implementation.BCryptGenRandom, -); -else mixin entropyImpl!( - EntropySourceID.none, -); +/// +enum EntropyStatus +{ + /// catch-all error + unknownError = 1, + + /// success + ok = 0, + + /// An entropy source was unavailable. + unavailable, + + /// A dependency providing the entropy source turned out unavailable. + unavailableLibrary, + + /// The requested entropy source is not supported on this platform. + unavailablePlatform, + + /// Could not retrieve entropy from the selected source. + readError, +} /+ - Building blocks and implementation helpers + Building blocks +/ private { /++ A “Chunks” implementation that works with `void[]`. +/ - struct VoidChunks + struct VoidChunks() { void[] _data; size_t _chunkSize; @@ -137,1017 +111,886 @@ private _data = _data[_chunkSize .. $]; } } +} - template isValidSupportedSource(alias SupportedSource) - { - enum isValidSupportedSource = ( - is(SupportedSource == struct) && - is(typeof(SupportedSource.id) == EntropySourceID) && - SupportedSource.id != EntropySourceID.tryAll && - SupportedSource.id != EntropySourceID.none - ); - } +/++ + A CSPRNG suitable to retrieve cryptographically-secure random data from. - /++ - `getEntropyImpl()` implementation helper. - To be instantiated and mixed in with platform-specific configuration. + (No actual low-level entropy sources are provided on purpose.) + +/ +enum EntropySourceID +{ + /// Implements a $(I hunting) strategy for finding an entropy source that + /// is available at runtime. + /// + /// Try supported sources one-by-one until one is available. + /// This exists to enable the use of this the entropy library + /// in a backwards compatibility way. + /// + /// It is recommended against using this in places that do not strictly + /// have to to meet compatibility requirements. + /// Like any kind of crypto-agility, this approach may suffer from + /// practical issues. + /// + /// See_also: + /// While the following article focuses on cipher agility in protocols, + /// it elaborates why agility can lead to problems: + /// $(LINK https://web.archive.org/web/20191102211148/https://paragonie.com/blog/2019/10/against-agility-in-cryptography-protocols) + tryAll = -1, - Params: - defaultSource = Default entropy source of the platform - SupportedSources = Sequence of `SrcFunPair` - representing the supported sources of the platform - +/ - mixin template entropyImpl(EntropySourceID defaultSource, SupportedSources...) - if (allSatisfy!(isValidSupportedSource, SupportedSources)) - { - private: - /// Preconfigured entropy source preset of the platform. - enum defaultEntropySource = defaultSource; + /// Always fail. + none = 0, - alias _InnerEntropySourceHandle = SumType!( - Implementation.None, - SupportedSources, - ); + /// `/dev/urandom` + charDevURandom = 1, - @nogc nothrow @safe: + /// `/dev/random` + charDevRandom = 2, - EntropyStatus _openEntropySourceImpl(out EntropySourceHandle.Inner handle) @safe - { - switch (_entropySource) - { - static foreach (Source; SupportedSources) - {{ - case Source.id: - auto source = Source(); - const status = source.open(); - () @trusted { handle = EntropySourceHandle.Inner(source); }(); - return status; - }} - - case EntropySourceID.tryAll: - { - const status = _tryOpenEntropySources(handle); - handle.saveSourceForNextUse(); - return status; - } + /// `getrandom` syscall or wrapper + getrandom = 3, + + /// `arc4random` + arc4random = 4, + + // `getentropy` + getentropy = 5, - case EntropySourceID.none: - auto none = Implementation.None(); - () @trusted { handle = EntropySourceHandle.Inner(none); }(); - return none.open(); + /// Windows legacy CryptoAPI + cryptGenRandom = 6, - default: - return EntropyStatus.unavailablePlatform; - } + /// Windows Cryptography API: Next Generation (“BCrypt”) + bcryptGenRandom = 7, +} + +/++ + Entropy Source implementations + +/ +private struct Implementation +{ +static: + + version (all) + @(EntropySourceID.none) + struct None + { + @nogc nothrow @safe: + + EntropyStatus open() scope + { + return EntropyStatus.unavailable; } - EntropyStatus openEntropySourceImpl(out EntropySourceHandle.InnerRefCounted handle) @safe + void close() scope { - EntropySourceHandle.Inner innerHandle; - const status = _openEntropySourceImpl(innerHandle); - handle = safeRefCounted(innerHandle); + // no-op + } - return status; + EntropyStatus getEntropy(scope void[]) scope + { + return EntropyStatus.unavailable; } - EntropyStatus _tryOpenEntropySources(out EntropySourceHandle.Inner handle) @safe + static string getErrorMessage(EntropyStatus status) { - static foreach (Source; SupportedSources) - {{ - auto source = Source(); - const status = source.open(); - if (status == EntropyStatus.ok) - { - () @trusted { handle = EntropySourceHandle.Inner(source); }(); - return status; - } - }} + if (status == EntropyStatus.unavailable) + return "getEntropy(): Error - No suitable entropy source was available."; - auto fallback = Implementation.None(); - () @trusted { handle = EntropySourceHandle.Inner(fallback); }(); - return fallback.open(); + return null; } } - auto matchCall(string methodName, Args...)(ref scope EntropySourceHandle.Inner source, Args args) @nogc @safe + version (Posix) + template CharDevImpl(EntropySourceID id, string path) { - import std.array : join; - import std.string : chomp; - - enum methodCall = (args.length == 0) - ? `matched.` ~ methodName - : `matched.` ~ methodName ~ `(args)`; - enum handler(T) = `(ref scope ` ~ __traits(fullyQualifiedName, T) ~ ` matched) @safe => ` ~ methodCall; - enum handlers = `AliasSeq!(` ~ [staticMap!(handler, EntropySourceHandle.Inner.Types)].join(",\n") ~ `)`; - - return source.match!(mixin(handlers)); - } + @id + struct CharDev + { + import core.stdc.stdio : FILE, fclose, fopen, fread; + import core.sys.posix.unistd; - auto borrowMatchCall(string methodName, Args...)(EntropySourceHandle.InnerRefCounted source, Args args) @nogc @safe - { - return source.borrow!((EntropySourceHandle.Inner borrowed) => matchCall!methodName(borrowed, args)); - } -} + private + { + enum string _path = path ~ "\0"; + FILE* _file = null; + } -// Self-test: Detect potentially unsuitable default entropy source. -@safe unittest -{ - auto buffer = new ubyte[](32); - forceEntropySource(defaultEntropySource); - const result = getEntropy(buffer); + @nogc nothrow @safe: - assert( - !result.isUnavailable, - "The default entropy source for the target platform" - ~ " is unavailable on this machine. Please consider" - ~ " patching it to accommodate to your environment." - ); - assert(result.isOK); -} + EntropyStatus open() scope @trusted + { + _file = fopen(_path.ptr, "r"); -// Self-test: Detect faulty implementation. -@system unittest -{ - forceEntropySource(defaultEntropySource); + if (_file is null) + return EntropyStatus.unavailable; - bool test() @system - { - static immutable pattern = 0xDEAD_BEEF_1337_0000; - long number = pattern; - const result = getEntropy(&number, number.sizeof); - assert(result.isOK); - return number != pattern; - } + return EntropyStatus.ok; + } - size_t timesFailed = 0; - foreach (n; 0 .. 3) - if (!test()) - ++timesFailed; + void close() scope @trusted + { + if (_file is null) + return; - assert( - timesFailed <= 1, - "Suspicious random data: Potential security issue or really unlucky; please retry." - ); -} + fclose(_file); + } -// Self-test: Detect faulty implementation. -@safe unittest -{ - forceEntropySource(defaultEntropySource); + EntropyStatus getEntropy(scope void[] buffer) scope @trusted + { + if (_file is null) + return EntropyStatus.unavailable; - bool test() @safe - { - ubyte[32] data; - data[] = 0; + const bytesRead = fread(buffer.ptr, 1, buffer.length, _file); + if (bytesRead != buffer.length) + return EntropyStatus.readError; - const result = getEntropy(data[]); - assert(result.isOK); + return EntropyStatus.ok; + } - size_t zeros = 0; - foreach (b; data) - if (b == 0) - ++zeros; + static string getErrorMessage(EntropyStatus status) + { + if (status == EntropyStatus.unavailable) + return "getEntropy(): `" ~ path ~ "` is unavailable."; + if (status == EntropyStatus.readError) + return "getEntropy(): Reading from `" ~ path ~ "` failed."; - enum threshold = 24; - return zeros < threshold; + return null; + } + } } - size_t timesFailed = 0; - foreach (n; 0 .. 3) - if (!test()) - ++timesFailed; - - assert( - timesFailed <= 1, - "Suspicious random data: Potential security issue or really unlucky; please retry." - ); -} - -@nogc nothrow: - -// Flagship function -/++ - Opens a handle to an applicable system CSPRNG. - +/ -EntropyStatus openEntropySource(out EntropySourceHandle handle) @safe -{ - EntropySourceHandle.InnerRefCounted innerHandle; - const status = openEntropySourceImpl(innerHandle); - handle = EntropySourceHandle(innerHandle); - return status; -} + version (Posix) + alias CharDevURandom = CharDevImpl!(EntropySourceID.charDevURandom, "/dev/urandom").CharDev; -// Flagship function -EntropyStatus getEntropy(EntropySourceHandle source, scope void[] buffer) @safe -{ - return source.handle.borrowMatchCall!"getEntropy"(buffer); -} + version (Posix) + alias CharDevRandom = CharDevImpl!(EntropySourceID.charDevRandom, "/dev/random").CharDev; -string getErrorMessage(EntropySourceHandle source, EntropyStatus status) @safe -{ - static string genericErrorMessage(EntropyStatus status) + version (linux) + @(EntropySourceID.getrandom) + struct Getrandom { - if (status == EntropyStatus.unavailable || - status == EntropyStatus.unavailableLibrary) - return "getEntropy(): An entropy source was unavailable."; - if (status == EntropyStatus.unavailablePlatform) - return "getEntropy(): The requested entropy source is not supported on this platform."; - if (status == EntropyStatus.readError) - return "getEntropy(): Could not retrieve entropy from the selected source."; - - return "getEntropy(): An unknown error occurred."; - } + @nogc nothrow @safe: - const msg = source.handle.borrowMatchCall!"getErrorMessage"(status); - if (msg is null) - return genericErrorMessage(status); + EntropyStatus open() scope + { + return (testAvailability()) + ? EntropyStatus.ok + : EntropyStatus.unavailable; + } - return msg; -} + private bool testAvailability() scope @trusted + { + import core.sys.linux.errno : ENOSYS, errno; -EntropySourceID id(EntropySourceHandle source) @safe -{ - return source.handle.borrowMatchCall!"id"(); -} + const got = syscallGetrandom(null, 0, 0); + if (got == -1) + if (errno == ENOSYS) + return false; -private EntropySourceID id(EntropySourceHandle.Inner source) @safe -{ - return source.matchCall!"id"(); -} + return true; + } -// Legacy flagship function -/++ - Retrieves random data from an applicable system CSPRNG. - - Params: - buffer = An output buffer to store the retrieved entropy in. - The length of it will determine the amount of random data to - be obtained. - - This function (and all overloads) always attempt to fill - the entire buffer. Therefore, they can block, spin or report - an error. - - Returns: - An `EntropyResult` that either reports success - or the type of error that has occurred. - - In case of an error, the data in `buffer` MUST NOT be used. - The recommended way to check for success is through the `isOK()` - helper function. - +/ -EntropyResult getEntropy(scope void[] buffer) @safe -{ - EntropySourceHandle handle; - const statusOpen = openEntropySource(handle); - if (statusOpen != EntropyStatus.ok) - return EntropyResult(statusOpen, handle.id); - - const statusGet = handle.getEntropy(buffer); - return EntropyResult(statusGet, handle.id); -} + void close() scope + { + // no-op + } -/// -@safe unittest -{ - int[4] bytes; - if (getEntropy(cast(void[]) bytes).isOK) - { - // Success; data in `bytes` may be used. - } + EntropyStatus getEntropy(scope void[] buffer) scope @trusted + { + return syscallGetrandomLoop(buffer, 0); + } - assert((cast(void[]) bytes).length == bytes.sizeof); -} + private static auto syscallGetrandom(scope void* buf, size_t buflen, uint flags) @system + { + import core.sys.linux.sys.syscall : SYS_getrandom; + import core.sys.linux.unistd : syscall; -// Convenience overload -/// ditto -EntropyResult getEntropy(scope ubyte[] buffer) @safe -{ - return getEntropy(cast(void[]) buffer); -} + return syscall(SYS_getrandom, buf, buflen, flags); + } -/// -@safe unittest -{ - ubyte[16] bytes; - if (getEntropy(bytes).isOK) - { - // Success; data in `bytes` may be used. - } -} + private static EntropyStatus syscallGetrandomLoop(scope void[] buffer, uint flags) @system + { + import core.sys.linux.errno : EINTR, ENOSYS, errno; -// Convenience wrapper -/// ditto -/++ - Retrieves random data from an applicable system CSPRNG. + while (buffer.length > 0) + { + const got = syscallGetrandom(buffer.ptr, buffer.length, flags); - Params: - buffer = An output buffer to store the retrieved entropy in. - length = Length of the provided `buffer`. - Specifying a wrong value here, will lead to memory corruption. + if (got == -1) + { + switch (errno) + { + case EINTR: + break; // That’s fine. + case ENOSYS: + return EntropyStatus.unavailable; + default: + return EntropyStatus.readError; + } + } - Returns: - An `EntropyResult` that either reports success - or the type of error that has occurred. + if (got > 0) + buffer = buffer[got .. $]; + } - In case of an error, the data in `buffer` MUST NOT be used. - The recommended way to check for success is through the `isOK()` - helper function. - +/ -EntropyResult getEntropy(scope void* buffer, size_t length) @system -{ - return getEntropy(buffer[0 .. length]); -} + return EntropyStatus.ok; + } -/// -@system unittest -{ - ubyte[16] bytes; - if (getEntropy(cast(void*) bytes.ptr, bytes.length).isOK) - { - // Success; data in `bytes` may be used. - } -} + static string getErrorMessage(EntropyStatus status) + { + if (status == EntropyStatus.readError) + return "getEntropy(): `syscall(SYS_getrandom, …)` failed."; -/// -@system unittest -{ - int number = void; - if (getEntropy(&number, number.sizeof).isOK) - { - // Success; value of `number` may be used. + return null; + } } -} - -/++ - Manually set the entropy source to use for the current thread. - As a rule of thumb, this SHOULD NOT be done. - - It might be useful in cases where the default entropy source — as chosen by - the maintainer of the used compiler package — is unavailable on a system. - Usually, `EntropySourceID.tryAll` will be the most reasonable option - in such cases. - - Params: - source = The requested default entropy source to use for the current thread. + // BSD + version (Darwin) + version = SecureARC4Random; + version (DragonFlyBSD) + version = UseGetentropy; + version (FreeBSD) + version = UseGetentropy; + version (NetBSD) + version = SecureARC4Random; + version (OpenBSD) + version = SecureARC4Random; - Examples: + version (SecureARC4Random) + @(EntropySourceID.arc4random) + struct ARC4Random + { + @nogc nothrow @safe: - --- - // Using `forceEntropySource` almost always is a bad idea. - // As a rule of thumb, this SHOULD NOT be done. - forceEntropySource(EntropySourceID.none); - --- - +/ -void forceEntropySource(EntropySourceID source) @safe -{ - _entropySource = source; -} + EntropyStatus open() scope + { + return EntropyStatus.ok; + } -// (In-)Convenience wrapper -/++ - Retrieves random data from the requested entropy source. + void close() scope + { + // no-op + } - In general, it’s a $(B bad idea) to let users pick sources themselves. - A sane option should be used by default instead. + EntropyStatus getEntropy(scope void[] buffer) scope @trusted + { + arc4random_buf(buffer.ptr, buffer.length); + return EntropyStatus.ok; + } - This overload only exists because its used by Phobos. + private static + { + extern(C) void arc4random_buf(scope void* buf, size_t nbytes) @system; + } - See_also: - Use `forceEntropySource` instead. - - Params: - buffer = An output buffer to store the retrieved entropy in. - The length of it will determine the amount of entropy to be - obtained. - length = Length of the provided `buffer`. - Specifying a wrong value here, will lead to memory corruption. - source = The entropy source to use for the operation. - - Returns: - An `EntropyResult` that either reports success - or the type of error that has occurred. - - In case of an error, the data in `buffer` MUST NOT be used. - The recommended way to check for success is through the `isOK()` - helper function. - +/ -EntropyResult getEntropy(scope void* buffer, size_t length, EntropySourceID source) @system -{ - const sourcePrevious = _entropySource; - scope (exit) _entropySource = sourcePrevious; + static string getErrorMessage(EntropyStatus status) + { + // `arc4random_buf()` will always succeed (or segfault). + return null; + } + } - _entropySource = source; - return getEntropy(buffer[0 .. length]); -} + version (UseGetentropy) + @(EntropySourceID.getentropy) + struct Getentropy + { + @nogc nothrow @safe: -/// -@system unittest -{ - ubyte[4] bytes; + EntropyStatus open() scope + { + return EntropyStatus.ok; + } - // `EntropySourceID.none` always fails. - assert(!getEntropy(bytes.ptr, bytes.length, EntropySourceID.none).isOK); -} + void close() scope + { + // no-op + } -/++ - A CSPRNG suitable to retrieve cryptographically-secure random data from. + EntropyStatus getEntropy(scope void[] buffer) scope @trusted + { + /+ + genentropy(3): + The maximum buflen permitted is 256 bytes. + +/ + foreach (chunk; VoidChunks!()(buffer, 256)) + { + const status = getentropy(buffer.ptr, buffer.length); + if (status != 0) + return EntropyStatus.readError; + } - (No actual low-level entropy sources are provided on purpose.) - +/ -enum EntropySourceID -{ - /// Implements a $(I hunting) strategy for finding an entropy source that - /// is available at runtime. - /// - /// Try supported sources one-by-one until one is available. - /// This exists to enable the use of this the entropy library - /// in a backwards compatibility way. - /// - /// It is recommended against using this in places that do not strictly - /// have to to meet compatibility requirements. - /// Like any kind of crypto-agility, this approach may suffer from - /// practical issues. - /// - /// See_also: - /// While the following article focuses on cipher agility in protocols, - /// it elaborates why agility can lead to problems: - /// $(LINK https://web.archive.org/web/20191102211148/https://paragonie.com/blog/2019/10/against-agility-in-cryptography-protocols) - tryAll = -1, + return EntropyStatus.ok; + } - /// Always fail. - none = 0, + private static + { + extern(C) int getentropy(scope void* buf, size_t buflen) @system; + } - /// `/dev/urandom` - charDevURandom = 1, + static string getErrorMessage(EntropyStatus status) + { + if (status == EntropyStatus.readError) + return "getEntropy(): `getentropy()` failed."; - /// `/dev/random` - charDevRandom = 2, + return null; + } + } - /// `getrandom` syscall or wrapper - getrandom = 3, + version (Windows) + @(EntropySourceID.bcryptGenRandom) + struct BCryptGenRandom + { + import core.sys.windows.bcrypt : BCryptGenRandom, BCRYPT_USE_SYSTEM_PREFERRED_RNG; + import core.sys.windows.windef : HMODULE, PUCHAR, ULONG; + import core.sys.windows.ntdef : NT_SUCCESS; - /// `arc4random` - arc4random = 4, + private + { + HMODULE _hBcrypt = null; + typeof(BCryptGenRandom)* _ptrBCryptGenRandom; + } - // `getentropy` - getentropy = 5, + @nogc nothrow @safe: - /// Windows legacy CryptoAPI - cryptGenRandom = 6, + EntropyStatus open() scope @trusted + { + import core.sys.windows.winbase : GetProcAddress, LoadLibraryA; - /// Windows Cryptography API: Next Generation (“BCrypt”) - bcryptGenRandom = 7, -} + if (_hBcrypt !is null) + return EntropyStatus.ok; -/// -enum EntropyStatus -{ - /// catch-all error - unknownError = 1, + _hBcrypt = LoadLibraryA("bcrypt.dll"); + if (!hBcrypt) + return EntropyStatus.unavailableLibrary; - /// success - ok = 0, + _ptrBCryptGenRandom = cast(typeof(_ptrBCryptGenRandom)) GetProcAddress(_hBcrypt, "BCryptGenRandom"); + if (!_ptrBCryptGenRandom) + return EntropyStatus.unavailable; - /// An entropy source was unavailable. - unavailable, + return EntropyStatus.ok; + } - /// A dependency providing the entropy source turned out unavailable. - unavailableLibrary, + void close() scope @trusted + { + import core.sys.windows.winbase : FreeLibrary; - /// The requested entropy source is not supported on this platform. - unavailablePlatform, + if (hBcrypt is null) + return; - /// Could not retrieve entropy from the selected source. - readError, -} + if (!FreeLibrary(hBcrypt)) + return; // Error -/++ - Status report returned by legacy `getEntropy` functions. + hBcrypt = null; + ptrBCryptGenRandom = null; + } - Use the `isOK` helper function to test for success. - +/ -struct EntropyResult -{ - /// - EntropyStatus status = EntropyStatus.unknownError; + EntropyStatus getEntropy(scope void[] buffer) scope @trusted + { + foreach (chunk; VoidChunks!()(buffer, ULONG.max)) + { + assert(chunk.length <= ULONG.max, "Bad chunk length."); - /// - EntropySourceID source; + const gotRandom = _ptrBCryptGenRandom( + null, + cast(PUCHAR) buffer.ptr, + cast(ULONG) buffer.length, + BCRYPT_USE_SYSTEM_PREFERRED_RNG, + ); - /++ - Returns: - A human-readable status message. - +/ - string toString() const @nogc nothrow pure @safe - { - if (status == EntropyStatus.ok) - return "getEntropy(): OK."; + if (!NT_SUCCESS(gotRandom)) + return EntropyStatus.readError; + } - if (source == EntropySourceID.none) - { - if (status == EntropyStatus.unavailable) - return "getEntropy(): Error - No suitable entropy source was available."; - } - else if (source == EntropySourceID.getrandom) - { - if (status == EntropyStatus.unavailableLibrary) - return "getEntropy(): `dlopen(\"libc\")` failed."; - if (status == EntropyStatus.unavailable) - return "getEntropy(): `dlsym(\"libc\", \"getrandom\")` failed."; - if (status == EntropyStatus.readError) - return "getEntropy(): `getrandom()` failed."; - } - else if (source == EntropySourceID.getentropy) - { - if (status == EntropyStatus.readError) - return "getEntropy(): `getentropy()` failed."; - } - else if (source == EntropySourceID.charDevURandom) - { - if (status == EntropyStatus.unavailable) - return "getEntropy(): `/dev/urandom` is unavailable."; - if (status == EntropyStatus.readError) - return "getEntropy(): Reading from `/dev/urandom` failed."; - } - else if (source == EntropySourceID.charDevURandom) - { - if (status == EntropyStatus.unavailable) - return "getEntropy(): `/dev/random` is unavailable."; - if (status == EntropyStatus.readError) - return "getEntropy(): Reading from `/dev/random` failed."; + return EntropyStatus.ok; } - else if (source == EntropySourceID.bcryptGenRandom) + + static string getErrorMessage(EntropyStatus status) { if (status == EntropyStatus.unavailableLibrary) - return "getEntropy(): `LoadLibraryA(\"Bcrypt.dll\")` failed."; + return "getEntropy(): `LoadLibraryA(\"bcrypt.dll\")` failed."; if (status == EntropyStatus.unavailable) return "getEntropy(): `GetProcAddress(hBcrypt , \"BCryptGenRandom\")` failed."; if (status == EntropyStatus.readError) return "getEntropy(): `BCryptGenRandom()` failed."; - } - // generic errors - { - if (status == EntropyStatus.unavailable || - status == EntropyStatus.unavailableLibrary) - return "getEntropy(): An entropy source was unavailable."; - if (status == EntropyStatus.unavailablePlatform) - return "getEntropy(): The requested entropy source is not supported on this platform."; - if (status == EntropyStatus.readError) - return "getEntropy(): Could not retrieve entropy from the selected source."; - - return "getEntropy(): An unknown error occurred."; + return null; } } } -/// -@safe unittest +// idOf!(T) and friends +private { - ubyte[4] data; - EntropyResult result = getEntropy(data[]); + enum hasValidEntropySourceID(T) = (getUDAs!(T, EntropySourceID).length == 1); - if (result.isOK) + template idOf(EntropySource) + if (hasValidEntropySourceID!EntropySource) { - // Success; data in `bytes` may be used. + enum idOf = getUDAs!(EntropySource, EntropySourceID)[0]; } - else - { - // Failure - if (result.isUnavailable) + @safe unittest + { + static assert(idOf!(Implementation.None) == EntropySourceID.none); + version (Posix) { - // System’s entropy source was unavailable. + static assert(idOf!(Implementation.CharDevRandom) == EntropySourceID.charDevRandom); + static assert(idOf!(Implementation.CharDevURandom) == EntropySourceID.charDevURandom); } - - // Call `toString` to obtain a user-readable error message. - assert(result.toString() !is null); - assert(result.toString().length > 0); } } -/++ - Determines whether an `EntropyResult` reports the success of an operation. - - Params: - value = test subject - - Returns: - `true` on success - +/ -pragma(inline, true) bool isOK(const EntropyResult value) pure @safe -{ - return value.status == EntropyStatus.ok; -} - -/++ - Determines whether an `EntropyResult` reports the unvailability of the - requested entropy source. - - Params: - value = test subject - - Returns: - `true` if entropy source requested to use with the operation was unavailable. - +/ -pragma(inline, true) bool isUnavailable(const EntropyResult value) pure @safe +// Constraints for `multiSourceImpl` +private { - return ( - value.status == EntropyStatus.unavailable || - value.status == EntropyStatus.unavailableLibrary || - value.status == EntropyStatus.unavailablePlatform + enum isNoneImplementation(T) = is(T == Implementation.None); + enum isNotNoneImplementation(T) = !isNoneImplementation!(T); + enum hasValidConfigurableID(T) = ( + hasValidEntropySourceID!(T) + && (idOf!(T) != EntropySourceID.none) + && (idOf!(T) != EntropySourceID.tryAll) ); } -package(std): - -// If the system let us down, we'll let the system down. -pragma(inline, true) void crashOnError(const EntropyResult value) pure @safe -{ - if (value.isOK) - return; - - assert(false, value.toString()); -} - /++ - Depending on the underlying implementation, this handle might only be a dummy. - Some implementations build upon handles themselves, - hence this generic wrapper has to provide support for doing so. + Multi-source implementation code + + `SupportedSources` to be provided sorted by priority in descending order. +/ -struct EntropySourceHandle +private mixin template multiSourceImpl(SupportedSources...) +if ( + is(SupportedSources == NoDuplicates!SupportedSources) + && allSatisfy!(isNotNoneImplementation, SupportedSources) + && allSatisfy!(hasValidConfigurableID, SupportedSources) +) { - private - { - alias Inner = _InnerEntropySourceHandle; - alias InnerRefCounted = SafeRefCounted!( - EntropySourceHandle.Inner, - RefCountedAutoInitialize.no - ); - } + private alias AllSupportedSources = AliasSeq!(SupportedSources, Implementation.None); + private alias DefaultSource = AllSupportedSources[0]; - private - { - InnerRefCounted handle; - } + private enum isSupportedSource(T) = (staticIndexOf!(T, AllSupportedSources) >= 0); - private this(InnerRefCounted handle) @nogc nothrow pure @safe + private struct AutoClosed(TSubject) { - this.handle = handle; - } -} + TSubject subject; -private -{ - static EntropySourceID _entropySource = defaultEntropySource; + @disable this(); + @disable this(this); - void saveSourceForNextUse(EntropySourceHandle.Inner source) @safe - { - if (source.id == EntropySourceID.none) - return; + this(TSubject subject) @nogc nothrow pure @safe + { + this.subject = subject; + } - _entropySource = source.id; + ~this() scope @safe + { + subject.close(); + } } -} -private struct Implementation -{ -static: + private alias AutoClosedSources = staticMap!(AutoClosed, AllSupportedSources); + private alias AutoClosedHandle = SumType!(AutoClosedSources); + private alias RefCountedHandle = SafeRefCounted!(AutoClosedHandle, RefCountedAutoInitialize.no); - version(all) - struct None - { - enum id = EntropySourceID.none; - - @nogc nothrow @safe: + /++ + Handle to an opened entropy source - EntropyStatus open() scope + Depending on the underlying implementation, this handle might only be a dummy. + Some implementations build upon handles themselves, + hence this generic wrapper has to provide support for doing so. + +/ + struct EntropySourceHandle + { + private { - return EntropyStatus.unavailable; + RefCountedHandle handle; } - void close() scope + private this(RefCountedHandle handle) @nogc nothrow @safe { - // no-op + this.handle = handle; } - EntropyStatus getEntropy(scope void[]) scope + private static typeof(this) pack(EntropySource)(EntropySource source) @safe { - return EntropyStatus.unavailable; + return EntropySourceHandle( + safeRefCounted(AutoClosedHandle(AutoClosed!EntropySource(source))) + ); } - static string getErrorMessage(EntropyStatus status) + public @nogc nothrow @safe { - if (status == EntropyStatus.unavailable) - return "getEntropy(): Error - No suitable entropy source was available."; - - return null; - } - } - - version(Posix) - template CharDevImpl(EntropySourceID sourceID, string path) - { - struct CharDev - { - import core.stdc.stdio : FILE, fclose, fopen, fread; - - private - { - enum string _path = path ~ "\0"; - FILE* _file = null; - } - - @nogc nothrow @safe: + /++ + Retrieves random data from the corresponding system CSPRNG. - enum id = sourceID; - - EntropyStatus open() scope @trusted - { - _file = fopen(_path.ptr, "r"); + Params: + buffer = An output buffer to store the retrieved entropy in. + The length of it will determine the amount of random data to + be obtained. - if (_file is null) - return EntropyStatus.unavailable; - - return EntropyStatus.ok; - } - - void close() scope @trusted - { - if (_file is null) - return; - - fclose(_file); - } - - EntropyStatus getEntropy(scope void[] buffer) scope @trusted - { - if (_file is null) - return EntropyStatus.unavailable; - - const bytesRead = fread(buffer.ptr, 1, buffer.length, _file); - if (bytesRead != buffer.length) - return EntropyStatus.readError; + This function always attempts to fill the entire buffer. + Therefore, it can block, spin or report an error. - return EntropyStatus.ok; - } + Returns: + An `EntropyStatus` that either reports success + or the type of error that has occurred. - static string getErrorMessage(EntropyStatus status) + In case of an error, the data in `buffer` MUST NOT be used. + +/ + EntropyStatus getEntropy(scope void[] buffer) { - if (status == EntropyStatus.unavailable) - return "getEntropy(): `" ~ path ~ "` is unavailable."; - if (status == EntropyStatus.readError) - return "getEntropy(): Reading from `" ~ path ~ "` failed."; - - return null; + return handle.borrow!((ref scope borrowed) + => borrowed.match!((ref scope matched) + => matched.subject.getEntropy(buffer) + ) + ); } - } - } - - version(Posix) - alias CharDevURandom = CharDevImpl!( - EntropySourceID.charDevURandom, - "/dev/urandom", - ).CharDev; - - version(Posix) - alias CharDevRandom = CharDevImpl!( - EntropySourceID.charDevRandom, - "/dev/random", - ).CharDev; - - version(linux) - struct Getrandom - { - - enum id = EntropySourceID.getrandom; - - @nogc nothrow @safe: - - EntropyStatus open() scope - { - return EntropyStatus.ok; - } - - void close() scope - { - // no-op - } - EntropyStatus getEntropy(scope void[] buffer) scope @trusted - { - return syscallGetrandom(buffer, 0); - } + /++ + Retrieves a suitable error message to the provided status code. - private EntropyStatus syscallGetrandom(scope void[] buffer, uint flags) scope @system - { - import core.sys.linux.errno : EINTR, ENOSYS, errno; - import core.sys.linux.sys.syscall : SYS_getrandom; - import core.sys.linux.unistd : syscall; - - while (buffer.length > 0) + The exact error messages are determined by the underlying + implementation. + +/ + string getErrorMessage(EntropyStatus status) const { - const got = syscall(SYS_getrandom, buffer.ptr, buffer.length, flags); - - if (got == -1) + static string genericErrorMessage(EntropyStatus status) { - switch (errno) - { - case EINTR: - break; // That’s fine. - case ENOSYS: - return EntropyStatus.unavailable; - default: - return EntropyStatus.readError; - } + if (status == EntropyStatus.unavailable || + status == EntropyStatus.unavailableLibrary) + return "getEntropy(): An entropy source was unavailable."; + if (status == EntropyStatus.unavailablePlatform) + return "getEntropy(): The requested entropy source is not supported on this platform."; + if (status == EntropyStatus.readError) + return "getEntropy(): Could not retrieve entropy from the selected source."; + + return "getEntropy(): An unknown error occurred."; } - if (got > 0) - buffer = buffer[got .. $]; - } - - return EntropyStatus.ok; - } + if (status == EntropyStatus.ok) + return null; - static string getErrorMessage(EntropyStatus status) - { - if (status == EntropyStatus.readError) - return "getEntropy(): `syscall(SYS_getrandom, …)` failed."; + const specificErrorMessage = handle.borrow!((ref scope borrowed) + => borrowed.match!((ref scope matched) + => matched.subject.getErrorMessage(status) + ) + ); - return null; + return (specificErrorMessage is null) + ? genericErrorMessage(status) + : specificErrorMessage; + } } } - // BSD - version (Darwin) - version = SecureARC4Random; - version (DragonFlyBSD) - version = UseGetentropy; - version (FreeBSD) - version = UseGetentropy; - version (NetBSD) - version = SecureARC4Random; - version (OpenBSD) - version = SecureARC4Random; + /// + enum EntropySourceID defaultEntropySource = idOf!(DefaultSource); - version (SecureARC4Random) - struct ARC4Random + private static _entropySource = defaultEntropySource; + + /++ + Manually set the entropy source to use for the current thread. + + As a rule of thumb, this SHOULD NOT be done. + + It might be useful in cases where the default entropy source — as chosen by + the maintainer of the used compiler package — is unavailable on a system. + Usually, `EntropySourceID.tryAll` will be the most reasonable option + in such cases. + + Params: + source = The requested default entropy source to use for the current thread. + + Examples: + + --- + // Using `forceEntropySource` almost always is a bad idea. + // As a rule of thumb, this SHOULD NOT be done. + forceEntropySource(EntropySourceID.none); + --- + +/ + void forceEntropySource(EntropySourceID source) @safe { - @nogc nothrow @safe: + _entropySource = source; + } - EntropyStatus open() scope + /// + struct EntropySourceOpenResult + { + private { - return EntropyStatus.ok; + EntropyStatus _status; + EntropySourceHandle _handle; } - void close() scope + public @nogc nothrow @safe { - // no-op - } + EntropyStatus status() const pure => _status; + bool isOK() const pure => (status == EntropyStatus.ok); + string errorMessage() const => _handle.getErrorMessage(_status); - EntropyStatus getEntropy(scope void[] buffer) scope @trusted - { - arc4random_buf(buffer.ptr, buffer.length); - return EntropyStatus.ok; - } + inout(EntropySourceHandle) handle() inout + { + if (!isOK) + assert(false, "Trying to retrieve handle after a failed opening."); - private static - { - extern(C) void arc4random_buf(scope void* buf, size_t nbytes) @system; + return _handle; + } } + } - static string getErrorMessage(EntropyStatus status) - { - // `arc4random_buf()` will always succeed (or segfault). - return null; - } + private struct DetailedEntropySourceOpenResult + { + EntropySourceID sourceID; + EntropySourceOpenResult result; + + bool isOK() const @nogc nothrow pure @safe => result.isOK; } - version (UseGetentropy) - struct Getentropy + private DetailedEntropySourceOpenResult openEntropySourceByType(EntropySource)() { - @nogc nothrow @safe: + auto source = EntropySource(); + const status = source.open(); + + return DetailedEntropySourceOpenResult( + idOf!(EntropySource), + EntropySourceOpenResult( + status, + EntropySourceHandle.pack(source), + ) + ); + } - EntropyStatus open() scope + @nogc nothrow @safe + { + private DetailedEntropySourceOpenResult openEntropySourceTryAll() { - return EntropyStatus.ok; - } + DetailedEntropySourceOpenResult result; - void close() scope - { - // no-op + static foreach (EntropySource; SupportedSources) + { + result = openEntropySourceByType!EntropySource(); + if (result.isOK) + return result; + } + + return openEntropySourceByType!(Implementation.None)(); } - EntropyStatus getEntropy(scope void[] buffer) scope @trusted + private DetailedEntropySourceOpenResult openEntropySourceByID(EntropySourceID id) { - /+ - genentropy(3): - The maximum buflen permitted is 256 bytes. - +/ - foreach (chunk; VoidChunks(buffer, 256)) + if (id == EntropySourceID.tryAll) + return openEntropySourceTryAll(); + + if (id == EntropySourceID.none) + return openEntropySourceByType!(Implementation.None)(); + + static foreach (EntropySource; SupportedSources) { - const status = getentropy(buffer.ptr, buffer.length); - if (status != 0) - return EntropyStatus.readError; + if (id == idOf!(EntropySource)) + return openEntropySourceByType!EntropySource(); } - return EntropyStatus.ok; + return DetailedEntropySourceOpenResult( + id, + EntropySourceOpenResult(EntropyStatus.unavailablePlatform) + ); } - private static + /++ + Opens a handle to the requested system CSPRNG. + + In general, it’s a $(B bad idea) to let users pick sources themselves. + A sane option should be used by default instead. + + This overload only exists because it is used by Phobos. + +/ + EntropySourceOpenResult openEntropySource(EntropySourceID id) { - extern(C) int getentropy(scope void* buf, size_t buflen) @system; + return openEntropySourceByID(id).result; } - static string getErrorMessage(EntropyStatus status) + /++ + Opens a handle to an applicable system CSPRNG. + +/ + EntropySourceOpenResult openEntropySource() { - if (status == EntropyStatus.readError) - return "getEntropy(): `getentropy()` failed."; + auto result = openEntropySourceByID(_entropySource); - return null; + // Save used entropy source for later if applicable. + if ((_entropySource == EntropySourceID.tryAll) && result.result.isOK) + _entropySource = result.sourceID; + + return result.result; } } +} - version (Windows) - struct BCryptGenRandom - { - import core.sys.windows.bcrypt : BCryptGenRandom, BCRYPT_USE_SYSTEM_PREFERRED_RNG; - import core.sys.windows.windef : HMODULE, PUCHAR, ULONG; - import core.sys.windows.ntdef : NT_SUCCESS; +// Platform configurations +version (Darwin) mixin multiSourceImpl!( + Implementation.ARC4Random, + Implementation.CharDevURandom, + Implementation.CharDevRandom, +); +else version (DragonFlyBSD) mixin multiSourceImpl!( + Implementation.Getentropy, + Implementation.CharDevURandom, + Implementation.CharDevRandom, +); +else version (FreeBSD) mixin multiSourceImpl!( + Implementation.Getentropy, + Implementation.CharDevURandom, + Implementation.CharDevRandom, +); +else version (linux) mixin multiSourceImpl!( + Implementation.Getrandom, + Implementation.CharDevURandom, + Implementation.CharDevRandom, +); +else version (NetBSD) mixin multiSourceImpl!( + Implementation.ARC4Random, + Implementation.CharDevURandom, + Implementation.CharDevRandom, +); +else version (OpenBSD) mixin multiSourceImpl!( + Implementation.ARC4Random, + Implementation.CharDevURandom, + Implementation.CharDevRandom, +); +else version (Posix) mixin multiSourceImpl!( + Implementation.CharDevURandom, + Implementation.CharDevRandom, +); +else version (Windows) mixin multiSourceImpl!( + Implementation.BCryptGenRandom, +); +else mixin multiSourceImpl!( +); - private - { - HMODULE _hBcrypt = null; - typeof(BCryptGenRandom)* _ptrBCryptGenRandom; - } +// One-shot functions +private @nogc nothrow @safe +{ + EntropyStatus getEntropy(scope void[] buffer) + { + auto opened = openEntropySource(); + if (!opened.isOK) + return opened.status; - @nogc nothrow @safe: + return opened.handle.getEntropy(buffer); + } - EntropyStatus open() scope @trusted - { - import core.sys.windows.winbase : GetProcAddress, LoadLibraryA; + EntropyStatus getEntropy(scope ubyte[] buffer) + { + return getEntropy(cast(void[]) buffer); + } - if (_hBcrypt !is null) - return EntropyStatus.ok; + EntropyStatus getEntropy(scope void* ptr, size_t length) @system + { + return getEntropy(ptr[0 .. length]); + } +} - _hBcrypt = LoadLibraryA("bcrypt.dll"); - if (!hBcrypt) - return EntropyStatus.unavailableLibrary; +// getEntropy() or crash: +// If the system let us down, we'll let the system down. +package(std) @nogc nothrow @safe +{ + void getEntropyOrCrash(scope void[] buffer) + { + auto opened = openEntropySource(EntropySourceID.tryAll); + if (!opened.isOK) + assert(false, opened.errorMessage); + + auto handle = opened.handle; + const status = handle.getEntropy(buffer); + if (status != EntropyStatus.ok) + assert(false, handle.getErrorMessage(status)); + } - _ptrBCryptGenRandom = cast(typeof(_ptrBCryptGenRandom)) GetProcAddress(_hBcrypt, "BCryptGenRandom"); - if (!_ptrBCryptGenRandom) - return EntropyStatus.unavailable; + void getEntropyOrCrash(scope void* ptr, size_t length) @system + { + return getEntropyOrCrash(ptr[0 .. length]); + } +} - return EntropyStatus.ok; - } +// Self-test: Detect potentially unsuitable default entropy source. +@safe unittest +{ + static bool isUnavailable(EntropyStatus status) + { + return ( + status == EntropyStatus.unavailable || + status == EntropyStatus.unavailableLibrary || + status == EntropyStatus.unavailablePlatform + ); + } - void close() scope @trusted - { - import core.sys.windows.winbase : FreeLibrary; + auto buffer = new ubyte[](32); + forceEntropySource(defaultEntropySource); + const status = getEntropy(buffer); - if (hBcrypt is null) - return; + assert( + !isUnavailable(status), + "The default entropy source for the target platform" + ~ " is unavailable on this machine. Please consider" + ~ " patching it to accommodate to your environment." + ); + assert(status == EntropyStatus.ok); +} - if (!FreeLibrary(hBcrypt)) - return; // Error +// Self-test: Detect faulty implementation. +@system unittest +{ + forceEntropySource(defaultEntropySource); - hBcrypt = null; - ptrBCryptGenRandom = null; - } + bool test() @system + { + static immutable pattern = 0xDEAD_BEEF_1337_0000; + long number = pattern; + const status = getEntropy(&number, number.sizeof); + assert(status == EntropyStatus.ok); + return number != pattern; + } + size_t timesFailed = 0; + foreach (n; 0 .. 3) + if (!test()) + ++timesFailed; - EntropyStatus getEntropy(scope void[] buffer) scope @trusted - { - foreach (chunk; VoidChunks(buffer, ULONG.max)) - { - assert(chunk.length <= ULONG.max, "Bad chunk length."); + assert( + timesFailed <= 1, + "Suspicious random data: Potential security issue or really unlucky; please retry." + ); +} - const gotRandom = _ptrBCryptGenRandom( - null, - cast(PUCHAR) buffer.ptr, - cast(ULONG) buffer.length, - BCRYPT_USE_SYSTEM_PREFERRED_RNG, - ); +// Self-test: Detect faulty implementation. +@safe unittest +{ + forceEntropySource(defaultEntropySource); - if (!NT_SUCCESS(gotRandom)) - return EntropyStatus.readError; - } + bool test() @safe + { + ubyte[32] data; + data[] = 0; - return EntropyStatus.ok; - } + const status = getEntropy(data[]); + assert(status == EntropyStatus.ok); - static string getErrorMessage(EntropyStatus status) - { - if (status == EntropyStatus.unavailableLibrary) - return "getEntropy(): `LoadLibraryA(\"bcrypt.dll\")` failed."; - if (status == EntropyStatus.unavailable) - return "getEntropy(): `GetProcAddress(hBcrypt , \"BCryptGenRandom\")` failed."; - if (status == EntropyStatus.readError) - return "getEntropy(): `BCryptGenRandom()` failed."; + size_t zeros = 0; + foreach (b; data) + if (b == 0) + ++zeros; - return null; - } + enum threshold = 24; + return zeros < threshold; } + + size_t timesFailed = 0; + foreach (n; 0 .. 3) + if (!test()) + ++timesFailed; + + assert( + timesFailed <= 1, + "Suspicious random data: Potential security issue or really unlucky; please retry." + ); } diff --git a/std/random.d b/std/random.d index 4c6026639d9..fc295943083 100644 --- a/std/random.d +++ b/std/random.d @@ -1819,11 +1819,10 @@ how excellent the source of entropy is. { version (SeedUseGetEntropy) { - import std.internal.entropy : crashOnError, EntropySourceID, getEntropy; + import std.internal.entropy : getEntropyOrCrash; uint buffer; - const status = (() @trusted => getEntropy(&buffer, buffer.sizeof, EntropySourceID.tryAll))(); - crashOnError(status); + getEntropyOrCrash(&buffer, buffer.sizeof); return buffer; } else version (AnyARC4Random) @@ -1877,11 +1876,10 @@ if (isUnsigned!UIntType) { version (SeedUseGetEntropy) { - import std.internal.entropy : crashOnError, EntropySourceID, getEntropy; + import std.internal.entropy : getEntropyOrCrash; UIntType buffer; - const status = (() @trusted => getEntropy(&buffer, buffer.sizeof, EntropySourceID.tryAll))(); - crashOnError(status); + getEntropyOrCrash(&buffer, buffer.sizeof); return buffer; } else version (AnyARC4Random) From 97bed30b2894c10b8f9d8ad27f993335c89626c5 Mon Sep 17 00:00:00 2001 From: Mindy Batek Date: Sun, 10 Aug 2025 05:16:49 +0200 Subject: [PATCH 4/8] Fix field id names of Windows implementation --- std/internal/entropy.d | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/std/internal/entropy.d b/std/internal/entropy.d index ceb2ad4bb93..adccff90035 100644 --- a/std/internal/entropy.d +++ b/std/internal/entropy.d @@ -475,14 +475,14 @@ static: { import core.sys.windows.winbase : FreeLibrary; - if (hBcrypt is null) + if (_hBcrypt is null) return; - if (!FreeLibrary(hBcrypt)) + if (!FreeLibrary(_hBcrypt)) return; // Error - hBcrypt = null; - ptrBCryptGenRandom = null; + _hBcrypt = null; + _ptrBCryptGenRandom = null; } EntropyStatus getEntropy(scope void[] buffer) scope @trusted From 318d71ebd85ba6db2e927600910123260a922867 Mon Sep 17 00:00:00 2001 From: Mindy Batek Date: Sun, 10 Aug 2025 05:29:34 +0200 Subject: [PATCH 5/8] Fix variable name --- std/internal/entropy.d | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/std/internal/entropy.d b/std/internal/entropy.d index adccff90035..e394f48a14f 100644 --- a/std/internal/entropy.d +++ b/std/internal/entropy.d @@ -461,7 +461,7 @@ static: return EntropyStatus.ok; _hBcrypt = LoadLibraryA("bcrypt.dll"); - if (!hBcrypt) + if (!_hBcrypt) return EntropyStatus.unavailableLibrary; _ptrBCryptGenRandom = cast(typeof(_ptrBCryptGenRandom)) GetProcAddress(_hBcrypt, "BCryptGenRandom"); From 1cbbe55f3a2fdc67c6eeacbd06dd09d674c0634b Mon Sep 17 00:00:00 2001 From: Mindy Batek Date: Sun, 10 Aug 2025 05:30:53 +0200 Subject: [PATCH 6/8] Move BSD versions --- std/internal/entropy.d | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/std/internal/entropy.d b/std/internal/entropy.d index e394f48a14f..cbe24a6c19d 100644 --- a/std/internal/entropy.d +++ b/std/internal/entropy.d @@ -35,6 +35,18 @@ import std.sumtype; import std.traits; import std.typecons; +// BSD +version (Darwin) + version = SecureARC4Random; +version (DragonFlyBSD) + version = UseGetentropy; +version (FreeBSD) + version = UseGetentropy; +version (NetBSD) + version = SecureARC4Random; +version (OpenBSD) + version = SecureARC4Random; + version (OSX) version = Darwin; else version (iOS) @@ -345,18 +357,6 @@ static: } } - // BSD - version (Darwin) - version = SecureARC4Random; - version (DragonFlyBSD) - version = UseGetentropy; - version (FreeBSD) - version = UseGetentropy; - version (NetBSD) - version = SecureARC4Random; - version (OpenBSD) - version = SecureARC4Random; - version (SecureARC4Random) @(EntropySourceID.arc4random) struct ARC4Random From 8f042c1c5883fcb99132cbe77553fec8ffd8a907 Mon Sep 17 00:00:00 2001 From: Mindy Batek Date: Sun, 10 Aug 2025 15:11:16 +0200 Subject: [PATCH 7/8] Flip the script --- std/internal/entropy.d | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/std/internal/entropy.d b/std/internal/entropy.d index cbe24a6c19d..9ca942b2909 100644 --- a/std/internal/entropy.d +++ b/std/internal/entropy.d @@ -35,6 +35,15 @@ import std.sumtype; import std.traits; import std.typecons; +version (OSX) + version = Darwin; +else version (iOS) + version = Darwin; +else version (TVOS) + version = Darwin; +else version (WatchOS) + version = Darwin; + // BSD version (Darwin) version = SecureARC4Random; @@ -47,15 +56,6 @@ version (NetBSD) version (OpenBSD) version = SecureARC4Random; -version (OSX) - version = Darwin; -else version (iOS) - version = Darwin; -else version (TVOS) - version = Darwin; -else version (WatchOS) - version = Darwin; - /// enum EntropyStatus { From 09862973b65ee4b808ddb6f90da46ed6370cdc9e Mon Sep 17 00:00:00 2001 From: Mindy Batek Date: Sun, 10 Aug 2025 15:21:06 +0200 Subject: [PATCH 8/8] Attempt to fix binding nested in struct --- std/internal/entropy.d | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/std/internal/entropy.d b/std/internal/entropy.d index 9ca942b2909..b05bf128303 100644 --- a/std/internal/entropy.d +++ b/std/internal/entropy.d @@ -379,9 +379,9 @@ static: return EntropyStatus.ok; } - private static + private { - extern(C) void arc4random_buf(scope void* buf, size_t nbytes) @system; + extern(C) static void arc4random_buf(scope void* buf, size_t nbytes) @system; } static string getErrorMessage(EntropyStatus status) @@ -423,9 +423,9 @@ static: return EntropyStatus.ok; } - private static + private { - extern(C) int getentropy(scope void* buf, size_t buflen) @system; + extern(C) static int getentropy(scope void* buf, size_t buflen) @system; } static string getErrorMessage(EntropyStatus status)