import 'dart:convert'; import 'dart:typed_data'; import 'package:halkyon_keys/halkyon_keys.dart'; import 'package:sodium/sodium_sumo.dart'; import 'package:test/test.dart'; Future main() async { final sodium = await SodiumSumoInit.init(); Matcher refusedFor(KeyRefusal reason) => throwsA( isA().having((refused) => refused.reason, 'reason', reason), ); group('data keys, sealed to the account', () { final account = AccountKeys.generate(sodium); final dataKey = DataKey.generate(sodium, 'agenda-2026'); final sealed = sealDataKey(sodium, dataKey, account.encryptionPublicKey); test("any device that opens the account opens the store's key", () { final opened = openDataKey( sodium, sealed, keyId: 'agenda-2026', keys: account, ); expect(opened.keyId, 'agenda-2026'); expect(opened.key.extractBytes(), dataKey.key.extractBytes()); }); test("another account's keys don't open it", () { expect( () => openDataKey( sodium, sealed, keyId: 'agenda-2026', keys: AccountKeys.generate(sodium), ), refusedFor(KeyRefusal.wrongKeyOrTampered), ); }); test('a sealed key changed is refused', () { final tampered = Uint8List.fromList(sealed)..[40] ^= 1; expect( () => openDataKey( sodium, tampered, keyId: 'agenda-2026', keys: account, ), refusedFor(KeyRefusal.wrongKeyOrTampered), ); }); test("a key sealed under one ID doesn't open as another's: a server " "can't swap stores' keys", () { expect( () => openDataKey( sodium, sealed, keyId: 'studyset-7', keys: account, ), refusedFor(KeyRefusal.wrongDataKey), ); }); test('an ID must be 1 to 255 bytes', () { expect(() => DataKey.generate(sodium, ''), throwsArgumentError); expect( () => DataKey.generate(sodium, 'x' * 256), throwsArgumentError, ); }); }); group('content, sealed and signed', () { final author = AccountKeys.generate(sodium); final dataKey = DataKey.generate(sodium, 'agenda-2026'); final context = utf8.encode('agenda_item/0192f3a4'); final plainText = Uint8List.fromList( utf8.encode('{"title": "Study N3: 34 cards"}'), ); final sealed = sealContent( sodium, plainText: plainText, dataKey: dataKey, author: author, context: context, ); Uint8List open( SealedContent what, { DataKey? withKey, Uint8List? signedBy, List? at, }) => openContent( sodium, what, dataKey: withKey ?? dataKey, authorSigningPublicKey: signedBy ?? author.signingPublicKey, context: at ?? context, ); test("opens with the data key, its signature the author's", () { expect(open(sealed), plainText); expect(sealed.keyId, 'agenda-2026'); expect(sealed.nonce, hasLength(24)); expect(sealed.signature, hasLength(64)); // The payload is no plain text. expect( utf8.decode(sealed.payload, allowMalformed: true), isNot(contains('Study')), ); }); test('every sealing takes a new nonce', () { final again = sealContent( sodium, plainText: plainText, dataKey: dataKey, author: author, context: context, ); expect(again.nonce, isNot(sealed.nonce)); expect(again.payload, isNot(sealed.payload)); }); test('another data key is refused, even under the same ID', () { expect( () => open(sealed, withKey: DataKey.generate(sodium, 'studyset-7')), refusedFor(KeyRefusal.wrongDataKey), ); expect( () => open(sealed, withKey: DataKey.generate(sodium, 'agenda-2026')), refusedFor(KeyRefusal.wrongKeyOrTampered), ); }); test('a payload, nonce, ID or signature changed is refused as forged', () { SealedContent changed({ String? keyId, Uint8List? nonce, Uint8List? payload, Uint8List? signature, }) => SealedContent( keyId: keyId ?? sealed.keyId, nonce: nonce ?? sealed.nonce, payload: payload ?? sealed.payload, signature: signature ?? sealed.signature, ); for (final tampered in [ changed(payload: Uint8List.fromList(sealed.payload)..[0] ^= 1), changed(nonce: Uint8List.fromList(sealed.nonce)..[23] ^= 1), changed(signature: Uint8List.fromList(sealed.signature)..[10] ^= 1), changed(signature: sealed.signature.sublist(1)), ]) { expect(() => open(tampered), refusedFor(KeyRefusal.forgedSignature)); } // Another ID than the key's is caught before anything else. expect( () => open(changed(keyId: 'agenda-2027')), refusedFor(KeyRefusal.wrongDataKey), ); }); test("someone else's signature, or content moved to another row, is " 'refused', () { expect( () => open( sealed, signedBy: AccountKeys.generate(sodium).signingPublicKey, ), refusedFor(KeyRefusal.forgedSignature), ); expect( () => open(sealed, at: utf8.encode('agenda_item/0192f3a5')), refusedFor(KeyRefusal.forgedSignature), ); }); test('content resealed by someone holding the data key but not the ' "author's signing key doesn't pass as the author's", () { final forger = AccountKeys.generate(sodium); final forged = sealContent( sodium, plainText: Uint8List.fromList(utf8.encode('{"title": "forged"}')), dataKey: dataKey, author: forger, context: context, ); expect(() => open(forged), refusedFor(KeyRefusal.forgedSignature)); expect(open(forged, signedBy: forger.signingPublicKey), isNotEmpty); }); }); }