// Helpers and tests of the author keys over test/vectors/authorkey.json, as // tool/authorkey_go_vectors.go writes it: the signatures of Go's // crypto/ed25519, the scalars modulo ℓ, and package authorkey of // datekeys-go with the texts of its errors. authorkey_test.dart runs them // over the part in authorkey.g.dart, also compiled to JavaScript, and // authorkey_vm_test.dart over the whole file. They read no file. import 'dart:typed_data'; import 'package:datekeys/src/age_writer.dart'; import 'package:datekeys/src/authorkey.dart'; import 'package:datekeys/src/bytes.dart'; import 'package:datekeys/src/curve25519.dart' show verifyStrict; import 'package:datekeys/src/ed25519_sign.dart'; import 'package:datekeys/src/recipient.dart'; import 'package:datekeys/src/sha256.dart'; import 'package:test/test.dart'; import 'age_support.dart' show pattern; import 'random_support.dart'; export 'random_support.dart'; /// The text of index [i] of the vectors, null for no error. String? textOf(Json v, Object? i) { final t = (v['texts']! as List)[i! as int] as String; return t.isEmpty ? null : t; } /// The text of what [f] throws, null when it returns. String? errorOf(void Function() f) { try { f(); return null; } on AuthorKeyException catch (e) { return e.message; } on ArgumentError catch (e) { return e.message as String; } } /// The message of a signing case. Uint8List messageOf(Json c) { final n = c['message_pattern'] as int?; return n != null ? pattern(n) : fromHex(c['message']! as String); } /// The bytes of a file of the vectors: parts {hex}, {byte, n} and {sealed: /// {seed, passphrase, work_factor, plain}, length, sha256}, which is /// written again here with SeededRandomSource and checked. Uint8List fileOf(List parts) { final out = BytesBuilder(copy: false); for (final p in parts.cast()) { if (p['hex'] case final String h) { out.add(fromHex(h)); } else if (p['sealed'] case final Json s) { final f = ageEncrypt(fileOf(s['plain']! as List), [ ScryptRecipient( s['passphrase']! as String, workFactor: s['work_factor']! as int, ), ], random: seeded(s['seed']! as String)); expect(f.length, p['length']); expect(toHex(sha256(f)), p['sha256']); out.add(f); } else { out.add( Uint8List(p['n']! as int) ..fillRange(0, p['n']! as int, p['byte']! as int), ); } } return out.takeBytes(); } /// The result of a parse as the vectors write it: the text, and the public /// key when it passes. Json parsed(Uint8List Function() f) { try { return {'public_key': toHex(f()), 'text': null}; } on AuthorKeyException catch (e) { return {'text': e.message}; } } Json expectedParse(Json v, Json c) => { if (c['public_key'] != null) 'public_key': c['public_key'], 'text': textOf(v, c['text']), }; /// The tests of the signing cases [cases]. void signTests(List cases) { test('Ed25519 signatures of crypto/ed25519 (${cases.length})', () { for (final c in cases) { final seed = fromHex(c['seed']! as String); final pub = fromHex(c['public_key']! as String); final msg = messageOf(c); final sig = ed25519Sign(seed, pub, msg); expect(toHex(sig), c['signature'], reason: c['name'] as String?); if (c['valid'] == true) { expect(toHex(ed25519PublicKey(seed)), c['public_key']); expect(toHex(AuthorKey.fromSeed(seed).sign(msg)), c['signature']); expect( verifyStrict(pub, msg, sig), isTrue, reason: c['name'] as String?, ); } else { expect(verifyStrict(pub, msg, sig), isFalse); } } }); } /// The tests of the vectors [v] that authorkey.g.dart holds whole. void authorKeyTests(Json v) { test('the scalars modulo ℓ of math/big', () { final s = v['scalars']! as Json; for (final c in listOf(s['reduce'])) { expect( toHex(ed25519ReduceScalar(fromHex(c['in']! as String))), c['out'], reason: c['in'] as String?, ); } for (final c in listOf(s['muladd'])) { final r = ed25519MulAdd( fromHex(c['a']! as String), fromHex(c['b']! as String), fromHex(c['c']! as String), ); expect(toHex(r), c['out']); } }); test('keys: the public key, its string, the secret, Marshal, String', () { final k = v['keys']! as Json; for (final c in listOf(k['keys'])) { final key = AuthorKey.fromSeed(fromHex(c['seed']! as String)); expect(toHex(key.publicKey), c['public_key']); expect(key.publicString, c['public']); expect(authorPublicString(key.publicKey), c['public']); expect(key.secret, c['secret']); expect(decodeUtf8(marshalAuthorKey(key)), c['marshal']); expect(key.toString(), c['string']); expect('$key', c['gostring']); expect(toHex(parseAuthorPublic(c['public']! as String)), c['public_key']); expect(parseAuthorSecret(c['secret']! as String).secret, c['secret']); } for (final c in listOf(k['seed_errors'])) { expect( errorOf(() => AuthorKey.fromSeed(Uint8List(c['length']! as int))), c['error'], ); } for (final c in listOf(k['public_errors'])) { expect( errorOf(() => authorPublicString(Uint8List(c['length']! as int))), c['error'], ); } }); test('Generate draws its seed as Go does', () { for (final c in listOf(v['generate'])) { final r = RecordingSource(seeded(c['seed']! as String)); final key = AuthorKey.generate(r); expect(r.json, c['draws']); expect(key.secret, c['secret']); expect(toHex(key.publicKey), c['public_key']); } }); for (final c in listOf(v['encrypt'])) { if (c['error'] != null) { test('Encrypt refuses an empty passphrase', () { final key = AuthorKey.fromSeed(Uint8List(32)); expect(errorOf(() => encryptAuthorKey(key, '')), c['error']); }); continue; } if (isWeb && c['node'] != true) continue; test('Encrypt writes the bytes of Go: ${c['passphrase']}', () { final key = AuthorKey.fromSeed(fromHex(c['key_seed']! as String)); final r = RecordingSource(seeded(c['seed']! as String)); final file = encryptAuthorKey(key, c['passphrase']! as String, random: r); expect(r.json, c['draws']); expect(toHex(file), c['file']); final back = readAuthorKey(file, passphrase: c['passphrase']! as String); expect(back.secret, key.secret); }); } test('ParsePublic, with the texts of Go', () { for (final c in listOf(v['public'])) { final b = fromHex(c['in']! as String); final want = expectedParse(v, c); expect( parsed(() => parseAuthorPublicUtf8(b)), want, reason: c['in'] as String?, ); final s = decodeUtf8(b); if (s != null) expect(parsed(() => parseAuthorPublic(s)), want); } }); test('ParseSecret, with the texts of Go', () { for (final c in listOf(v['secret'])) { final b = fromHex(c['in']! as String); final want = expectedParse(v, c); expect( parsed(() => parseAuthorSecretUtf8(b).publicKey), want, reason: c['in'] as String?, ); final s = decodeUtf8(b); if (s != null) { expect(parsed(() => parseAuthorSecret(s).publicKey), want); } } }); runeTests(v); for (final c in listOf(v['read'])) { if (isWeb && c['node'] != true) continue; test('Read: ${c['name']}', () { final file = fileOf(c['file']! as List); final want = { 'text': textOf(v, c['text']), if (c['public_key'] != null) 'public_key': c['public_key'], if (c['secret'] != null) 'secret': c['secret'], }; Json got; try { final k = readAuthorKey(file, passphrase: c['passphrase']! as String); got = { 'text': null, 'public_key': toHex(k.publicKey), 'secret': k.secret, }; } on AuthorKeyException catch (e) { got = {'text': e.message}; } expect(got, want); }); } } /// The rune cases of [v]: a valid string with one character replaced by a /// rune of as many bytes, at each edge of the case and space sets of Go. void runeTests(Json v) { final r = v['runes']! as Json; final cases = (r['cases']! as List).cast>(); test('the runes at the edges of the sets of Go (${cases.length})', () { final bases = [ utf8Bytes(r['public']! as String), utf8Bytes(r['secret']! as String), ]; for (final c in cases) { final kind = c[0]! as int; final at = c[1]! as int; final rune = c[2]! as int; final e = utf8Bytes(String.fromCharCode(rune)); final b = Uint8List.fromList(bases[kind])..setRange(at, at + e.length, e); final got = errorOf( () => kind == 0 ? parseAuthorPublicUtf8(b) : parseAuthorSecretUtf8(b), ); expect(got, textOf(v, c[3]), reason: 'U+${rune.toRadixString(16)}'); } }); }