fix(auth): an account with a password deletes itself only with it and a code
A session alone deleted any account, with everything encrypted under its keys (the second reading's lows; decided by the developer: the password and a code, as a change of password takes). account.delete now takes a DeletionProof (the password's SRP proof for a beginChange challenge, a TOTP or recovery code, the host the device addressed); the register checks it (requireOwnerToDelete), a member passing it on bound to its own host. Accounts without a password, anonymous ones among them, delete with their session as before. In the app, deleting an account with a password asks for it and a code in a sheet of its own; a wrong one deletes nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
c4f863ff8b
commit
d53844bc93
30 changed files with 873 additions and 32 deletions
|
|
@ -209,5 +209,6 @@
|
||||||
"passwordFailed": "Die Instanz war nicht erreichbar. Prüf die Verbindung und versuch es noch einmal.",
|
"passwordFailed": "Die Instanz war nicht erreichbar. Prüf die Verbindung und versuch es noch einmal.",
|
||||||
"passwordMinLength": "Mindestens {count} Zeichen",
|
"passwordMinLength": "Mindestens {count} Zeichen",
|
||||||
"passwordTooShort": "Ein neues Passwort braucht mindestens {count} Zeichen: Sollte die Datenbank der Instanz je nach außen gelangen, wird ein kurzes zuerst erraten.",
|
"passwordTooShort": "Ein neues Passwort braucht mindestens {count} Zeichen: Sollte die Datenbank der Instanz je nach außen gelangen, wird ein kurzes zuerst erraten.",
|
||||||
"passwordCodesWait": "Zu viele falsche Codes hintereinander: Bis {date} wird kein Code angenommen. Waren es nicht deine, kennt jemand dein Passwort: Ändere es dann."
|
"passwordCodesWait": "Zu viele falsche Codes hintereinander: Bis {date} wird kein Code angenommen. Waren es nicht deine, kennt jemand dein Passwort: Ändere es dann.",
|
||||||
|
"passwordDeleteIntro": "Dein Konto wird mit allem darin gelöscht, in jedem Dienst dieser Instanz. Gib zur Bestätigung dein Passwort und einen Code deiner Authenticator-App ein."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -456,5 +456,6 @@
|
||||||
"type": "String"
|
"type": "String"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"passwordDeleteIntro": "Your account goes with everything in it, in every service of this instance. To confirm, type your password and a code from your authenticator app."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1362,6 +1362,12 @@ abstract class HalkyonAppLocalizations {
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Too many wrong codes in a row: no code is taken until {date}. If they weren\'t yours, someone knows your password: change it then.'**
|
/// **'Too many wrong codes in a row: no code is taken until {date}. If they weren\'t yours, someone knows your password: change it then.'**
|
||||||
String passwordCodesWait(String date);
|
String passwordCodesWait(String date);
|
||||||
|
|
||||||
|
/// No description provided for @passwordDeleteIntro.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Your account goes with everything in it, in every service of this instance. To confirm, type your password and a code from your authenticator app.'**
|
||||||
|
String get passwordDeleteIntro;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HalkyonAppLocalizationsDelegate
|
class _HalkyonAppLocalizationsDelegate
|
||||||
|
|
|
||||||
|
|
@ -788,4 +788,8 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
||||||
String passwordCodesWait(String date) {
|
String passwordCodesWait(String date) {
|
||||||
return 'Zu viele falsche Codes hintereinander: Bis $date wird kein Code angenommen. Waren es nicht deine, kennt jemand dein Passwort: Ändere es dann.';
|
return 'Zu viele falsche Codes hintereinander: Bis $date wird kein Code angenommen. Waren es nicht deine, kennt jemand dein Passwort: Ändere es dann.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get passwordDeleteIntro =>
|
||||||
|
'Dein Konto wird mit allem darin gelöscht, in jedem Dienst dieser Instanz. Gib zur Bestätigung dein Passwort und einen Code deiner Authenticator-App ein.';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -771,6 +771,10 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
||||||
String passwordCodesWait(String date) {
|
String passwordCodesWait(String date) {
|
||||||
return 'Too many wrong codes in a row: no code is taken until $date. If they weren\'t yours, someone knows your password: change it then.';
|
return 'Too many wrong codes in a row: no code is taken until $date. If they weren\'t yours, someone knows your password: change it then.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get passwordDeleteIntro =>
|
||||||
|
'Your account goes with everything in it, in every service of this instance. To confirm, type your password and a code from your authenticator app.';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The translations for English, as used in the United Kingdom (`en_GB`).
|
/// The translations for English, as used in the United Kingdom (`en_GB`).
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import 'package:halkyon_app/src/app/app_scope.dart';
|
||||||
import 'package:halkyon_app/src/auth/bloc/session_bloc.dart';
|
import 'package:halkyon_app/src/auth/bloc/session_bloc.dart';
|
||||||
import 'package:halkyon_app/src/instance/instance_scope.dart';
|
import 'package:halkyon_app/src/instance/instance_scope.dart';
|
||||||
import 'package:halkyon_app/src/instance/widget/account_switcher_sheet.dart';
|
import 'package:halkyon_app/src/instance/widget/account_switcher_sheet.dart';
|
||||||
|
import 'package:halkyon_app/src/password/model/password_failed.dart';
|
||||||
|
import 'package:halkyon_app/src/password/widget/delete_account_sheet.dart';
|
||||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
@ -104,17 +106,37 @@ class const AccountTiles({
|
||||||
session.add(const SessionEventSignOutRequested());
|
session.add(const SessionEventSignOutRequested());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deletes the account with everything in it, after asking.
|
/// Deletes the account with everything in it, after asking; an account
|
||||||
|
/// with a password only with it and a code (ADR 0017), which a sheet asks
|
||||||
|
/// for.
|
||||||
Future<void> _delete(BuildContext context) async {
|
Future<void> _delete(BuildContext context) async {
|
||||||
final l10n = HalkyonAppLocalizations.of(context);
|
final l10n = HalkyonAppLocalizations.of(context);
|
||||||
final session = context.read<SessionBloc>();
|
final session = context.read<SessionBloc>();
|
||||||
if (await confirmDestructiveAction(
|
final passwords = AppScope.of(context).passwords;
|
||||||
|
final identified = switch (session.state) {
|
||||||
|
SessionStateActive(:final person) => person.isIdentified,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
if (!await confirmDestructiveAction(
|
||||||
context,
|
context,
|
||||||
title: l10n.accountDeleteTitle,
|
title: l10n.accountDeleteTitle,
|
||||||
message: deleteMessage,
|
message: deleteMessage,
|
||||||
confirmLabel: l10n.accountDelete,
|
confirmLabel: l10n.accountDelete,
|
||||||
)) {
|
)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final bool hasPassword;
|
||||||
|
try {
|
||||||
|
hasPassword = identified && (await passwords.security()).hasPassword;
|
||||||
|
} on PasswordFailed {
|
||||||
|
// Unreachable: deleting would fail too, as the session bloc says.
|
||||||
session.add(const SessionEventDeleteRequested());
|
session.add(const SessionEventDeleteRequested());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!hasPassword) {
|
||||||
|
session.add(const SessionEventDeleteRequested());
|
||||||
|
} else if (context.mounted) {
|
||||||
|
await showDeleteAccountWithPassword(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,16 @@ abstract interface class PasswordGateway() {
|
||||||
/// A change of password's first step.
|
/// A change of password's first step.
|
||||||
Future<protocol.PasswordChallenge> beginChange();
|
Future<protocol.PasswordChallenge> beginChange();
|
||||||
|
|
||||||
|
/// Deletes the signed-in person's account, which has a password: [answer]
|
||||||
|
/// to [challenge] ([beginChange]) proves it, with a [totpCode] or
|
||||||
|
/// [recoveryCode]; this device then forgets its session.
|
||||||
|
Future<void> deleteAccount(
|
||||||
|
protocol.PasswordChallenge challenge,
|
||||||
|
SrpAnswer answer, {
|
||||||
|
String? totpCode,
|
||||||
|
String? recoveryCode,
|
||||||
|
});
|
||||||
|
|
||||||
/// Changes the password: [answer] to [challenge] proves the old one.
|
/// Changes the password: [answer] to [challenge] proves the old one.
|
||||||
Future<void> change(
|
Future<void> change(
|
||||||
protocol.PasswordChallenge challenge,
|
protocol.PasswordChallenge challenge,
|
||||||
|
|
|
||||||
|
|
@ -344,6 +344,29 @@ class PasswordRepository(
|
||||||
_changed();
|
_changed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deletes the signed-in person's account, which has a password, with
|
||||||
|
/// [password] and a [totpCode] or [recoveryCode], as a change of password
|
||||||
|
/// takes (a session alone deletes only an account without one): on the
|
||||||
|
/// instance and every service of it, then signed out here.
|
||||||
|
Future<void> deleteAccount({
|
||||||
|
required String password,
|
||||||
|
String? totpCode,
|
||||||
|
String? recoveryCode,
|
||||||
|
}) async {
|
||||||
|
final challenge = await _gateway.beginChange();
|
||||||
|
final unlock = await _crypto.unlock(
|
||||||
|
password,
|
||||||
|
salt: Uint8List.sublistView(challenge.salt),
|
||||||
|
costs: challenge.costs,
|
||||||
|
);
|
||||||
|
await _gateway.deleteAccount(
|
||||||
|
challenge,
|
||||||
|
await _crypto.answer(unlock, challenge, host: _gateway.host),
|
||||||
|
totpCode: totpCode?.replaceAll(' ', ''),
|
||||||
|
recoveryCode: recoveryCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// A new recovery phrase for the signed-in person's keys, to show; the
|
/// A new recovery phrase for the signed-in person's keys, to show; the
|
||||||
/// instance takes it with [keepPhrase].
|
/// instance takes it with [keepPhrase].
|
||||||
Future<NewPhrase> newPhrase() async {
|
Future<NewPhrase> newPhrase() async {
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,28 @@ class ServerpodPasswordGateway(
|
||||||
Future<protocol.PasswordChallenge> beginChange() =>
|
Future<protocol.PasswordChallenge> beginChange() =>
|
||||||
_failing(_base.password.beginChange);
|
_failing(_base.password.beginChange);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deleteAccount(
|
||||||
|
protocol.PasswordChallenge challenge,
|
||||||
|
SrpAnswer answer, {
|
||||||
|
String? totpCode,
|
||||||
|
String? recoveryCode,
|
||||||
|
}) => _failing(() async {
|
||||||
|
await _base.account.delete(
|
||||||
|
proof: protocol.DeletionProof(
|
||||||
|
challenge: challenge.challenge,
|
||||||
|
clientValue: answer.clientValue,
|
||||||
|
clientProof: answer.clientProof,
|
||||||
|
totpCode: totpCode,
|
||||||
|
recoveryCode: recoveryCode,
|
||||||
|
host: host,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// The server ended every session of the person; forget this device's
|
||||||
|
// copy, which signs out here too.
|
||||||
|
await _client.auth.updateSignedInUser(null);
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> change(
|
Future<void> change(
|
||||||
protocol.PasswordChallenge challenge,
|
protocol.PasswordChallenge challenge,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:halkyon_app/l10n/halkyon_app_localizations.dart';
|
||||||
|
import 'package:halkyon_app/src/app/app_scope.dart';
|
||||||
|
import 'package:halkyon_app/src/password/bloc/account_security_cubit.dart';
|
||||||
|
import 'package:halkyon_app/src/password/widget/password_texts.dart';
|
||||||
|
import 'package:halkyon_app/src/password/widget/password_widgets.dart';
|
||||||
|
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||||
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
|
/// Deletes the signed-in person's account, which has a password: the
|
||||||
|
/// password and a code from an authenticator, as changing the password
|
||||||
|
/// takes (ADR 0017; the second reading). Once deleted, the person is signed
|
||||||
|
/// out here, and the app asks how to start.
|
||||||
|
Future<void> showDeleteAccountWithPassword(BuildContext context) =>
|
||||||
|
showAdaptiveSheet<void>(
|
||||||
|
context,
|
||||||
|
swipeable: false,
|
||||||
|
builder: (context) => const _DeleteAccount(),
|
||||||
|
);
|
||||||
|
|
||||||
|
class const _DeleteAccount() extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
State<_DeleteAccount> createState() => _DeleteAccountState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DeleteAccountState() extends State<_DeleteAccount> {
|
||||||
|
final _password = TextEditingController();
|
||||||
|
final _code = TextEditingController();
|
||||||
|
late final _step = SecurityStepCubit<bool>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_password.dispose();
|
||||||
|
_code.dispose();
|
||||||
|
_step.close();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _submit() async {
|
||||||
|
final passwords = AppScope.of(context).passwords;
|
||||||
|
await _step.run(() async {
|
||||||
|
await passwords.deleteAccount(
|
||||||
|
password: _password.text,
|
||||||
|
totpCode: _code.text,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = HalkyonAppLocalizations.of(context);
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
return BlocBuilder<SecurityStepCubit<bool>, SecurityStepState<bool>>(
|
||||||
|
bloc: _step,
|
||||||
|
builder: (context, state) => SheetScaffold(
|
||||||
|
title: l10n.accountDeleteTitle,
|
||||||
|
busy: state.busy,
|
||||||
|
body: AutofillGroup(
|
||||||
|
child: state.busy
|
||||||
|
? const SheetWorking()
|
||||||
|
: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
SheetIntro(l10n.passwordDeleteIntro),
|
||||||
|
PasswordField(
|
||||||
|
controller: _password,
|
||||||
|
label: l10n.passwordCurrent,
|
||||||
|
autofocus: true,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
CodeField(
|
||||||
|
controller: _code,
|
||||||
|
kind: CodeKind.totp,
|
||||||
|
errorText: switch (state.problem) {
|
||||||
|
final failed? => passwordProblemText(context, failed),
|
||||||
|
null => null,
|
||||||
|
},
|
||||||
|
onSubmitted: (_) => _submit(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
footer: FilledButton(
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
backgroundColor: colors.error,
|
||||||
|
foregroundColor: colors.onError,
|
||||||
|
),
|
||||||
|
onPressed: state.busy ? null : _submit,
|
||||||
|
child: Text(l10n.accountDelete),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -512,6 +512,21 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
||||||
return _challengeFor(account.address);
|
return _challengeFor(account.address);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deleteAccount(
|
||||||
|
protocol.PasswordChallenge challenge,
|
||||||
|
SrpAnswer answer, {
|
||||||
|
String? totpCode,
|
||||||
|
String? recoveryCode,
|
||||||
|
}) async {
|
||||||
|
final account = await _call(
|
||||||
|
'deleteAccount',
|
||||||
|
() => _ownerProved(challenge, answer, totpCode, recoveryCode),
|
||||||
|
);
|
||||||
|
accounts.remove(account.address);
|
||||||
|
await _auth.deleteAccount();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> change(
|
Future<void> change(
|
||||||
protocol.PasswordChallenge challenge,
|
protocol.PasswordChallenge challenge,
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,54 @@ void main() {
|
||||||
expect(find.text('Signed out'), findsOneWidget);
|
expect(find.text('Signed out'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('deleting an account with a password asks for it and a '
|
||||||
|
'code, which a wrong one fails, deleting nothing', (tester) async {
|
||||||
|
// The first person the fake password gateway makes.
|
||||||
|
const person = '00000000-0000-4000-8000-000000000001';
|
||||||
|
gateway = FakeAuthGateway(
|
||||||
|
status: const SignedIn(authUserId: person, isIdentified: true),
|
||||||
|
);
|
||||||
|
await start(tester);
|
||||||
|
dependencies.passwordGateway.addAccount(
|
||||||
|
'anna@example.org',
|
||||||
|
password: 'correct horse',
|
||||||
|
crypto: dependencies.crypto,
|
||||||
|
);
|
||||||
|
Finder field(String label) => find.widgetWithText(TextField, label);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Delete account'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('Delete account').last);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(gateway.accountDeletions, 0);
|
||||||
|
expect(field('Current password'), findsOneWidget);
|
||||||
|
await tester.enterText(field('Current password'), 'wrong horse');
|
||||||
|
await tester.enterText(
|
||||||
|
field('Code from your authenticator app'),
|
||||||
|
fakeTotpCode,
|
||||||
|
);
|
||||||
|
await tester.tap(find.text('Delete account').last);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(
|
||||||
|
find.text("The address, the password or the code isn't right."),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
expect(gateway.accountDeletions, 0);
|
||||||
|
expect(dependencies.passwordGateway.accounts, contains('anna@example.org'));
|
||||||
|
|
||||||
|
await tester.enterText(field('Current password'), 'correct horse');
|
||||||
|
await tester.tap(find.text('Delete account').last);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(gateway.accountDeletions, 1);
|
||||||
|
expect(
|
||||||
|
dependencies.passwordGateway.accounts,
|
||||||
|
isNot(contains('anna@example.org')),
|
||||||
|
);
|
||||||
|
expect(find.text('Signed out'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('speaks German', (tester) async {
|
testWidgets('speaks German', (tester) async {
|
||||||
await start(tester);
|
await start(tester);
|
||||||
final german = lookupHalkyonAppLocalizations(const Locale('de'));
|
final german = lookupHalkyonAppLocalizations(const Locale('de'));
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,28 @@ void main() {
|
||||||
expect(anna.password, 'correct horse');
|
expect(anna.password, 'correct horse');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('deletes the account with the password and a code alone, then '
|
||||||
|
'signed out here; a wrong one or none deletes nothing', () async {
|
||||||
|
await expectLater(
|
||||||
|
passwords.deleteAccount(password: 'wrong', totpCode: fakeTotpCode),
|
||||||
|
failsWith(PasswordProblem.wrongCredentials),
|
||||||
|
);
|
||||||
|
await expectLater(
|
||||||
|
passwords.deleteAccount(password: 'correct horse'),
|
||||||
|
failsWith(PasswordProblem.wrongCredentials),
|
||||||
|
);
|
||||||
|
expect(gateway.accounts, contains('anna@example.org'));
|
||||||
|
expect(auth.status, isA<SignedIn>());
|
||||||
|
|
||||||
|
await passwords.deleteAccount(
|
||||||
|
password: 'correct horse',
|
||||||
|
totpCode: fakeTotpCode,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(gateway.accounts, isNot(contains('anna@example.org')));
|
||||||
|
expect(auth.status, const SignedOut());
|
||||||
|
});
|
||||||
|
|
||||||
test('makes a new recovery phrase, which the instance takes for the '
|
test('makes a new recovery phrase, which the instance takes for the '
|
||||||
'password and a code alone', () async {
|
'password and a code alone', () async {
|
||||||
final phrase = await passwords.newPhrase();
|
final phrase = await passwords.newPhrase();
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ import 'package:halkyon_base_client/src/protocol/password/authenticator.dart'
|
||||||
as _ivfmflch;
|
as _ivfmflch;
|
||||||
import 'package:halkyon_base_client/src/protocol/password/authenticator_enrolment.dart'
|
import 'package:halkyon_base_client/src/protocol/password/authenticator_enrolment.dart'
|
||||||
as _it1lo7kq;
|
as _it1lo7kq;
|
||||||
|
import 'package:halkyon_base_client/src/protocol/password/deletion_proof.dart'
|
||||||
|
as _ifxctuoq;
|
||||||
import 'package:halkyon_base_client/src/protocol/password/key_proof_challenge.dart'
|
import 'package:halkyon_base_client/src/protocol/password/key_proof_challenge.dart'
|
||||||
as _ir3ecgdn;
|
as _ir3ecgdn;
|
||||||
import 'package:halkyon_base_client/src/protocol/password/new_password.dart'
|
import 'package:halkyon_base_client/src/protocol/password/new_password.dart'
|
||||||
|
|
@ -57,12 +59,16 @@ class EndpointAccount extends _isc.EndpointRef {
|
||||||
/// 2026-09-30). Their other devices learn at once that their sessions
|
/// 2026-09-30). Their other devices learn at once that their sessions
|
||||||
/// ended. An account deletes everywhere (ADR 0016): on a member, the
|
/// ended. An account deletes everywhere (ADR 0016): on a member, the
|
||||||
/// register deletes it and tells every member, this one deleting its own
|
/// register deletes it and tells every member, this one deleting its own
|
||||||
/// data at once; an anonymous person is this service's alone.
|
/// data at once; an anonymous person is this service's alone. An account
|
||||||
_ida.Future<void> delete() => caller.callServerEndpoint<void>(
|
/// with a password deletes itself only with [proof] (the password's proof
|
||||||
'halkyon_base.account',
|
/// and a code, `requireOwnerToDelete`), which the register checks, its
|
||||||
'delete',
|
/// host the one the device addressed here.
|
||||||
{},
|
_ida.Future<void> delete({_ifxctuoq.DeletionProof? proof}) =>
|
||||||
);
|
caller.callServerEndpoint<void>(
|
||||||
|
'halkyon_base.account',
|
||||||
|
'delete',
|
||||||
|
{'proof': proof},
|
||||||
|
);
|
||||||
|
|
||||||
/// How the caller signs in besides their sessions: with GitHub, with an
|
/// How the caller signs in besides their sessions: with GitHub, with an
|
||||||
/// email address, both, or neither (anonymous).
|
/// email address, both, or neither (anonymous).
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||||
|
/* To generate run: "serverpod generate" */
|
||||||
|
|
||||||
|
// ignore_for_file: implementation_imports
|
||||||
|
// ignore_for_file: library_private_types_in_public_api
|
||||||
|
// ignore_for_file: non_constant_identifier_names
|
||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
// ignore_for_file: type_literal_in_constant_pattern
|
||||||
|
// ignore_for_file: use_super_parameters
|
||||||
|
// ignore_for_file: invalid_use_of_internal_member
|
||||||
|
|
||||||
|
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||||
|
import 'dart:typed_data' as _idt;
|
||||||
|
import 'package:serverpod_client/serverpod_client.dart' as _isc;
|
||||||
|
|
||||||
|
/// What deleting an account that has a password takes (ADR 0017; the second
|
||||||
|
/// reading): the password's SRP proof for a challenge `password.beginChange`
|
||||||
|
/// gave, a TOTP or recovery code, and the host the device addressed, as a
|
||||||
|
/// change of password does. A session alone deletes only an account without
|
||||||
|
/// one.
|
||||||
|
abstract class DeletionProof
|
||||||
|
implements _isc.SerializableModel, _isc.ProtocolSerialization {
|
||||||
|
DeletionProof._({
|
||||||
|
required this.challenge,
|
||||||
|
required this.clientValue,
|
||||||
|
required this.clientProof,
|
||||||
|
this.totpCode,
|
||||||
|
this.recoveryCode,
|
||||||
|
required this.host,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory DeletionProof({
|
||||||
|
required _isc.UuidValue challenge,
|
||||||
|
required _idt.ByteData clientValue,
|
||||||
|
required _idt.ByteData clientProof,
|
||||||
|
String? totpCode,
|
||||||
|
String? recoveryCode,
|
||||||
|
required String host,
|
||||||
|
}) = _DeletionProofImpl;
|
||||||
|
|
||||||
|
factory DeletionProof.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||||
|
return DeletionProof(
|
||||||
|
challenge: _isc.UuidValueJsonExtension.fromJson(
|
||||||
|
jsonSerialization['challenge'],
|
||||||
|
),
|
||||||
|
clientValue: _isc.ByteDataJsonExtension.fromJson(
|
||||||
|
jsonSerialization['clientValue'],
|
||||||
|
),
|
||||||
|
clientProof: _isc.ByteDataJsonExtension.fromJson(
|
||||||
|
jsonSerialization['clientProof'],
|
||||||
|
),
|
||||||
|
totpCode: jsonSerialization['totpCode'] as String?,
|
||||||
|
recoveryCode: jsonSerialization['recoveryCode'] as String?,
|
||||||
|
host: jsonSerialization['host'] as String,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_isc.UuidValue challenge;
|
||||||
|
|
||||||
|
_idt.ByteData clientValue;
|
||||||
|
|
||||||
|
_idt.ByteData clientProof;
|
||||||
|
|
||||||
|
String? totpCode;
|
||||||
|
|
||||||
|
String? recoveryCode;
|
||||||
|
|
||||||
|
/// The host the device addressed the instance by: the server it reached
|
||||||
|
/// checks it is its own before the register binds the proof to it.
|
||||||
|
String host;
|
||||||
|
|
||||||
|
/// Returns a shallow copy of this [DeletionProof]
|
||||||
|
/// with some or all fields replaced by the given arguments.
|
||||||
|
@_isc.useResult
|
||||||
|
DeletionProof copyWith({
|
||||||
|
_isc.UuidValue? challenge,
|
||||||
|
_idt.ByteData? clientValue,
|
||||||
|
_idt.ByteData? clientProof,
|
||||||
|
String? totpCode,
|
||||||
|
String? recoveryCode,
|
||||||
|
String? host,
|
||||||
|
});
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'__className__': 'halkyon_base.DeletionProof',
|
||||||
|
'challenge': challenge.toJson(),
|
||||||
|
'clientValue': clientValue.toJson(),
|
||||||
|
'clientProof': clientProof.toJson(),
|
||||||
|
if (totpCode != null) 'totpCode': totpCode,
|
||||||
|
if (recoveryCode != null) 'recoveryCode': recoveryCode,
|
||||||
|
'host': host,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJsonForProtocol() {
|
||||||
|
return {
|
||||||
|
'__className__': 'halkyon_base.DeletionProof',
|
||||||
|
'challenge': challenge.toJson(),
|
||||||
|
'clientValue': clientValue.toJson(),
|
||||||
|
'clientProof': clientProof.toJson(),
|
||||||
|
if (totpCode != null) 'totpCode': totpCode,
|
||||||
|
if (recoveryCode != null) 'recoveryCode': recoveryCode,
|
||||||
|
'host': host,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return _isc.SerializationManager.encode(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Undefined {}
|
||||||
|
|
||||||
|
class _DeletionProofImpl extends DeletionProof {
|
||||||
|
_DeletionProofImpl({
|
||||||
|
required _isc.UuidValue challenge,
|
||||||
|
required _idt.ByteData clientValue,
|
||||||
|
required _idt.ByteData clientProof,
|
||||||
|
String? totpCode,
|
||||||
|
String? recoveryCode,
|
||||||
|
required String host,
|
||||||
|
}) : super._(
|
||||||
|
challenge: challenge,
|
||||||
|
clientValue: clientValue,
|
||||||
|
clientProof: clientProof,
|
||||||
|
totpCode: totpCode,
|
||||||
|
recoveryCode: recoveryCode,
|
||||||
|
host: host,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Returns a shallow copy of this [DeletionProof]
|
||||||
|
/// with some or all fields replaced by the given arguments.
|
||||||
|
@_isc.useResult
|
||||||
|
@override
|
||||||
|
DeletionProof copyWith({
|
||||||
|
_isc.UuidValue? challenge,
|
||||||
|
_idt.ByteData? clientValue,
|
||||||
|
_idt.ByteData? clientProof,
|
||||||
|
Object? totpCode = _Undefined,
|
||||||
|
Object? recoveryCode = _Undefined,
|
||||||
|
String? host,
|
||||||
|
}) {
|
||||||
|
return DeletionProof(
|
||||||
|
challenge: challenge ?? this.challenge,
|
||||||
|
clientValue: clientValue ?? this.clientValue.clone(),
|
||||||
|
clientProof: clientProof ?? this.clientProof.clone(),
|
||||||
|
totpCode: totpCode is String? ? totpCode : this.totpCode,
|
||||||
|
recoveryCode: recoveryCode is String? ? recoveryCode : this.recoveryCode,
|
||||||
|
host: host ?? this.host,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,7 @@ import 'instance/instance_basics.dart' as _i8jixqx6;
|
||||||
import 'password/account_reset_kind.dart' as _i91ka1it;
|
import 'password/account_reset_kind.dart' as _i91ka1it;
|
||||||
import 'password/authenticator.dart' as _icpzuvvi;
|
import 'password/authenticator.dart' as _icpzuvvi;
|
||||||
import 'password/authenticator_enrolment.dart' as _iv7tra7v;
|
import 'password/authenticator_enrolment.dart' as _iv7tra7v;
|
||||||
|
import 'password/deletion_proof.dart' as _ix1u7hdq;
|
||||||
import 'password/key_proof_challenge.dart' as _iehtx95q;
|
import 'password/key_proof_challenge.dart' as _iehtx95q;
|
||||||
import 'password/new_password.dart' as _itvvd6m6;
|
import 'password/new_password.dart' as _itvvd6m6;
|
||||||
import 'password/password_challenge.dart' as _iuxisac7;
|
import 'password/password_challenge.dart' as _iuxisac7;
|
||||||
|
|
@ -51,6 +52,7 @@ export 'instance/instance_basics.dart';
|
||||||
export 'password/account_reset_kind.dart';
|
export 'password/account_reset_kind.dart';
|
||||||
export 'password/authenticator.dart';
|
export 'password/authenticator.dart';
|
||||||
export 'password/authenticator_enrolment.dart';
|
export 'password/authenticator_enrolment.dart';
|
||||||
|
export 'password/deletion_proof.dart';
|
||||||
export 'password/key_proof_challenge.dart';
|
export 'password/key_proof_challenge.dart';
|
||||||
export 'password/new_password.dart';
|
export 'password/new_password.dart';
|
||||||
export 'password/password_challenge.dart';
|
export 'password/password_challenge.dart';
|
||||||
|
|
@ -140,6 +142,9 @@ class Protocol extends _isc.SerializationManager {
|
||||||
if (t == _iv7tra7v.AuthenticatorEnrolment) {
|
if (t == _iv7tra7v.AuthenticatorEnrolment) {
|
||||||
return _iv7tra7v.AuthenticatorEnrolment.fromJson(data) as T;
|
return _iv7tra7v.AuthenticatorEnrolment.fromJson(data) as T;
|
||||||
}
|
}
|
||||||
|
if (t == _ix1u7hdq.DeletionProof) {
|
||||||
|
return _ix1u7hdq.DeletionProof.fromJson(data) as T;
|
||||||
|
}
|
||||||
if (t == _iehtx95q.KeyProofChallenge) {
|
if (t == _iehtx95q.KeyProofChallenge) {
|
||||||
return _iehtx95q.KeyProofChallenge.fromJson(data) as T;
|
return _iehtx95q.KeyProofChallenge.fromJson(data) as T;
|
||||||
}
|
}
|
||||||
|
|
@ -228,6 +233,10 @@ class Protocol extends _isc.SerializationManager {
|
||||||
: null)
|
: null)
|
||||||
as T;
|
as T;
|
||||||
}
|
}
|
||||||
|
if (t == _isc.getType<_ix1u7hdq.DeletionProof?>()) {
|
||||||
|
return (data != null ? _ix1u7hdq.DeletionProof.fromJson(data) : null)
|
||||||
|
as T;
|
||||||
|
}
|
||||||
if (t == _isc.getType<_iehtx95q.KeyProofChallenge?>()) {
|
if (t == _isc.getType<_iehtx95q.KeyProofChallenge?>()) {
|
||||||
return (data != null ? _iehtx95q.KeyProofChallenge.fromJson(data) : null)
|
return (data != null ? _iehtx95q.KeyProofChallenge.fromJson(data) : null)
|
||||||
as T;
|
as T;
|
||||||
|
|
@ -333,6 +342,7 @@ class Protocol extends _isc.SerializationManager {
|
||||||
_i91ka1it.AccountResetKind => 'AccountResetKind',
|
_i91ka1it.AccountResetKind => 'AccountResetKind',
|
||||||
_icpzuvvi.Authenticator => 'Authenticator',
|
_icpzuvvi.Authenticator => 'Authenticator',
|
||||||
_iv7tra7v.AuthenticatorEnrolment => 'AuthenticatorEnrolment',
|
_iv7tra7v.AuthenticatorEnrolment => 'AuthenticatorEnrolment',
|
||||||
|
_ix1u7hdq.DeletionProof => 'DeletionProof',
|
||||||
_iehtx95q.KeyProofChallenge => 'KeyProofChallenge',
|
_iehtx95q.KeyProofChallenge => 'KeyProofChallenge',
|
||||||
_itvvd6m6.NewPassword => 'NewPassword',
|
_itvvd6m6.NewPassword => 'NewPassword',
|
||||||
_iuxisac7.PasswordChallenge => 'PasswordChallenge',
|
_iuxisac7.PasswordChallenge => 'PasswordChallenge',
|
||||||
|
|
@ -383,6 +393,8 @@ class Protocol extends _isc.SerializationManager {
|
||||||
return 'Authenticator';
|
return 'Authenticator';
|
||||||
case _iv7tra7v.AuthenticatorEnrolment():
|
case _iv7tra7v.AuthenticatorEnrolment():
|
||||||
return 'AuthenticatorEnrolment';
|
return 'AuthenticatorEnrolment';
|
||||||
|
case _ix1u7hdq.DeletionProof():
|
||||||
|
return 'DeletionProof';
|
||||||
case _iehtx95q.KeyProofChallenge():
|
case _iehtx95q.KeyProofChallenge():
|
||||||
return 'KeyProofChallenge';
|
return 'KeyProofChallenge';
|
||||||
case _itvvd6m6.NewPassword():
|
case _itvvd6m6.NewPassword():
|
||||||
|
|
@ -450,6 +462,9 @@ class Protocol extends _isc.SerializationManager {
|
||||||
if (dataClassName == 'AuthenticatorEnrolment') {
|
if (dataClassName == 'AuthenticatorEnrolment') {
|
||||||
return deserialize<_iv7tra7v.AuthenticatorEnrolment>(data['data']);
|
return deserialize<_iv7tra7v.AuthenticatorEnrolment>(data['data']);
|
||||||
}
|
}
|
||||||
|
if (dataClassName == 'DeletionProof') {
|
||||||
|
return deserialize<_ix1u7hdq.DeletionProof>(data['data']);
|
||||||
|
}
|
||||||
if (dataClassName == 'KeyProofChallenge') {
|
if (dataClassName == 'KeyProofChallenge') {
|
||||||
return deserialize<_iehtx95q.KeyProofChallenge>(data['data']);
|
return deserialize<_iehtx95q.KeyProofChallenge>(data['data']);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -245,6 +245,11 @@ Built on the three parts above, on the register (members forward every step thro
|
||||||
password; `password.setRecoveryKeys` replaces the phrase's wrapping with the same
|
password; `password.setRecoveryKeys` replaces the phrase's wrapping with the same
|
||||||
proof and code (a phrase of one's own would otherwise set a new password through the
|
proof and code (a phrase of one's own would otherwise set a new password through the
|
||||||
phrase's reset). A session alone changes neither, nor a session and a code.
|
phrase's reset). A session alone changes neither, nor a session and a code.
|
||||||
|
- **Deleting an account with a password** (`account.delete(proof:)`, a `DeletionProof`)
|
||||||
|
takes the same: the password's proof for a `beginChange` challenge and a code, bound
|
||||||
|
to the host the device addressed (`requireOwnerToDelete`; a member passes the proof
|
||||||
|
on and the register checks it). An account without a password deletes with its
|
||||||
|
session, as before.
|
||||||
- **Sessions end everywhere:** a change of password ends every other session of the
|
- **Sessions end everywhere:** a change of password ends every other session of the
|
||||||
account; a reset of the password or the account ends all. The register ends its own
|
account; a reset of the password or the account ends all. The register ends its own
|
||||||
and keeps a `sessions_ended_notice` per member (told at once and hourly until it
|
and keeps a `sessions_ended_notice` per member (told at once and hourly until it
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import 'package:halkyon_base_server/src/auth/account.dart';
|
||||||
import 'package:halkyon_base_server/src/auth/membership.dart';
|
import 'package:halkyon_base_server/src/auth/membership.dart';
|
||||||
import 'package:halkyon_base_server/src/auth/scopes.dart';
|
import 'package:halkyon_base_server/src/auth/scopes.dart';
|
||||||
import 'package:halkyon_base_server/src/generated/protocol.dart';
|
import 'package:halkyon_base_server/src/generated/protocol.dart';
|
||||||
|
import 'package:halkyon_base_server/src/password/key_proof.dart';
|
||||||
|
import 'package:halkyon_base_server/src/password/password_change.dart';
|
||||||
import 'package:halkyon_base_server/src/register/account_register.dart';
|
import 'package:halkyon_base_server/src/register/account_register.dart';
|
||||||
import 'package:serverpod/serverpod.dart';
|
import 'package:serverpod/serverpod.dart';
|
||||||
import 'package:serverpod_auth_idp_server/core.dart';
|
import 'package:serverpod_auth_idp_server/core.dart';
|
||||||
|
|
@ -16,13 +18,19 @@ class AccountEndpoint extends Endpoint {
|
||||||
/// 2026-09-30). Their other devices learn at once that their sessions
|
/// 2026-09-30). Their other devices learn at once that their sessions
|
||||||
/// ended. An account deletes everywhere (ADR 0016): on a member, the
|
/// ended. An account deletes everywhere (ADR 0016): on a member, the
|
||||||
/// register deletes it and tells every member, this one deleting its own
|
/// register deletes it and tells every member, this one deleting its own
|
||||||
/// data at once; an anonymous person is this service's alone.
|
/// data at once; an anonymous person is this service's alone. An account
|
||||||
Future<void> delete(Session session) async {
|
/// with a password deletes itself only with [proof] (the password's proof
|
||||||
|
/// and a code, `requireOwnerToDelete`), which the register checks, its
|
||||||
|
/// host the one the device addressed here.
|
||||||
|
Future<void> delete(Session session, {DeletionProof? proof}) async {
|
||||||
|
final pod = session.serverpod;
|
||||||
final person = session.authenticated!.authUserId;
|
final person = session.authenticated!.authUserId;
|
||||||
if (isMember(session.serverpod) &&
|
final bound = proof?.copyWith(host: deviceHostOn(pod, proof.host));
|
||||||
session.authenticated!.scopes.contains(identified)) {
|
if (!isMember(pod)) {
|
||||||
await AccountRegister.of(session.serverpod)
|
await requireOwnerToDelete(session, person, bound);
|
||||||
.deleteAccount(session, person);
|
} else if (session.authenticated!.scopes.contains(identified)) {
|
||||||
|
await AccountRegister.of(pod)
|
||||||
|
.deleteAccount(session, person, proof: bound);
|
||||||
}
|
}
|
||||||
await deletePerson(session, person);
|
await deletePerson(session, person);
|
||||||
await session.messages.authenticationRevoked(
|
await session.messages.authenticationRevoked(
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@
|
||||||
|
|
||||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||||
import 'dart:typed_data' as _idt;
|
import 'dart:typed_data' as _idt;
|
||||||
|
import 'package:halkyon_base_server/src/generated/password/deletion_proof.dart'
|
||||||
|
as _i619l6vv;
|
||||||
import 'package:halkyon_base_server/src/generated/password/new_password.dart'
|
import 'package:halkyon_base_server/src/generated/password/new_password.dart'
|
||||||
as _ijye8mcv;
|
as _ijye8mcv;
|
||||||
import 'package:halkyon_base_server/src/generated/password/password_setup.dart'
|
import 'package:halkyon_base_server/src/generated/password/password_setup.dart'
|
||||||
|
|
@ -117,13 +119,22 @@ class Endpoints extends _is.EndpointDispatch {
|
||||||
methodConnectors: {
|
methodConnectors: {
|
||||||
'delete': _is.MethodConnector(
|
'delete': _is.MethodConnector(
|
||||||
name: 'delete',
|
name: 'delete',
|
||||||
params: {},
|
params: {
|
||||||
|
'proof': _is.ParameterDescription(
|
||||||
|
name: 'proof',
|
||||||
|
type: _is.getType<_i619l6vv.DeletionProof?>(),
|
||||||
|
nullable: true,
|
||||||
|
),
|
||||||
|
},
|
||||||
call:
|
call:
|
||||||
(
|
(
|
||||||
_is.Session session,
|
_is.Session session,
|
||||||
Map<String, dynamic> params,
|
Map<String, dynamic> params,
|
||||||
) async => (endpoints['account'] as _ii6ycf6v.AccountEndpoint)
|
) async =>
|
||||||
.delete(session),
|
(endpoints['account'] as _ii6ycf6v.AccountEndpoint).delete(
|
||||||
|
session,
|
||||||
|
proof: params['proof'],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
'signInWays': _is.MethodConnector(
|
'signInWays': _is.MethodConnector(
|
||||||
name: 'signInWays',
|
name: 'signInWays',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||||
|
/* To generate run: "serverpod generate" */
|
||||||
|
|
||||||
|
// ignore_for_file: implementation_imports
|
||||||
|
// ignore_for_file: library_private_types_in_public_api
|
||||||
|
// ignore_for_file: non_constant_identifier_names
|
||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
// ignore_for_file: type_literal_in_constant_pattern
|
||||||
|
// ignore_for_file: use_super_parameters
|
||||||
|
// ignore_for_file: invalid_use_of_internal_member
|
||||||
|
|
||||||
|
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||||
|
import 'dart:typed_data' as _idt;
|
||||||
|
import 'package:serverpod/serverpod.dart' as _is;
|
||||||
|
|
||||||
|
/// What deleting an account that has a password takes (ADR 0017; the second
|
||||||
|
/// reading): the password's SRP proof for a challenge `password.beginChange`
|
||||||
|
/// gave, a TOTP or recovery code, and the host the device addressed, as a
|
||||||
|
/// change of password does. A session alone deletes only an account without
|
||||||
|
/// one.
|
||||||
|
abstract class DeletionProof
|
||||||
|
implements _is.SerializableModel, _is.ProtocolSerialization {
|
||||||
|
DeletionProof._({
|
||||||
|
required this.challenge,
|
||||||
|
required this.clientValue,
|
||||||
|
required this.clientProof,
|
||||||
|
this.totpCode,
|
||||||
|
this.recoveryCode,
|
||||||
|
required this.host,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory DeletionProof({
|
||||||
|
required _is.UuidValue challenge,
|
||||||
|
required _idt.ByteData clientValue,
|
||||||
|
required _idt.ByteData clientProof,
|
||||||
|
String? totpCode,
|
||||||
|
String? recoveryCode,
|
||||||
|
required String host,
|
||||||
|
}) = _DeletionProofImpl;
|
||||||
|
|
||||||
|
factory DeletionProof.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||||
|
return DeletionProof(
|
||||||
|
challenge: _is.UuidValueJsonExtension.fromJson(
|
||||||
|
jsonSerialization['challenge'],
|
||||||
|
),
|
||||||
|
clientValue: _is.ByteDataJsonExtension.fromJson(
|
||||||
|
jsonSerialization['clientValue'],
|
||||||
|
),
|
||||||
|
clientProof: _is.ByteDataJsonExtension.fromJson(
|
||||||
|
jsonSerialization['clientProof'],
|
||||||
|
),
|
||||||
|
totpCode: jsonSerialization['totpCode'] as String?,
|
||||||
|
recoveryCode: jsonSerialization['recoveryCode'] as String?,
|
||||||
|
host: jsonSerialization['host'] as String,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_is.UuidValue challenge;
|
||||||
|
|
||||||
|
_idt.ByteData clientValue;
|
||||||
|
|
||||||
|
_idt.ByteData clientProof;
|
||||||
|
|
||||||
|
String? totpCode;
|
||||||
|
|
||||||
|
String? recoveryCode;
|
||||||
|
|
||||||
|
/// The host the device addressed the instance by: the server it reached
|
||||||
|
/// checks it is its own before the register binds the proof to it.
|
||||||
|
String host;
|
||||||
|
|
||||||
|
/// Returns a shallow copy of this [DeletionProof]
|
||||||
|
/// with some or all fields replaced by the given arguments.
|
||||||
|
@_is.useResult
|
||||||
|
DeletionProof copyWith({
|
||||||
|
_is.UuidValue? challenge,
|
||||||
|
_idt.ByteData? clientValue,
|
||||||
|
_idt.ByteData? clientProof,
|
||||||
|
String? totpCode,
|
||||||
|
String? recoveryCode,
|
||||||
|
String? host,
|
||||||
|
});
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'__className__': 'halkyon_base.DeletionProof',
|
||||||
|
'challenge': challenge.toJson(),
|
||||||
|
'clientValue': clientValue.toJson(),
|
||||||
|
'clientProof': clientProof.toJson(),
|
||||||
|
if (totpCode != null) 'totpCode': totpCode,
|
||||||
|
if (recoveryCode != null) 'recoveryCode': recoveryCode,
|
||||||
|
'host': host,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJsonForProtocol() {
|
||||||
|
return {
|
||||||
|
'__className__': 'halkyon_base.DeletionProof',
|
||||||
|
'challenge': challenge.toJson(),
|
||||||
|
'clientValue': clientValue.toJson(),
|
||||||
|
'clientProof': clientProof.toJson(),
|
||||||
|
if (totpCode != null) 'totpCode': totpCode,
|
||||||
|
if (recoveryCode != null) 'recoveryCode': recoveryCode,
|
||||||
|
'host': host,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return _is.SerializationManager.encode(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Undefined {}
|
||||||
|
|
||||||
|
class _DeletionProofImpl extends DeletionProof {
|
||||||
|
_DeletionProofImpl({
|
||||||
|
required _is.UuidValue challenge,
|
||||||
|
required _idt.ByteData clientValue,
|
||||||
|
required _idt.ByteData clientProof,
|
||||||
|
String? totpCode,
|
||||||
|
String? recoveryCode,
|
||||||
|
required String host,
|
||||||
|
}) : super._(
|
||||||
|
challenge: challenge,
|
||||||
|
clientValue: clientValue,
|
||||||
|
clientProof: clientProof,
|
||||||
|
totpCode: totpCode,
|
||||||
|
recoveryCode: recoveryCode,
|
||||||
|
host: host,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Returns a shallow copy of this [DeletionProof]
|
||||||
|
/// with some or all fields replaced by the given arguments.
|
||||||
|
@_is.useResult
|
||||||
|
@override
|
||||||
|
DeletionProof copyWith({
|
||||||
|
_is.UuidValue? challenge,
|
||||||
|
_idt.ByteData? clientValue,
|
||||||
|
_idt.ByteData? clientProof,
|
||||||
|
Object? totpCode = _Undefined,
|
||||||
|
Object? recoveryCode = _Undefined,
|
||||||
|
String? host,
|
||||||
|
}) {
|
||||||
|
return DeletionProof(
|
||||||
|
challenge: challenge ?? this.challenge,
|
||||||
|
clientValue: clientValue ?? this.clientValue.clone(),
|
||||||
|
clientProof: clientProof ?? this.clientProof.clone(),
|
||||||
|
totpCode: totpCode is String? ? totpCode : this.totpCode,
|
||||||
|
recoveryCode: recoveryCode is String? ? recoveryCode : this.recoveryCode,
|
||||||
|
host: host ?? this.host,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue