feat(auth): change the password and the recovery phrase while signed in
ADR 0017, step 3. `password.beginChange` and `password.change` take the old password's SRP proof and a TOTP or recovery code, and the same keys wrapped under the new password; a session alone changes nothing, and an account without a confirmed authenticator is told to set one up first. `password.setRecoveryKeys` replaces the keys' wrapping under the recovery phrase for a TOTP code, so a device holding the keys can make a new phrase. A change of password ends every other session of the account in every service of the instance; resets of the password and of the account end them all. The register ends its own and keeps a notice per member (`sessions_ended_notice`, a migration that only creates it), told at once and hourly until it confirms, as deletions are told: it names a moment, so sessions made since stay, and the session that asked for a change. A member answering one ends its sessions made before that moment; a member signing someone in after a reset ends its own first and makes the new session only once its clock is past the moment. A reset not due says when it takes effect (`takesEffectAt` on `PasswordRefusedException`), across members too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
f8076dcacf
commit
d00da1f24a
2 changed files with 43506 additions and 126 deletions
|
|
@ -34,6 +34,8 @@ import 'package:halkyon_base_client/src/protocol/password/password_signed_in.dar
|
|||
as _ix90myvx;
|
||||
import 'package:halkyon_base_client/src/protocol/password/reset_pending.dart'
|
||||
as _ippuf33k;
|
||||
import 'package:halkyon_base_client/src/protocol/password/wrapped_keys.dart'
|
||||
as _i3so7e0p;
|
||||
import 'package:halkyon_base_client/src/protocol/profile/profile.dart'
|
||||
as _i8qdmv6n;
|
||||
import 'package:halkyon_base_client/src/protocol/profile/profile_refusal.dart'
|
||||
|
|
@ -308,8 +310,9 @@ class EndpointAccountRecovery extends _isc.EndpointRef {
|
|||
);
|
||||
|
||||
/// Sets a new [password] with the recovery phrase's [signature] for
|
||||
/// [challenge] and a [totpCode] or [recoveryCode]; the keys stay. Signs
|
||||
/// in. Refusals as `resetPasswordWithPhrase`'s.
|
||||
/// [challenge] and a [totpCode] or [recoveryCode]; the keys stay. Every
|
||||
/// session of the account ends, in every service of the instance, and
|
||||
/// this one signs in. Refusals as `resetPasswordWithPhrase`'s.
|
||||
_ida.Future<_iacc.AuthSuccess> resetPassword({
|
||||
required _isc.UuidValue challenge,
|
||||
required _idt.ByteData signature,
|
||||
|
|
@ -350,8 +353,9 @@ class EndpointAccountRecovery extends _isc.EndpointRef {
|
|||
|
||||
/// Carries out the reset of the whole account at [address] once due, as
|
||||
/// a new [code] mailed for [request] proves: [setup] replaces its
|
||||
/// password and keys, and an authenticator must be set up again. Signs
|
||||
/// in. Refusals as `completeAccountReset`'s.
|
||||
/// password and keys, and an authenticator must be set up again. Every
|
||||
/// session of the account ends, in every service of the instance, and
|
||||
/// this one signs in. Refusals as `completeAccountReset`'s.
|
||||
_ida.Future<_iacc.AuthSuccess> completeAccountReset({
|
||||
required String address,
|
||||
required _isc.UuidValue request,
|
||||
|
|
@ -472,6 +476,55 @@ class EndpointPassword extends _isc.EndpointRef {
|
|||
'code': code,
|
||||
},
|
||||
);
|
||||
|
||||
/// The first step of the caller proving their password to change it:
|
||||
/// its salt and costs, and SRP's B. Refusals as `beginPasswordChange`'s.
|
||||
_ida.Future<_iykren7a.PasswordChallenge> beginChange() =>
|
||||
caller.callServerEndpoint<_iykren7a.PasswordChallenge>(
|
||||
'halkyon_base.password',
|
||||
'beginChange',
|
||||
{},
|
||||
);
|
||||
|
||||
/// Changes the caller's password to [password] (the same keys, wrapped
|
||||
/// under the new password's key on the device), with the old password's
|
||||
/// proof for [challenge] ([beginChange]) and a [totpCode] or
|
||||
/// [recoveryCode]. Every other session of theirs ends, in every service of
|
||||
/// the instance; this one stays. Refusals as `changePassword`'s.
|
||||
_ida.Future<void> change({
|
||||
required _isc.UuidValue challenge,
|
||||
required _idt.ByteData clientValue,
|
||||
required _idt.ByteData clientProof,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
required _i95l60wq.NewPassword password,
|
||||
}) => caller.callServerEndpoint<void>(
|
||||
'halkyon_base.password',
|
||||
'change',
|
||||
{
|
||||
'challenge': challenge,
|
||||
'clientValue': clientValue,
|
||||
'clientProof': clientProof,
|
||||
'totpCode': totpCode,
|
||||
'recoveryCode': recoveryCode,
|
||||
'password': password,
|
||||
},
|
||||
);
|
||||
|
||||
/// A new recovery phrase: [recoveryKeys], the caller's keys wrapped under
|
||||
/// it on the device, replace the old wrapping, for a [code] from one of
|
||||
/// their authenticators. Refusals as `setRecoveryKeys`'.
|
||||
_ida.Future<void> setRecoveryKeys({
|
||||
required String code,
|
||||
required _i3so7e0p.WrappedKeys recoveryKeys,
|
||||
}) => caller.callServerEndpoint<void>(
|
||||
'halkyon_base.password',
|
||||
'setRecoveryKeys',
|
||||
{
|
||||
'code': code,
|
||||
'recoveryKeys': recoveryKeys,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Signing in with an address, a password and a code from an authenticator
|
||||
|
|
|
|||
|
|
@ -20,10 +20,14 @@ abstract class PasswordRefusedException
|
|||
_isc.SerializableException,
|
||||
_isc.SerializableModel,
|
||||
_isc.ProtocolSerialization {
|
||||
PasswordRefusedException._({required this.reason});
|
||||
PasswordRefusedException._({
|
||||
required this.reason,
|
||||
this.takesEffectAt,
|
||||
});
|
||||
|
||||
factory PasswordRefusedException({
|
||||
required _i647hpdf.PasswordRefusal reason,
|
||||
DateTime? takesEffectAt,
|
||||
}) = _PasswordRefusedExceptionImpl;
|
||||
|
||||
factory PasswordRefusedException.fromJson(
|
||||
|
|
@ -33,20 +37,33 @@ abstract class PasswordRefusedException
|
|||
reason: _i647hpdf.PasswordRefusal.fromJson(
|
||||
(jsonSerialization['reason'] as String),
|
||||
),
|
||||
takesEffectAt: jsonSerialization['takesEffectAt'] == null
|
||||
? null
|
||||
: _isc.DateTimeJsonExtension.fromJson(
|
||||
jsonSerialization['takesEffectAt'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_i647hpdf.PasswordRefusal reason;
|
||||
|
||||
/// When the reset waiting takes effect, for [PasswordRefusal.notDue];
|
||||
/// null otherwise.
|
||||
DateTime? takesEffectAt;
|
||||
|
||||
/// Returns a shallow copy of this [PasswordRefusedException]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
PasswordRefusedException copyWith({_i647hpdf.PasswordRefusal? reason});
|
||||
PasswordRefusedException copyWith({
|
||||
_i647hpdf.PasswordRefusal? reason,
|
||||
DateTime? takesEffectAt,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'halkyon_base.PasswordRefusedException',
|
||||
'reason': reason.toJson(),
|
||||
if (takesEffectAt != null) 'takesEffectAt': takesEffectAt?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -55,24 +72,40 @@ abstract class PasswordRefusedException
|
|||
return {
|
||||
'__className__': 'halkyon_base.PasswordRefusedException',
|
||||
'reason': reason.toJson(),
|
||||
if (takesEffectAt != null) 'takesEffectAt': takesEffectAt?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PasswordRefusedException(reason: $reason)';
|
||||
return 'PasswordRefusedException(reason: $reason, takesEffectAt: $takesEffectAt)';
|
||||
}
|
||||
}
|
||||
|
||||
class _Undefined {}
|
||||
|
||||
class _PasswordRefusedExceptionImpl extends PasswordRefusedException {
|
||||
_PasswordRefusedExceptionImpl({required _i647hpdf.PasswordRefusal reason})
|
||||
: super._(reason: reason);
|
||||
_PasswordRefusedExceptionImpl({
|
||||
required _i647hpdf.PasswordRefusal reason,
|
||||
DateTime? takesEffectAt,
|
||||
}) : super._(
|
||||
reason: reason,
|
||||
takesEffectAt: takesEffectAt,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [PasswordRefusedException]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
@override
|
||||
PasswordRefusedException copyWith({_i647hpdf.PasswordRefusal? reason}) {
|
||||
return PasswordRefusedException(reason: reason ?? this.reason);
|
||||
PasswordRefusedException copyWith({
|
||||
_i647hpdf.PasswordRefusal? reason,
|
||||
Object? takesEffectAt = _Undefined,
|
||||
}) {
|
||||
return PasswordRefusedException(
|
||||
reason: reason ?? this.reason,
|
||||
takesEffectAt: takesEffectAt is DateTime?
|
||||
? takesEffectAt
|
||||
: this.takesEffectAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue