import 'dart:convert'; import 'dart:typed_data'; import 'package:halkyon_content/halkyon_content.dart'; import 'package:test/test.dart'; /// Content sealed with a data key (ADR 0019), with libsodium as its build /// hook compiles it. void main() { late UnlockedAccountKeys alice; late UnlockedAccountKeys bob; late SodiumContentCipher device; final context = utf8.encode('card_revision/0192/payload'); final plain = Uint8List.fromList(utf8.encode('{"front":"火"}')); setUpAll(() async { alice = await newAccountKeys(); bob = await newAccountKeys(); }); setUp(() async { device = SodiumContentCipher(); await device.unlock(alice); }); tearDown(() => device.dispose()); test("a key made on one device opens on another of the account's, and " 'what it seals opens there', () async { final sealedKey = device.newKey('studyset-1'); final sealed = device.seal(plain, keyId: 'studyset-1', context: context); final other = SodiumContentCipher(); await other.unlock(alice); addTearDown(other.dispose); expect(other.has('studyset-1'), isFalse); expect(other.openKey('studyset-1', sealedKey), isTrue); expect(other.has('studyset-1'), isTrue); expect(other.unseal(sealed, context: context), plain); expect(sealed.keyId, 'studyset-1'); expect(String.fromCharCodes(sealed.payload), isNot(contains('front'))); }); test("another account's device opens neither the key nor what it " 'sealed', () async { final sealedKey = device.newKey('studyset-1'); final sealed = device.seal(plain, keyId: 'studyset-1', context: context); final bobs = SodiumContentCipher(); await bobs.unlock(bob); addTearDown(bobs.dispose); expect(bobs.openKey('studyset-1', sealedKey), isFalse); expect(bobs.unseal(sealed, context: context), isNull); }); test('a key opens only under the ID it was sealed with, and only once ' 'the account is unlocked', () async { final sealedKey = device.newKey('studyset-1'); final other = SodiumContentCipher(); addTearDown(other.dispose); expect(() => other.openKey('studyset-1', sealedKey), throwsStateError); await other.unlock(alice); expect(other.openKey('studyset-2', sealedKey), isFalse); expect(other.openKey('studyset-1', sealedKey), isTrue); }); test('a payload moved to another row, or changed, opens no more', () { device.newKey('studyset-1'); final sealed = device.seal(plain, keyId: 'studyset-1', context: context); final changed = Uint8List.fromList(sealed.payload)..last ^= 1; expect( device.unseal(sealed, context: utf8.encode('card_revision/0193/payload')), isNull, ); expect( device.unseal( SealedPayload( keyId: sealed.keyId, nonce: sealed.nonce, payload: changed, ), context: context, ), isNull, ); expect(device.unseal(sealed, context: context), plain); }); test( "a row's keyed ID (ADR 0030, D57) is the same on every device of the " "account, another on another account's, and none before unlocking", () async { final other = SodiumContentCipher(); await other.unlock(alice); addTearDown(other.dispose); final bobs = SodiumContentCipher(); await bobs.unlock(bob); addTearDown(bobs.dispose); final locked = SodiumContentCipher(); final id = device.rowId('train/day/2026-10-30'); expect(other.rowId('train/day/2026-10-30'), id); expect(bobs.rowId('train/day/2026-10-30'), isNot(id)); expect(device.rowId('train/day/2026-10-31'), isNot(id)); expect(() => locked.rowId('train/day/2026-10-30'), throwsStateError); }, ); test('without its key nothing opens, nor is sealed', () { final sealed = SealedPayload( keyId: 'unknown', nonce: Uint8List(24), payload: Uint8List(100), ); expect(device.unseal(sealed, context: context), isNull); expect( () => device.seal(plain, keyId: 'unknown', context: context), throwsStateError, ); }); test("a shared key's content opens when a member it trusts signed it, " "and only for that key (Halkyon's ADR 0044, D20)", () async { final carol = await newAccountKeys(); PublicKeys keysOf(UnlockedAccountKeys account) => ( encryption: account.encryptionPublicKey, signing: account.signingPublicKey, ); Future deviceOf(UnlockedAccountKeys account) async { final cipher = SodiumContentCipher(); await cipher.unlock(account); addTearDown(cipher.dispose); return cipher; } final bobs = await deviceOf(bob); final carols = await deviceOf(carol); for (final keyId in ['shared', 'other']) { device.newKey(keyId); for (final (cipher, keys) in [(bobs, bob), (carols, carol)]) { expect( cipher.openSharedKey( keyId, device.sealKeyTo(keyId, keysOf(keys)), sealedBy: alice.signingPublicKey, ), isTrue, ); } } final byBob = bobs.seal(plain, keyId: 'shared', context: context); final byCarol = carols.seal(plain, keyId: 'shared', context: context); final byAlice = device.seal(plain, keyId: 'shared', context: context); // Only the account's own, until told whom to trust. expect(device.unseal(byBob, context: context), isNull); expect(device.unseal(byAlice, context: context), plain); device.trustSigners('shared', [ alice.signingPublicKey, bob.signingPublicKey, ]); expect(device.unseal(byBob, context: context), plain); expect(device.unseal(byAlice, context: context), plain); expect(device.unseal(byCarol, context: context), isNull); expect( device.unseal( bobs.seal(plain, keyId: 'other', context: context), context: context, ), isNull, ); // What it trusts is replaced, not added to. device.trustSigners('shared', [carol.signingPublicKey]); expect(device.unseal(byCarol, context: context), plain); expect(device.unseal(byBob, context: context), isNull); expect(device.unseal(byAlice, context: context), isNull); }); }