Learn's PayloadEnvelope moves unchanged into halkyon_app, so that Train seals its rows with the same envelope; Learn imports it from there. It gets tests of its own: a payload sealed for its table, row, column and item opens there alone, a changed one or one under a key not open never does, and a readable one is trusted only signed by the account where it was signed for. Learn's app tests haven't run with the move (its build needs clang, missing on this machine); analysis passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
218 lines
5.7 KiB
Dart
218 lines
5.7 KiB
Dart
import 'dart:convert';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:halkyon_app/halkyon_app.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() {
|
|
final crypto = SodiumAccountCrypto();
|
|
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 crypto.newAccount('alice password')).keys;
|
|
bob = (await crypto.newAccount('bob password')).keys;
|
|
});
|
|
|
|
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));
|
|
});
|
|
}
|