// The tlock encryption and the tlock stanza (lib/src/ibe.dart, // lib/src/tlock.dart) against the Go reference: test/vectors/ // tlock_vectors.json, written by tool/tlock_go_vectors.go, and the unwrap // and recipient sections of test/vectors/release_vectors.json, written by // tool/release_go_vectors.go. A port of tlock.test.ts of datekeys-ts. @TestOn('vm') library; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:datekeys/datekeys.dart' show fromHex, toHex; import 'package:datekeys/src/errors.dart'; import 'package:datekeys/src/ibe.dart'; import 'package:datekeys/src/release.dart'; import 'package:datekeys/src/tlock.dart'; import 'package:test/test.dart'; import 'tlock_support.dart'; typedef Json = Map; Json readJson(String path) => jsonDecode(File(path).readAsStringSync()) as Json; final Json v = readJson('test/vectors/tlock_vectors.json'); final Json g = readJson('test/vectors/release_vectors.json'); List section(Json file, String name) => (file[name]! as List).cast(); String s(Json v, String key) => v[key]! as String; Uint8List h(Json v, String key) => fromHex(s(v, key)); final Map signatures = { for (final e in section(v, 'encrypt')) e['round']! as int: s(e, 'signature'), }; Release release(int round) => Release(round, fromHex(signatures[round]!)); // The profiles of release_vectors.json: Quicknet with another scheme or key. TestProfile profileOf(String name) { final c = section(g, 'profiles').firstWhere((p) => p['name'] == name); return quicknet().copyWith( scheme: s(c, 'scheme'), publicKey: h(c, 'public_key'), ); } // Go's TimeIdentity.Unwrap as stage 4 will compose it: the stanza rules of // agewrap that need the whole header (its count and the type of its // stanza), which stage 2 brings, then unwrapTlockStanza. The texts are // Go's. Uint8List timeIdentityUnwrap( PinnedProfile p, int round, Release r, List stanzas, ) { if (stanzas.length != 1) { throw DateKeysException( ErrorCode.policyStructureMismatch, 'agewrap: OUTER_TIME_AGE has ${stanzas.length} stanzas, want exactly ' 'one tlock stanza', ); } final st = stanzas.single; if (st.type != 'tlock') { throw DateKeysException( ErrorCode.policyStructureMismatch, 'agewrap: OUTER_TIME_AGE stanza type "${st.type}", want "tlock"', ); } return unwrapTlockStanza(p, round, r, st.args, st.body); } DateKeysException dateKeysError(void Function() body, String label) { try { body(); } on DateKeysException catch (e) { return e; } fail('$label: no DateKeysException'); } void main() { test('reads vectors of the Quicknet scheme and key', () { expect(v['generator'], 'tool/tlock_go_vectors.go'); expect([v['scheme'], v['public_key']], [quicknetScheme, quicknetPublicKey]); expect(signatures.keys.toSet(), {1000, 1001}); expect(g['generator'], 'tool/release_go_vectors.go'); expect(g['max_round'], quicknet().maxRound); }); test('encrypts as the reference, byte for byte, for a given sigma', () { final p = quicknet(); for (final e in section(v, 'encrypt')) { final name = s(e, 'name'); expect(toHex(roundIdentity(e['round']! as int)), s(e, 'id')); final c = encryptOnG2WithSigma( p.publicKey, h(e, 'id'), h(e, 'msg'), h(e, 'sigma'), ); expect( [toHex(c.u), toHex(c.v), toHex(c.w)], [s(e, 'u'), s(e, 'v'), s(e, 'w')], reason: name, ); expect(toHex(decryptOnG2(h(e, 'signature'), c)), s(e, 'msg')); } expect( [for (final e in section(v, 'encrypt')) h(e, 'msg').length]..sort(), [0, 1, 16, 16, 32], ); }); test('opens what datekeys-ts encrypted and the reference opened: IBE bodies, ' 'and age files whose header MAC the file key verifies', () { final interop = v['interop']! as Json; expect(interop['generator'], 'scripts/tlock-ts-samples.mjs'); final samples = section(interop, 'samples'); expect([for (final x in samples) '${x['kind']} ${x['round']}']..sort(), [ 'age 1000', 'age 1001', 'ibe 1000', 'ibe 1001', ]); for (final x in samples) { final name = s(x, 'name'); final round = x['round']! as int; expect(x['go'], 'ok', reason: name); if (x['kind'] == 'ibe') { expect(x['go_result'], x['file_key'], reason: name); final key = decryptOnG2( release(round).signature, ciphertextFromBody(h(x, 'body')), ); expect(toHex(key), s(x, 'file_key'), reason: name); } else { expect(x['go_result'], x['plaintext'], reason: name); final header = readAgeHeader(h(x, 'file')); final key = timeIdentityUnwrap( quicknet(), round, release(round), header.stanzas, ); expect( ageHeaderMacValid(key, header.macInput, header.mac), isTrue, reason: name, ); } } }); test( 'draws a new sigma every time, and the signature of the round opens every ' 'ciphertext', () { final p = quicknet(); final msg = fromHex('00112233445566778899aabbccddeeff'); final a = encryptOnG2(p.publicKey, roundIdentity(1001), msg); final b = encryptOnG2(p.publicKey, roundIdentity(1001), msg); expect(toHex(a.u), isNot(toHex(b.u))); for (final c in [a, b]) { expect(decryptOnG2(release(1001).signature, c), msg); } expect( () => decryptOnG2(release(1000).signature, a), throwsA(isA()), ); }, ); test( 'rejects a message longer than 32 bytes, a sigma of another length and a ' 'key that is not a canonical point', () { final p = quicknet(); final id = roundIdentity(1000); (IbeReason, String) failure(void Function() body) { try { body(); } on IbeException catch (e) { return (e.reason, e.message); } fail('no IbeException'); } final infinity = Uint8List(96)..[0] = 0xc0; final offCurve = Uint8List.fromList(p.publicKey)..[95] ^= 1; expect(failure(() => encryptOnG2(p.publicKey, id, Uint8List(33))), ( IbeReason.length, 'ibe: a message of 33 bytes, want at most 32', )); expect( failure( () => encryptOnG2WithSigma( p.publicKey, id, Uint8List(16), Uint8List(15), ), ), (IbeReason.length, 'ibe: sigma of 15 bytes for a message of 16'), ); expect( failure(() => encryptOnG2(p.publicKey.sublist(1), id, Uint8List(16))), (IbeReason.length, 'ibe: the public key of 95 bytes, want 96'), ); expect(failure(() => encryptOnG2(infinity, id, Uint8List(16))), ( IbeReason.identity, 'ibe: the public key is the point at infinity', )); expect( failure(() => encryptOnG2(offCurve, id, Uint8List(16))).$1, IbeReason.encoding, ); }, ); test('writes the stanza of tlock, which unwraps with the release', () { final p = quicknet(); final fileKey = fromHex('0f' * 16); final (args, body) = wrapTlockStanza(p, 1000, fileKey); expect(args, ['1000', quicknetChainHash]); expect(body, hasLength(tlockBodyLength)); expect(unwrapTlockStanza(p, 1000, release(1000), args, body), fileKey); final e = dateKeysError( () => unwrapTlockStanza(p, 1000, release(1001), args, body), 'another release', ); expect(e.code, ErrorCode.roundMismatch); }); test( 'checks the profile, then the round, as NewTimeRecipient, with its codes ' 'and texts', () { // Only the scheme of Quicknet is supported, as in datekeys-ts: for // another scheme the code is Go's, the text this library's. const onlyQuicknet = { 'another scheme of drand': 'agewrap: profile datekeys:quicknet:v1 uses scheme ' 'pedersen-bls-unchained; only bls-unchained-g1-rfc9380 is ' 'supported here: ERR_UNKNOWN_PROFILE', 'a scheme that is not of drand': 'agewrap: profile datekeys:quicknet:v1 uses scheme datekeys-test; ' 'only bls-unchained-g1-rfc9380 is supported here: ' 'ERR_UNKNOWN_PROFILE', }; for (final c in section(g, 'recipient')) { final name = s(c, 'name'); final p = profileOf(s(c, 'profile')); final round = c['round']! as int; if (c['go'] == 'ok') { final (args, body) = wrapTlockStanza(p, round, Uint8List(16)); expect(args.first, '$round', reason: name); expect(body, hasLength(tlockBodyLength), reason: name); continue; } final e = dateKeysError( () => wrapTlockStanza(p, round, Uint8List(16)), name, ); expect(e.code.code, c['code'], reason: name); expect(e.message, onlyQuicknet[name] ?? c['text'], reason: name); } }, ); test('unwraps the stanza of OUTER_TIME_AGE as TimeIdentity of Go, with its ' 'codes and texts, in its order', () { // As above, only the scheme of Quicknet; and the profile is checked // as NewTimeIdentity does, before the stanza. const onlyQuicknet = { 'another scheme of drand': 'agewrap: profile datekeys:quicknet:v1 uses scheme ' 'pedersen-bls-unchained; only bls-unchained-g1-rfc9380 is ' 'supported here: ERR_UNKNOWN_PROFILE', }; var opened = 0; for (final c in section(g, 'unwrap')) { final name = s(c, 'name'); final p = profileOf(s(c, 'profile')); final r = Release(c['release_round']! as int, h(c, 'signature')); final stanzas = [ for (final st in (c['stanzas']! as List).cast()) ( type: s(st, 'type'), args: (st['args']! as List).cast(), body: h(st, 'body'), ), ]; final round = c['round']! as int; if (c['go'] == 'ok') { expect( toHex(timeIdentityUnwrap(p, round, r, stanzas)), c['file_key'], reason: name, ); opened++; continue; } final e = dateKeysError( () => timeIdentityUnwrap(p, round, r, stanzas), name, ); expect(e.code.code, c['code'], reason: name); expect(e.message, onlyQuicknet[name] ?? c['text'], reason: name); } expect(opened, 1); }); }