random.dart: RandomSource, the injectable source of every random value of a writer; secureRandom, the default, Random.secure of the platform; SeededRandomSource, deterministic, for tests and vectors only; and, for the 16 slots of INNER_ACCESS_AGE, randomIndex, an integer drawn as crypto/rand.Int draws it, and permute, as the permute of capsule.Encrypt. encryptOnG2 and wrapTlockStanza take the source of sigma, secureRandom by default, so that a tlock stanza can be written again byte for byte; ibe.dart no longer holds a Random.secure of its own. The tests check the keystream, randomIndex and permute against the vectors of Go, randomIndex at its bounds, the uniformity of permute, and the CSPRNG on the VM: in dart test -p node there is no Random.secure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>v0.11
parent
6837b9c463
commit
0ff4bb937c
@ -0,0 +1,40 @@
|
||||
// Helpers of the tests of the random sources and of the age writer: the
|
||||
// deterministic source of a seed named as the vectors name it, and a source
|
||||
// that records the draws of another one. They read no file.
|
||||
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:datekeys/datekeys.dart' show toHex;
|
||||
import 'package:datekeys/src/bytes.dart' show utf8Bytes;
|
||||
import 'package:datekeys/src/random.dart';
|
||||
|
||||
typedef Json = Map<String, Object?>;
|
||||
|
||||
/// Whether the tests run compiled to JavaScript.
|
||||
const isWeb = identical(0, 0.0);
|
||||
|
||||
List<Json> listOf(Object? v) => (v! as List).cast<Json>();
|
||||
|
||||
/// A source that records every draw of another one: its size and bytes.
|
||||
final class RecordingSource implements RandomSource {
|
||||
RecordingSource(this._inner);
|
||||
|
||||
final RandomSource _inner;
|
||||
|
||||
/// The draws, in order.
|
||||
final List<Uint8List> draws = [];
|
||||
|
||||
@override
|
||||
void fill(Uint8List out) {
|
||||
_inner.fill(out);
|
||||
draws.add(Uint8List.fromList(out));
|
||||
}
|
||||
|
||||
/// The draws as the vectors write them.
|
||||
List<Json> get json => [
|
||||
for (final d in draws) {'n': d.length, 'hex': toHex(d)},
|
||||
];
|
||||
}
|
||||
|
||||
/// The deterministic source of [seed], a string, as the vectors name it.
|
||||
SeededRandomSource seeded(String seed) => SeededRandomSource(utf8Bytes(seed));
|
||||
@ -0,0 +1,145 @@
|
||||
// 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));
|
||||
}
|
||||
});
|
||||
}
|
||||
Loading…
Reference in new issue