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.
dateKeys-dart/test/wordlist_vm_test.dart

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',
]);
});
});
}

Powered by TurnKey Linux.