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:
parent
090d901e92
commit
7c58d23424
39 changed files with 3163 additions and 78 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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"',
|
||||||
|
|
|
||||||
|
|
@ -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']);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ export 'src/password/key_proof.dart'
|
||||||
canonicalInstanceUrl,
|
canonicalInstanceUrl,
|
||||||
isAccountSignature,
|
isAccountSignature,
|
||||||
keyProofMessage,
|
keyProofMessage,
|
||||||
|
newKeysPasswordDigest,
|
||||||
newPasswordDigest,
|
newPasswordDigest,
|
||||||
newPhraseDigest,
|
newPhraseDigest,
|
||||||
passwordIdentity,
|
passwordIdentity,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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"',
|
||||||
|
|
|
||||||
|
|
@ -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']);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,11 @@ instance:
|
||||||
keys:
|
keys:
|
||||||
- begin:
|
- begin:
|
||||||
- unlock:
|
- unlock:
|
||||||
|
- set:
|
||||||
|
- beginPhrase:
|
||||||
|
- changePassword:
|
||||||
|
- resetPassword:
|
||||||
|
- setRecoveryKeys:
|
||||||
accountRecovery:
|
accountRecovery:
|
||||||
- resetAuthenticator:
|
- resetAuthenticator:
|
||||||
- beginKeyProof:
|
- beginKeyProof:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
Loading…
Reference in a new issue