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
3 changed files with 873 additions and 32 deletions
|
|
@ -20,6 +20,8 @@ import 'package:halkyon_base_client/src/protocol/password/authenticator.dart'
|
|||
as _ivfmflch;
|
||||
import 'package:halkyon_base_client/src/protocol/password/authenticator_enrolment.dart'
|
||||
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'
|
||||
as _ir3ecgdn;
|
||||
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
|
||||
/// ended. An account deletes everywhere (ADR 0016): on a member, the
|
||||
/// register deletes it and tells every member, this one deleting its own
|
||||
/// data at once; an anonymous person is this service's alone.
|
||||
_ida.Future<void> delete() => caller.callServerEndpoint<void>(
|
||||
'halkyon_base.account',
|
||||
'delete',
|
||||
{},
|
||||
);
|
||||
/// data at once; an anonymous person is this service's alone. An account
|
||||
/// 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.
|
||||
_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
|
||||
/// 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/authenticator.dart' as _icpzuvvi;
|
||||
import 'password/authenticator_enrolment.dart' as _iv7tra7v;
|
||||
import 'password/deletion_proof.dart' as _ix1u7hdq;
|
||||
import 'password/key_proof_challenge.dart' as _iehtx95q;
|
||||
import 'password/new_password.dart' as _itvvd6m6;
|
||||
import 'password/password_challenge.dart' as _iuxisac7;
|
||||
|
|
@ -51,6 +52,7 @@ export 'instance/instance_basics.dart';
|
|||
export 'password/account_reset_kind.dart';
|
||||
export 'password/authenticator.dart';
|
||||
export 'password/authenticator_enrolment.dart';
|
||||
export 'password/deletion_proof.dart';
|
||||
export 'password/key_proof_challenge.dart';
|
||||
export 'password/new_password.dart';
|
||||
export 'password/password_challenge.dart';
|
||||
|
|
@ -140,6 +142,9 @@ class Protocol extends _isc.SerializationManager {
|
|||
if (t == _iv7tra7v.AuthenticatorEnrolment) {
|
||||
return _iv7tra7v.AuthenticatorEnrolment.fromJson(data) as T;
|
||||
}
|
||||
if (t == _ix1u7hdq.DeletionProof) {
|
||||
return _ix1u7hdq.DeletionProof.fromJson(data) as T;
|
||||
}
|
||||
if (t == _iehtx95q.KeyProofChallenge) {
|
||||
return _iehtx95q.KeyProofChallenge.fromJson(data) as T;
|
||||
}
|
||||
|
|
@ -228,6 +233,10 @@ class Protocol extends _isc.SerializationManager {
|
|||
: null)
|
||||
as T;
|
||||
}
|
||||
if (t == _isc.getType<_ix1u7hdq.DeletionProof?>()) {
|
||||
return (data != null ? _ix1u7hdq.DeletionProof.fromJson(data) : null)
|
||||
as T;
|
||||
}
|
||||
if (t == _isc.getType<_iehtx95q.KeyProofChallenge?>()) {
|
||||
return (data != null ? _iehtx95q.KeyProofChallenge.fromJson(data) : null)
|
||||
as T;
|
||||
|
|
@ -333,6 +342,7 @@ class Protocol extends _isc.SerializationManager {
|
|||
_i91ka1it.AccountResetKind => 'AccountResetKind',
|
||||
_icpzuvvi.Authenticator => 'Authenticator',
|
||||
_iv7tra7v.AuthenticatorEnrolment => 'AuthenticatorEnrolment',
|
||||
_ix1u7hdq.DeletionProof => 'DeletionProof',
|
||||
_iehtx95q.KeyProofChallenge => 'KeyProofChallenge',
|
||||
_itvvd6m6.NewPassword => 'NewPassword',
|
||||
_iuxisac7.PasswordChallenge => 'PasswordChallenge',
|
||||
|
|
@ -383,6 +393,8 @@ class Protocol extends _isc.SerializationManager {
|
|||
return 'Authenticator';
|
||||
case _iv7tra7v.AuthenticatorEnrolment():
|
||||
return 'AuthenticatorEnrolment';
|
||||
case _ix1u7hdq.DeletionProof():
|
||||
return 'DeletionProof';
|
||||
case _iehtx95q.KeyProofChallenge():
|
||||
return 'KeyProofChallenge';
|
||||
case _itvvd6m6.NewPassword():
|
||||
|
|
@ -450,6 +462,9 @@ class Protocol extends _isc.SerializationManager {
|
|||
if (dataClassName == 'AuthenticatorEnrolment') {
|
||||
return deserialize<_iv7tra7v.AuthenticatorEnrolment>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'DeletionProof') {
|
||||
return deserialize<_ix1u7hdq.DeletionProof>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'KeyProofChallenge') {
|
||||
return deserialize<_iehtx95q.KeyProofChallenge>(data['data']);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue