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
31 changed files with 3163 additions and 78 deletions
|
|
@ -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 |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `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 |
|
||||
|
|
@ -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
|
||||
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
|
||||
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`,
|
||||
on the register, on halkyon_oidc's `addOidcCallbackForwarding`, ADR 0023):
|
||||
`/simplelogin-callback`, the one address the instance's SimpleLogin
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export 'src/password/key_proof.dart'
|
|||
canonicalInstanceUrl,
|
||||
isAccountSignature,
|
||||
keyProofMessage,
|
||||
newKeysPasswordDigest,
|
||||
newPasswordDigest,
|
||||
newPhraseDigest,
|
||||
passwordIdentity,
|
||||
|
|
|
|||
|
|
@ -21,14 +21,25 @@ class AccountEndpoint extends Endpoint {
|
|||
/// 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
|
||||
/// with a password deletes itself only with [proof] (the password's proof
|
||||
/// and a code, `requireOwnerToDelete`), which the register checks, its
|
||||
/// base URL the one the device addressed here.
|
||||
Future<void> delete(Session session, {DeletionProof? proof}) async {
|
||||
/// and a code, `requireOwnerToDelete`), one that signs in with SimpleLogin
|
||||
/// only with [keysProof] (the password's proof for a challenge of
|
||||
/// `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 person = session.authenticated!.authUserId;
|
||||
final bound = proof?.copyWith(
|
||||
final boundPassword = proof?.copyWith(
|
||||
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
|
||||
// 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
|
||||
|
|
@ -37,14 +48,24 @@ class AccountEndpoint extends Endpoint {
|
|||
await asOwnerProofTry(
|
||||
session,
|
||||
bound,
|
||||
() => requireOwnerToDelete(session, person, bound),
|
||||
() => requireOwnerToDelete(
|
||||
session,
|
||||
person,
|
||||
boundPassword,
|
||||
keysProof: boundKeys,
|
||||
),
|
||||
);
|
||||
} else if (await _registersAccount(session, person)) {
|
||||
final register = AccountRegister.of(pod);
|
||||
await asOwnerProofTry(
|
||||
session,
|
||||
bound,
|
||||
() => register.deleteAccount(session, person, proof: bound),
|
||||
() => register.deleteAccount(
|
||||
session,
|
||||
person,
|
||||
proof: boundPassword,
|
||||
keysProof: boundKeys,
|
||||
),
|
||||
);
|
||||
}
|
||||
await deletePerson(session, person);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@
|
|||
import 'dart:typed_data' as _idt;
|
||||
import 'package:halkyon_base_server/src/generated/auth/email_code_purpose.dart'
|
||||
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'
|
||||
as _iy592cuk;
|
||||
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?>(),
|
||||
nullable: true,
|
||||
),
|
||||
'keysProof': _is.ParameterDescription(
|
||||
name: 'keysProof',
|
||||
type: _is.getType<_i561ypzv.KeysOwnerProof?>(),
|
||||
nullable: true,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
|
|
@ -159,6 +168,7 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
(endpoints['account'] as _ii6ycf6v.AccountEndpoint).delete(
|
||||
session,
|
||||
proof: params['proof'],
|
||||
keysProof: params['keysProof'],
|
||||
),
|
||||
),
|
||||
'signInWays': _is.MethodConnector(
|
||||
|
|
@ -361,6 +371,127 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
/// 0020, D19): to get the keys on a device, or for a change. Counted per
|
||||
/// 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) {
|
||||
switch (name) {
|
||||
|
|
@ -41,6 +46,8 @@ enum SignInChallengePurpose implements _is.SerializableModel {
|
|||
return SignInChallengePurpose.change;
|
||||
case 'passwordProof':
|
||||
return SignInChallengePurpose.passwordProof;
|
||||
case 'phraseProof':
|
||||
return SignInChallengePurpose.phraseProof;
|
||||
default:
|
||||
throw ArgumentError(
|
||||
'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 'keys/device_keys.dart' as _iz50cwqw;
|
||||
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_setup.dart' as _iqdhlx3t;
|
||||
import 'password/account_password.dart' as _ip37g5t3;
|
||||
|
|
@ -80,6 +82,8 @@ export 'auth/sign_in_ways.dart';
|
|||
export 'instance/instance_basics.dart';
|
||||
export 'keys/device_keys.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_setup.dart';
|
||||
export 'password/account_password.dart';
|
||||
|
|
@ -1101,6 +1105,12 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
if (t == _itebbsfc.KeysChallenge) {
|
||||
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) {
|
||||
return _iviximyh.KeysProof.fromJson(data) as T;
|
||||
}
|
||||
|
|
@ -1263,6 +1273,13 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
return (data != null ? _itebbsfc.KeysChallenge.fromJson(data) : null)
|
||||
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?>()) {
|
||||
return (data != null ? _iviximyh.KeysProof.fromJson(data) : null) as T;
|
||||
}
|
||||
|
|
@ -1477,6 +1494,8 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
_i8jixqx6.InstanceBasics => 'InstanceBasics',
|
||||
_iz50cwqw.DeviceKeys => 'DeviceKeys',
|
||||
_itebbsfc.KeysChallenge => 'KeysChallenge',
|
||||
_iis161ma.KeysOwnerProof => 'KeysOwnerProof',
|
||||
_i70xpmnz.KeysPassword => 'KeysPassword',
|
||||
_iviximyh.KeysProof => 'KeysProof',
|
||||
_iqdhlx3t.KeysSetup => 'KeysSetup',
|
||||
_ip37g5t3.AccountPassword => 'AccountPassword',
|
||||
|
|
@ -1554,6 +1573,10 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
return 'DeviceKeys';
|
||||
case _itebbsfc.KeysChallenge():
|
||||
return 'KeysChallenge';
|
||||
case _iis161ma.KeysOwnerProof():
|
||||
return 'KeysOwnerProof';
|
||||
case _i70xpmnz.KeysPassword():
|
||||
return 'KeysPassword';
|
||||
case _iviximyh.KeysProof():
|
||||
return 'KeysProof';
|
||||
case _iqdhlx3t.KeysSetup():
|
||||
|
|
@ -1677,6 +1700,12 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
if (dataClassName == 'KeysChallenge') {
|
||||
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') {
|
||||
return deserialize<_iviximyh.KeysProof>(data['data']);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@ instance:
|
|||
keys:
|
||||
- begin:
|
||||
- unlock:
|
||||
- set:
|
||||
- beginPhrase:
|
||||
- changePassword:
|
||||
- resetPassword:
|
||||
- setRecoveryKeys:
|
||||
accountRecovery:
|
||||
- resetAuthenticator:
|
||||
- beginKeyProof:
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ abstract class SimpleLoginCompleted
|
|||
|
||||
/// Whether the account signed in to, or SimpleLogin was added to, has its
|
||||
/// 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;
|
||||
|
||||
/// 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/password/key_proof.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';
|
||||
|
||||
// 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
|
||||
// keys and their wrappings, the password proven with its own proof key in
|
||||
// place of SRP. Refusals are `PasswordRefusedException`s.
|
||||
// register keeps them (Halkyon Learn's ADR 0020, 6, 7, D19 and D20): ADR
|
||||
// 0017's keys and their wrappings, the password proven with its own proof
|
||||
// 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
|
||||
/// ([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/password/key_proof.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';
|
||||
|
||||
/// 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
|
||||
/// with its own proof key, which the instance's register checks (ADR 0016).
|
||||
/// Refusals are `PasswordRefusedException`s.
|
||||
/// ADR 0020, 6, 7, D19 and D20), for its signed-in devices: the password
|
||||
/// proven with its own proof key, which the instance's register checks (ADR
|
||||
/// 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 {
|
||||
@override
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
### 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.
|
||||
class: KeysOwnerProof
|
||||
fields:
|
||||
challenge: UuidValue
|
||||
### Ed25519's signature over `keyProofMessage`, 64 bytes.
|
||||
signature: ByteData
|
||||
### 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.
|
||||
instanceUrl: String
|
||||
### The SimpleLogin sign-in's attempt.
|
||||
attempt: UuidValue
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
### 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.
|
||||
class: KeysPassword
|
||||
fields:
|
||||
salt: ByteData
|
||||
costs: PasswordCosts
|
||||
### The public half of the new password's proof key (Ed25519).
|
||||
passwordProofKey: ByteData
|
||||
passwordKeys: WrappedKeys
|
||||
|
|
@ -161,6 +161,20 @@ Uint8List newPasswordDigest(NewPassword password) => _sha256([
|
|||
Uint8List.sublistView(password.passwordKeys.cipherText),
|
||||
]);
|
||||
|
||||
/// The SHA-256 of [password], a new password for the keys of an account
|
||||
/// that signs in with SimpleLogin, as the device sent it, framed: what a
|
||||
/// proof for it binds (the old password's, or the phrase's).
|
||||
Uint8List newKeysPasswordDigest(KeysPassword password) => _sha256([
|
||||
utf8.encode('halkyon new keys password v1'),
|
||||
Uint8List.sublistView(password.salt),
|
||||
utf8.encode(password.costs.algorithm),
|
||||
utf8.encode('${password.costs.opsLimit}'),
|
||||
utf8.encode('${password.costs.memLimit}'),
|
||||
Uint8List.sublistView(password.passwordProofKey),
|
||||
Uint8List.sublistView(password.passwordKeys.nonce),
|
||||
Uint8List.sublistView(password.passwordKeys.cipherText),
|
||||
]);
|
||||
|
||||
/// The SHA-256 of a new recovery phrase as the device sent it (the keys
|
||||
/// wrapped under it, [recoveryKeys], and its proof key's public half,
|
||||
/// [proofKey]), framed: what the owner's proof for it binds.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'dart:typed_data';
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:halkyon_base_server/src/auth/email.dart';
|
||||
import 'package:halkyon_base_server/src/generated/protocol.dart';
|
||||
import 'package:halkyon_base_server/src/keys/keys.dart';
|
||||
import 'package:halkyon_base_server/src/password/key_proof.dart';
|
||||
import 'package:halkyon_base_server/src/password/password_config.dart';
|
||||
import 'package:halkyon_base_server/src/password/passwords.dart';
|
||||
|
|
@ -150,6 +151,10 @@ Future<void> setRecoveryKeys(
|
|||
/// reset taken effect; the re-review's eighth finding: such accounts
|
||||
/// couldn't delete themselves at all), which `SignInWays` tells the app.
|
||||
///
|
||||
/// An account that signs in with SimpleLogin, its keys under a password
|
||||
/// without SRP's verifier, deletes itself with [keysProof] alone
|
||||
/// (`requireKeysOwnerToDelete`, Halkyon Learn's ADR 0020, D20).
|
||||
///
|
||||
/// Refused as [proveOwner] refuses, without [proof] where one is needed,
|
||||
/// and without the code its account takes
|
||||
/// ([PasswordRefusal.wrongCredentials]); a code by mail's own refusals as
|
||||
|
|
@ -157,16 +162,20 @@ Future<void> setRecoveryKeys(
|
|||
Future<void> requireOwnerToDelete(
|
||||
Session session,
|
||||
UuidValue person,
|
||||
DeletionProof? proof,
|
||||
) async {
|
||||
// SELECT count(*) FROM "account_password" WHERE "authUserId" = $1
|
||||
if (await AccountPassword.db.count(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(person),
|
||||
) ==
|
||||
0) {
|
||||
DeletionProof? proof, {
|
||||
KeysOwnerProof? keysProof,
|
||||
}) async {
|
||||
// SELECT * FROM "account_password" WHERE "authUserId" = $1 LIMIT 1
|
||||
final password = await AccountPassword.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(person),
|
||||
);
|
||||
if (password == null) {
|
||||
return;
|
||||
}
|
||||
if (password.verifier == null) {
|
||||
return await requireKeysOwnerToDelete(session, person, keysProof);
|
||||
}
|
||||
final keys = requirePasswordKeys(session.serverpod);
|
||||
if (proof == null) {
|
||||
throw refusedFor(PasswordRefusal.wrongCredentials);
|
||||
|
|
|
|||
|
|
@ -1557,17 +1557,97 @@ Future<AccountPassword> provePasswordKey(
|
|||
required KeysProof proof,
|
||||
required KeyProofPurpose purpose,
|
||||
List<int> digest = const [],
|
||||
}) => _proveOwnKey(
|
||||
session,
|
||||
person: person,
|
||||
proof: proof,
|
||||
begunFor: SignInChallengePurpose.passwordProof,
|
||||
keyOf: (password) => password.passwordProofKey,
|
||||
purpose: purpose,
|
||||
digest: digest,
|
||||
);
|
||||
|
||||
/// The first step of proving the recovery phrase of [person], an account
|
||||
/// that signs in with SimpleLogin, signed in, for a lost password (Halkyon
|
||||
/// Learn's ADR 0020, D20): the account's keys wrapped under the phrase's key
|
||||
/// and its public keys, which the device opens with the phrase, and a nonce
|
||||
/// it signs with the phrase's proof key. A session holds the account
|
||||
/// already, so there is no decoy; the proofs that fail count against it
|
||||
/// (`asOwnerProofTry`). Refused for an account without such keys
|
||||
/// ([PasswordRefusal.noPassword]: one with SRP's verifier resets its
|
||||
/// password as ADR 0017 has it).
|
||||
Future<KeyProofChallenge> beginPhraseKeyProof(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
}) async {
|
||||
final kept = await takeChallenge(
|
||||
final password = await _passwordOf(session, person);
|
||||
if (password == null || password.passwordProofKey == null) {
|
||||
throw refusedFor(PasswordRefusal.noPassword);
|
||||
}
|
||||
final nonce = _randomBytes(32);
|
||||
// INSERT INTO "sign_in_challenge" (…) VALUES (…)
|
||||
final kept = await SignInChallenge.db.insertRow(
|
||||
session,
|
||||
proof.challenge,
|
||||
SignInChallengePurpose.passwordProof,
|
||||
SignInChallenge(
|
||||
authUserId: person,
|
||||
purpose: SignInChallengePurpose.phraseProof,
|
||||
// Begun for an account, not an address: counted toward none.
|
||||
addressHash: '',
|
||||
secret: _hex(nonce),
|
||||
createdAt: clock.now().toUtc(),
|
||||
),
|
||||
);
|
||||
return KeyProofChallenge(
|
||||
challenge: kept.id!,
|
||||
nonce: ByteData.sublistView(nonce),
|
||||
encryptionPublicKey: password.encryptionPublicKey,
|
||||
signingPublicKey: password.signingPublicKey,
|
||||
recoveryKeys: WrappedKeys(
|
||||
nonce: password.recoveryKeysNonce,
|
||||
cipherText: password.recoveryKeysCipherText,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// [provePasswordKey] for the recovery phrase: the challenge one of
|
||||
/// [beginPhraseKeyProof], the signature by the phrase's proof key, for an
|
||||
/// account that signs in with SimpleLogin alone. Refusals alike.
|
||||
Future<AccountPassword> provePhraseKey(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required KeysProof proof,
|
||||
required KeyProofPurpose purpose,
|
||||
List<int> digest = const [],
|
||||
}) => _proveOwnKey(
|
||||
session,
|
||||
person: person,
|
||||
proof: proof,
|
||||
begunFor: SignInChallengePurpose.phraseProof,
|
||||
keyOf: (password) =>
|
||||
password.passwordProofKey == null ? null : password.recoveryProofKey,
|
||||
purpose: purpose,
|
||||
digest: digest,
|
||||
);
|
||||
|
||||
/// [proof] checked for [person] by the key [keyOf] picks from their
|
||||
/// password row (none: [PasswordRefusal.wrongCredentials]), its challenge
|
||||
/// one begun [begunFor] them: [provePasswordKey]'s and [provePhraseKey]'s
|
||||
/// check.
|
||||
Future<AccountPassword> _proveOwnKey(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required KeysProof proof,
|
||||
required SignInChallengePurpose begunFor,
|
||||
required ByteData? Function(AccountPassword password) keyOf,
|
||||
required KeyProofPurpose purpose,
|
||||
required List<int> digest,
|
||||
}) async {
|
||||
final kept = await takeChallenge(session, proof.challenge, begunFor);
|
||||
if (kept.authUserId != person) {
|
||||
throw refusedFor(PasswordRefusal.challengeExpired);
|
||||
}
|
||||
final password = await _passwordOf(session, person);
|
||||
final proofKey = password?.passwordProofKey;
|
||||
final proofKey = password == null ? null : keyOf(password);
|
||||
if (password == null ||
|
||||
proofKey == null ||
|
||||
!await isAccountSignature(
|
||||
|
|
@ -1586,6 +1666,20 @@ Future<AccountPassword> provePasswordKey(
|
|||
return password;
|
||||
}
|
||||
|
||||
/// [proof]'s key proof alone, without the SimpleLogin sign-in it names.
|
||||
KeysProof keyProofOf(KeysOwnerProof proof) => KeysProof(
|
||||
challenge: proof.challenge,
|
||||
signature: proof.signature,
|
||||
instanceUrl: proof.instanceUrl,
|
||||
);
|
||||
|
||||
/// Whether [password] is a new password for keys this register takes.
|
||||
bool isKeysPassword(KeysPassword password) =>
|
||||
password.salt.lengthInBytes == _saltLength &&
|
||||
_areCosts(password.costs) &&
|
||||
password.passwordProofKey.lengthInBytes == _publicKeyLength &&
|
||||
_areWrappedKeys(password.passwordKeys);
|
||||
|
||||
/// The costs [password] was derived with.
|
||||
PasswordCosts costsOf(AccountPassword password) => PasswordCosts(
|
||||
algorithm: password.kdfAlgorithm,
|
||||
|
|
|
|||
|
|
@ -15,3 +15,7 @@ values:
|
|||
### 0020, D19): to get the keys on a device, or for a change. Counted per
|
||||
### session (`asOwnerProofTry`).
|
||||
- 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
|
||||
|
|
|
|||
|
|
@ -195,6 +195,57 @@ abstract interface class AccountRegister {
|
|||
KeysProof proof,
|
||||
);
|
||||
|
||||
/// Gives [person], an account without keys that SimpleLogin was added to,
|
||||
/// the keys [setup] describes and [handle] where it has none, with their
|
||||
/// SimpleLogin sign-in under [attempt]. Refusals as `setFirstKeys`'.
|
||||
Future<void> setFirstKeys(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required UuidValue attempt,
|
||||
required KeysSetup setup,
|
||||
String? handle,
|
||||
});
|
||||
|
||||
/// The first step of [person] proving their recovery phrase, signed in,
|
||||
/// for a lost password (an account that signs in with SimpleLogin).
|
||||
/// Refusals as `beginPhraseKeyProof`'s.
|
||||
Future<KeyProofChallenge> beginPhraseKeyProof(
|
||||
Session session,
|
||||
UuidValue person,
|
||||
);
|
||||
|
||||
/// Changes [person]'s password for their keys to [password], with
|
||||
/// [proof]: every other session ends but [keepSession], in every service.
|
||||
/// Refusals as `changeKeysPassword`'s.
|
||||
Future<void> changeKeysPassword(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required KeysOwnerProof proof,
|
||||
required KeysPassword password,
|
||||
UuidValue? keepSession,
|
||||
});
|
||||
|
||||
/// Sets [person]'s new [password] for their keys with the recovery
|
||||
/// phrase's [proof], as [changeKeysPassword] does. Refusals as
|
||||
/// `resetKeysPassword`'s.
|
||||
Future<void> resetKeysPassword(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required KeysOwnerProof proof,
|
||||
required KeysPassword password,
|
||||
UuidValue? keepSession,
|
||||
});
|
||||
|
||||
/// Replaces [person]'s recovery phrase's wrapping and proof key, with
|
||||
/// [proof]. Refusals as `setKeysRecovery`'.
|
||||
Future<void> setKeysRecovery(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required KeysOwnerProof proof,
|
||||
required WrappedKeys recoveryKeys,
|
||||
required ByteData recoveryProofKey,
|
||||
});
|
||||
|
||||
/// How [person] signs in: with GitHub, an email address, a password and
|
||||
/// whether an authenticator is confirmed; and their warning that a session
|
||||
/// was ended for its wrong proofs, if any.
|
||||
|
|
@ -411,11 +462,13 @@ abstract interface class AccountRegister {
|
|||
|
||||
/// Deletes the account [person] with everything the register's server has
|
||||
/// of them, and has every member of the instance delete theirs: one with a
|
||||
/// password only with [proof] (`requireOwnerToDelete`).
|
||||
/// password only with [proof], one that signs in with SimpleLogin only
|
||||
/// with [keysProof] (`requireOwnerToDelete`).
|
||||
Future<void> deleteAccount(
|
||||
Session session,
|
||||
UuidValue person, {
|
||||
DeletionProof? proof,
|
||||
KeysOwnerProof? keysProof,
|
||||
});
|
||||
|
||||
/// Those of [people] that are accounts of the register.
|
||||
|
|
|
|||
|
|
@ -240,6 +240,48 @@ Future<Map<String, Object?>?> _answer(
|
|||
arguments.model('proof', KeysProof.fromJson),
|
||||
);
|
||||
return {'keys': keys.toJson()};
|
||||
case RegisterOperation.setFirstKeys:
|
||||
await register.setFirstKeys(
|
||||
session,
|
||||
person: arguments.id('person'),
|
||||
attempt: arguments.id('attempt'),
|
||||
setup: arguments.model('setup', KeysSetup.fromJson),
|
||||
handle: arguments.optionalString('handle'),
|
||||
);
|
||||
return const {};
|
||||
case RegisterOperation.beginPhraseKeyProof:
|
||||
final challenge = await register.beginPhraseKeyProof(
|
||||
session,
|
||||
arguments.id('person'),
|
||||
);
|
||||
return {'challenge': challenge.toJson()};
|
||||
case RegisterOperation.changeKeysPassword:
|
||||
await register.changeKeysPassword(
|
||||
session,
|
||||
person: arguments.id('person'),
|
||||
proof: arguments.model('proof', KeysOwnerProof.fromJson),
|
||||
password: arguments.model('password', KeysPassword.fromJson),
|
||||
keepSession: arguments.optionalId('keepSession'),
|
||||
);
|
||||
return const {};
|
||||
case RegisterOperation.resetKeysPassword:
|
||||
await register.resetKeysPassword(
|
||||
session,
|
||||
person: arguments.id('person'),
|
||||
proof: arguments.model('proof', KeysOwnerProof.fromJson),
|
||||
password: arguments.model('password', KeysPassword.fromJson),
|
||||
keepSession: arguments.optionalId('keepSession'),
|
||||
);
|
||||
return const {};
|
||||
case RegisterOperation.setKeysRecovery:
|
||||
await register.setKeysRecovery(
|
||||
session,
|
||||
person: arguments.id('person'),
|
||||
proof: arguments.model('proof', KeysOwnerProof.fromJson),
|
||||
recoveryKeys: arguments.model('recoveryKeys', WrappedKeys.fromJson),
|
||||
recoveryProofKey: arguments.bytes('recoveryProofKey'),
|
||||
);
|
||||
return const {};
|
||||
case RegisterOperation.signInWaysOf:
|
||||
final ways = await register.signInWaysOf(
|
||||
session,
|
||||
|
|
@ -264,6 +306,9 @@ Future<Map<String, Object?>?> _answer(
|
|||
proof: arguments.has('proof')
|
||||
? arguments.model('proof', DeletionProof.fromJson)
|
||||
: null,
|
||||
keysProof: arguments.has('keysProof')
|
||||
? arguments.model('keysProof', KeysOwnerProof.fromJson)
|
||||
: null,
|
||||
);
|
||||
return const {};
|
||||
case RegisterOperation.knownAccounts:
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue