Every account has a way in, so the apps make no anonymous account any more. The start screen shows whenever nobody is signed in, a first start included, and offers SimpleLogin, a password and GitHub for those who have it, with no start without an account. An instance out of reach is said so, with Try again, and one offering no way to create an account too. An anonymous account's session left from before is deleted as the app starts (D25), falling back to signing out on the device. What only anonymous accounts needed goes: signing in anonymously, the signed-out mark, adding GitHub to an account and switching to a GitHub account from the Profile tab (with its Cancel, the only one), an anonymous device's own keys, the warning that signing out deletes the account, and the texts about keeping progress. Settings says on which instance the account is signed in. Learn's publishing, reports and suggestions and its learner count no longer ask whether the person has an identity: everyone signed in has one. The server still offers anonymous sign-in until the next commit, so older apps keep working until v0.20.0 raises the minimum build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
108 lines
3.4 KiB
Dart
108 lines
3.4 KiB
Dart
import 'dart:convert';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:halkyon_app/halkyon_app.dart';
|
|
|
|
/// Content sealed with a data key (ADR 0019), with libsodium as its build
|
|
/// hook compiles it.
|
|
void main() {
|
|
final crypto = SodiumAccountCrypto();
|
|
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 crypto.newAccount('alice password')).keys;
|
|
bob = (await crypto.newAccount('bob password')).keys;
|
|
});
|
|
|
|
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('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,
|
|
);
|
|
});
|
|
}
|