// The random sources of the writers against test/vectors/age_writer.json // (tool/age_writer_go_vectors.go): SeededRandomSource gives the keystream // that Go reads as crypto/rand.Reader, across fills of any size; // randomIndex gives what crypto/rand.Int gives over it, with the same draws; // and permute gives the order of the permute of capsule.Encrypt. Also the // bounds of randomIndex, the uniformity of permute and the CSPRNG of the // platform, on the VM: compiled to JavaScript, Random.secure is not // available to the tests. import 'dart:convert'; import 'dart:typed_data'; import 'package:datekeys/datekeys.dart'; import 'package:datekeys/src/random.dart'; import 'package:test/test.dart'; import 'random_support.dart'; import 'vectors/age_writer.g.dart'; final Json vectors = jsonDecode(ageWriterJson) as Json; // A source that replays the given bytes and fails past them. final class Replay implements RandomSource { Replay(List bytes) : _bytes = Uint8List.fromList(bytes); final Uint8List _bytes; int _at = 0; @override void fill(Uint8List out) { if (_at + out.length > _bytes.length) throw StateError('exhausted'); out.setRange(0, out.length, _bytes, _at); _at += out.length; } } void main() { test('SeededRandomSource is the keystream that Go reads', () { for (final c in listOf(vectors['seeded'])) { final source = seeded(c['seed']! as String); final out = BytesBuilder(); for (final n in (c['fills']! as List).cast()) { out.add(randomBytes(source, n)); } expect(toHex(out.takeBytes()), c['hex'], reason: c['seed'] as String?); // One fill of everything gives the same bytes. final all = randomBytes( seeded(c['seed']! as String), fromHex(c['hex']! as String).length, ); expect(toHex(all), c['hex']); } // fill overwrites what the buffer held. final b = Uint8List(8)..fillRange(0, 8, 0xff); seeded('').fill(b); expect(b, randomBytes(seeded(''), 8)); expect(() => randomBytes(seeded(''), -1), throwsRangeError); }); test('randomIndex is crypto/rand.Int, with the same draws', () { for (final c in listOf(vectors['rand_int'])) { final n = c['n']! as int; final source = RecordingSource(seeded(c['seed']! as String)); final got = [for (var i = 0; i < 24; i++) randomIndex(source, n)]; expect(got, c['results'], reason: '$n'); expect(source.draws, hasLength(c['reads']), reason: '$n'); // The stream goes on where Go's goes on. expect(toHex(randomBytes(source, 4)), c['next'], reason: '$n'); } }); test('randomIndex draws again exactly above the largest result', () { // n = 3: one byte, two bits kept; 3 is drawn again. expect(randomIndex(Replay([0xff, 0xfe]), 3), 2); expect(randomIndex(Replay([0x03, 0x01]), 3), 1); // n = 256: one byte, all kept, none drawn again. expect(randomIndex(Replay([0xff]), 256), 255); // n = 257: two bytes, the first with one bit; 257 to 511 drawn again. expect(randomIndex(Replay([0x01, 0x01, 0x01, 0x00]), 257), 256); expect(randomIndex(Replay([0xff, 0xff, 0x00, 0x05]), 257), 5); // n = 2^32: four bytes, any. (No shift of 32 bits: the web would // truncate it.) expect( randomIndex(Replay([0xff, 0xff, 0xff, 0xff]), 0x100000000), 0xffffffff, ); // n = 2^31 + 1: four bytes, 32 bits kept. expect( randomIndex(Replay([0x80, 0, 0, 1, 0x80, 0, 0, 0]), 0x80000001), 0x80000000, ); // n = 1: nothing drawn. expect(randomIndex(Replay(const []), 1), 0); for (final n in [0, -1, 0x100000001]) { expect(() => randomIndex(Replay(const []), n), throwsRangeError); } }); test('permute is the permute of capsule.Encrypt', () { for (final c in listOf(vectors['permute'])) { final n = c['n']! as int; final items = List.generate(n, (i) => i); final source = RecordingSource(seeded(c['seed']! as String)); permute(items, source); expect(items, c['order'], reason: c['seed'] as String?); expect(source.draws, hasLength(c['reads'])); } }); // As TestStanzaOrderIsUniform of the Go reference and the test of // datekeys-ts: the positions of the first and of the last of 16 items // over many permutations, each a chi-square with 15 degrees of freedom // under 60 (p ≈ 10⁻⁷ by chance). test('permute puts each item in every position uniformly', () { final source = seeded('uniform'); const n = 16; final rounds = isWeb ? 4000 : 32000; final first = List.filled(n, 0); final last = List.filled(n, 0); for (var r = 0; r < rounds; r++) { final items = List.generate(n, (i) => i); permute(items, source); first[items.indexOf(0)]++; last[items.indexOf(n - 1)]++; } for (final counts in [first, last]) { final e = rounds / n; var chi = 0.0; for (final o in counts) { chi += (o - e) * (o - e) / e; } expect(chi, lessThan(60), reason: '$counts'); } }); test('the CSPRNG of the platform', testOn: 'vm', () { final a = randomBytes(secureRandom, 32); final b = randomBytes(secureRandom, 32); expect(a, isNot(b)); expect(a.toSet().length, greaterThan(16)); for (var i = 0; i < 100; i++) { final v = randomIndex(secureRandom, 16); expect(v, inInclusiveRange(0, 15)); } }); }