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.
185 lines
6.9 KiB
185 lines
6.9 KiB
// The word lists against files, and at length: the copy of
|
|
// test/vectors/wordlist_vectors.json that wordlist_test.dart reads, a Dart
|
|
// constant for the tests compiled to JavaScript, is the JSON file byte for
|
|
// byte; checkWordList gives the result of Go's CheckList for every code
|
|
// point of planes 0, 1 and 14; readWordList reads wordlists/es.txt and
|
|
// wordlists/en.txt, the lists of datekeys-go, as Go's List reads the lists
|
|
// built into it (TestBuiltInLists), and generateWords draws from the Spanish
|
|
// one the words of Go; and,
|
|
// as TestGenerate and TestGenerateUniform of Go, the words drawn with the
|
|
// CSPRNG of the platform are a key, and each about as likely. On the VM
|
|
// only: they read files, use Random.secure or take a few seconds.
|
|
@TestOn('vm')
|
|
library;
|
|
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:datekeys/src/bytes.dart';
|
|
import 'package:datekeys/src/pathrule.dart' show codePointName;
|
|
import 'package:datekeys/src/sha256.dart';
|
|
import 'package:datekeys/src/wordkey.dart' show checkWords, normalizeWords;
|
|
import 'package:datekeys/src/wordlist.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
import 'vectors/wordlist_vectors.g.dart';
|
|
import 'wordlist_support.dart';
|
|
|
|
void main() {
|
|
final vectors = jsonDecode(wordlistVectorsJson) as Json;
|
|
|
|
test('wordlist_vectors.g.dart holds wordlist_vectors.json', () {
|
|
final file = File('test/vectors/wordlist_vectors.json').readAsStringSync();
|
|
expect(wordlistVectorsJson, file);
|
|
});
|
|
|
|
test('the alphabet of every code point of planes 0, 1 and 14', () {
|
|
// The first word of the base list is pala, the code point and zz: the
|
|
// lines of every result, but for the surrogates, have Go's SHA-256.
|
|
final a = vectors['alphabet']! as Json;
|
|
final words = caseWords(const {'size': minListSize});
|
|
final digest = Sha256();
|
|
final accepted = <String>[];
|
|
var count = 0;
|
|
for (final p in (a['planes']! as List).cast<int>()) {
|
|
for (var r = p << 16; r <= (p << 16 | 0xffff); r++) {
|
|
if (r >= 0xd800 && r <= 0xdfff) continue;
|
|
final letter = String.fromCharCode(r);
|
|
words[0] = utf8Bytes('pala${letter}zz');
|
|
final result = outcome(() => checkWordListUtf8('es', words));
|
|
digest.add(utf8Bytes('${codePointName(r)} $result\n'));
|
|
count++;
|
|
if (result == 'ok') accepted.add(letter);
|
|
}
|
|
}
|
|
expect(count, a['count']);
|
|
expect(toHex(digest.finish()), a['sha256']);
|
|
expect(accepted, a['ok']);
|
|
});
|
|
|
|
group('the Spanish list of wordlists/', () {
|
|
final file = File('wordlists/es.txt').readAsBytesSync();
|
|
final es = readWordList('es', file);
|
|
List<Json> cases(String name) => (vectors[name]! as List).cast<Json>();
|
|
|
|
test('is the list of Go, with its pinned SHA-256 (TestBuiltInLists)', () {
|
|
expect(wordListLanguages(), ['en', 'es']);
|
|
final go = cases('lists').singleWhere((l) => l['lang'] == 'es');
|
|
expect(go['lang'], 'es');
|
|
expect(toHex(sha256(file)), wordListSha256['es']);
|
|
expect(file, hasLength(go['bytes']));
|
|
expect(es, hasLength(go['words']));
|
|
expect(es, hasLength(7776));
|
|
// wordlists/SOURCE.json records the pinned SHA-256 of each list.
|
|
final source =
|
|
jsonDecode(File('wordlists/SOURCE.json').readAsStringSync()) as Json;
|
|
final files = source['files']! as Json;
|
|
for (final lang in wordListLanguages()) {
|
|
expect(files['$lang.txt'], wordListSha256[lang], reason: lang);
|
|
}
|
|
// The words are the lines of the file, and they pass checkWordList.
|
|
expect(utf8Bytes('${es.join('\n')}\n'), file);
|
|
checkWordList('es', es);
|
|
expect(
|
|
outcome(() => readWordList('xx', file)),
|
|
'wordkey: no word list for "xx"; the lists are en, es',
|
|
);
|
|
});
|
|
|
|
test('readWordList refuses the list with a byte changed, more or less', () {
|
|
for (final changed in [
|
|
for (final i in [0, 1, file.length ~/ 2, file.length - 1])
|
|
Uint8List.fromList(file)..[i] ^= 0x20,
|
|
file.sublist(0, file.length - 1),
|
|
Uint8List.fromList([...file, 0x0a]),
|
|
]) {
|
|
expect(
|
|
outcome(() => readWordList('es', changed)),
|
|
'wordkey: the list "es" has the SHA-256 ${toHex(sha256(changed))}, '
|
|
'not ${wordListSha256['es']}',
|
|
);
|
|
}
|
|
});
|
|
|
|
test('generateWords draws from it the words of Go', () {
|
|
var drawn = 0;
|
|
for (final c in cases('generate')) {
|
|
if (c['list'] != 'es' || c['error'] != null) continue;
|
|
final name = c['name']! as String;
|
|
final words = generateWords(
|
|
es,
|
|
c['n']! as int,
|
|
streamOf(c['stream']! as Json),
|
|
);
|
|
final indices = (c['indices']! as List).cast<int>();
|
|
expect(words, [for (final i in indices) es[i]], reason: name);
|
|
if (c['words'] != null) expect(words, c['words'], reason: name);
|
|
drawn++;
|
|
}
|
|
expect(drawn, greaterThan(10));
|
|
});
|
|
|
|
test('the words drawn with the CSPRNG of the platform are a key '
|
|
'(TestGenerate)', () {
|
|
final words = generateWords(es);
|
|
expect(words, hasLength(defaultWordCount));
|
|
checkWords(normalizeWords(words.join(' ')));
|
|
expect(words.toSet(), hasLength(words.length));
|
|
for (final (n, want) in [
|
|
(5, 'at least 6 words, not 5'),
|
|
(es.length, '7776 words of a list of 7776'),
|
|
]) {
|
|
expect(outcome(() => generateWords(es, n)), contains(want));
|
|
}
|
|
});
|
|
|
|
// Over 7776·40 draws of one word, each index falls in its bucket of 64
|
|
// between 0.8 and 1.2 times the mean.
|
|
test('every word is about as likely (TestGenerateUniform)', () {
|
|
final index = {for (var i = 0; i < es.length; i++) es[i]: i};
|
|
const buckets = 64;
|
|
final count = List.filled(buckets, 0);
|
|
var draws = 0;
|
|
while (draws < es.length * 40) {
|
|
for (final w in generateWords(es, 6)) {
|
|
count[index[w]! * buckets ~/ es.length]++;
|
|
draws++;
|
|
}
|
|
}
|
|
final mean = draws / buckets;
|
|
for (var i = 0; i < buckets; i++) {
|
|
expect(
|
|
count[i],
|
|
inInclusiveRange(0.8 * mean, 1.2 * mean),
|
|
reason: 'bucket $i',
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
group('the English list of wordlists/', () {
|
|
final file = File('wordlists/en.txt').readAsBytesSync();
|
|
|
|
test('is the list of the EFF that Go builds in, in its order', () {
|
|
final en = readWordList('en', file);
|
|
final go = (vectors['lists']! as List).cast<Json>().singleWhere(
|
|
(l) => l['lang'] == 'en',
|
|
);
|
|
expect(toHex(sha256(file)), wordListSha256['en']);
|
|
expect(go['sha256'], wordListSha256['en']);
|
|
expect(file, hasLength(go['bytes']));
|
|
expect(en, hasLength(go['words']));
|
|
expect(utf8Bytes('${en.join('\n')}\n'), file);
|
|
// The dice 11111 give its first word and 66666 its last.
|
|
expect([en[0], en[1], en.last], ['abacus', 'abdomen', 'zoom']);
|
|
expect(en.where((w) => w.contains('-')), [
|
|
'drop-down',
|
|
'felt-tip',
|
|
't-shirt',
|
|
'yo-yo',
|
|
]);
|
|
});
|
|
});
|
|
}
|