Skip to content

Commit 0ddd7f4

Browse files
committed
Ensure seed uniqueness in RandomGenerators across Isolates
1 parent 6650ea1 commit 0ddd7f4

File tree

7 files changed

+95
-49
lines changed

7 files changed

+95
-49
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# 1.20.4
2+
3+
- Ensure seed uniqueness in `RandomGenerators` across Isolates
4+
15
# 1.20.3
26

37
- Exports hashlib_codecs from current package. To get it: `import 'package:hashlib/codecs.dart';`

lib/src/algorithms/random_generators.dart renamed to lib/src/algorithms/random/random.dart

Lines changed: 16 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) 2024, Sudipto Chandra
22
// All rights reserved. Check LICENSE file for details.
33

4-
import 'dart:math';
4+
import 'dart:math' show Random;
55
import 'dart:typed_data';
66

77
import 'package:hashlib/src/algorithms/keccak/keccak.dart';
@@ -11,8 +11,9 @@ import 'package:hashlib/src/algorithms/sm3.dart';
1111
import 'package:hashlib/src/algorithms/xxh64/xxh64.dart';
1212
import 'package:hashlib/src/core/hash_base.dart';
1313

14+
import 'random_vm.dart' if (dart.library.js) 'random_js.dart';
15+
1416
const int _mask32 = 0xFFFFFFFF;
15-
const int _maxSafeNumber = 0x1FFFFFFFFFFFFF;
1617

1718
enum RandomGenerator {
1819
secure,
@@ -28,38 +29,28 @@ extension RandomGeneratorIterable on RandomGenerator {
2829
Iterable<int> build([int? seed]) {
2930
switch (this) {
3031
case RandomGenerator.keccak:
31-
return RandomGenerators.$keccakGenerateor(seed);
32+
return Generators.$keccakGenerateor(seed);
3233
case RandomGenerator.sha256:
33-
return RandomGenerators.$hashGenerateor(SHA256Hash(), seed);
34+
return Generators.$hashGenerateor(SHA256Hash(), seed);
3435
case RandomGenerator.md5:
35-
return RandomGenerators.$hashGenerateor(MD4Hash(), seed);
36+
return Generators.$hashGenerateor(MD4Hash(), seed);
3637
case RandomGenerator.xxh64:
37-
return RandomGenerators.$hashGenerateor(XXHash64Sink(111), seed);
38+
return Generators.$hashGenerateor(XXHash64Sink(111), seed);
3839
case RandomGenerator.sm3:
39-
return RandomGenerators.$hashGenerateor(SM3Hash(), seed);
40+
return Generators.$hashGenerateor(SM3Hash(), seed);
4041
case RandomGenerator.secure:
41-
return RandomGenerators.$secureGenerator();
42+
return Generators.$secureGenerator();
4243
case RandomGenerator.system:
4344
default:
44-
return RandomGenerators.$systemGenerator(seed);
45+
return Generators.$systemGenerator(seed);
4546
}
4647
}
4748
}
4849

49-
abstract class RandomGenerators {
50-
static int _seedCounter = 0x9BDC06A7;
51-
52-
/// Generate a 64-bit random seed based on current time
53-
static int $generateSeed() {
54-
var now = DateTime.now();
55-
var code = now.microsecondsSinceEpoch;
56-
code -= _seedCounter++;
57-
if (code.bitLength & 1 == 1) {
58-
code *= ~code;
59-
}
60-
code ^= ~_seedCounter++ << 5;
61-
return code & _maxSafeNumber;
62-
}
50+
abstract class Generators {
51+
/// Get a random seed
52+
@pragma('vm:prefer-inline')
53+
static int $nextSeed() => $generateSeed();
6354

6455
/// Expand the seed to fill the list
6556
static void $seedList(TypedData data, int seed) {
@@ -100,12 +91,7 @@ abstract class RandomGenerators {
10091

10192
/// Returns a iterable of 32-bit integers backed by system's [Random].
10293
static Iterable<int> $secureGenerator() sync* {
103-
Random random;
104-
try {
105-
random = Random.secure();
106-
} catch (err) {
107-
random = Random($generateSeed());
108-
}
94+
var random = secureRandom();
10995
while (true) {
11096
yield random.nextInt(_mask32);
11197
}
@@ -114,7 +100,7 @@ abstract class RandomGenerators {
114100
/// Returns a iterable of 32-bit integers backed by system's [Random].
115101
static Iterable<int> $systemGenerator([int? seed]) sync* {
116102
seed ??= $generateSeed();
117-
Random random = Random(seed);
103+
var random = Random(seed);
118104
while (true) {
119105
yield random.nextInt(_mask32);
120106
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) 2024, Sudipto Chandra
2+
// All rights reserved. Check LICENSE file for details.
3+
4+
import 'dart:js' show context;
5+
import 'dart:math' show Random;
6+
7+
const int _mask32 = 0xFFFFFFFF;
8+
9+
int _seedCounter = context.hashCode;
10+
11+
@pragma('vm:prefer-inline')
12+
Random secureRandom() => Random($generateSeed());
13+
14+
int $generateSeed() {
15+
int code = DateTime.now().microsecondsSinceEpoch;
16+
code -= _seedCounter++;
17+
if (code.bitLength & 1 == 1) {
18+
code *= ~code;
19+
}
20+
code ^= ~_seedCounter << 5;
21+
_seedCounter += code & 7;
22+
return code & _mask32;
23+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) 2024, Sudipto Chandra
2+
// All rights reserved. Check LICENSE file for details.
3+
4+
import 'dart:math' show Random;
5+
6+
const int _mask32 = 0xFFFFFFFF;
7+
8+
final secure = Random.secure();
9+
10+
@pragma('vm:prefer-inline')
11+
Random secureRandom() => secure;
12+
13+
@pragma('vm:prefer-inline')
14+
int $generateSeed() =>
15+
(DateTime.now().microsecondsSinceEpoch & _mask32) ^ secure.nextInt(_mask32);

lib/src/core/hashlib_random.dart

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@
33

44
import 'dart:typed_data';
55

6-
import 'package:hashlib/src/algorithms/random_generators.dart';
6+
import 'package:hashlib/src/algorithms/random/random.dart';
77

8-
export 'package:hashlib/src/algorithms/random_generators.dart'
9-
show RandomGenerator;
8+
export 'package:hashlib/src/algorithms/random/random.dart' show RandomGenerator;
109

1110
const int _mask32 = 0xFFFFFFFF;
1211

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: hashlib
22
description: Secure hash functions, checksum generators, and key derivation algorithms optimized for Dart.
33
homepage: https://github.com/bitanon/hashlib
4-
version: 1.20.3
4+
version: 1.20.4
55

66
environment:
77
sdk: '>=2.14.0 <4.0.0'

test/random_test.dart

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33

44
// ignore_for_file: no_leading_underscores_for_local_identifiers
55

6+
import 'dart:io';
7+
import 'dart:isolate';
68
import 'dart:typed_data';
79

810
import 'package:hashlib/hashlib.dart';
9-
import 'package:hashlib/src/algorithms/random_generators.dart';
11+
import 'package:hashlib/src/algorithms/random/random.dart';
1012
import 'package:test/test.dart';
1113

1214
const int _maxInt = 0xFFFFFFFF;
@@ -78,15 +80,32 @@ void main() {
7880
}, tags: ['vm-only']);
7981
});
8082

81-
test('seed generator uniqueness', () {
82-
int n = 10000;
83-
var m = <int>{};
84-
for (int i = 0; i < n; ++i) {
85-
m.add(RandomGenerators.$generateSeed());
86-
}
87-
expect(m.length, n);
83+
test('seed generator uniqueness with futures', () async {
84+
final seeds = await Future.wait(List.generate(
85+
1000,
86+
(_) => Future.microtask(() {
87+
return Generators.$nextSeed();
88+
}),
89+
));
90+
expect(seeds.toSet().length, 1000);
8891
});
8992

93+
test('seed generator uniqueness with isolates', () async {
94+
var version = Platform.version;
95+
var major = int.parse(version.split('.')[0]);
96+
var minor = int.parse(version.split('.')[1]);
97+
if (major > 2 || (major == 2 && minor >= 19)) {
98+
final seeds = await Future.wait(List.generate(
99+
1000,
100+
// ignore: sdk_version_since
101+
(_) => Isolate.run(() {
102+
return Generators.$nextSeed();
103+
}),
104+
));
105+
expect(seeds.toSet().length, 1000);
106+
}
107+
}, tags: ['vm-only']);
108+
90109
test('random bytes length = 0', () {
91110
expect(randomBytes(0), []);
92111
});
@@ -369,15 +388,15 @@ void main() {
369388
test('Test with a normal length list', () {
370389
int seed = 123456789;
371390
var data = Uint8List(64);
372-
RandomGenerators.$seedList(data, seed);
391+
Generators.$seedList(data, seed);
373392
expect(data, isNot(equals(Uint8List(64))));
374393
});
375394

376395
test('Test with small list', () {
377396
int seed = 123456789;
378397
for (int i = 1; i < 8; ++i) {
379398
var data = Uint8List(i);
380-
RandomGenerators.$seedList(data, seed);
399+
Generators.$seedList(data, seed);
381400
expect(data, isNot(equals(Uint8List(i))));
382401
}
383402
});
@@ -386,7 +405,7 @@ void main() {
386405
int seed = 123456789;
387406
for (int i = 1; i < 4; ++i) {
388407
var data = Uint8List(64 + i);
389-
RandomGenerators.$seedList(data, seed);
408+
Generators.$seedList(data, seed);
390409
expect(data.skip(64), isNot(equals(Uint8List(i))));
391410
}
392411
});
@@ -395,8 +414,8 @@ void main() {
395414
int seed = 123456789;
396415
var data1 = Uint8List(255);
397416
var data2 = Uint8List(255);
398-
RandomGenerators.$seedList(data1, seed);
399-
RandomGenerators.$seedList(data2, seed);
417+
Generators.$seedList(data1, seed);
418+
Generators.$seedList(data2, seed);
400419
expect(data1, equals(data2));
401420
});
402421

@@ -405,8 +424,8 @@ void main() {
405424
int seed2 = 987654321;
406425
var data1 = Uint8List(255);
407426
var data2 = Uint8List(255);
408-
RandomGenerators.$seedList(data1, seed1);
409-
RandomGenerators.$seedList(data2, seed2);
427+
Generators.$seedList(data1, seed1);
428+
Generators.$seedList(data2, seed2);
410429
expect(data1, isNot(equals(data2)));
411430
});
412431
});

0 commit comments

Comments
 (0)