diff --git a/cryptography/lib/src/cryptography/secure_random.dart b/cryptography/lib/src/cryptography/secure_random.dart index 25a8481b..c5cab776 100644 --- a/cryptography/lib/src/cryptography/secure_random.dart +++ b/cryptography/lib/src/cryptography/secure_random.dart @@ -22,58 +22,34 @@ const _bit20 = 0x100000; const _bit32 = 0x100000000; const _bit52 = 0x10000000000000; -/// An abstract base class for reasonably secure [Random] implementations. -/// -/// This contains two important static final variables: -/// * [SecureRandom.fast] -/// * A reasonably secure random number generator that can be over 100 times -/// faster than [Random.secure]. -/// * [SecureRandom.safe] -/// * An instance of [Random.secure] use by "package:cryptography" -/// implementations by default. +/// An abstract base class for secure [Random] implementations. abstract class SecureRandom implements Random { - /// A [Random] that is over 100 times faster than [Random.secure]. - /// - /// The throughput is up to about 0.25 GB of random data per second. - /// - /// ## Current algorithm - /// The default behavior is: - /// * 12 rounds of ChaCha20. No key extraction attack has been proposed - /// against ChaCha with more than 6 rounds so the choice has a good margin - /// of safety. - /// * 256-bit secret key is mixed (XOR) with numbers from [Random.secure] at - /// least once every 8192 blocks. This is also done if more than 10 - /// milliseconds has passed since the last reseed event. - /// * After a block has been computed, the block counter is incremented. - /// * After a block has been computed, the last 128 bits of the secret key - /// is immediately mixed (XOR) with the first 128 bits of the state. - /// The first 128-bits of the state are then zeroed and skipped. - /// This provides backtracking resistance. - /// * State bits are erased after they have been read so a memory dump won't - /// reveal them. - /// - /// ## Example - /// ```dart - /// import 'package:cryptography/random.dart'; + /// An instance of [ChaChaRandom], which can be over 100 times faster than + /// [Random.secure]. /// - /// void main() { - /// final random = SecureRandom.instance; - /// final x = random.nextInt(100); - /// print('x = $x'); - /// } - /// ``` - static final SecureRandom fast = _ChachaRandom( - rounds: 12, - maxBlocksBeforeReseed: 8192, - maxDurationBeforeReseed: const Duration(milliseconds: 10), - ); + /// The algorithm is based on ChaCha20 stream cipher and is cryptographically + /// secure. Reseeding is done from [Random.secure] at least once every 8192 + /// blocks or 10 milliseconds. + static final SecureRandom fast = ChaChaRandom(); /// [Random] instance that is used as default by "package:cryptography". /// - /// Currently this is always [Random.secure]. - static final Random safe = Random.secure(); + /// Currently this is always [SecureRandom.system]. + static final Random defaultRandom = system; + + @Deprecated('Use SecureRandom.defaultRandom instead') + static Random get safe => defaultRandom; - /// A previously generated random unsigned 32-bit integer. + /// System-provided secure random number generator. + /// + /// This is equivalent to [Random.secure]. + static final Random system = Random.secure(); + + /// A previously generated random bits (maximum 32). + /// + /// The remaining bits are stored in the lowest bits. + /// + /// The field [_wordBitsRemaining] tells how many bits are left. int _word = 0; /// How many random bits [_word] has left. @@ -85,7 +61,7 @@ abstract class SecureRandom implements Random { /// Returns a deterministic [Random] for testing purposes. /// /// The sequence of outputs is a pure function of the [seed] you give to the - /// constructor. In other words, this is only for meant for testing. + /// constructor. This is only for meant for testing. /// /// ## Example /// ```dart @@ -107,57 +83,80 @@ abstract class SecureRandom implements Random { factory SecureRandom.forTesting({ int seed = 0, }) { - return _ChachaRandom.forTesting(seed: seed); + return ChaChaRandom.forTesting(seed: seed); } /// Tells whether the algorithm is cryptographically secure and /// the initial entropy is from [Random.secure]. bool get isSecure; + @nonVirtual @override bool nextBool() { - // Use 1 bit of `_word` + // We will use the lowest bit of `_word`. var word = _word; var wordBitsRemaining = _wordBitsRemaining; if (wordBitsRemaining >= 1) { + // Drop the lowest bit. _word = word >> 1; _wordBitsRemaining = wordBitsRemaining - 1; } else { + // Get a new word. word = nextUint32(); _word = word >> 1; _wordBitsRemaining = 31; } + + // Return the lowest bit. return 0x1 & word != 0; } + @nonVirtual @override double nextDouble() { + // We need to generate a double in [0.0, 1.0). while (true) { + // Generate random 20 bits. final a = (_bit20 - 1) & nextUint32(); + + // Generate random 32 bits. final b = nextUint32(); + + // Combine to a 52-bit integer and convert to double in [0.0, 1.0). final x = (a * _bit32 + b) / _bit52; + + // Return the value if it's in [0.0, 1.0). if (x >= 0.0 && x < 1.0) { return x; } + // This should never happen, // but we will just generate another pair if it does. assert(false); } } + @nonVirtual @override int nextInt(int max) { if (max < 0 || max > _bit32) { throw ArgumentError.value(max, 'max'); } - if (max == 0x100000000) { - return nextUint32(); - } var attempts = 0; while (true) { final x = nextUint32(); - final samplingMax = _bit32 ~/ max * max; - if (x < samplingMax || attempts >= 100) { + + // To avoid modulo bias, we only accept values + // that are within a multiple of [max]. + final nonAcceptedRange = _bit32 % max; + if (x < _bit32 - nonAcceptedRange) { + // Accept the value. + return x % max; + } + if (attempts == 128) { + // Give up and accept the value anyway. + // This should never happen. + assert(false); return x % max; } attempts++; @@ -171,19 +170,30 @@ abstract class SecureRandom implements Random { /// /// Note that 52-bit integers, unlike 64-bit integers, can always be /// accurately represented in JavaScript. + @nonVirtual int nextUint52([int? max]) { if (max != null && (max < 0 || max > _bit52)) { throw ArgumentError.value(max, 'max'); } var attempts = 0; while (true) { + // Generate a 52-bit integer from two 32-bit integers. final high = nextUint32(); final low = nextUint32(); final x = ((_bit32 * (0xFFFFF & high)) + low); if (max == null) { return x; } - if (attempts == 64 || x < _bit52 ~/ max * max) { + // To avoid modulo bias, we only accept values + // that are within a multiple of [max]. + final nonAcceptedRange = _bit52 % max; + if (x < _bit52 - nonAcceptedRange) { + return x % max; + } + if (attempts == 128) { + // Give up and accept the value anyway. + // This should never happen. + assert(false); return x % max; } attempts++; @@ -197,7 +207,38 @@ abstract class SecureRandom implements Random { } } -class _ChachaRandom extends SecureRandom { +/// ChaCha20-based [SecureRandom] implementation. +/// +/// The throughput is up to about 0.25 GB of random data per second. This is +/// over 100 times faster than [Random.secure] on many platforms. +/// +/// ## Current algorithm +/// The default behavior is: +/// * 12 rounds of ChaCha. No key extraction attack has been proposed against +/// ChaCha with more than 6 rounds so the choice has a good margin of +/// safety. +/// * 256-bit secret key is mixed (XOR) with numbers from [Random.secure] at +/// least once every 8192 blocks. This is also done if more than 10 +/// milliseconds has passed since the last reseed event. +/// * After a block has been computed, the block counter is incremented. +/// * After a block has been computed, the last 128 bits of the secret key +/// is immediately mixed (XOR) with the first 128 bits of the state. +/// The first 128-bits of the state are then zeroed and skipped. +/// This provides backtracking resistance. +/// * State bits are erased after they have been read so a memory dump won't +/// reveal them. +/// +/// ## Example +/// ```dart +/// import 'package:cryptography/random.dart'; +/// +/// void main() { +/// final random = SecureRandom.fast; +/// final x = random.nextInt(100); +/// print('x = $x'); +/// } +/// ``` +class ChaChaRandom extends SecureRandom { static const int _bit32 = 0x100000000; /// Default value for [rounds]. @@ -233,7 +274,7 @@ class _ChachaRandom extends SecureRandom { /// Number of ChaCha rounds. final int rounds; - final int? _seed; + final int? _seedForDeterministicSequence; /// Maximum number of 64 byte blocks since the last reseed before the random /// number generator must be reseeded. @@ -241,13 +282,13 @@ class _ChachaRandom extends SecureRandom { /// Maximum elapsed time since the last reseed before the random number /// generator must be reseeded. - final Duration maxDurationBeforeReseed; + final Duration? maxDurationBeforeReseed; - _ChachaRandom({ + ChaChaRandom({ this.rounds = defaultRounds, this.maxBlocksBeforeReseed = defaultMaxBlocksBeforeReseed, this.maxDurationBeforeReseed = defaultMaxDurationBeforeReseed, - }) : _seed = null, + }) : _seedForDeterministicSequence = null, super.constructor() { if (maxBlocksBeforeReseed < 0 || maxBlocksBeforeReseed > _bit32) { throw ArgumentError.value( @@ -257,9 +298,9 @@ class _ChachaRandom extends SecureRandom { } } - _ChachaRandom.forTesting({ + ChaChaRandom.forTesting({ int seed = 0, - }) : _seed = seed, + }) : _seedForDeterministicSequence = seed, rounds = defaultRounds, maxBlocksBeforeReseed = defaultMaxBlocksBeforeReseed, maxDurationBeforeReseed = defaultMaxDurationBeforeReseed, @@ -270,11 +311,30 @@ class _ChachaRandom extends SecureRandom { } @override - bool get isSecure => _seed == null; + bool get isSecure => _seedForDeterministicSequence == null; @visibleForTesting int get reseedCount => _reseedCount; + /// Whether it's time to mix in new values from [Random.secure]. + /// + /// We reseed when: + /// * No blocks have been generated yet. + /// * The number of generated blocks since the last reseed is greater than or + /// equal to [maxBlocksBeforeReseed]. + /// * The elapsed time since the last reseed is greater than or equal to + /// [maxDurationBeforeReseed]. + bool get _isTimeForMixinSystemRandom { + final blockCount = _blockCount; + final maxDurationBeforeReseed = this.maxDurationBeforeReseed; + final isFirstBlock = blockCount == 0; + return isFirstBlock || + blockCount >= maxBlocksBeforeReseed || + (maxDurationBeforeReseed != null && + _stopwatch.elapsedMicroseconds > + maxDurationBeforeReseed.inMicroseconds); + } + @override int nextUint32() { final state = _state; @@ -309,59 +369,62 @@ class _ChachaRandom extends SecureRandom { @override String toString() { - final seed = _seed; + final seed = _seedForDeterministicSequence; if (seed != null) { return 'SecureRandom.forTesting(seed: $seed)'; } return 'SecureRandom()'; } - void _nextBlock() { - // Reseed? - var blockCount = _blockCount; + /// Mixes new entropy from [Random.secure] into the state. + void _mixSystemRandom() { + // Reset stopwatch and block count. + _stopwatch.reset(); + _blockCount = 0; + + // For debugging purposes, count reseeds. + _reseedCount++; + + // Constants are stored at indices 0-3. final initialState = _initialState; - if (blockCount == 0 || - blockCount >= maxBlocksBeforeReseed || - _stopwatch.elapsedMicroseconds > - maxDurationBeforeReseed.inMicroseconds) { - _stopwatch.reset(); - blockCount = 0; - - _reseedCount++; - - // Constants are stored at indices 0-3. - initialState[0] = 0x61707865; - initialState[1] = 0x3320646e; - initialState[2] = 0x79622d32; - initialState[3] = 0x6b206574; - - final random = _random; - final isNotSeeded = _seed == null; - - // Mix the secret key with Random.secure bits. - for (var i = 4; i < 12; i++) { - // We want to call Random.secure even when this is seeded so the - // cost of the operation is the same. - final x = random.nextInt(_bit32); - if (isNotSeeded) { - initialState[i] ^= x; - } + initialState[0] = 0x61707865; + initialState[1] = 0x3320646e; + initialState[2] = 0x79622d32; + initialState[3] = 0x6b206574; + + final random = _random; + + // Whether we are producing a deterministic sequence. + final isDeterministicSequence = _seedForDeterministicSequence != null; + + // Mix the secret key with Random.secure bits. + for (var i = 4; i < 12; i++) { + final x = random.nextInt(_bit32); + if (!isDeterministicSequence) { + initialState[i] ^= x; } + } - // Block counter is stored at index 12. + // Block counter is stored at index 12. - // Mix the nonce with Random.secure bits. - for (var i = 13; i < 16; i++) { - // We want to call Random.secure even when this is seeded so the - // cost of the operation is the same. - final x = random.nextInt(_bit32); - if (isNotSeeded) { - initialState[i] ^= x; - } + // Mix the nonce with Random.secure bits. + for (var i = 13; i < 16; i++) { + final x = random.nextInt(_bit32); + if (!isDeterministicSequence) { + initialState[i] ^= x; } } + } + + void _nextBlock() { + // Reseed? + final initialState = _initialState; + if (_isTimeForMixinSystemRandom) { + _mixSystemRandom(); + } // Set block counter + final blockCount = _blockCount; initialState[12] = blockCount; _blockCount = blockCount + 1; diff --git a/cryptography/lib/src/helpers/random_bytes.dart b/cryptography/lib/src/helpers/random_bytes.dart index db013d03..49f8b0c5 100644 --- a/cryptography/lib/src/helpers/random_bytes.dart +++ b/cryptography/lib/src/helpers/random_bytes.dart @@ -55,7 +55,7 @@ Uint8List randomBytes(int length, {Random? random}) { /// By default, cryptographically secure [SecureRandom.fast] is used. String randomBytesAsHexString(int length, {Random? random}) { final sb = StringBuffer(); - random ??= SecureRandom.safe; + random ??= SecureRandom.defaultRandom; for (var i = 0; i < length; i++) { final x = random.nextInt(256); sb.write(_hexChars[x >> 4]); diff --git a/cryptography/lib/src/helpers/random_bytes_impl_browser.dart b/cryptography/lib/src/helpers/random_bytes_impl_browser.dart index b4b2f43d..d1bde10b 100644 --- a/cryptography/lib/src/helpers/random_bytes_impl_browser.dart +++ b/cryptography/lib/src/helpers/random_bytes_impl_browser.dart @@ -33,7 +33,7 @@ void fillBytesWithSecureRandom(Uint8List bytes, {Random? random}) { } return; } - random ??= SecureRandom.safe; + random ??= SecureRandom.defaultRandom; for (var i = 0; i < bytes.length;) { if (i + 3 < bytes.length) { // Read 32 bits at a time. diff --git a/cryptography/lib/src/helpers/random_bytes_impl_default.dart b/cryptography/lib/src/helpers/random_bytes_impl_default.dart index 0cfebb3b..7c7b7ae1 100644 --- a/cryptography/lib/src/helpers/random_bytes_impl_default.dart +++ b/cryptography/lib/src/helpers/random_bytes_impl_default.dart @@ -21,7 +21,7 @@ const _bit32 = 0x10000 * 0x10000; /// Fills a list with random bytes (using [Random.secure()]. void fillBytesWithSecureRandom(Uint8List bytes, {Random? random}) { - random ??= SecureRandom.safe; + random ??= SecureRandom.defaultRandom; for (var i = 0; i < bytes.length;) { if (i + 3 < bytes.length) { // Read 32 bits at a time. diff --git a/cryptography/test/benchmark/random.dart b/cryptography/test/benchmark/random.dart index fb086d47..9a073435 100644 --- a/cryptography/test/benchmark/random.dart +++ b/cryptography/test/benchmark/random.dart @@ -21,7 +21,7 @@ import 'benchmark_helpers.dart'; Future main() async { const times = 1 << 16; await _Random(SecureRandom.fast, times).report(); - await _Random(SecureRandom.safe, times).report(); + await _Random(SecureRandom.defaultRandom, times).report(); } class _Random extends SimpleBenchmark { diff --git a/cryptography/test/secure_random_test.dart b/cryptography/test/secure_random_test.dart index 09ecb5ce..e6127097 100644 --- a/cryptography/test/secure_random_test.dart +++ b/cryptography/test/secure_random_test.dart @@ -16,7 +16,7 @@ import 'package:cryptography/cryptography.dart'; import 'package:test/test.dart'; void main() { - group('SecureRandom.instance', () { + group('SecureRandom.fast', () { test('toString()', () { expect(SecureRandom.fast.toString(), 'SecureRandom()'); }); @@ -32,51 +32,71 @@ void main() { }); group('SecureRandom.forTesting(...):', () { + late SecureRandom random; + setUp(() { + random = SecureRandom.forTesting(); + }); + test('example', () { - final random = SecureRandom.forTesting(); expect(random.nextUint32(), 3150129412); expect(random.nextUint32(), 343203913); expect(random.nextUint32(), 2777219198); }); test('example with non-default seed', () { - final random = SecureRandom.forTesting(seed: 1); + random = SecureRandom.forTesting(seed: 1); expect(random.nextUint32(), 663495753); expect(random.nextUint32(), 257774224); expect(random.nextUint32(), 4279763603); }); test('isSecure', () { - expect( - SecureRandom.forTesting().isSecure, - isFalse, - ); + expect(random.isSecure, isFalse); }); test('toString()', () { expect( - SecureRandom.forTesting().toString(), + random.toString(), 'SecureRandom.forTesting(seed: 0)', ); }); }); - group('SecureRandom', () { - final random = SecureRandom.fast; + testSecureRandom('ChaChaRandom.chaCha', () => ChaChaRandom()); + testSecureRandom('ChaChaRandom.chaCha(maxDurationBeforeReseed: null)', + () => ChaChaRandom(maxDurationBeforeReseed: null)); +} - test('nextBool', () { +void testSecureRandom(String name, SecureRandom Function() f) { + group(name, () { + test('nextBool(): first values are random', () { const n = 1000; var trueCount = 0; + for (var i = 0; i < n; i++) { + if (f().nextBool()) { + trueCount++; + } + } + expect(trueCount, greaterThan(n ~/ 3)); + expect(trueCount, lessThan(2 * n ~/ 3)); + }); + + test('nextBool(): distribution looks correct', () { + final random = f(); + const n = 10000; + var trueCount = 0; for (var i = 0; i < n; i++) { if (random.nextBool()) { trueCount++; } } expect(trueCount, greaterThan(n ~/ 3)); + expect(trueCount, lessThan(2 * n ~/ 3)); }); - test('nextInt', () { - const n = 1000; + test('nextInt(): distribution looks correct', () { + final random = f(); + const n = 10000; final counts = {}; for (var i = 0; i < n; i++) { final x = random.nextInt(3); @@ -87,20 +107,34 @@ void main() { final count0 = counts[0] ?? 0; final count1 = counts[1] ?? 0; final count2 = counts[2] ?? 0; + expect(count0, greaterThan(n ~/ 4)); expect(count1, greaterThan(n ~/ 4)); expect(count2, greaterThan(n ~/ 4)); + + expect(count0, lessThan(3 * n ~/ 4)); + expect(count1, lessThan(3 * n ~/ 4)); + expect(count2, lessThan(3 * n ~/ 4)); }); - test('nextUint52', () { - const n = 1000; + test('nextUint52(): distribution looks correct', () { + final random = f(); + const n = 10000; + var count = 0; for (var i = 0; i < n; i++) { final x = random.nextDouble(); expect(x, greaterThanOrEqualTo(0.0)); expect(x, lessThan(1.0)); + if (x < 0.5) { + count++; + } } + expect(count, greaterThan(n ~/ 4)); + expect(count, lessThan(3 * n ~/ 4)); }); - test('nextUint52', () { + + test('nextUint52(): distribution looks correct', () { + final random = f(); const n = 1000; var countOf51bitIntegers = 0; @@ -114,11 +148,11 @@ void main() { } } expect(countOf51bitIntegers, greaterThan(100)); + expect(countOf51bitIntegers, lessThan(900)); }); }); } int _bit16 = 0x10000; int _bit32 = 0x100000000; - int _bit52 = (1 << 4) * _bit16 * _bit32;