You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
146 lines
5.3 KiB
146 lines
5.3 KiB
// 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<int> 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<int>()) {
|
|
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));
|
|
}
|
|
});
|
|
}
|