From 4737b0f1ec4ed67931bde52a52902a2f924bede7 Mon Sep 17 00:00:00 2001 From: Wu Tingfeng Date: Wed, 17 Dec 2025 05:30:20 +0800 Subject: [PATCH 1/9] Secure random number generation. --- lib/src/random/generator_js.dart | 71 ++++++++++++++++++++++++++------ lib/src/random/generator_vm.dart | 4 +- lib/src/random/generators.dart | 10 ++--- 3 files changed, 64 insertions(+), 21 deletions(-) diff --git a/lib/src/random/generator_js.dart b/lib/src/random/generator_js.dart index 99f6fbb..d934c7e 100644 --- a/lib/src/random/generator_js.dart +++ b/lib/src/random/generator_js.dart @@ -1,24 +1,69 @@ // Copyright (c) 2024, Sudipto Chandra // All rights reserved. Check LICENSE file for details. -import 'dart:async'; import 'dart:math' show Random; +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; +import 'dart:typed_data' show ByteData, Uint8List; const int _mask32 = 0xFFFFFFFF; -int _seedCounter = Zone.current.hashCode; +@JS() +external JSObject require(String id); + +@JS() +@staticInterop +class NodeCrypto { + static void randomFillSync(JSArrayBuffer buf) => + require('crypto').callMethod('randomFillSync'.toJS, buf); +} + +class NodeRandom implements Random { + @override + bool nextBool() => nextInt(2) == 1; + + @override + double nextDouble() { + final int a = nextInt(1 << 26); // 26 bits + final int b = nextInt(1 << 27); // 27 bits + return ((a << 27) + b) / (1 << 53); // 26 + 27 = 53 bits + } + + @override + int nextInt(int max) { + // Generate 32-bit unsigned random values and use rejection sampling + // to avoid modulo bias. + const int maxUint = _mask32 + 1; // (1 << 32) + if (max < 1 || max > maxUint) { + throw RangeError.range( + max, 1, maxUint, 'max', 'max must be <= (1 << 32)'); + } + + final Uint8List list = Uint8List(4); + final int rejectionLimit = maxUint - (maxUint % max); + + int v = rejectionLimit; + while (v >= rejectionLimit) { + NodeCrypto.randomFillSync(list.buffer.toJS); + v = ByteData.sublistView(list).getUint32(0); + } + return v % max; + } +} /// Returns a secure random generator in JS runtime -Random secureRandom() => Random($generateSeed()); - -/// Generates a random seed in JS runtime -int $generateSeed() { - int code = DateTime.now().millisecondsSinceEpoch; - code -= _seedCounter++; - if (code.bitLength & 1 == 1) { - code *= ~code; +Random secureRandom() { + try { + return Random.secure(); + } catch (e) { + // For Node.js with dart2js compiler, the following error is expected. + if (e.runtimeType.toString() == 'UnknownJsTypeError') { + // This error is internal to 'dart:_js_helper'. + return NodeRandom(); + } + rethrow; } - code ^= ~_seedCounter << 5; - _seedCounter += code & 7; - return code & _mask32; } + +/// Generates a random seed +int $generateSeed() => secureRandom().nextInt(_mask32); diff --git a/lib/src/random/generator_vm.dart b/lib/src/random/generator_vm.dart index 3c2bb5d..6e23e0c 100644 --- a/lib/src/random/generator_vm.dart +++ b/lib/src/random/generator_vm.dart @@ -11,6 +11,4 @@ Random secureRandom() => Random.secure(); /// Generates a random seed @pragma('vm:prefer-inline') -int $generateSeed() => - (DateTime.now().microsecondsSinceEpoch & _mask32) ^ - Random.secure().nextInt(_mask32); +int $generateSeed() => Random.secure().nextInt(_mask32); diff --git a/lib/src/random/generators.dart b/lib/src/random/generators.dart index 31462e1..5831cee 100644 --- a/lib/src/random/generators.dart +++ b/lib/src/random/generators.dart @@ -34,13 +34,13 @@ enum RNG { case RNG.keccak: return _keccakGenerateor(seed); case RNG.sha256: - return _hashGenerateor(SHA256Hash(), seed); + return _hashGenerator(SHA256Hash(), seed); case RNG.md5: - return _hashGenerateor(MD4Hash(), seed); + return _hashGenerator(MD4Hash(), seed); case RNG.xxh64: - return _hashGenerateor(XXHash64Sink(111), seed); + return _hashGenerator(XXHash64Sink(111), seed); case RNG.sm3: - return _hashGenerateor(SM3Hash(), seed); + return _hashGenerator(SM3Hash(), seed); case RNG.system: return _systemGenerator(seed); case RNG.secure: @@ -115,7 +115,7 @@ NextIntFunction _keccakGenerateor([int? seed]) { } /// Returns a iterable of 32-bit integers generated from the [sink]. -NextIntFunction _hashGenerateor( +NextIntFunction _hashGenerator( HashDigestSink sink, [ int? seed, ]) { From c6a4c07b05a9232f620260bb150daed5ea852ec7 Mon Sep 17 00:00:00 2001 From: Wu Tingfeng Date: Wed, 17 Dec 2025 06:16:22 +0800 Subject: [PATCH 2/9] Avoid extra allocation. --- lib/src/random/generator_js.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/random/generator_js.dart b/lib/src/random/generator_js.dart index d934c7e..cd70ea6 100644 --- a/lib/src/random/generator_js.dart +++ b/lib/src/random/generator_js.dart @@ -14,8 +14,8 @@ external JSObject require(String id); @JS() @staticInterop class NodeCrypto { - static void randomFillSync(JSArrayBuffer buf) => - require('crypto').callMethod('randomFillSync'.toJS, buf); + static void randomFillSync(JSTypedArray buffer) => + require('crypto').callMethod('randomFillSync'.toJS, buffer); } class NodeRandom implements Random { @@ -44,7 +44,7 @@ class NodeRandom implements Random { int v = rejectionLimit; while (v >= rejectionLimit) { - NodeCrypto.randomFillSync(list.buffer.toJS); + NodeCrypto.randomFillSync(list.toJS); v = ByteData.sublistView(list).getUint32(0); } return v % max; From 4667cc31f847169d28afee3f3308b187ca29cf50 Mon Sep 17 00:00:00 2001 From: Wu Tingfeng Date: Wed, 17 Dec 2025 06:30:20 +0800 Subject: [PATCH 3/9] Avoid dart:js_interop_unsafe --- lib/src/random/generator_js.dart | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/src/random/generator_js.dart b/lib/src/random/generator_js.dart index cd70ea6..dbde549 100644 --- a/lib/src/random/generator_js.dart +++ b/lib/src/random/generator_js.dart @@ -3,8 +3,6 @@ import 'dart:math' show Random; import 'dart:js_interop'; -import 'dart:js_interop_unsafe'; -import 'dart:typed_data' show ByteData, Uint8List; const int _mask32 = 0xFFFFFFFF; @@ -13,12 +11,23 @@ external JSObject require(String id); @JS() @staticInterop -class NodeCrypto { - static void randomFillSync(JSTypedArray buffer) => - require('crypto').callMethod('randomFillSync'.toJS, buffer); +class Buffer {} + +extension BufferExt on Buffer { + external int readUInt32LE(int offset); +} + +@JS() +@staticInterop +class Crypto {} + +extension CryptoExt on Crypto { + external Buffer randomBytes(int size); } class NodeRandom implements Random { + final Crypto _crypto = require('crypto') as Crypto; + @override bool nextBool() => nextInt(2) == 1; @@ -26,7 +35,7 @@ class NodeRandom implements Random { double nextDouble() { final int a = nextInt(1 << 26); // 26 bits final int b = nextInt(1 << 27); // 27 bits - return ((a << 27) + b) / (1 << 53); // 26 + 27 = 53 bits + return ((a << 27) + b) / (1 << 53); // 26 + 27 = 53 bits (JS limit) } @override @@ -39,13 +48,11 @@ class NodeRandom implements Random { max, 1, maxUint, 'max', 'max must be <= (1 << 32)'); } - final Uint8List list = Uint8List(4); final int rejectionLimit = maxUint - (maxUint % max); int v = rejectionLimit; while (v >= rejectionLimit) { - NodeCrypto.randomFillSync(list.toJS); - v = ByteData.sublistView(list).getUint32(0); + v = _crypto.randomBytes(4).readUInt32LE(0); } return v % max; } From 7f0109dbe5b318c0bce842070e8521619228e3b5 Mon Sep 17 00:00:00 2001 From: Wu Tingfeng Date: Wed, 17 Dec 2025 18:25:51 +0800 Subject: [PATCH 4/9] Use Crypto.randomInt. --- lib/src/random/generator_js.dart | 30 +++++------------------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/lib/src/random/generator_js.dart b/lib/src/random/generator_js.dart index dbde549..1b65ea9 100644 --- a/lib/src/random/generator_js.dart +++ b/lib/src/random/generator_js.dart @@ -9,25 +9,15 @@ const int _mask32 = 0xFFFFFFFF; @JS() external JSObject require(String id); -@JS() -@staticInterop -class Buffer {} - -extension BufferExt on Buffer { - external int readUInt32LE(int offset); -} - @JS() @staticInterop class Crypto {} -extension CryptoExt on Crypto { - external Buffer randomBytes(int size); +extension on Crypto { + external int randomInt(int max); } class NodeRandom implements Random { - final Crypto _crypto = require('crypto') as Crypto; - @override bool nextBool() => nextInt(2) == 1; @@ -40,21 +30,11 @@ class NodeRandom implements Random { @override int nextInt(int max) { - // Generate 32-bit unsigned random values and use rejection sampling - // to avoid modulo bias. - const int maxUint = _mask32 + 1; // (1 << 32) - if (max < 1 || max > maxUint) { + if (max < 1 || max > _mask32 + 1) { throw RangeError.range( - max, 1, maxUint, 'max', 'max must be <= (1 << 32)'); - } - - final int rejectionLimit = maxUint - (maxUint % max); - - int v = rejectionLimit; - while (v >= rejectionLimit) { - v = _crypto.randomBytes(4).readUInt32LE(0); + max, 1, _mask32 + 1, 'max', 'max must be <= (1 << 32)'); } - return v % max; + return (require('crypto') as Crypto).randomInt(max); } } From 5983164a044f4c36b6f7cdea555db774f84836e2 Mon Sep 17 00:00:00 2001 From: Wu Tingfeng Date: Wed, 17 Dec 2025 20:02:33 +0800 Subject: [PATCH 5/9] Use final. --- lib/src/random/generator_js.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/src/random/generator_js.dart b/lib/src/random/generator_js.dart index 1b65ea9..96f6dc7 100644 --- a/lib/src/random/generator_js.dart +++ b/lib/src/random/generator_js.dart @@ -6,17 +6,17 @@ import 'dart:js_interop'; const int _mask32 = 0xFFFFFFFF; -@JS() -external JSObject require(String id); - @JS() @staticInterop class Crypto {} extension on Crypto { - external int randomInt(int max); + external int randomInt(final int max); } +@JS() +external Crypto require(final String id); + class NodeRandom implements Random { @override bool nextBool() => nextInt(2) == 1; @@ -29,12 +29,12 @@ class NodeRandom implements Random { } @override - int nextInt(int max) { + int nextInt(final int max) { if (max < 1 || max > _mask32 + 1) { throw RangeError.range( max, 1, _mask32 + 1, 'max', 'max must be <= (1 << 32)'); } - return (require('crypto') as Crypto).randomInt(max); + return require('crypto').randomInt(max); } } From c7f197023ece16b2c11f69427843cdb8b64b9e72 Mon Sep 17 00:00:00 2001 From: Wu Tingfeng Date: Fri, 19 Dec 2025 21:55:57 +0800 Subject: [PATCH 6/9] Use process.versions.node. --- lib/src/random/generator_js.dart | 43 ++++++++++++++++---------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/lib/src/random/generator_js.dart b/lib/src/random/generator_js.dart index 96f6dc7..2bf113b 100644 --- a/lib/src/random/generator_js.dart +++ b/lib/src/random/generator_js.dart @@ -1,11 +1,19 @@ // Copyright (c) 2024, Sudipto Chandra // All rights reserved. Check LICENSE file for details. +import 'dart:js_interop_unsafe'; import 'dart:math' show Random; import 'dart:js_interop'; const int _mask32 = 0xFFFFFFFF; +@JS('process') +external JSObject? get _process; + +bool get _isNodeJS => ((_process?.getProperty('versions'.toJS) as JSObject?) + ?.getProperty('node'.toJS)) + .isDefinedAndNotNull; + @JS() @staticInterop class Crypto {} @@ -17,17 +25,8 @@ extension on Crypto { @JS() external Crypto require(final String id); +/// For Node.js environment + dart2js compiler class NodeRandom implements Random { - @override - bool nextBool() => nextInt(2) == 1; - - @override - double nextDouble() { - final int a = nextInt(1 << 26); // 26 bits - final int b = nextInt(1 << 27); // 27 bits - return ((a << 27) + b) / (1 << 53); // 26 + 27 = 53 bits (JS limit) - } - @override int nextInt(final int max) { if (max < 1 || max > _mask32 + 1) { @@ -36,21 +35,21 @@ class NodeRandom implements Random { } return require('crypto').randomInt(max); } -} -/// Returns a secure random generator in JS runtime -Random secureRandom() { - try { - return Random.secure(); - } catch (e) { - // For Node.js with dart2js compiler, the following error is expected. - if (e.runtimeType.toString() == 'UnknownJsTypeError') { - // This error is internal to 'dart:_js_helper'. - return NodeRandom(); - } - rethrow; + @override + double nextDouble() { + final int first26Bits = nextInt(1 << 26); + final int next27Bits = nextInt(1 << 27); + final int random53Bits = (first26Bits << 27) + next27Bits; // JS int limit + return random53Bits / (1 << 53); } + + @override + bool nextBool() => nextInt(2) == 1; } +/// Returns a secure random generator in JS runtime +Random secureRandom() => _isNodeJS ? NodeRandom() : Random.secure(); + /// Generates a random seed int $generateSeed() => secureRandom().nextInt(_mask32); From c5816ecc5837309e22f368df2a0c8774b434dc6f Mon Sep 17 00:00:00 2001 From: Wu Tingfeng Date: Fri, 19 Dec 2025 22:21:01 +0800 Subject: [PATCH 7/9] Avoid dart:js_interop_unsafe --- lib/src/random/generator_js.dart | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/lib/src/random/generator_js.dart b/lib/src/random/generator_js.dart index 2bf113b..e75ab18 100644 --- a/lib/src/random/generator_js.dart +++ b/lib/src/random/generator_js.dart @@ -1,18 +1,31 @@ // Copyright (c) 2024, Sudipto Chandra // All rights reserved. Check LICENSE file for details. -import 'dart:js_interop_unsafe'; import 'dart:math' show Random; import 'dart:js_interop'; const int _mask32 = 0xFFFFFFFF; +@JS() +@staticInterop +class Process {} + +@JS() +@staticInterop +class Versions {} + @JS('process') -external JSObject? get _process; +external Process? get _process; + +extension on Process { + external Versions? get versions; +} + +extension on Versions { + external JSAny get node; +} -bool get _isNodeJS => ((_process?.getProperty('versions'.toJS) as JSObject?) - ?.getProperty('node'.toJS)) - .isDefinedAndNotNull; +bool get _isNodeJS => (_process?.versions)?.node != null; @JS() @staticInterop From a9afb0d15a7a6bb1590ce3986d7f5ed9b87172dd Mon Sep 17 00:00:00 2001 From: Wu Tingfeng Date: Sat, 20 Dec 2025 12:23:27 +0800 Subject: [PATCH 8/9] Check for Node + Dart2JS. --- lib/src/random/generator_js.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/src/random/generator_js.dart b/lib/src/random/generator_js.dart index e75ab18..ef6bb65 100644 --- a/lib/src/random/generator_js.dart +++ b/lib/src/random/generator_js.dart @@ -6,6 +6,8 @@ import 'dart:js_interop'; const int _mask32 = 0xFFFFFFFF; +const bool isWASM = bool.fromEnvironment('dart.tool.dart2wasm'); + @JS() @staticInterop class Process {} @@ -25,7 +27,7 @@ extension on Versions { external JSAny get node; } -bool get _isNodeJS => (_process?.versions)?.node != null; +bool get isNodeDart2JS => _process?.versions?.node != null && !isWASM; @JS() @staticInterop @@ -62,7 +64,7 @@ class NodeRandom implements Random { } /// Returns a secure random generator in JS runtime -Random secureRandom() => _isNodeJS ? NodeRandom() : Random.secure(); +Random secureRandom() => isNodeDart2JS ? NodeRandom() : Random.secure(); /// Generates a random seed int $generateSeed() => secureRandom().nextInt(_mask32); From 371cfaae0b65dd16814a03fba28583ad1871cd4b Mon Sep 17 00:00:00 2001 From: Wu Tingfeng Date: Sat, 20 Dec 2025 17:03:08 +0800 Subject: [PATCH 9/9] Check dart2js. --- lib/src/random/generator_js.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/random/generator_js.dart b/lib/src/random/generator_js.dart index ef6bb65..fec936e 100644 --- a/lib/src/random/generator_js.dart +++ b/lib/src/random/generator_js.dart @@ -6,7 +6,7 @@ import 'dart:js_interop'; const int _mask32 = 0xFFFFFFFF; -const bool isWASM = bool.fromEnvironment('dart.tool.dart2wasm'); +const bool isDart2JS = bool.fromEnvironment('dart.tool.dart2js'); @JS() @staticInterop @@ -27,7 +27,7 @@ extension on Versions { external JSAny get node; } -bool get isNodeDart2JS => _process?.versions?.node != null && !isWASM; +bool get isNodeDart2JS => _process?.versions?.node != null && isDart2JS; @JS() @staticInterop