feat(app): sign in with a password and TOTP on the device
ADR 0017, step 3: halkyon_app's side, which Learn and Todo take. - The account's cryptography on the device (`SodiumAccountCrypto`): halkyon_keys and SRP's client half, each step on an isolate of its own. sodium hands out results as views of native memory its finalizers free once the isolate that made them ends, so everything crossing back is copied first: a view read freed memory and crashed the process. - `PasswordGateway` on the module's endpoints (steps without a session through a client holding none; an authenticator set up with a session granted but not yet the device's), and `PasswordRepository` running the flows: signing in (M2 checked, the keys opened before anything is trusted), signing up, a password for someone signed in, an account never finished, the authenticators, recovery codes, changing the password, a new recovery phrase, and getting back in. - The unlocked keys in the system's keyring per instance and person (`SecureAccountKeyStore`), gone once another person signs in or nobody, and with the instance's removal. - The screens: the start screen's Sign in and Create an account (and when GitHub sign-in ends), the sign-in sheet (codes, recovery codes, an account never finished, a lost authenticator), the account's setup (the recovery phrase shown once, three random words typed back before anything reaches the instance; the authenticator's QR code, drawn with pub.dev's qr; the recovery codes), getting back in, and the account section (`AccountSecuritySection`). - `FileSaver` moves from Learn into halkyon_app, for the phrase and the codes in Todo too; `AppDependencies` names it and `passwords`. - halkyon_keys rebuilds account keys from the secret keys a device kept; the key proof's message is in halkyon_base_client, checked against the register's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
1daa1b649a
commit
a4819c3d39
2 changed files with 9456 additions and 35 deletions
|
|
@ -14,7 +14,6 @@ import 'package:halkyon_learn_flutter/src/core/dependencies/person_dependencies.
|
|||
import 'package:halkyon_learn_flutter/src/core/device_settings/data/device_settings_store.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/device_settings/model/device_settings.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/files/file_picker.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/files/file_saver.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/version/app_version.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/video/media_players.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/video/youtube_videos.dart';
|
||||
|
|
@ -107,6 +106,10 @@ class Dependencies({
|
|||
/// Whether the person here signed out, so the app asks how to start.
|
||||
@override required final SignedOutMark signedOut,
|
||||
|
||||
/// Signing in with a password and TOTP, and what keeps an account (ADR
|
||||
/// 0017).
|
||||
@override required final PasswordRepository passwords,
|
||||
|
||||
/// Where the source of what the instance runs is, as it says
|
||||
/// ([sourceCodeUrl]).
|
||||
required final Future<String> Function() _sourceCodeUrl,
|
||||
|
|
@ -134,7 +137,7 @@ class Dependencies({
|
|||
required final FilePicker files,
|
||||
|
||||
/// Lets the person keep files the app made, such as exports.
|
||||
required final FileSaver fileSaver,
|
||||
@override required final FileSaver fileSaver,
|
||||
|
||||
/// Plays the YouTube videos in content.
|
||||
required final YoutubeVideos videos,
|
||||
|
|
@ -229,6 +232,14 @@ class Dependencies({
|
|||
callbackScheme: 'app.halkyon.learn',
|
||||
appName: 'Halkyon Learn',
|
||||
);
|
||||
final addresses = FileSignInAddresses(device.storage.peopleOf(instance));
|
||||
final passwords = PasswordRepository(
|
||||
ServerpodPasswordGateway(client, base: client.modules.base),
|
||||
SodiumAccountCrypto(),
|
||||
SecureAccountKeyStore(device.storage.keysOf(instance)),
|
||||
authGateway,
|
||||
addresses,
|
||||
);
|
||||
final folders = PersonFolders(device.storage.peopleOf(instance));
|
||||
// One person at a time: before the next person's data opens, the previous
|
||||
// person's database has closed, so their folder can go. If it doesn't
|
||||
|
|
@ -242,11 +253,9 @@ class Dependencies({
|
|||
);
|
||||
return Dependencies(
|
||||
serverName: instance.name,
|
||||
auth: AuthRepository(
|
||||
authGateway,
|
||||
FileSignInAddresses(device.storage.peopleOf(instance)),
|
||||
),
|
||||
auth: AuthRepository(authGateway, addresses),
|
||||
signedOut: FileSignedOutMark(device.storage.peopleOf(instance)),
|
||||
passwords: passwords,
|
||||
sourceCodeUrl: () async => (await client.instance.info()).sourceCodeUrl,
|
||||
dictionaries: DictionaryRepository(
|
||||
ServerpodDictionaryGateway(client),
|
||||
|
|
@ -300,6 +309,7 @@ class Dependencies({
|
|||
// The device's parts stay, for the next instance.
|
||||
onDispose: () async {
|
||||
await deviceDictionaries.dispose();
|
||||
await passwords.dispose();
|
||||
await authGateway.dispose();
|
||||
client.close();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:file_picker/file_picker.dart' as file_picker;
|
||||
import 'package:file_selector/file_selector.dart';
|
||||
|
||||
/// Lets the person keep a file the app made, where they choose.
|
||||
abstract interface class FileSaver() {
|
||||
/// Asks where to keep [bytes], a file of the media type [mimeType] named
|
||||
/// [name] for a start; completes with whether it was kept, false when the
|
||||
/// person cancelled.
|
||||
Future<bool> save({
|
||||
required String name,
|
||||
required Uint8List bytes,
|
||||
required String mimeType,
|
||||
});
|
||||
}
|
||||
|
||||
/// [FileSaver] with the platform's own dialog: `file_selector`'s on the
|
||||
/// desktop; on Android and iOS, where that has none, the system's through
|
||||
/// `file_picker` (Android's Storage Access Framework, iOS's document picker).
|
||||
class PlatformFileSaver() implements FileSaver {
|
||||
@override
|
||||
Future<bool> save({
|
||||
required String name,
|
||||
required Uint8List bytes,
|
||||
required String mimeType,
|
||||
}) async {
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
return await file_picker.FilePicker.saveFile(
|
||||
fileName: name,
|
||||
bytes: bytes,
|
||||
mimeType: mimeType,
|
||||
) !=
|
||||
null;
|
||||
}
|
||||
final location = await getSaveLocation(suggestedName: name);
|
||||
if (location == null) {
|
||||
return false;
|
||||
}
|
||||
await File(location.path).writeAsBytes(bytes);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue