Stage 6a: the random source of the writers, and sigma of tlock from it

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
dev 2 days ago
parent 6837b9c463
commit 0ff4bb937c

@ -30,7 +30,6 @@
library;
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
@ -41,6 +40,7 @@ import 'bls12381_hash.dart';
import 'bls12381_pairing.dart';
import 'bls12381_tower.dart';
import 'bytes.dart';
import 'random.dart';
/// The size of a compressed signature of Quicknet, on G1.
const signatureLength = g1ByteLength;
@ -329,17 +329,20 @@ Uint8List decryptWithSignaturePoint(G1Point signature, TlockCiphertext ct) {
/// Encrypts [msg], at most 32 bytes, for the identity [id] under the public
/// key of a Quicknet-style scheme, a compressed point of G2, as
/// EncryptCCAonG2 of kyber with the suite of tlock: Q_id = H(id) on G1 with
/// the DST of RFC 9380, a random sigma, r = H3(sigma, msg), U = r·G2, V =
/// sigma XOR H2(e(Q_id, key)^r) and W = msg XOR H4(sigma). The key passes
/// the gate of the canonical encoding first.
/// the DST of RFC 9380, a random sigma of the length of [msg], drawn from
/// [random] as kyber draws it from crypto/rand, r = H3(sigma, msg), U =
/// r·G2, V = sigma XOR H2(e(Q_id, key)^r) and W = msg XOR H4(sigma). The key
/// passes the gate of the canonical encoding first.
///
/// Not constant time: sigma and r are secret (see the library comment).
TlockCiphertext encryptOnG2(List<int> publicKey, List<int> id, List<int> msg) {
TlockCiphertext encryptOnG2(
List<int> publicKey,
List<int> id,
List<int> msg, [
RandomSource random = secureRandom,
]) {
_checkMessage(msg);
final sigma = Uint8List(msg.length);
for (var i = 0; i < sigma.length; i++) {
sigma[i] = _random.nextInt(256);
}
final sigma = randomBytes(random, msg.length);
try {
return encryptOnG2WithSigma(publicKey, id, msg, sigma);
} finally {
@ -384,9 +387,6 @@ TlockCiphertext encryptOnG2WithSigma(
}
}
// The source of sigma: the generator of the platform, created on first use.
final Random _random = Random.secure();
void _checkMessage(List<int> msg) {
if (msg.length > maxMessageLength) {
throw IbeException(

@ -0,0 +1,116 @@
/// The random values of the writers (spec §62.1 rule 5), drawn from one
/// injectable [RandomSource]: the file keys, the nonces of the STREAM and
/// the labels that age draws, the ephemeral X25519 secrets, the scrypt salts
/// and sigma of tlock; and, for the writer of a capsule, its own values and
/// the permutation of the slots of INNER_ACCESS_AGE (spec §39).
///
/// [secureRandom], the default of every writer, draws from Random.secure of
/// dart:math, the CSPRNG of the platform. [SeededRandomSource] is
/// deterministic, for tests and vectors only: with it a writer draws the
/// values that Go draws when its crypto/rand reads the same stream, and
/// writes the same bytes.
///
/// [randomIndex] draws an integer as Go's crypto/rand.Int, and [permute]
/// permutes a list as the permute of capsule.Encrypt in Go, so that a
/// deterministic source gives the order that Go gives.
///
/// Internal: lib/datekeys.dart does not export it.
library;
import 'dart:math' as math;
import 'dart:typed_data';
import 'chacha20poly1305.dart';
import 'sha256.dart';
/// A source of random bytes.
abstract interface class RandomSource {
/// Fills [out] with random bytes.
void fill(Uint8List out);
}
/// [n] bytes of [source].
Uint8List randomBytes(RandomSource source, int n) {
RangeError.checkNotNegative(n, 'n');
final out = Uint8List(n);
source.fill(out);
return out;
}
/// The CSPRNG of the platform, Random.secure of dart:math, created on first
/// use: a platform without one throws its UnsupportedError then, not
/// before.
final class SecureRandomSource implements RandomSource {
/// The source.
const SecureRandomSource();
static math.Random? _random;
@override
void fill(Uint8List out) {
final r = _random ??= math.Random.secure();
for (var i = 0; i < out.length; i++) {
out[i] = r.nextInt(256);
}
}
}
/// The default source of the writers: the CSPRNG of the platform.
const RandomSource secureRandom = SecureRandomSource();
/// A deterministic source, for tests and vectors only: the ChaCha20
/// keystream (RFC 8439) under the key SHA-256([seed]) and a zero nonce, from
/// block 0, one fill after the other. Go draws the same bytes from a
/// crypto/rand.Reader that reads that keystream (tool/
/// age_writer_go_vectors.go). Its values are not secret: a writer that uses
/// it gives its keys away.
final class SeededRandomSource implements RandomSource {
/// The source of [seed], any bytes.
SeededRandomSource(List<int> seed)
: _cipher = ChaCha20(sha256(seed), Uint8List(chachaNonceSize));
final ChaCha20 _cipher;
@override
void fill(Uint8List out) {
out.fillRange(0, out.length, 0);
_cipher.xorInPlace(out);
}
}
const _two32 = 0x100000000;
/// A uniform integer in [0, n), for 1 ≤ [n] ≤ 2^32, drawn from [source] as
/// Go's crypto/rand.Int(source, n) draws it: k bytes, the length of n - 1,
/// read big-endian with the bits above the length of n - 1 cleared, and
/// drawn again while they are n or more. For n = 1 nothing is drawn.
int randomIndex(RandomSource source, int n) {
if (n < 1 || n > _two32) throw RangeError.range(n, 1, _two32, 'n');
final bits = (n - 1).bitLength;
if (bits == 0) return 0;
final k = (bits + 7) ~/ 8;
final top = bits % 8 == 0 ? 8 : bits % 8;
final b = Uint8List(k);
for (;;) {
source.fill(b);
b[0] &= (1 << top) - 1;
// At most four bytes, read without a shift that the web would truncate.
var v = 0;
for (final x in b) {
v = v * 256 + x;
}
if (v < n) return v;
}
}
/// Puts [items] in a uniformly random order, in place, as the permute of
/// capsule.Encrypt in Go (spec §39): Fisher–Yates from the last position
/// down, each position swapped with [randomIndex] of its index plus one.
void permute<T>(List<T> items, RandomSource source) {
for (var i = items.length - 1; i > 0; i--) {
final j = randomIndex(source, i + 1);
final t = items[i];
items[i] = items[j];
items[j] = t;
}
}

@ -18,6 +18,7 @@ import 'dart:typed_data';
import 'bytes.dart';
import 'errors.dart';
import 'ibe.dart';
import 'random.dart';
import 'release.dart';
String _q(String s) => goQuote(utf8Bytes(s));
@ -96,16 +97,17 @@ Uint8List unwrapTlockStanza(
/// The arguments and the body of the tlock stanza that wraps [fileKey] for
/// [round] under the pinned profile [p], of the scheme of Quicknet, as Wrap
/// of Go's TimeRecipient. Its checks and texts are those of
/// NewTimeRecipient: the profile first, then the range of the round.
/// of Go's TimeRecipient, with sigma drawn from [random]. Its checks and
/// texts are those of NewTimeRecipient: the profile first, then the range
/// of the round. TimeRecipient of recipient.dart writes its stanza with it.
///
/// Encryption: not constant time, see ibe.dart. The writer, stage 6 of the
/// plan, exports it.
/// Encryption: not constant time, see ibe.dart.
(List<String>, Uint8List) wrapTlockStanza(
PinnedProfile p,
int round,
List<int> fileKey,
) {
List<int> fileKey, [
RandomSource random = secureRandom,
]) {
checkTlockProfile(p);
if (round < 1 || round > p.maxRound) {
throw DateKeysException(
@ -113,7 +115,7 @@ Uint8List unwrapTlockStanza(
'agewrap: round $round outside the range of ${p.id}',
);
}
final ct = encryptOnG2(p.publicKey, roundIdentity(round), fileKey);
final ct = encryptOnG2(p.publicKey, roundIdentity(round), fileKey, random);
return (['$round', toHex(p.chainHash)], ciphertextToBody(ct));
}

@ -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…
Cancel
Save

Powered by TurnKey Linux.