// The key of words (spec §38.1) against test/vectors/wordkey_vectors.json, // whose expected values the Go reference computed // (tool/wordkey_go_vectors.go): the words of wordkey.Normalize, the refusals // of wordkey.Check with their texts, and the salt and the password of // wordkey.Key, with PBKDF2 at 1000 iterations. The vectors come from a Dart // constant, so that these tests also run compiled to JavaScript; the keys of // 600 000 iterations and their identities are in wordkey_vm_test.dart. import 'dart:convert'; import 'dart:typed_data'; import 'package:datekeys/src/bytes.dart'; import 'package:datekeys/src/sha256.dart'; import 'package:datekeys/src/wordkey.dart'; import 'package:test/test.dart'; import 'vectors/wordkey_vectors.g.dart'; import 'wordkey_support.dart'; final Map vectors = jsonDecode(wordkeyVectorsJson) as Map; List> cases(String name) => (vectors[name]! as List).cast>(); /// "ok", or the message of the [WordKeyException] that [f] throws. String outcome(void Function() f) { try { f(); return 'ok'; } on WordKeyException catch (e) { return e.message; } } String label(Map c) => c['name'] as String? ?? '${c['in'] ?? c['words']}'; void main() { test('the constants are those of Go', () { expect(vectors['rounds'], wordKeyRounds); expect(vectors['min_words'], minWords); expect(vectors['min_letters'], minLetters); }); test('normalizeWords reads the words as wordkey.Normalize', () { final all = cases('normalize'); expect(all, hasLength(greaterThan(350))); var strings = 0; for (final c in all) { final input = fromHex(c['in']! as String); final want = [ for (final w in (c['words']! as List).cast()) decodeUtf8(fromHex(w))!, ]; expect(normalizeWordsUtf8(input), want, reason: label(c)); final s = fromWtf8(input); if (s != null) { strings++; expect(normalizeWords(s), want, reason: label(c)); } } expect(strings, greaterThan(300)); }); test('checkWords refuses words as wordkey.Check, with its texts', () { final all = cases('check'); expect(all, hasLength(greaterThan(450))); var refused = 0; for (final c in all) { final words = wordBytes(c); final want = c['result']! as String; expect(outcome(() => checkWordsUtf8(words)), want, reason: label(c)); if (want != 'ok') refused++; final strings = [for (final w in words) fromWtf8(w)]; if (strings.every((s) => s != null)) { expect( outcome(() => checkWords([for (final s in strings) s!])), want, reason: label(c), ); } } expect(refused, greaterThan(400)); }); test('the salt and the password of wordkey.Key', () { for (final c in cases('keys')) { final words = [for (final w in wordBytes(c)) fromWtf8(w)!]; final chain = fromHex(c['chain_hash']! as String); final round = c['round']! as int; final capsuleId = fromHex(c['capsule_id']! as String); final salt = wordKeySalt(chain, round, capsuleId); expect(salt, utf8Bytes(c['salt']! as String), reason: label(c)); // The password is the words joined by one U+0020, in UTF-8: with the // salt and 1000 iterations, the PBKDF2 of Go. wordKey derives with // the same two and 600 000 iterations, on the VM in // wordkey_vm_test.dart. final password = wordKeyPassword(words); expect(toHex(password), c['password'], reason: label(c)); expect( toHex(pbkdf2HmacSha256(password, salt, 1000, 32)), c['key_1000'], reason: label(c), ); } }); test('a round is an int of 0 to 2^53-1', () { final chain = Uint8List(32); final id = Uint8List(16); expect(() => wordKeySalt(chain, -1, id), throwsRangeError); expect( () => wordKeySalt(chain, 9007199254740991 + 1, id), throwsRangeError, ); expect( utf8.decode(wordKeySalt(chain, 9007199254740991, id)), contains('|9007199254740991|'), ); }); test('the examples of spec §38.1 give the same words', () { // The three spellings of «abaco arbol», by the names of their cases, and // the punctuation that counts. final examples = [ for (final c in cases('normalize')) if ((c['name'] as String? ?? '').contains('38.1: ')) c, ]; final spellings = [ for (final c in examples) if ((c['name']! as String).contains('baco')) c['words'], ]; expect(spellings, hasLength(3)); expect(spellings[0], hasLength(2)); expect(spellings[1], spellings[0]); expect(spellings[2], spellings[0]); final punctuation = examples.singleWhere( (c) => (c['name']! as String).endsWith('punctuation counts'), ); expect(normalizeWordsUtf8(fromHex(punctuation['in']! as String)), [ 'perro,', 'perro', ]); }); }