|
|
// 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<Object?> parts) {
|
|
|
final out = BytesBuilder(copy: false);
|
|
|
for (final p in parts.cast<Json>()) {
|
|
|
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<Json> 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 = <String, Object?>{
|
|
|
'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<List<Object?>>();
|
|
|
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)}');
|
|
|
}
|
|
|
});
|
|
|
}
|