feat(base): the changes of SimpleLogin accounts' keys, each with the password and a fresh sign-in (ADR 0020, D20)

Every change takes the password's proof by its own proof key (the
recovery phrase's for a lost password) and a SimpleLogin sign-in of the
last ten minutes on the device (`KeysOwnerProof`, its attempt), as ADR
0017's take the password and a code. The sign-in is checked before the
proof and taken with the change, so a wrong proof leaves it standing and
each sign-in serves one change.

- `keys.set(attempt, setup, handle?)`: the first keys of an account that
  SimpleLogin was added to (anonymous before, or signing in with GitHub
  or a code), with the handle where it has none; the sign-in alone, as
  there is no password to prove yet.
- `keys.changePassword`, `keys.beginPhrase` and `keys.resetPassword` (a
  lost password, with the phrase): every other session ends, in every
  service, as ADR 0017's change has it.
- `keys.setRecoveryKeys`: a new phrase.
- `account.delete(keysProof)`: an account without SRP's verifier deletes
  itself with the proof and the sign-in; `requireOwnerToDelete` no longer
  asks such an account for SRP.
- `newKeysPasswordDigest` on both sides, held to each other by a test; a
  challenge purpose of its own for the phrase's proof while signed in.
- Members ask the register for all of it; `RemoteRegister` and the
  instance API carry the five new operations and the deletion's proof.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-23 22:11:49 +02:00
commit 7c58d23424
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
39 changed files with 3163 additions and 78 deletions

View file

@ -38,8 +38,13 @@
a migration that drops nothing); `simpleLogin.signUp` in one call (D18) and a migration that drops nothing); `simpleLogin.signUp` in one call (D18) and
`simpleLogin.checkHandle`, both for someone new's identity alone; `keys.begin` and `simpleLogin.checkHandle`, both for someone new's identity alone; `keys.begin` and
`keys.unlock`, the proof counted as ADR 0017's; every sign-in into an account kept ten `keys.unlock`, the proof counted as ADR 0017's; every sign-in into an account kept ten
minutes under its attempt as D20's confirmation. The changes (first keys, a new or minutes under its attempt as D20's confirmation. Then the changes, each through the
lost password, a new phrase, the deletion) and the mending come next. register: the first keys (and handle, where none) of an account SimpleLogin was added
to (`keys.set`), with the sign-in alone, as there is no password to prove yet; a new
password, a lost one with the phrase (`keys.beginPhrase`, `keys.resetPassword`), a new
phrase and the deletion (`account.delete`'s `keysProof`), each with the proof and the
sign-in, which the change takes: one change for each sign-in. A new or lost password
ends every other session, as ADR 0017's change does. The mending comes next.
- **Builds on:** ADR 0010 (email sign-in), ADR 0011 (profile pages), ADR 0016 (the - **Builds on:** ADR 0010 (email sign-in), ADR 0011 (profile pages), ADR 0016 (the
register), ADR 0017 (a password, TOTP and the keys). register), ADR 0017 (a password, TOTP and the keys).
- **Changes:** ADR 0017's way in on an instance that offers SimpleLogin (its keys, their - **Changes:** ADR 0017's way in on an instance that offers SimpleLogin (its keys, their

View file

@ -22,6 +22,10 @@ import 'package:halkyon_base_client/src/protocol/keys/device_keys.dart'
as _invv3u3l; as _invv3u3l;
import 'package:halkyon_base_client/src/protocol/keys/keys_challenge.dart' import 'package:halkyon_base_client/src/protocol/keys/keys_challenge.dart'
as _ivkzjgkp; as _ivkzjgkp;
import 'package:halkyon_base_client/src/protocol/keys/keys_owner_proof.dart'
as _i1sdq72h;
import 'package:halkyon_base_client/src/protocol/keys/keys_password.dart'
as _i7elvg0l;
import 'package:halkyon_base_client/src/protocol/keys/keys_proof.dart' import 'package:halkyon_base_client/src/protocol/keys/keys_proof.dart'
as _iznbn55x; as _iznbn55x;
import 'package:halkyon_base_client/src/protocol/keys/keys_setup.dart' import 'package:halkyon_base_client/src/protocol/keys/keys_setup.dart'
@ -78,14 +82,22 @@ class EndpointAccount extends _isc.EndpointRef {
/// 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. An account /// 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 /// with a password deletes itself only with [proof] (the password's proof
/// and a code, `requireOwnerToDelete`), which the register checks, its /// and a code, `requireOwnerToDelete`), one that signs in with SimpleLogin
/// base URL the one the device addressed here. /// only with [keysProof] (the password's proof for a challenge of
_ida.Future<void> delete({_ifxctuoq.DeletionProof? proof}) => /// `keys.begin` and a SimpleLogin sign-in of the last ten minutes on the
caller.callServerEndpoint<void>( /// device, Halkyon Learn's ADR 0020, D20), which the register checks,
'halkyon_base.account', /// their base URL the one the device addressed here.
'delete', _ida.Future<void> delete({
{'proof': proof}, _ifxctuoq.DeletionProof? proof,
); _i1sdq72h.KeysOwnerProof? keysProof,
}) => caller.callServerEndpoint<void>(
'halkyon_base.account',
'delete',
{
'proof': proof,
'keysProof': keysProof,
},
);
/// 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).
@ -231,9 +243,11 @@ class EndpointInstance extends _isc.EndpointRef {
} }
/// The keys of an account that signs in with SimpleLogin (Halkyon Learn's /// The keys of an account that signs in with SimpleLogin (Halkyon Learn's
/// ADR 0020, 7 and D19), for its signed-in devices: the password proven /// ADR 0020, 6, 7, D19 and D20), for its signed-in devices: the password
/// with its own proof key, which the instance's register checks (ADR 0016). /// proven with its own proof key, which the instance's register checks (ADR
/// Refusals are `PasswordRefusedException`s. /// 0016), and every change with that proof and a SimpleLogin sign-in of the
/// last ten minutes on the device (`KeysOwnerProof`). Refusals are
/// `PasswordRefusedException`s; the sign-in's, `OidcRefusedException`s.
/// {@category Endpoint} /// {@category Endpoint}
class EndpointKeys extends _isc.EndpointRef { class EndpointKeys extends _isc.EndpointRef {
EndpointKeys(_isc.EndpointCaller caller) : super(caller); EndpointKeys(_isc.EndpointCaller caller) : super(caller);
@ -265,6 +279,93 @@ class EndpointKeys extends _isc.EndpointRef {
'unlock', 'unlock',
{'proof': proof}, {'proof': proof},
); );
/// Your first keys, for an account SimpleLogin was added to that has none
/// (`simpleLogin.complete` says `hasKeys`), all made on your device and
/// described by [setup]; [handle] where the account has none (it said no
/// `handle`), null otherwise. Takes your SimpleLogin sign-in of the last
/// ten minutes on this device, under [attempt]. Refused for an account
/// with keys (`PasswordRefusal.passwordExists`), a setup out of range
/// (`PasswordRefusal.invalidSetup`), a handle refused or missing
/// (`ProfileRefusedException`), and without that sign-in
/// (`OidcRefusal.unknownAttempt`).
_ida.Future<void> set({
required _isc.UuidValue attempt,
required _iec2fqr0.KeysSetup setup,
String? handle,
}) => caller.callServerEndpoint<void>(
'halkyon_base.keys',
'set',
{
'attempt': attempt,
'setup': setup,
'handle': handle,
},
);
/// The first step of proving your recovery phrase, for a password you
/// lost: your keys wrapped under the phrase's key and your public keys,
/// which your device opens with the phrase, and a nonce to sign with its
/// proof key, within two minutes, once. Refused for an account without
/// such keys (`PasswordRefusal.noPassword`).
_ida.Future<_ir3ecgdn.KeyProofChallenge> beginPhrase() =>
caller.callServerEndpoint<_ir3ecgdn.KeyProofChallenge>(
'halkyon_base.keys',
'beginPhrase',
{},
);
/// Changes your password to [password] (the same keys, wrapped under the
/// new password's key on your device), with [proof]: the old password's
/// for a challenge of [begin], binding the new password, and your
/// SimpleLogin sign-in of the last ten minutes on this device. Every
/// other session of yours ends, in every service of the instance; this
/// one stays. The proof counts against this session, as [unlock]'s.
_ida.Future<void> changePassword({
required _i1sdq72h.KeysOwnerProof proof,
required _i7elvg0l.KeysPassword password,
}) => caller.callServerEndpoint<void>(
'halkyon_base.keys',
'changePassword',
{
'proof': proof,
'password': password,
},
);
/// Sets a new [password] for the one you lost, with [proof]: your
/// recovery phrase's for a challenge of [beginPhrase], binding the new
/// password, and your SimpleLogin sign-in of the last ten minutes on this
/// device. Every other session of yours ends, as [changePassword] has it.
_ida.Future<void> resetPassword({
required _i1sdq72h.KeysOwnerProof proof,
required _i7elvg0l.KeysPassword password,
}) => caller.callServerEndpoint<void>(
'halkyon_base.keys',
'resetPassword',
{
'proof': proof,
'password': password,
},
);
/// A new recovery phrase: [recoveryKeys], your keys wrapped under it on
/// your device, replace the old wrapping, and [recoveryProofKey], the
/// public half of its proof key, the old one, with [proof] as
/// [changePassword] takes it, binding the new phrase.
_ida.Future<void> setRecoveryKeys({
required _i1sdq72h.KeysOwnerProof proof,
required _i3so7e0p.WrappedKeys recoveryKeys,
required _idt.ByteData recoveryProofKey,
}) => caller.callServerEndpoint<void>(
'halkyon_base.keys',
'setRecoveryKeys',
{
'proof': proof,
'recoveryKeys': recoveryKeys,
'recoveryProofKey': recoveryProofKey,
},
);
} }
/// Getting back into an account (ADR 0017): a lost authenticator, a lost /// Getting back into an account (ADR 0017): a lost authenticator, a lost

View file

@ -0,0 +1,134 @@
/* 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 a change of an account that signs in with SimpleLogin takes
/// (Halkyon Learn's ADR 0020, D20), as ADR 0017's `OwnerProof` takes the
/// password and a code: a key proof for a challenge of `keys.begin` (the
/// password's proof key), or of `keys.beginPhrase` for a lost password
/// (the recovery phrase's), and a SimpleLogin sign-in of the last ten
/// minutes on this device, the attempt it ended (`simpleLogin.complete`),
/// taken by the change.
abstract class KeysOwnerProof
implements _isc.SerializableModel, _isc.ProtocolSerialization {
KeysOwnerProof._({
required this.challenge,
required this.signature,
required this.instanceUrl,
required this.attempt,
});
factory KeysOwnerProof({
required _isc.UuidValue challenge,
required _idt.ByteData signature,
required String instanceUrl,
required _isc.UuidValue attempt,
}) = _KeysOwnerProofImpl;
factory KeysOwnerProof.fromJson(Map<String, dynamic> jsonSerialization) {
return KeysOwnerProof(
challenge: _isc.UuidValueJsonExtension.fromJson(
jsonSerialization['challenge'],
),
signature: _isc.ByteDataJsonExtension.fromJson(
jsonSerialization['signature'],
),
instanceUrl: jsonSerialization['instanceUrl'] as String,
attempt: _isc.UuidValueJsonExtension.fromJson(
jsonSerialization['attempt'],
),
);
}
_isc.UuidValue challenge;
/// Ed25519's signature over `keyProofMessage`, 64 bytes.
_idt.ByteData signature;
/// The instance's base URL the device addressed (scheme, host, port,
/// path): the server it reached checks it is its own before the
/// register binds the proof to it.
String instanceUrl;
/// The SimpleLogin sign-in's attempt.
_isc.UuidValue attempt;
/// Returns a shallow copy of this [KeysOwnerProof]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
KeysOwnerProof copyWith({
_isc.UuidValue? challenge,
_idt.ByteData? signature,
String? instanceUrl,
_isc.UuidValue? attempt,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'halkyon_base.KeysOwnerProof',
'challenge': challenge.toJson(),
'signature': signature.toJson(),
'instanceUrl': instanceUrl,
'attempt': attempt.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'halkyon_base.KeysOwnerProof',
'challenge': challenge.toJson(),
'signature': signature.toJson(),
'instanceUrl': instanceUrl,
'attempt': attempt.toJson(),
};
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _KeysOwnerProofImpl extends KeysOwnerProof {
_KeysOwnerProofImpl({
required _isc.UuidValue challenge,
required _idt.ByteData signature,
required String instanceUrl,
required _isc.UuidValue attempt,
}) : super._(
challenge: challenge,
signature: signature,
instanceUrl: instanceUrl,
attempt: attempt,
);
/// Returns a shallow copy of this [KeysOwnerProof]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
KeysOwnerProof copyWith({
_isc.UuidValue? challenge,
_idt.ByteData? signature,
String? instanceUrl,
_isc.UuidValue? attempt,
}) {
return KeysOwnerProof(
challenge: challenge ?? this.challenge,
signature: signature ?? this.signature.clone(),
instanceUrl: instanceUrl ?? this.instanceUrl,
attempt: attempt ?? this.attempt,
);
}
}

View file

@ -0,0 +1,132 @@
/* 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:halkyon_base_client/src/protocol/protocol.dart' as _iwgbpyo5;
import 'package:serverpod_client/serverpod_client.dart' as _isc;
import '../password/password_costs.dart' as _ioe6tior;
import '../password/wrapped_keys.dart' as _i2rxumte;
/// A new password for the keys of an account that signs in with SimpleLogin
/// (Halkyon Learn's ADR 0020, 7 and D19), all made on the device, which
/// holds the keys: the salt and costs it derived the password's keys with,
/// the public half of the password's proof key, and the account's private
/// keys wrapped under the password's key. As ADR 0017's `NewPassword`,
/// without SRP's verifier.
abstract class KeysPassword
implements _isc.SerializableModel, _isc.ProtocolSerialization {
KeysPassword._({
required this.salt,
required this.costs,
required this.passwordProofKey,
required this.passwordKeys,
});
factory KeysPassword({
required _idt.ByteData salt,
required _ioe6tior.PasswordCosts costs,
required _idt.ByteData passwordProofKey,
required _i2rxumte.WrappedKeys passwordKeys,
}) = _KeysPasswordImpl;
factory KeysPassword.fromJson(Map<String, dynamic> jsonSerialization) {
return KeysPassword(
salt: _isc.ByteDataJsonExtension.fromJson(jsonSerialization['salt']),
costs: _iwgbpyo5.Protocol().deserialize<_ioe6tior.PasswordCosts>(
jsonSerialization['costs'],
),
passwordProofKey: _isc.ByteDataJsonExtension.fromJson(
jsonSerialization['passwordProofKey'],
),
passwordKeys: _iwgbpyo5.Protocol().deserialize<_i2rxumte.WrappedKeys>(
jsonSerialization['passwordKeys'],
),
);
}
_idt.ByteData salt;
_ioe6tior.PasswordCosts costs;
/// The public half of the new password's proof key (Ed25519).
_idt.ByteData passwordProofKey;
_i2rxumte.WrappedKeys passwordKeys;
/// Returns a shallow copy of this [KeysPassword]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
KeysPassword copyWith({
_idt.ByteData? salt,
_ioe6tior.PasswordCosts? costs,
_idt.ByteData? passwordProofKey,
_i2rxumte.WrappedKeys? passwordKeys,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'halkyon_base.KeysPassword',
'salt': salt.toJson(),
'costs': costs.toJson(),
'passwordProofKey': passwordProofKey.toJson(),
'passwordKeys': passwordKeys.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'halkyon_base.KeysPassword',
'salt': salt.toJson(),
'costs': costs.toJsonForProtocol(),
'passwordProofKey': passwordProofKey.toJson(),
'passwordKeys': passwordKeys.toJsonForProtocol(),
};
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _KeysPasswordImpl extends KeysPassword {
_KeysPasswordImpl({
required _idt.ByteData salt,
required _ioe6tior.PasswordCosts costs,
required _idt.ByteData passwordProofKey,
required _i2rxumte.WrappedKeys passwordKeys,
}) : super._(
salt: salt,
costs: costs,
passwordProofKey: passwordProofKey,
passwordKeys: passwordKeys,
);
/// Returns a shallow copy of this [KeysPassword]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
KeysPassword copyWith({
_idt.ByteData? salt,
_ioe6tior.PasswordCosts? costs,
_idt.ByteData? passwordProofKey,
_i2rxumte.WrappedKeys? passwordKeys,
}) {
return KeysPassword(
salt: salt ?? this.salt.clone(),
costs: costs ?? this.costs.copyWith(),
passwordProofKey: passwordProofKey ?? this.passwordProofKey.clone(),
passwordKeys: passwordKeys ?? this.passwordKeys.copyWith(),
);
}
}

View file

@ -29,7 +29,12 @@ enum SignInChallengePurpose implements _isc.SerializableModel {
/// by signing a nonce with the password's proof key (Halkyon Learn's ADR /// by signing a nonce with the password's proof key (Halkyon Learn's ADR
/// 0020, D19): to get the keys on a device, or for a change. Counted per /// 0020, D19): to get the keys on a device, or for a change. Counted per
/// session (`asOwnerProofTry`). /// session (`asOwnerProofTry`).
passwordProof; passwordProof,
/// Proving the recovery phrase of an account without SRP's verifier,
/// signed in, by signing a nonce with the phrase's proof key, for a lost
/// password (Halkyon Learn's ADR 0020, D20). Counted per session.
phraseProof;
static SignInChallengePurpose fromJson(String name) { static SignInChallengePurpose fromJson(String name) {
switch (name) { switch (name) {
@ -41,6 +46,8 @@ enum SignInChallengePurpose implements _isc.SerializableModel {
return SignInChallengePurpose.change; return SignInChallengePurpose.change;
case 'passwordProof': case 'passwordProof':
return SignInChallengePurpose.passwordProof; return SignInChallengePurpose.passwordProof;
case 'phraseProof':
return SignInChallengePurpose.phraseProof;
default: default:
throw ArgumentError( throw ArgumentError(
'Value "$name" cannot be converted to "SignInChallengePurpose"', 'Value "$name" cannot be converted to "SignInChallengePurpose"',

View file

@ -28,6 +28,8 @@ import 'auth/sign_in_ways.dart' as _ie6tbh85;
import 'instance/instance_basics.dart' as _i8jixqx6; import 'instance/instance_basics.dart' as _i8jixqx6;
import 'keys/device_keys.dart' as _iz50cwqw; import 'keys/device_keys.dart' as _iz50cwqw;
import 'keys/keys_challenge.dart' as _itebbsfc; import 'keys/keys_challenge.dart' as _itebbsfc;
import 'keys/keys_owner_proof.dart' as _iis161ma;
import 'keys/keys_password.dart' as _i70xpmnz;
import 'keys/keys_proof.dart' as _iviximyh; import 'keys/keys_proof.dart' as _iviximyh;
import 'keys/keys_setup.dart' as _iqdhlx3t; import 'keys/keys_setup.dart' as _iqdhlx3t;
import 'password/account_reset_kind.dart' as _i91ka1it; import 'password/account_reset_kind.dart' as _i91ka1it;
@ -63,6 +65,8 @@ export 'auth/sign_in_ways.dart';
export 'instance/instance_basics.dart'; export 'instance/instance_basics.dart';
export 'keys/device_keys.dart'; export 'keys/device_keys.dart';
export 'keys/keys_challenge.dart'; export 'keys/keys_challenge.dart';
export 'keys/keys_owner_proof.dart';
export 'keys/keys_password.dart';
export 'keys/keys_proof.dart'; export 'keys/keys_proof.dart';
export 'keys/keys_setup.dart'; export 'keys/keys_setup.dart';
export 'password/account_reset_kind.dart'; export 'password/account_reset_kind.dart';
@ -163,6 +167,12 @@ class Protocol extends _isc.SerializationManager {
if (t == _itebbsfc.KeysChallenge) { if (t == _itebbsfc.KeysChallenge) {
return _itebbsfc.KeysChallenge.fromJson(data) as T; return _itebbsfc.KeysChallenge.fromJson(data) as T;
} }
if (t == _iis161ma.KeysOwnerProof) {
return _iis161ma.KeysOwnerProof.fromJson(data) as T;
}
if (t == _i70xpmnz.KeysPassword) {
return _i70xpmnz.KeysPassword.fromJson(data) as T;
}
if (t == _iviximyh.KeysProof) { if (t == _iviximyh.KeysProof) {
return _iviximyh.KeysProof.fromJson(data) as T; return _iviximyh.KeysProof.fromJson(data) as T;
} }
@ -281,6 +291,13 @@ class Protocol extends _isc.SerializationManager {
return (data != null ? _itebbsfc.KeysChallenge.fromJson(data) : null) return (data != null ? _itebbsfc.KeysChallenge.fromJson(data) : null)
as T; as T;
} }
if (t == _isc.getType<_iis161ma.KeysOwnerProof?>()) {
return (data != null ? _iis161ma.KeysOwnerProof.fromJson(data) : null)
as T;
}
if (t == _isc.getType<_i70xpmnz.KeysPassword?>()) {
return (data != null ? _i70xpmnz.KeysPassword.fromJson(data) : null) as T;
}
if (t == _isc.getType<_iviximyh.KeysProof?>()) { if (t == _isc.getType<_iviximyh.KeysProof?>()) {
return (data != null ? _iviximyh.KeysProof.fromJson(data) : null) as T; return (data != null ? _iviximyh.KeysProof.fromJson(data) : null) as T;
} }
@ -436,6 +453,8 @@ class Protocol extends _isc.SerializationManager {
_i8jixqx6.InstanceBasics => 'InstanceBasics', _i8jixqx6.InstanceBasics => 'InstanceBasics',
_iz50cwqw.DeviceKeys => 'DeviceKeys', _iz50cwqw.DeviceKeys => 'DeviceKeys',
_itebbsfc.KeysChallenge => 'KeysChallenge', _itebbsfc.KeysChallenge => 'KeysChallenge',
_iis161ma.KeysOwnerProof => 'KeysOwnerProof',
_i70xpmnz.KeysPassword => 'KeysPassword',
_iviximyh.KeysProof => 'KeysProof', _iviximyh.KeysProof => 'KeysProof',
_iqdhlx3t.KeysSetup => 'KeysSetup', _iqdhlx3t.KeysSetup => 'KeysSetup',
_i91ka1it.AccountResetKind => 'AccountResetKind', _i91ka1it.AccountResetKind => 'AccountResetKind',
@ -497,6 +516,10 @@ class Protocol extends _isc.SerializationManager {
return 'DeviceKeys'; return 'DeviceKeys';
case _itebbsfc.KeysChallenge(): case _itebbsfc.KeysChallenge():
return 'KeysChallenge'; return 'KeysChallenge';
case _iis161ma.KeysOwnerProof():
return 'KeysOwnerProof';
case _i70xpmnz.KeysPassword():
return 'KeysPassword';
case _iviximyh.KeysProof(): case _iviximyh.KeysProof():
return 'KeysProof'; return 'KeysProof';
case _iqdhlx3t.KeysSetup(): case _iqdhlx3t.KeysSetup():
@ -586,6 +609,12 @@ class Protocol extends _isc.SerializationManager {
if (dataClassName == 'KeysChallenge') { if (dataClassName == 'KeysChallenge') {
return deserialize<_itebbsfc.KeysChallenge>(data['data']); return deserialize<_itebbsfc.KeysChallenge>(data['data']);
} }
if (dataClassName == 'KeysOwnerProof') {
return deserialize<_iis161ma.KeysOwnerProof>(data['data']);
}
if (dataClassName == 'KeysPassword') {
return deserialize<_i70xpmnz.KeysPassword>(data['data']);
}
if (dataClassName == 'KeysProof') { if (dataClassName == 'KeysProof') {
return deserialize<_iviximyh.KeysProof>(data['data']); return deserialize<_iviximyh.KeysProof>(data['data']);
} }

View file

@ -66,7 +66,7 @@ abstract class SimpleLoginCompleted
/// Whether the account signed in to, or SimpleLogin was added to, has its /// Whether the account signed in to, or SimpleLogin was added to, has its
/// keys (`keys.unlock` opens them); one without (an account of before, /// keys (`keys.unlock` opens them); one without (an account of before,
/// adding SimpleLogin) makes them next. /// adding SimpleLogin) makes them next (`keys.set`).
bool hasKeys; bool hasKeys;
/// The account's handle; null for one without (it chooses one with its /// The account's handle; null for one without (it chooses one with its

View file

@ -158,6 +158,28 @@ Uint8List newPasswordDigest({
keysCipherText, keysCipherText,
]); ]);
/// The SHA-256 of a new password for the keys of an account that signs in
/// with SimpleLogin as the device sends it, framed: what a proof for it
/// binds (the old password's, or the phrase's).
Uint8List newKeysPasswordDigest({
required List<int> salt,
required String algorithm,
required int opsLimit,
required int memLimit,
required List<int> proofKey,
required List<int> keysNonce,
required List<int> keysCipherText,
}) => _sha256([
utf8.encode('halkyon new keys password v1'),
salt,
utf8.encode(algorithm),
utf8.encode('$opsLimit'),
utf8.encode('$memLimit'),
proofKey,
keysNonce,
keysCipherText,
]);
/// The SHA-256 of a new recovery phrase as the device sends it (the keys /// The SHA-256 of a new recovery phrase as the device sends it (the keys
/// wrapped under it and its proof key's public half), framed: what the /// wrapped under it and its proof key's public half), framed: what the
/// owner's proof for it binds. /// owner's proof for it binds.

View file

@ -20,11 +20,11 @@ Endpoints, under the nickname `base` in a host's client (`client.modules.base.em
| `authenticator` | `enrol`, `confirm` (the first brings ten recovery codes; another takes a code from one confirmed before), `list`, `remove`, `newRecoveryCodes` | signed in | | `authenticator` | `enrol`, `confirm` (the first brings ten recovery codes; another takes a code from one confirmed before), `list`, `remove`, `newRecoveryCodes` | signed in |
| `accountRecovery` | `resetAuthenticator`, `beginKeyProof`, `resetPassword` (with the recovery phrase), `startAccountReset`, `completeAccountReset` | without a session | | `accountRecovery` | `resetAuthenticator`, `beginKeyProof`, `resetPassword` (with the recovery phrase), `startAccountReset`, `completeAccountReset` | without a session |
| `simpleLogin` | `begin(platform)`: an attempt and SimpleLogin's address to open, at most 30 an hour per network; `complete(attempt, code)`: signed out, a session for the account with that SimpleLogin ID, or for someone new the handle suggested from their name; signed in, SimpleLogin added to the caller's account, who becomes `identified`; for an account, whether it has its keys and its handle, the attempt standing ten minutes for the sign-in; `checkHandle(attempt, handle)`: whether someone new could take a handle, at most 30 an hour per attempt and 60 per network; `signUp(attempt, handle, setup)`, without a session: someone new's account, `identified`, with the handle and the keys the device made; a session | anyone | | `simpleLogin` | `begin(platform)`: an attempt and SimpleLogin's address to open, at most 30 an hour per network; `complete(attempt, code)`: signed out, a session for the account with that SimpleLogin ID, or for someone new the handle suggested from their name; signed in, SimpleLogin added to the caller's account, who becomes `identified`; for an account, whether it has its keys and its handle, the attempt standing ten minutes for the sign-in; `checkHandle(attempt, handle)`: whether someone new could take a handle, at most 30 an hour per attempt and 60 per network; `signUp(attempt, handle, setup)`, without a session: someone new's account, `identified`, with the handle and the keys the device made; a session | anyone |
| `keys` | `begin`, `unlock`: the caller's keys for their device, once the password is proven with its own proof key (an account that signs in with SimpleLogin, Halkyon Learn's ADR 0020, D19); the tenth wrong proof in a row ends the session, as ADR 0017's do | signed in | | `keys` | for an account that signs in with SimpleLogin (Halkyon Learn's ADR 0020, D19 and D20): `begin`, `unlock`: the caller's keys for their device, once the password is proven with its own proof key; `set`: the first keys (and handle) of an account SimpleLogin was added to, with a SimpleLogin sign-in of the last ten minutes; `changePassword`, `beginPhrase` and `resetPassword` (a lost password, with the recovery phrase), `setRecoveryKeys`: each with the password's proof (the phrase's) and such a sign-in (`KeysOwnerProof`), a new password ending every other session; the tenth wrong proof in a row ends the session, as ADR 0017's do | signed in |
| `gitHubUpgrade` | `upgrade`: links a GitHub account to the caller, who becomes `identified`; refused where passwords or SimpleLogin are offered (deprecated) | signed in | | `gitHubUpgrade` | `upgrade`: links a GitHub account to the caller, who becomes `identified`; refused where passwords or SimpleLogin are offered (deprecated) | signed in |
| `gitHubSignIn` | `signIn`: a session for whoever has that GitHub account, unless they sign in with a password; none once `GITHUB_SIGN_IN_ENDS` has come | without a session | | `gitHubSignIn` | `signIn`: a session for whoever has that GitHub account, unless they sign in with a password; none once `GITHUB_SIGN_IN_ENDS` has come | without a session |
| `emailCode` | `sendCode`: a code by mail, which proves the address and signs nobody in | anyone | | `emailCode` | `sendCode`: a code by mail, which proves the address and signs nobody in | anyone |
| `account` | `delete`, `signInWays` | signed in | | `account` | `delete` (with the password's proof where the account has one, `keysProof` for one that signs in with SimpleLogin), `signInWays` | signed in |
| `instance` | `basics`: the app it serves, GitHub's client ID if any and when GitHub sign-in ends, whether codes by mail, password sign-in and SimpleLogin are on, the source link, the oldest app build, where data and backups are | anyone | | `instance` | `basics`: the app it serves, GitHub's client ID if any and when GitHub sign-in ends, whether codes by mail, password sign-in and SimpleLogin are on, the source link, the oldest app build, where data and backups are | anyone |
| `profile` | `mine`, `save`, `checkHandle`, `checkLinks` (at most 10 an hour), `setAvatar`, `removeAvatar`, `remove`: the caller's own public profile | `identified` | | `profile` | `mine`, `save`, `checkHandle`, `checkLinks` (at most 10 an hour), `setAvatar`, `removeAvatar`, `remove`: the caller's own public profile | `identified` |
| `profiles` | `byHandle`: anyone's public profile, whatever the handle's case | signed in | | `profiles` | `byHandle`: anyone's public profile, whatever the handle's case | signed in |
@ -60,7 +60,9 @@ Endpoints, under the nickname `base` in a host's client (`client.modules.base.em
password's own proof key (Ed25519) in place of SRP's verifier (D19). The password password's own proof key (Ed25519) in place of SRP's verifier (D19). The password
opens the keys and signs nobody in, so such an account has no authenticator (`enrol` opens the keys and signs nobody in, so such an account has no authenticator (`enrol`
refuses it `noPassword`). A sign-in into an account keeps its attempt ten minutes refuses it `noPassword`). A sign-in into an account keeps its attempt ten minutes
more, the account its caller: it confirms a change the device asks for next (D20). more, the account its caller: it confirms a change the device asks for next (D20),
which takes it, so each sign-in serves one change. A change checks the sign-in before
the proof, and takes it with the change, so a wrong proof leaves it standing.
- SimpleLogin's forwarding page (Halkyon Learn's ADR 0020; `addSimpleLoginCallbackRoute`, - SimpleLogin's forwarding page (Halkyon Learn's ADR 0020; `addSimpleLoginCallbackRoute`,
on the register, on halkyon_oidc's `addOidcCallbackForwarding`, ADR 0023): on the register, on halkyon_oidc's `addOidcCallbackForwarding`, ADR 0023):
`/simplelogin-callback`, the one address the instance's SimpleLogin `/simplelogin-callback`, the one address the instance's SimpleLogin

View file

@ -37,6 +37,7 @@ export 'src/password/key_proof.dart'
canonicalInstanceUrl, canonicalInstanceUrl,
isAccountSignature, isAccountSignature,
keyProofMessage, keyProofMessage,
newKeysPasswordDigest,
newPasswordDigest, newPasswordDigest,
newPhraseDigest, newPhraseDigest,
passwordIdentity, passwordIdentity,

View file

@ -21,14 +21,25 @@ class AccountEndpoint extends Endpoint {
/// 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. An account /// 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 /// with a password deletes itself only with [proof] (the password's proof
/// and a code, `requireOwnerToDelete`), which the register checks, its /// and a code, `requireOwnerToDelete`), one that signs in with SimpleLogin
/// base URL the one the device addressed here. /// only with [keysProof] (the password's proof for a challenge of
Future<void> delete(Session session, {DeletionProof? proof}) async { /// `keys.begin` and a SimpleLogin sign-in of the last ten minutes on the
/// device, Halkyon Learn's ADR 0020, D20), which the register checks,
/// their base URL the one the device addressed here.
Future<void> delete(
Session session, {
DeletionProof? proof,
KeysOwnerProof? keysProof,
}) async {
final pod = session.serverpod; final pod = session.serverpod;
final person = session.authenticated!.authUserId; final person = session.authenticated!.authUserId;
final bound = proof?.copyWith( final boundPassword = proof?.copyWith(
instanceUrl: deviceInstanceUrlOn(pod, proof.instanceUrl), instanceUrl: deviceInstanceUrlOn(pod, proof.instanceUrl),
); );
final boundKeys = keysProof?.copyWith(
instanceUrl: deviceInstanceUrlOn(pod, keysProof.instanceUrl),
);
final bound = boundPassword ?? boundKeys;
// The proof counts for this session (`asOwnerProofTry`), as every // The proof counts for this session (`asOwnerProofTry`), as every
// change of how the account is secured does: only around what checks // change of how the account is secured does: only around what checks
// it, so asking the register whose account it is spends no try (the // it, so asking the register whose account it is spends no try (the
@ -37,14 +48,24 @@ class AccountEndpoint extends Endpoint {
await asOwnerProofTry( await asOwnerProofTry(
session, session,
bound, bound,
() => requireOwnerToDelete(session, person, bound), () => requireOwnerToDelete(
session,
person,
boundPassword,
keysProof: boundKeys,
),
); );
} else if (await _registersAccount(session, person)) { } else if (await _registersAccount(session, person)) {
final register = AccountRegister.of(pod); final register = AccountRegister.of(pod);
await asOwnerProofTry( await asOwnerProofTry(
session, session,
bound, bound,
() => register.deleteAccount(session, person, proof: bound), () => register.deleteAccount(
session,
person,
proof: boundPassword,
keysProof: boundKeys,
),
); );
} }
await deletePerson(session, person); await deletePerson(session, person);

View file

@ -13,6 +13,10 @@
import 'dart:typed_data' as _idt; import 'dart:typed_data' as _idt;
import 'package:halkyon_base_server/src/generated/auth/email_code_purpose.dart' import 'package:halkyon_base_server/src/generated/auth/email_code_purpose.dart'
as _i5775c0l; as _i5775c0l;
import 'package:halkyon_base_server/src/generated/keys/keys_owner_proof.dart'
as _i561ypzv;
import 'package:halkyon_base_server/src/generated/keys/keys_password.dart'
as _iri6zrp4;
import 'package:halkyon_base_server/src/generated/keys/keys_proof.dart' import 'package:halkyon_base_server/src/generated/keys/keys_proof.dart'
as _iy592cuk; as _iy592cuk;
import 'package:halkyon_base_server/src/generated/keys/keys_setup.dart' import 'package:halkyon_base_server/src/generated/keys/keys_setup.dart'
@ -150,6 +154,11 @@ class Endpoints extends _is.EndpointDispatch {
type: _is.getType<_i619l6vv.DeletionProof?>(), type: _is.getType<_i619l6vv.DeletionProof?>(),
nullable: true, nullable: true,
), ),
'keysProof': _is.ParameterDescription(
name: 'keysProof',
type: _is.getType<_i561ypzv.KeysOwnerProof?>(),
nullable: true,
),
}, },
call: call:
( (
@ -159,6 +168,7 @@ class Endpoints extends _is.EndpointDispatch {
(endpoints['account'] as _ii6ycf6v.AccountEndpoint).delete( (endpoints['account'] as _ii6ycf6v.AccountEndpoint).delete(
session, session,
proof: params['proof'], proof: params['proof'],
keysProof: params['keysProof'],
), ),
), ),
'signInWays': _is.MethodConnector( 'signInWays': _is.MethodConnector(
@ -361,6 +371,127 @@ class Endpoints extends _is.EndpointDispatch {
params['proof'], params['proof'],
), ),
), ),
'set': _is.MethodConnector(
name: 'set',
params: {
'attempt': _is.ParameterDescription(
name: 'attempt',
type: _is.getType<_is.UuidValue>(),
nullable: false,
),
'setup': _is.ParameterDescription(
name: 'setup',
type: _is.getType<_ii64ejy6.KeysSetup>(),
nullable: false,
),
'handle': _is.ParameterDescription(
name: 'handle',
type: _is.getType<String?>(),
nullable: true,
),
},
call:
(
_is.Session session,
Map<String, dynamic> params,
) async => (endpoints['keys'] as _icowoccn.KeysEndpoint).set(
session,
attempt: params['attempt'],
setup: params['setup'],
handle: params['handle'],
),
),
'beginPhrase': _is.MethodConnector(
name: 'beginPhrase',
params: {},
call:
(
_is.Session session,
Map<String, dynamic> params,
) async => (endpoints['keys'] as _icowoccn.KeysEndpoint)
.beginPhrase(session),
),
'changePassword': _is.MethodConnector(
name: 'changePassword',
params: {
'proof': _is.ParameterDescription(
name: 'proof',
type: _is.getType<_i561ypzv.KeysOwnerProof>(),
nullable: false,
),
'password': _is.ParameterDescription(
name: 'password',
type: _is.getType<_iri6zrp4.KeysPassword>(),
nullable: false,
),
},
call:
(
_is.Session session,
Map<String, dynamic> params,
) async =>
(endpoints['keys'] as _icowoccn.KeysEndpoint).changePassword(
session,
proof: params['proof'],
password: params['password'],
),
),
'resetPassword': _is.MethodConnector(
name: 'resetPassword',
params: {
'proof': _is.ParameterDescription(
name: 'proof',
type: _is.getType<_i561ypzv.KeysOwnerProof>(),
nullable: false,
),
'password': _is.ParameterDescription(
name: 'password',
type: _is.getType<_iri6zrp4.KeysPassword>(),
nullable: false,
),
},
call:
(
_is.Session session,
Map<String, dynamic> params,
) async =>
(endpoints['keys'] as _icowoccn.KeysEndpoint).resetPassword(
session,
proof: params['proof'],
password: params['password'],
),
),
'setRecoveryKeys': _is.MethodConnector(
name: 'setRecoveryKeys',
params: {
'proof': _is.ParameterDescription(
name: 'proof',
type: _is.getType<_i561ypzv.KeysOwnerProof>(),
nullable: false,
),
'recoveryKeys': _is.ParameterDescription(
name: 'recoveryKeys',
type: _is.getType<_iji0wmu8.WrappedKeys>(),
nullable: false,
),
'recoveryProofKey': _is.ParameterDescription(
name: 'recoveryProofKey',
type: _is.getType<_idt.ByteData>(),
nullable: false,
),
},
call:
(
_is.Session session,
Map<String, dynamic> params,
) async =>
(endpoints['keys'] as _icowoccn.KeysEndpoint).setRecoveryKeys(
session,
proof: params['proof'],
recoveryKeys: params['recoveryKeys'],
recoveryProofKey: params['recoveryProofKey'],
),
),
}, },
); );
connectors['accountRecovery'] = _is.EndpointConnector( connectors['accountRecovery'] = _is.EndpointConnector(

View file

@ -0,0 +1,134 @@
/* 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 a change of an account that signs in with SimpleLogin takes
/// (Halkyon Learn's ADR 0020, D20), as ADR 0017's `OwnerProof` takes the
/// password and a code: a key proof for a challenge of `keys.begin` (the
/// password's proof key), or of `keys.beginPhrase` for a lost password
/// (the recovery phrase's), and a SimpleLogin sign-in of the last ten
/// minutes on this device, the attempt it ended (`simpleLogin.complete`),
/// taken by the change.
abstract class KeysOwnerProof
implements _is.SerializableModel, _is.ProtocolSerialization {
KeysOwnerProof._({
required this.challenge,
required this.signature,
required this.instanceUrl,
required this.attempt,
});
factory KeysOwnerProof({
required _is.UuidValue challenge,
required _idt.ByteData signature,
required String instanceUrl,
required _is.UuidValue attempt,
}) = _KeysOwnerProofImpl;
factory KeysOwnerProof.fromJson(Map<String, dynamic> jsonSerialization) {
return KeysOwnerProof(
challenge: _is.UuidValueJsonExtension.fromJson(
jsonSerialization['challenge'],
),
signature: _is.ByteDataJsonExtension.fromJson(
jsonSerialization['signature'],
),
instanceUrl: jsonSerialization['instanceUrl'] as String,
attempt: _is.UuidValueJsonExtension.fromJson(
jsonSerialization['attempt'],
),
);
}
_is.UuidValue challenge;
/// Ed25519's signature over `keyProofMessage`, 64 bytes.
_idt.ByteData signature;
/// The instance's base URL the device addressed (scheme, host, port,
/// path): the server it reached checks it is its own before the
/// register binds the proof to it.
String instanceUrl;
/// The SimpleLogin sign-in's attempt.
_is.UuidValue attempt;
/// Returns a shallow copy of this [KeysOwnerProof]
/// with some or all fields replaced by the given arguments.
@_is.useResult
KeysOwnerProof copyWith({
_is.UuidValue? challenge,
_idt.ByteData? signature,
String? instanceUrl,
_is.UuidValue? attempt,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'halkyon_base.KeysOwnerProof',
'challenge': challenge.toJson(),
'signature': signature.toJson(),
'instanceUrl': instanceUrl,
'attempt': attempt.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'halkyon_base.KeysOwnerProof',
'challenge': challenge.toJson(),
'signature': signature.toJson(),
'instanceUrl': instanceUrl,
'attempt': attempt.toJson(),
};
}
@override
String toString() {
return _is.SerializationManager.encode(this);
}
}
class _KeysOwnerProofImpl extends KeysOwnerProof {
_KeysOwnerProofImpl({
required _is.UuidValue challenge,
required _idt.ByteData signature,
required String instanceUrl,
required _is.UuidValue attempt,
}) : super._(
challenge: challenge,
signature: signature,
instanceUrl: instanceUrl,
attempt: attempt,
);
/// Returns a shallow copy of this [KeysOwnerProof]
/// with some or all fields replaced by the given arguments.
@_is.useResult
@override
KeysOwnerProof copyWith({
_is.UuidValue? challenge,
_idt.ByteData? signature,
String? instanceUrl,
_is.UuidValue? attempt,
}) {
return KeysOwnerProof(
challenge: challenge ?? this.challenge,
signature: signature ?? this.signature.clone(),
instanceUrl: instanceUrl ?? this.instanceUrl,
attempt: attempt ?? this.attempt,
);
}
}

View file

@ -0,0 +1,132 @@
/* 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:halkyon_base_server/src/generated/protocol.dart' as _ibewtqgw;
import 'package:serverpod/serverpod.dart' as _is;
import '../password/password_costs.dart' as _ioe6tior;
import '../password/wrapped_keys.dart' as _i2rxumte;
/// A new password for the keys of an account that signs in with SimpleLogin
/// (Halkyon Learn's ADR 0020, 7 and D19), all made on the device, which
/// holds the keys: the salt and costs it derived the password's keys with,
/// the public half of the password's proof key, and the account's private
/// keys wrapped under the password's key. As ADR 0017's `NewPassword`,
/// without SRP's verifier.
abstract class KeysPassword
implements _is.SerializableModel, _is.ProtocolSerialization {
KeysPassword._({
required this.salt,
required this.costs,
required this.passwordProofKey,
required this.passwordKeys,
});
factory KeysPassword({
required _idt.ByteData salt,
required _ioe6tior.PasswordCosts costs,
required _idt.ByteData passwordProofKey,
required _i2rxumte.WrappedKeys passwordKeys,
}) = _KeysPasswordImpl;
factory KeysPassword.fromJson(Map<String, dynamic> jsonSerialization) {
return KeysPassword(
salt: _is.ByteDataJsonExtension.fromJson(jsonSerialization['salt']),
costs: _ibewtqgw.Protocol().deserialize<_ioe6tior.PasswordCosts>(
jsonSerialization['costs'],
),
passwordProofKey: _is.ByteDataJsonExtension.fromJson(
jsonSerialization['passwordProofKey'],
),
passwordKeys: _ibewtqgw.Protocol().deserialize<_i2rxumte.WrappedKeys>(
jsonSerialization['passwordKeys'],
),
);
}
_idt.ByteData salt;
_ioe6tior.PasswordCosts costs;
/// The public half of the new password's proof key (Ed25519).
_idt.ByteData passwordProofKey;
_i2rxumte.WrappedKeys passwordKeys;
/// Returns a shallow copy of this [KeysPassword]
/// with some or all fields replaced by the given arguments.
@_is.useResult
KeysPassword copyWith({
_idt.ByteData? salt,
_ioe6tior.PasswordCosts? costs,
_idt.ByteData? passwordProofKey,
_i2rxumte.WrappedKeys? passwordKeys,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'halkyon_base.KeysPassword',
'salt': salt.toJson(),
'costs': costs.toJson(),
'passwordProofKey': passwordProofKey.toJson(),
'passwordKeys': passwordKeys.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'halkyon_base.KeysPassword',
'salt': salt.toJson(),
'costs': costs.toJsonForProtocol(),
'passwordProofKey': passwordProofKey.toJson(),
'passwordKeys': passwordKeys.toJsonForProtocol(),
};
}
@override
String toString() {
return _is.SerializationManager.encode(this);
}
}
class _KeysPasswordImpl extends KeysPassword {
_KeysPasswordImpl({
required _idt.ByteData salt,
required _ioe6tior.PasswordCosts costs,
required _idt.ByteData passwordProofKey,
required _i2rxumte.WrappedKeys passwordKeys,
}) : super._(
salt: salt,
costs: costs,
passwordProofKey: passwordProofKey,
passwordKeys: passwordKeys,
);
/// Returns a shallow copy of this [KeysPassword]
/// with some or all fields replaced by the given arguments.
@_is.useResult
@override
KeysPassword copyWith({
_idt.ByteData? salt,
_ioe6tior.PasswordCosts? costs,
_idt.ByteData? passwordProofKey,
_i2rxumte.WrappedKeys? passwordKeys,
}) {
return KeysPassword(
salt: salt ?? this.salt.clone(),
costs: costs ?? this.costs.copyWith(),
passwordProofKey: passwordProofKey ?? this.passwordProofKey.clone(),
passwordKeys: passwordKeys ?? this.passwordKeys.copyWith(),
);
}
}

View file

@ -29,7 +29,12 @@ enum SignInChallengePurpose implements _is.SerializableModel {
/// by signing a nonce with the password's proof key (Halkyon Learn's ADR /// by signing a nonce with the password's proof key (Halkyon Learn's ADR
/// 0020, D19): to get the keys on a device, or for a change. Counted per /// 0020, D19): to get the keys on a device, or for a change. Counted per
/// session (`asOwnerProofTry`). /// session (`asOwnerProofTry`).
passwordProof; passwordProof,
/// Proving the recovery phrase of an account without SRP's verifier,
/// signed in, by signing a nonce with the phrase's proof key, for a lost
/// password (Halkyon Learn's ADR 0020, D20). Counted per session.
phraseProof;
static SignInChallengePurpose fromJson(String name) { static SignInChallengePurpose fromJson(String name) {
switch (name) { switch (name) {
@ -41,6 +46,8 @@ enum SignInChallengePurpose implements _is.SerializableModel {
return SignInChallengePurpose.change; return SignInChallengePurpose.change;
case 'passwordProof': case 'passwordProof':
return SignInChallengePurpose.passwordProof; return SignInChallengePurpose.passwordProof;
case 'phraseProof':
return SignInChallengePurpose.phraseProof;
default: default:
throw ArgumentError( throw ArgumentError(
'Value "$name" cannot be converted to "SignInChallengePurpose"', 'Value "$name" cannot be converted to "SignInChallengePurpose"',

View file

@ -31,6 +31,8 @@ import 'auth/sign_in_ways.dart' as _ie6tbh85;
import 'instance/instance_basics.dart' as _i8jixqx6; import 'instance/instance_basics.dart' as _i8jixqx6;
import 'keys/device_keys.dart' as _iz50cwqw; import 'keys/device_keys.dart' as _iz50cwqw;
import 'keys/keys_challenge.dart' as _itebbsfc; import 'keys/keys_challenge.dart' as _itebbsfc;
import 'keys/keys_owner_proof.dart' as _iis161ma;
import 'keys/keys_password.dart' as _i70xpmnz;
import 'keys/keys_proof.dart' as _iviximyh; import 'keys/keys_proof.dart' as _iviximyh;
import 'keys/keys_setup.dart' as _iqdhlx3t; import 'keys/keys_setup.dart' as _iqdhlx3t;
import 'password/account_password.dart' as _ip37g5t3; import 'password/account_password.dart' as _ip37g5t3;
@ -80,6 +82,8 @@ export 'auth/sign_in_ways.dart';
export 'instance/instance_basics.dart'; export 'instance/instance_basics.dart';
export 'keys/device_keys.dart'; export 'keys/device_keys.dart';
export 'keys/keys_challenge.dart'; export 'keys/keys_challenge.dart';
export 'keys/keys_owner_proof.dart';
export 'keys/keys_password.dart';
export 'keys/keys_proof.dart'; export 'keys/keys_proof.dart';
export 'keys/keys_setup.dart'; export 'keys/keys_setup.dart';
export 'password/account_password.dart'; export 'password/account_password.dart';
@ -1101,6 +1105,12 @@ class Protocol extends _is.DatabaseSerializationManager {
if (t == _itebbsfc.KeysChallenge) { if (t == _itebbsfc.KeysChallenge) {
return _itebbsfc.KeysChallenge.fromJson(data) as T; return _itebbsfc.KeysChallenge.fromJson(data) as T;
} }
if (t == _iis161ma.KeysOwnerProof) {
return _iis161ma.KeysOwnerProof.fromJson(data) as T;
}
if (t == _i70xpmnz.KeysPassword) {
return _i70xpmnz.KeysPassword.fromJson(data) as T;
}
if (t == _iviximyh.KeysProof) { if (t == _iviximyh.KeysProof) {
return _iviximyh.KeysProof.fromJson(data) as T; return _iviximyh.KeysProof.fromJson(data) as T;
} }
@ -1263,6 +1273,13 @@ class Protocol extends _is.DatabaseSerializationManager {
return (data != null ? _itebbsfc.KeysChallenge.fromJson(data) : null) return (data != null ? _itebbsfc.KeysChallenge.fromJson(data) : null)
as T; as T;
} }
if (t == _is.getType<_iis161ma.KeysOwnerProof?>()) {
return (data != null ? _iis161ma.KeysOwnerProof.fromJson(data) : null)
as T;
}
if (t == _is.getType<_i70xpmnz.KeysPassword?>()) {
return (data != null ? _i70xpmnz.KeysPassword.fromJson(data) : null) as T;
}
if (t == _is.getType<_iviximyh.KeysProof?>()) { if (t == _is.getType<_iviximyh.KeysProof?>()) {
return (data != null ? _iviximyh.KeysProof.fromJson(data) : null) as T; return (data != null ? _iviximyh.KeysProof.fromJson(data) : null) as T;
} }
@ -1477,6 +1494,8 @@ class Protocol extends _is.DatabaseSerializationManager {
_i8jixqx6.InstanceBasics => 'InstanceBasics', _i8jixqx6.InstanceBasics => 'InstanceBasics',
_iz50cwqw.DeviceKeys => 'DeviceKeys', _iz50cwqw.DeviceKeys => 'DeviceKeys',
_itebbsfc.KeysChallenge => 'KeysChallenge', _itebbsfc.KeysChallenge => 'KeysChallenge',
_iis161ma.KeysOwnerProof => 'KeysOwnerProof',
_i70xpmnz.KeysPassword => 'KeysPassword',
_iviximyh.KeysProof => 'KeysProof', _iviximyh.KeysProof => 'KeysProof',
_iqdhlx3t.KeysSetup => 'KeysSetup', _iqdhlx3t.KeysSetup => 'KeysSetup',
_ip37g5t3.AccountPassword => 'AccountPassword', _ip37g5t3.AccountPassword => 'AccountPassword',
@ -1554,6 +1573,10 @@ class Protocol extends _is.DatabaseSerializationManager {
return 'DeviceKeys'; return 'DeviceKeys';
case _itebbsfc.KeysChallenge(): case _itebbsfc.KeysChallenge():
return 'KeysChallenge'; return 'KeysChallenge';
case _iis161ma.KeysOwnerProof():
return 'KeysOwnerProof';
case _i70xpmnz.KeysPassword():
return 'KeysPassword';
case _iviximyh.KeysProof(): case _iviximyh.KeysProof():
return 'KeysProof'; return 'KeysProof';
case _iqdhlx3t.KeysSetup(): case _iqdhlx3t.KeysSetup():
@ -1677,6 +1700,12 @@ class Protocol extends _is.DatabaseSerializationManager {
if (dataClassName == 'KeysChallenge') { if (dataClassName == 'KeysChallenge') {
return deserialize<_itebbsfc.KeysChallenge>(data['data']); return deserialize<_itebbsfc.KeysChallenge>(data['data']);
} }
if (dataClassName == 'KeysOwnerProof') {
return deserialize<_iis161ma.KeysOwnerProof>(data['data']);
}
if (dataClassName == 'KeysPassword') {
return deserialize<_i70xpmnz.KeysPassword>(data['data']);
}
if (dataClassName == 'KeysProof') { if (dataClassName == 'KeysProof') {
return deserialize<_iviximyh.KeysProof>(data['data']); return deserialize<_iviximyh.KeysProof>(data['data']);
} }

View file

@ -14,6 +14,11 @@ instance:
keys: keys:
- begin: - begin:
- unlock: - unlock:
- set:
- beginPhrase:
- changePassword:
- resetPassword:
- setRecoveryKeys:
accountRecovery: accountRecovery:
- resetAuthenticator: - resetAuthenticator:
- beginKeyProof: - beginKeyProof:

View file

@ -66,7 +66,7 @@ abstract class SimpleLoginCompleted
/// Whether the account signed in to, or SimpleLogin was added to, has its /// Whether the account signed in to, or SimpleLogin was added to, has its
/// keys (`keys.unlock` opens them); one without (an account of before, /// keys (`keys.unlock` opens them); one without (an account of before,
/// adding SimpleLogin) makes them next. /// adding SimpleLogin) makes them next (`keys.set`).
bool hasKeys; bool hasKeys;
/// The account's handle; null for one without (it chooses one with its /// The account's handle; null for one without (it chooses one with its

View file

@ -1,12 +1,20 @@
import 'dart:typed_data';
import 'package:clock/clock.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/key_proof.dart';
import 'package:halkyon_base_server/src/password/passwords.dart'; import 'package:halkyon_base_server/src/password/passwords.dart';
import 'package:halkyon_base_server/src/register/sessions_ended.dart';
import 'package:halkyon_base_server/src/simplelogin/simplelogin.dart';
import 'package:serverpod/serverpod.dart'; import 'package:serverpod/serverpod.dart';
// The keys of an account that signs in with SimpleLogin, as the instance's // The keys of an account that signs in with SimpleLogin, as the instance's
// register keeps them (Halkyon Learn's ADR 0020, 6, 7 and D19): ADR 0017's // register keeps them (Halkyon Learn's ADR 0020, 6, 7, D19 and D20): ADR
// keys and their wrappings, the password proven with its own proof key in // 0017's keys and their wrappings, the password proven with its own proof
// place of SRP. Refusals are `PasswordRefusedException`s. // key in place of SRP, and every change taking that proof (the phrase's,
// for a lost password) with a SimpleLogin sign-in of the last ten minutes,
// as ADR 0017's take the password and a code. Refusals are
// `PasswordRefusedException`s; the sign-in's, `OidcRefusedException`s.
/// [person]'s keys for a device, once [proof] proved their password for it /// [person]'s keys for a device, once [proof] proved their password for it
/// ([KeyProofPurpose.unlockKeys]): their public keys and their private keys /// ([KeyProofPurpose.unlockKeys]): their public keys and their private keys
@ -33,3 +41,324 @@ Future<DeviceKeys> unlockKeys(
), ),
); );
} }
/// Gives [person], an account without keys that SimpleLogin was added to
/// (one of before: anonymous, or signing in with GitHub or a code), the
/// keys [setup] describes, and [handle] where it has none (ADR 0020, 6 and
/// D5), with their SimpleLogin sign-in under [attempt] of the last ten
/// minutes, taken (D20). Nothing else proves the owner: the account has no
/// password yet, and nothing encrypted under keys of its own.
///
/// Refused ([PasswordRefusedException]) for a setup out of range
/// ([PasswordRefusal.invalidSetup]) and an account with keys already, under
/// either kind of password ([PasswordRefusal.passwordExists]);
/// ([ProfileRefusedException]) for a handle refused as saving a profile
/// refuses it, and one missing where the account has none or given where it
/// has one ([ProfileRefusal.handleInvalid]); the sign-in's refusals as
/// [takeSimpleLoginConfirmation]'s. Nothing changes then.
Future<void> setFirstKeys(
Session session, {
required UuidValue person,
required UuidValue attempt,
required KeysSetup setup,
String? handle,
}) async {
if (!isKeysSetup(setup)) {
throw refusedFor(PasswordRefusal.invalidSetup);
}
// SELECT count(*) FROM "account_password" WHERE "authUserId" = $1
if (await AccountPassword.db.count(
session,
where: (t) => t.authUserId.equals(person),
) >
0) {
throw refusedFor(PasswordRefusal.passwordExists);
}
// SELECT count(*) FROM "public_profile" WHERE "authUserId" = $1
final hasHandle =
await PublicProfile.db.count(
session,
where: (t) => t.authUserId.equals(person),
) >
0;
if (hasHandle == (handle != null)) {
throw ProfileRefusedException(reason: ProfileRefusal.handleInvalid);
}
try {
await session.db.transaction((transaction) async {
if (handle != null) {
if (await refusalOfNewHandle(session, handle, transaction: transaction)
case final reason?) {
throw ProfileRefusedException(reason: reason);
}
}
await takeSimpleLoginConfirmation(
session,
person: person,
attempt: attempt,
transaction: transaction,
);
final now = clock.now().toUtc();
if (handle != null) {
await insertFirstProfile(
session,
person,
handle,
now: now,
transaction: transaction,
);
}
// INSERT INTO "account_password" (…) VALUES (…)
await AccountPassword.db.insertRow(
session,
keysRowOf(person, setup, now: now),
transaction: transaction,
);
});
} on DatabaseQueryException catch (error) {
// A device of theirs set keys, or someone took the handle, between the
// checks and the writes: a unique index refused it (unique_violation),
// the sign-in given back.
if (error.code == '23505') {
throw error.constraintName == 'account_password_person_idx'
? refusedFor(PasswordRefusal.passwordExists)
: ProfileRefusedException(reason: ProfileRefusal.handleTaken);
}
rethrow;
}
}
/// Changes [person]'s password to [password] (the same keys, wrapped under
/// the new password's key on the device), with [proof]: the old password's
/// proof for it ([KeyProofPurpose.newPassword], binding the new password)
/// and their SimpleLogin sign-in of the last ten minutes, taken (D20). Every
/// other session of the account ends, in every service of the instance;
/// [keepSession], the one asking, stays.
///
/// Refused ([PasswordRefusedException]) for a new password out of range
/// ([PasswordRefusal.invalidSetup]), and as `provePasswordKey` refuses; the
/// sign-in's refusals as [requireSimpleLoginConfirmation]'s and
/// [takeSimpleLoginConfirmation]'s. Nothing changes then.
Future<void> changeKeysPassword(
Session session, {
required UuidValue person,
required KeysOwnerProof proof,
required KeysPassword password,
UuidValue? keepSession,
}) => _newPassword(
session,
person: person,
proof: proof,
password: password,
keepSession: keepSession,
prove: provePasswordKey,
purpose: KeyProofPurpose.newPassword,
);
/// Sets a new [password] for [person], who lost theirs, with [proof]: the
/// recovery phrase's proof for it, for a challenge of `beginPhraseKeyProof`
/// ([KeyProofPurpose.passwordReset], binding the new password), and their
/// SimpleLogin sign-in of the last ten minutes, taken (D20); the device
/// opened the keys with the phrase and wrapped them under the new
/// password's key. Every other session of the account ends, as
/// [changeKeysPassword] has it. Refusals as its, the phrase's as
/// `provePhraseKey`'s.
Future<void> resetKeysPassword(
Session session, {
required UuidValue person,
required KeysOwnerProof proof,
required KeysPassword password,
UuidValue? keepSession,
}) => _newPassword(
session,
person: person,
proof: proof,
password: password,
keepSession: keepSession,
prove: provePhraseKey,
purpose: KeyProofPurpose.passwordReset,
);
/// [changeKeysPassword] and [resetKeysPassword], the proof checked by
/// [prove] for [purpose].
Future<void> _newPassword(
Session session, {
required UuidValue person,
required KeysOwnerProof proof,
required KeysPassword password,
required UuidValue? keepSession,
required Future<AccountPassword> Function(
Session session, {
required UuidValue person,
required KeysProof proof,
required KeyProofPurpose purpose,
List<int> digest,
})
prove,
required KeyProofPurpose purpose,
}) async {
if (!isKeysPassword(password)) {
throw refusedFor(PasswordRefusal.invalidSetup);
}
await requireSimpleLoginConfirmation(
session,
person: person,
attempt: proof.attempt,
);
final row = await prove(
session,
person: person,
proof: keyProofOf(proof),
purpose: purpose,
digest: newKeysPasswordDigest(password),
);
await session.db.transaction((transaction) async {
await takeSimpleLoginConfirmation(
session,
person: person,
attempt: proof.attempt,
transaction: transaction,
);
// UPDATE "account_password" SET "salt" = $1, "kdfAlgorithm" = $2,
// "kdfOpsLimit" = $3, "kdfMemLimit" = $4, "passwordProofKey" = $5,
// "passwordKeysNonce" = $6, "passwordKeysCipherText" = $7,
// "sessionEndedForProofsAt" = NULL, "updatedAt" = $8 WHERE "id" = $9
// (a new password answers the warning of a session ended for its
// proofs: whoever tried the old one no longer knows it).
await AccountPassword.db.updateRow(
session,
row.copyWith(
salt: password.salt,
kdfAlgorithm: password.costs.algorithm,
kdfOpsLimit: password.costs.opsLimit,
kdfMemLimit: password.costs.memLimit,
passwordProofKey: password.passwordProofKey,
passwordKeysNonce: password.passwordKeys.nonce,
passwordKeysCipherText: password.passwordKeys.cipherText,
// A generated copyWith tells an explicit null (clear it) from no
// argument (keep it), which the lint can't see.
// ignore: avoid_redundant_argument_values
sessionEndedForProofsAt: null,
updatedAt: clock.now().toUtc(),
),
columns: (t) => [
t.salt,
t.kdfAlgorithm,
t.kdfOpsLimit,
t.kdfMemLimit,
t.passwordProofKey,
t.passwordKeysNonce,
t.passwordKeysCipherText,
t.sessionEndedForProofsAt,
t.updatedAt,
],
transaction: transaction,
);
await endSessionsEverywhere(
session,
person,
keepSession: keepSession,
transaction: transaction,
);
});
await announceSessionsEnded(session, person);
}
/// Replaces the wrapping of [person]'s keys under their recovery phrase
/// with [recoveryKeys] (a new phrase, made on the device, which holds the
/// keys) and the phrase's proof key with [recoveryProofKey], with [proof]:
/// the password's proof for it ([KeyProofPurpose.newPhrase], binding the
/// new phrase) and their SimpleLogin sign-in of the last ten minutes, taken
/// (D20). The old phrase opens and proves nothing any more.
///
/// Refused ([PasswordRefusedException]) for wrapped keys or a proof key out
/// of range ([PasswordRefusal.invalidSetup]), and as `provePasswordKey`
/// refuses; the sign-in's refusals as [changeKeysPassword]'s. Nothing
/// changes then.
Future<void> setKeysRecovery(
Session session, {
required UuidValue person,
required KeysOwnerProof proof,
required WrappedKeys recoveryKeys,
required ByteData recoveryProofKey,
}) async {
if (!isWrappedKeys(recoveryKeys) || !isPublicKey(recoveryProofKey)) {
throw refusedFor(PasswordRefusal.invalidSetup);
}
await requireSimpleLoginConfirmation(
session,
person: person,
attempt: proof.attempt,
);
final row = await provePasswordKey(
session,
person: person,
proof: keyProofOf(proof),
purpose: KeyProofPurpose.newPhrase,
digest: newPhraseDigest(recoveryKeys, recoveryProofKey),
);
await session.db.transaction((transaction) async {
await takeSimpleLoginConfirmation(
session,
person: person,
attempt: proof.attempt,
transaction: transaction,
);
// UPDATE "account_password" SET "recoveryKeysNonce" = $1,
// "recoveryKeysCipherText" = $2, "recoveryProofKey" = $3,
// "updatedAt" = $4 WHERE "id" = $5
// (Those columns alone: a change of password meanwhile stays.)
await AccountPassword.db.updateRow(
session,
row.copyWith(
recoveryKeysNonce: recoveryKeys.nonce,
recoveryKeysCipherText: recoveryKeys.cipherText,
recoveryProofKey: recoveryProofKey,
updatedAt: clock.now().toUtc(),
),
columns: (t) => [
t.recoveryKeysNonce,
t.recoveryKeysCipherText,
t.recoveryProofKey,
t.updatedAt,
],
transaction: transaction,
);
});
}
/// Proves [person], an account that signs in with SimpleLogin, may delete
/// itself: with [proof], the password's proof for it
/// ([KeyProofPurpose.deleteAccount]) and their SimpleLogin sign-in of the
/// last ten minutes, taken (D20); a session alone would otherwise delete the
/// account and everything encrypted under its keys. Refused without [proof]
/// ([PasswordRefusal.wrongCredentials]), and as [changeKeysPassword]
/// refuses.
Future<void> requireKeysOwnerToDelete(
Session session,
UuidValue person,
KeysOwnerProof? proof,
) async {
if (proof == null) {
throw refusedFor(PasswordRefusal.wrongCredentials);
}
await requireSimpleLoginConfirmation(
session,
person: person,
attempt: proof.attempt,
);
await provePasswordKey(
session,
person: person,
proof: keyProofOf(proof),
purpose: KeyProofPurpose.deleteAccount,
);
await session.db.transaction(
(transaction) => takeSimpleLoginConfirmation(
session,
person: person,
attempt: proof.attempt,
transaction: transaction,
),
);
}

View file

@ -1,3 +1,5 @@
import 'dart:typed_data';
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/key_proof.dart';
import 'package:halkyon_base_server/src/password/owner_proof_tries.dart'; import 'package:halkyon_base_server/src/password/owner_proof_tries.dart';
@ -6,9 +8,11 @@ import 'package:serverpod/serverpod.dart';
import 'package:serverpod_auth_idp_server/core.dart'; import 'package:serverpod_auth_idp_server/core.dart';
/// The keys of an account that signs in with SimpleLogin (Halkyon Learn's /// The keys of an account that signs in with SimpleLogin (Halkyon Learn's
/// ADR 0020, 7 and D19), for its signed-in devices: the password proven /// ADR 0020, 6, 7, D19 and D20), for its signed-in devices: the password
/// with its own proof key, which the instance's register checks (ADR 0016). /// proven with its own proof key, which the instance's register checks (ADR
/// Refusals are `PasswordRefusedException`s. /// 0016), and every change with that proof and a SimpleLogin sign-in of the
/// last ten minutes on the device (`KeysOwnerProof`). Refusals are
/// `PasswordRefusedException`s; the sign-in's, `OidcRefusedException`s.
class KeysEndpoint extends Endpoint { class KeysEndpoint extends Endpoint {
@override @override
bool get requireLogin => true; bool get requireLogin => true;
@ -45,4 +49,123 @@ class KeysEndpoint extends Endpoint {
), ),
); );
} }
/// Your first keys, for an account SimpleLogin was added to that has none
/// (`simpleLogin.complete` says `hasKeys`), all made on your device and
/// described by [setup]; [handle] where the account has none (it said no
/// `handle`), null otherwise. Takes your SimpleLogin sign-in of the last
/// ten minutes on this device, under [attempt]. Refused for an account
/// with keys (`PasswordRefusal.passwordExists`), a setup out of range
/// (`PasswordRefusal.invalidSetup`), a handle refused or missing
/// (`ProfileRefusedException`), and without that sign-in
/// (`OidcRefusal.unknownAttempt`).
Future<void> set(
Session session, {
required UuidValue attempt,
required KeysSetup setup,
String? handle,
}) => AccountRegister.of(session.serverpod).setFirstKeys(
session,
person: session.authenticated!.authUserId,
attempt: attempt,
setup: setup,
handle: handle,
);
/// The first step of proving your recovery phrase, for a password you
/// lost: your keys wrapped under the phrase's key and your public keys,
/// which your device opens with the phrase, and a nonce to sign with its
/// proof key, within two minutes, once. Refused for an account without
/// such keys (`PasswordRefusal.noPassword`).
Future<KeyProofChallenge> beginPhrase(Session session) =>
AccountRegister.of(session.serverpod).beginPhraseKeyProof(
session,
session.authenticated!.authUserId,
);
/// Changes your password to [password] (the same keys, wrapped under the
/// new password's key on your device), with [proof]: the old password's
/// for a challenge of [begin], binding the new password, and your
/// SimpleLogin sign-in of the last ten minutes on this device. Every
/// other session of yours ends, in every service of the instance; this
/// one stays. The proof counts against this session, as [unlock]'s.
Future<void> changePassword(
Session session, {
required KeysOwnerProof proof,
required KeysPassword password,
}) {
final bound = _bound(session, proof);
return asOwnerProofTry(
session,
bound,
() => AccountRegister.of(session.serverpod).changeKeysPassword(
session,
person: session.authenticated!.authUserId,
proof: bound,
password: password,
keepSession: _asking(session),
),
);
}
/// Sets a new [password] for the one you lost, with [proof]: your
/// recovery phrase's for a challenge of [beginPhrase], binding the new
/// password, and your SimpleLogin sign-in of the last ten minutes on this
/// device. Every other session of yours ends, as [changePassword] has it.
Future<void> resetPassword(
Session session, {
required KeysOwnerProof proof,
required KeysPassword password,
}) {
final bound = _bound(session, proof);
return asOwnerProofTry(
session,
bound,
() => AccountRegister.of(session.serverpod).resetKeysPassword(
session,
person: session.authenticated!.authUserId,
proof: bound,
password: password,
keepSession: _asking(session),
),
);
}
/// A new recovery phrase: [recoveryKeys], your keys wrapped under it on
/// your device, replace the old wrapping, and [recoveryProofKey], the
/// public half of its proof key, the old one, with [proof] as
/// [changePassword] takes it, binding the new phrase.
Future<void> setRecoveryKeys(
Session session, {
required KeysOwnerProof proof,
required WrappedKeys recoveryKeys,
required ByteData recoveryProofKey,
}) {
final bound = _bound(session, proof);
return asOwnerProofTry(
session,
bound,
() => AccountRegister.of(session.serverpod).setKeysRecovery(
session,
person: session.authenticated!.authUserId,
proof: bound,
recoveryKeys: recoveryKeys,
recoveryProofKey: recoveryProofKey,
),
);
}
/// [proof] bound to the base URL the device addressed here.
static KeysOwnerProof _bound(Session session, KeysOwnerProof proof) =>
proof.copyWith(
instanceUrl: deviceInstanceUrlOn(session.serverpod, proof.instanceUrl),
);
/// The session asking: a server-side session's ID.
static UuidValue? _asking(Session session) {
final authId = session.authenticated!.authId;
return Uuid.isValidUUID(fromString: authId)
? UuidValue.fromString(authId)
: null;
}
} }

Some files were not shown because too many files have changed in this diff Show more