import 'dart:convert'; import 'dart:typed_data'; import 'package:halkyon_content/halkyon_content.dart'; import 'package:test/test.dart'; /// A synced row's envelope (ADR 0019): sealed for its table, row, column and /// the row it belongs to, readable payloads signed, and nothing an instance /// moves, changes or makes up read as the person's. void main() { late UnlockedAccountKeys alice; late UnlockedAccountKeys bob; late SodiumContentCipher device; late PayloadEnvelope envelope; final plain = ByteData.sublistView( Uint8List.fromList(utf8.encode('{"schemaVersion":1,"name":"Run"}')), ); Opened openAt( Envelope sealed, { String table = 'workout', String id = 'w1', String column = 'payload', String bound = '', }) => envelope.open( sealed, table: table, id: id, column: column, bound: bound, ); setUpAll(() async { alice = await newAccountKeys(); bob = await newAccountKeys(); }); setUp(() async { device = SodiumContentCipher(); await device.unlock(alice); device.newKey('person'); envelope = PayloadEnvelope(device, canSign: () => true); }); tearDown(() => device.dispose()); test('a sealed payload opens where it was sealed for', () { final sealed = envelope.seal( plain, keyId: 'person', table: 'workout', id: 'w1', ); expect(sealed.keyId, 'person'); expect(sealed.nonce, isNotNull); expect( Uint8List.sublistView(sealed.payload), isNot(containsAllInOrder(Uint8List.sublistView(plain))), ); expect(openAt(sealed), Readable(plain)); }); test('a sealed payload moved to another table, row, column or item ' "doesn't open", () { final sealed = envelope.seal( plain, keyId: 'person', table: 'workout', id: 'w1', bound: 'workout:w0', ); expect(openAt(sealed, bound: 'workout:w0'), Readable(plain)); expect( openAt(sealed, table: 'workout_series', bound: 'workout:w0'), const Locked(), ); expect(openAt(sealed, id: 'w2', bound: 'workout:w0'), const Locked()); expect(openAt(sealed, column: 'name', bound: 'workout:w0'), const Locked()); expect(openAt(sealed, bound: 'workout:w9'), const Locked()); }); test("a sealed payload changed, or without its nonce, doesn't open", () { final sealed = envelope.seal( plain, keyId: 'person', table: 'workout', id: 'w1', ); final changed = Uint8List.fromList(Uint8List.sublistView(sealed.payload)); changed[changed.length - 1] ^= 1; expect( openAt(( keyId: 'person', nonce: sealed.nonce, payload: ByteData.sublistView(changed), )), const Locked(), ); expect( openAt((keyId: 'person', nonce: null, payload: sealed.payload)), const Locked(), ); }); test("a payload under a key not open here doesn't open, and none is " 'sealed with it', () async { final sealed = envelope.seal( plain, keyId: 'person', table: 'workout', id: 'w1', ); final other = SodiumContentCipher(); await other.unlock(alice); addTearDown(other.dispose); final elsewhere = PayloadEnvelope(other); expect( elsewhere.open(sealed, table: 'workout', id: 'w1'), const Locked(), ); expect( () => elsewhere.seal(plain, keyId: 'person', table: 'workout', id: 'w1'), throwsStateError, ); }); test( "another account's device can't open a key sealed to this one", () async { final sealedKey = device.newKey('person-2'); final sealed = envelope.seal( plain, keyId: 'person-2', table: 'workout', id: 'w1', ); final bobs = SodiumContentCipher(); await bobs.unlock(bob); addTearDown(bobs.dispose); expect(bobs.openKey('person-2', sealedKey), isFalse); expect( PayloadEnvelope(bobs).open(sealed, table: 'workout', id: 'w1'), const Locked(), ); }, ); test('a readable payload is signed where the device can sign, and trusted ' 'only where it stays', () { final readable = envelope.seal( plain, keyId: null, table: 'published_workout', id: 'p1', ); expect(readable.nonce, isNotNull); expect( openAt(readable, table: 'published_workout', id: 'p1'), Readable(plain), ); expect( openAt(readable, table: 'published_workout', id: 'p2'), Readable(plain, trusted: false), ); }); test('a readable payload nobody signed, or signed by another account, ' 'is not trusted', () async { final unsigned = PayloadEnvelope(device).seal( plain, keyId: null, table: 'published_workout', id: 'p1', ); expect(unsigned.nonce, isNull); expect( openAt(unsigned, table: 'published_workout', id: 'p1'), Readable(plain, trusted: false), ); final bobs = SodiumContentCipher(); await bobs.unlock(bob); addTearDown(bobs.dispose); final bobsSigned = PayloadEnvelope(bobs, canSign: () => true).seal( plain, keyId: null, table: 'published_workout', id: 'p1', ); expect( openAt(bobsSigned, table: 'published_workout', id: 'p1'), Readable(plain, trusted: false), ); }); test('a blank payload, a row being deleted, stays blank and reads as ' 'blank', () { final blank = ByteData(0); final sealed = envelope.seal( blank, keyId: 'person', table: 'workout', id: 'w1', ); expect(sealed.payload.lengthInBytes, 0); expect(sealed.nonce, isNull); expect(openAt(sealed), Readable(blank)); }); }