feat(base): a reset of the keys without the phrase, and the keys' proof key for a password that signs in (ADR 0020, D31–D34)
An account that signs in with SimpleLogin alone, its password and its recovery phrase both lost, asks for new keys with a SimpleLogin sign-in (`keys.startReset`); another sign-in carries it out 72 hours later (`keys.completeReset`, `notDue` with when before), with the same keys rewrapped where the device still holds them (D33). No mail: every signed-in device shows it, and any right proof of the password's or the phrase's key, or a mend, calls it off; `keys.callOffResets` takes the password's proof alone (D32). The row is ADR 0017's account reset of kind `account`, so no migration; ADR 0017's reset by mail now takes only an account whose password signs in. An account with ADR 0017's password that added SimpleLogin had keys but no proof key, so no new device signed in with SimpleLogin could open them: `password.setProofKey` sets the one that same password makes, with the owner's proof binding it (D34), and ADR 0017's new password takes it away. Only an account that signs in with SimpleLogin alone changes through `keys`. `simpleLogin.complete` says whether the password signs in, whether the keys have the proof key, and when a reset takes effect. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
9e9f5bf406
commit
fe2ef02c92
33 changed files with 1973 additions and 52 deletions
|
|
@ -1,6 +1,6 @@
|
|||
# ADR 0020: Signing in with SimpleLogin only
|
||||
|
||||
- **Status:** decided 23 Oct 2026, by the developer (D1–D20 below; D21–D30 on 24
|
||||
- **Status:** decided 23 Oct 2026, by the developer (D1–D20 below; D21–D34 on 24
|
||||
Oct), the *Proposal* with it (D15), as D18 to D20 change it and D24 drops the
|
||||
cut-off's machinery (D8's code, D10, D22, 10's `ANONYMOUS_SESSIONS_END`, 11, step 5).
|
||||
- **Built (23 Oct 2026), step 1:** the forwarding page (3),
|
||||
|
|
@ -113,6 +113,18 @@
|
|||
device opens the keys with the phrase, wraps them under the new password and signs the
|
||||
phrase's proof bound to it (`keys.beginPhrase`, `keys.resetPassword`); the keys stay,
|
||||
every other session ends.
|
||||
Then the reset of the keys without the phrase, the server's half (D31–D33):
|
||||
`keys.startReset` with a SimpleLogin sign-in, taken, `keys.completeReset` once due
|
||||
with another (`notDue` with when, before), and `keys.callOffResets` with the
|
||||
password's proof (the new purpose `call off resets`), each through the register; every
|
||||
right proof of the password's or the phrase's key, and a mend's, calls the reset off.
|
||||
ADR 0017's reset by mail takes only an account whose password signs in. And D34's
|
||||
`password.setProofKey`, the owner's proof binding the key (the new purpose `keys proof
|
||||
key`); ADR 0017's new password takes the key away. Only an account that signs in with
|
||||
SimpleLogin alone changes through `keys` (`opensKeysAlone`). `simpleLogin.complete`
|
||||
says whether the password signs in too (`passwordSignsIn`), whether the keys have the
|
||||
proof key (`hasProofKey`) and when a reset takes effect (`accountResetAt`). No
|
||||
migration: the reset is ADR 0017's `account_reset` row of kind `account`.
|
||||
- **Builds on:** ADR 0010 (email sign-in), ADR 0011 (profile pages), ADR 0016 (the
|
||||
register), ADR 0017 (a password, TOTP and the keys).
|
||||
- **Changes:** ADR 0017's way in on an instance that offers SimpleLogin (its keys, their
|
||||
|
|
@ -302,6 +314,38 @@ against app.simplelogin.io on 23 Oct:
|
|||
own): the same field, checking as it is typed, for password sign-up, setting a
|
||||
password, SimpleLogin's sign-up and its keys step; the handle the instance suggested
|
||||
counts as free without asking.
|
||||
- **D31 A reset of the keys after 72 hours, with a banner** (24 Oct, evening,
|
||||
recommended, over 7 and 14 days): the password and the recovery phrase both lost, a
|
||||
SimpleLogin sign-in asks for new keys (`keys.startReset`), which a SimpleLogin sign-in
|
||||
carries out 72 hours later (`keys.completeReset`). No mail tells the owner, as
|
||||
SimpleLogin is their way in and its mailbox may be what was taken over: every
|
||||
signed-in device of theirs shows a banner meanwhile, and the next SimpleLogin sign-in
|
||||
says when it takes effect. Any proof of the password or the phrase calls it off: the
|
||||
keys opened on any device, a lost password set with the phrase, a link mended. The row
|
||||
is ADR 0017's account reset (kind `account`, with its lapses, no hold-outs); only an
|
||||
account that signs in with SimpleLogin alone has one this way, and ADR 0017's reset by
|
||||
mail is for passwords that sign in alone, so a mailbox never resets a SimpleLogin
|
||||
account. The handle, the profile, the link and what was published stay; what was
|
||||
encrypted under keys no device holds any more is lost (Learn shows it as Locked).
|
||||
- **D32 Called off with the password's proof** (24 Oct, recommended, over the password
|
||||
and a fresh SimpleLogin sign-in, which guards little, as a SimpleLogin account taken
|
||||
over is what starts a reset, and over the session alone, which would let a thief who
|
||||
holds one block the owner): `keys.callOffResets` from a signed-in device, under the
|
||||
banner.
|
||||
- **D33 A device that holds the keys keeps them** (24 Oct, recommended, over new keys
|
||||
always): finishing the reset there wraps the same keys under the new password and
|
||||
phrase, so nothing is lost.
|
||||
- **D34 A password that signs in gets the keys' proof key** (24 Oct, recommended, over
|
||||
leaving the gap for later, and over unlocking with SRP): found while planning D31, an
|
||||
account with ADR 0017's password that added SimpleLogin had keys but no proof key, so
|
||||
no new device signed in with SimpleLogin could open them. Its password and a code (ADR
|
||||
0017's owner proof) set the proof key that same password makes
|
||||
(`password.setProofKey`): on adding SimpleLogin, and again after a new password of ADR
|
||||
0017's, which takes it away, as it was made of the old one (over carrying it in ADR
|
||||
0017's new password, which would change those proofs' digests). Such an account opens
|
||||
its keys through `keys.unlock` but changes nothing through `keys`: its changes and
|
||||
resets stay ADR 0017's, which keep SRP's verifier in step. `simpleLogin.complete` says
|
||||
whether the password signs in and has the proof key.
|
||||
- **Also (23 Oct):** the developer's own account moves by adding SimpleLogin to it while
|
||||
signed in with GitHub; the instance's SimpleLogin app goes up for approval once
|
||||
halkyon.app/learn exists, before learn.halkyon.eu switches.
|
||||
|
|
|
|||
|
|
@ -193,9 +193,11 @@ class EndpointInstance extends _isc.EndpointRef {
|
|||
/// The keys of an account that signs in with SimpleLogin (Halkyon Learn'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.
|
||||
/// 0016), every change with that proof and a SimpleLogin sign-in of the
|
||||
/// last ten minutes on the device (`KeysOwnerProof`), and, the password and
|
||||
/// the phrase lost, a reset to new keys 72 hours later (D31 to D33).
|
||||
/// Refusals are `PasswordRefusedException`s; the sign-in's,
|
||||
/// `OidcRefusedException`s.
|
||||
/// {@category Endpoint}
|
||||
class EndpointKeys extends _isc.EndpointRef {
|
||||
EndpointKeys(_isc.EndpointCaller caller) : super(caller);
|
||||
|
|
@ -314,6 +316,57 @@ class EndpointKeys extends _isc.EndpointRef {
|
|||
'recoveryProofKey': recoveryProofKey,
|
||||
},
|
||||
);
|
||||
|
||||
/// Asks for the reset of your keys, your password and your recovery
|
||||
/// phrase lost (D31), with your SimpleLogin sign-in of the last ten
|
||||
/// minutes on this device, under [attempt]: it takes effect 72 hours
|
||||
/// later, which this answers, when [completeReset] gives you new keys.
|
||||
/// Your signed-in devices show it meanwhile, and your password or your
|
||||
/// phrase opening your keys anywhere calls it off, as [callOffResets]
|
||||
/// does. Asked again while it waits, it keeps its time. Refused for an
|
||||
/// account without such keys (`PasswordRefusal.noPassword`; one whose
|
||||
/// password signs in resets as `accountRecovery` has it) and without that
|
||||
/// sign-in (`OidcRefusal.unknownAttempt`).
|
||||
_ida.Future<_ippuf33k.ResetPending> startReset({
|
||||
required _isc.UuidValue attempt,
|
||||
}) => caller.callServerEndpoint<_ippuf33k.ResetPending>(
|
||||
'halkyon_base.keys',
|
||||
'startReset',
|
||||
{'attempt': attempt},
|
||||
);
|
||||
|
||||
/// Carries out the reset of your keys, due now, with your SimpleLogin
|
||||
/// sign-in of the last ten minutes on this device, under [attempt]:
|
||||
/// [setup], all made on your device (your old keys wrapped anew where it
|
||||
/// still holds them, D33), replaces your password, keys and phrase. What
|
||||
/// was encrypted under keys no device holds any more is lost; your
|
||||
/// handle, profile and SimpleLogin stay. Every other session of yours
|
||||
/// ends, in every service of the instance; this one stays. Refused before
|
||||
/// its time (`PasswordRefusal.notDue`, with when), without such a reset
|
||||
/// or once it lapsed (`PasswordRefusal.noReset`), for a setup out of range
|
||||
/// (`PasswordRefusal.invalidSetup`), and as [startReset] refuses.
|
||||
_ida.Future<void> completeReset({
|
||||
required _isc.UuidValue attempt,
|
||||
required _iec2fqr0.KeysSetup setup,
|
||||
}) => caller.callServerEndpoint<void>(
|
||||
'halkyon_base.keys',
|
||||
'completeReset',
|
||||
{
|
||||
'attempt': attempt,
|
||||
'setup': setup,
|
||||
},
|
||||
);
|
||||
|
||||
/// Calls off a reset of your keys waiting (D32), once [proof] proved your
|
||||
/// password for it, for a challenge of [begin]: whoever asked for it took
|
||||
/// over your SimpleLogin account. The proof counts against this session,
|
||||
/// as [unlock]'s. Refusals as [unlock]'s.
|
||||
_ida.Future<void> callOffResets({required _iznbn55x.KeysProof proof}) =>
|
||||
caller.callServerEndpoint<void>(
|
||||
'halkyon_base.keys',
|
||||
'callOffResets',
|
||||
{'proof': proof},
|
||||
);
|
||||
}
|
||||
|
||||
/// Getting back into an account (ADR 0017): a lost authenticator, a lost
|
||||
|
|
@ -605,6 +658,25 @@ class EndpointPassword extends _isc.EndpointRef {
|
|||
{'proof': proof},
|
||||
);
|
||||
|
||||
/// Sets [proofKey], the public half of the keys' proof key the caller's
|
||||
/// password makes on the device, with the owner's [proof] binding it: the
|
||||
/// keys then open with the password alone on a device signed in with
|
||||
/// SimpleLogin (`keys.unlock`; Halkyon Learn's ADR 0020, D34). A new
|
||||
/// password takes it away, as it is made of the password. Counted against
|
||||
/// this session as every owner's proof is. Refusals as
|
||||
/// `setPasswordProofKey`'s.
|
||||
_ida.Future<void> setProofKey({
|
||||
required _ikq9b2yz.OwnerProof proof,
|
||||
required _idt.ByteData proofKey,
|
||||
}) => caller.callServerEndpoint<void>(
|
||||
'halkyon_base.password',
|
||||
'setProofKey',
|
||||
{
|
||||
'proof': proof,
|
||||
'proofKey': proofKey,
|
||||
},
|
||||
);
|
||||
|
||||
/// A new recovery phrase: [recoveryKeys], the caller's keys wrapped under
|
||||
/// it on the device, replace the old wrapping, and [recoveryProofKey], the
|
||||
/// public half of its proof key, the old one, with the owner's [proof], as
|
||||
|
|
@ -901,7 +973,9 @@ class EndpointSimpleLogin extends _isc.EndpointRef {
|
|||
/// signed in, SimpleLogin added to the caller's account. Serverpod issues
|
||||
/// no session for another person inside a signed-in call, and nobody is
|
||||
/// switched to another account. For an account, whether it has its keys
|
||||
/// (`keys.unlock` opens them; one without makes them) and its handle; and
|
||||
/// (`keys.unlock` opens them; one without makes them), whether its
|
||||
/// password signs in too and has the keys' proof key (D34), when a reset
|
||||
/// of its keys takes effect (D31), and its handle; and
|
||||
/// for ten minutes the attempt's ID stands for this sign-in, for a change
|
||||
/// the device asks for (ADR 0020, D20). Refused as [begin] refuses its
|
||||
/// caller, and as `completeSimpleLogin` refuses.
|
||||
|
|
|
|||
|
|
@ -28,7 +28,12 @@ abstract class SimpleLoginCompleted
|
|||
this.suggestedHandle,
|
||||
bool? hasKeys,
|
||||
this.handle,
|
||||
}) : hasKeys = hasKeys ?? false;
|
||||
bool? passwordSignsIn,
|
||||
bool? hasProofKey,
|
||||
this.accountResetAt,
|
||||
}) : hasKeys = hasKeys ?? false,
|
||||
passwordSignsIn = passwordSignsIn ?? false,
|
||||
hasProofKey = hasProofKey ?? false;
|
||||
|
||||
factory SimpleLoginCompleted({
|
||||
required _i2lb2chw.SimpleLoginOutcome outcome,
|
||||
|
|
@ -36,6 +41,9 @@ abstract class SimpleLoginCompleted
|
|||
String? suggestedHandle,
|
||||
bool? hasKeys,
|
||||
String? handle,
|
||||
bool? passwordSignsIn,
|
||||
bool? hasProofKey,
|
||||
DateTime? accountResetAt,
|
||||
}) = _SimpleLoginCompletedImpl;
|
||||
|
||||
factory SimpleLoginCompleted.fromJson(
|
||||
|
|
@ -55,6 +63,19 @@ abstract class SimpleLoginCompleted
|
|||
? null
|
||||
: _isc.BoolJsonExtension.fromJson(jsonSerialization['hasKeys']),
|
||||
handle: jsonSerialization['handle'] as String?,
|
||||
passwordSignsIn: jsonSerialization['passwordSignsIn'] == null
|
||||
? null
|
||||
: _isc.BoolJsonExtension.fromJson(
|
||||
jsonSerialization['passwordSignsIn'],
|
||||
),
|
||||
hasProofKey: jsonSerialization['hasProofKey'] == null
|
||||
? null
|
||||
: _isc.BoolJsonExtension.fromJson(jsonSerialization['hasProofKey']),
|
||||
accountResetAt: jsonSerialization['accountResetAt'] == null
|
||||
? null
|
||||
: _isc.DateTimeJsonExtension.fromJson(
|
||||
jsonSerialization['accountResetAt'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -73,6 +94,22 @@ abstract class SimpleLoginCompleted
|
|||
/// keys).
|
||||
String? handle;
|
||||
|
||||
/// Whether the account's password signs in too (ADR 0017's, with SRP
|
||||
/// and a code): its changes and resets are the password sign-in's, never
|
||||
/// `keys`' (D34).
|
||||
bool passwordSignsIn;
|
||||
|
||||
/// Whether `keys.unlock` opens the keys: they have the password's proof
|
||||
/// key (D19). A password that signs in gets it once its password and a
|
||||
/// code set it (`password.setProofKey`, D34): on adding SimpleLogin, and
|
||||
/// again after a new password, as the key is made of the password.
|
||||
bool hasProofKey;
|
||||
|
||||
/// When a reset of the keys waiting (or due, not carried out) takes
|
||||
/// effect (D31): the password or the phrase opening them calls it off;
|
||||
/// due, `keys.completeReset` carries it out. Null for none.
|
||||
DateTime? accountResetAt;
|
||||
|
||||
/// Returns a shallow copy of this [SimpleLoginCompleted]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
|
|
@ -82,6 +119,9 @@ abstract class SimpleLoginCompleted
|
|||
String? suggestedHandle,
|
||||
bool? hasKeys,
|
||||
String? handle,
|
||||
bool? passwordSignsIn,
|
||||
bool? hasProofKey,
|
||||
DateTime? accountResetAt,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
|
|
@ -92,6 +132,9 @@ abstract class SimpleLoginCompleted
|
|||
if (suggestedHandle != null) 'suggestedHandle': suggestedHandle,
|
||||
'hasKeys': hasKeys,
|
||||
if (handle != null) 'handle': handle,
|
||||
'passwordSignsIn': passwordSignsIn,
|
||||
'hasProofKey': hasProofKey,
|
||||
if (accountResetAt != null) 'accountResetAt': accountResetAt?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -104,6 +147,9 @@ abstract class SimpleLoginCompleted
|
|||
if (suggestedHandle != null) 'suggestedHandle': suggestedHandle,
|
||||
'hasKeys': hasKeys,
|
||||
if (handle != null) 'handle': handle,
|
||||
'passwordSignsIn': passwordSignsIn,
|
||||
'hasProofKey': hasProofKey,
|
||||
if (accountResetAt != null) 'accountResetAt': accountResetAt?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -122,12 +168,18 @@ class _SimpleLoginCompletedImpl extends SimpleLoginCompleted {
|
|||
String? suggestedHandle,
|
||||
bool? hasKeys,
|
||||
String? handle,
|
||||
bool? passwordSignsIn,
|
||||
bool? hasProofKey,
|
||||
DateTime? accountResetAt,
|
||||
}) : super._(
|
||||
outcome: outcome,
|
||||
session: session,
|
||||
suggestedHandle: suggestedHandle,
|
||||
hasKeys: hasKeys,
|
||||
handle: handle,
|
||||
passwordSignsIn: passwordSignsIn,
|
||||
hasProofKey: hasProofKey,
|
||||
accountResetAt: accountResetAt,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [SimpleLoginCompleted]
|
||||
|
|
@ -140,6 +192,9 @@ class _SimpleLoginCompletedImpl extends SimpleLoginCompleted {
|
|||
Object? suggestedHandle = _Undefined,
|
||||
bool? hasKeys,
|
||||
Object? handle = _Undefined,
|
||||
bool? passwordSignsIn,
|
||||
bool? hasProofKey,
|
||||
Object? accountResetAt = _Undefined,
|
||||
}) {
|
||||
return SimpleLoginCompleted(
|
||||
outcome: outcome ?? this.outcome,
|
||||
|
|
@ -151,6 +206,11 @@ class _SimpleLoginCompletedImpl extends SimpleLoginCompleted {
|
|||
: this.suggestedHandle,
|
||||
hasKeys: hasKeys ?? this.hasKeys,
|
||||
handle: handle is String? ? handle : this.handle,
|
||||
passwordSignsIn: passwordSignsIn ?? this.passwordSignsIn,
|
||||
hasProofKey: hasProofKey ?? this.hasProofKey,
|
||||
accountResetAt: accountResetAt is DateTime?
|
||||
? accountResetAt
|
||||
: this.accountResetAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@ enum PasswordProofPurpose(final String label) {
|
|||
|
||||
/// Deleting the account.
|
||||
deleteAccount('delete account'),
|
||||
|
||||
/// Setting the keys' proof key, made of the same password, for devices
|
||||
/// signed in with SimpleLogin (Halkyon Learn's ADR 0020, D34).
|
||||
proofKey('keys proof key'),
|
||||
}
|
||||
|
||||
/// SRP's identity I for the [challenge] (its 16 bytes) on [instanceUrl],
|
||||
|
|
@ -112,6 +116,10 @@ enum KeyProofPurpose(final String label) {
|
|||
|
||||
/// Linking a new SimpleLogin ID in place of a lost one, by the phrase.
|
||||
mendLink('mend link'),
|
||||
|
||||
/// Calling off a reset of the keys waiting, by the password (Halkyon
|
||||
/// Learn's ADR 0020, D32).
|
||||
callOffResets('call off resets'),
|
||||
}
|
||||
|
||||
/// What the device signs with the recovery phrase's proof key to prove the
|
||||
|
|
@ -205,6 +213,12 @@ Uint8List mendLinkDigest(List<int> attempt) =>
|
|||
Uint8List authenticatorDigest(List<int> id) =>
|
||||
_sha256([utf8.encode('halkyon authenticator v1'), id]);
|
||||
|
||||
/// The SHA-256 of the public half of the keys' proof key a password makes
|
||||
/// (Halkyon Learn's ADR 0020, D34), framed: what the owner's proof for
|
||||
/// setting it binds.
|
||||
Uint8List proofKeyDigest(List<int> proofKey) =>
|
||||
_sha256([utf8.encode('halkyon keys proof key v1'), proofKey]);
|
||||
|
||||
Uint8List _sha256(List<List<int>> parts) =>
|
||||
Uint8List.fromList(sha256.convert(_framed(parts)).bytes);
|
||||
|
||||
|
|
|
|||
|
|
@ -14,12 +14,12 @@ Endpoints, under the nickname `base` in a host's client (`client.modules.base.em
|
|||
|
||||
| Endpoint | Methods | Who may call |
|
||||
|---|---|---|
|
||||
| `password` | `set`: the caller's password, with the address a mailed code proves unless they have one, and their first handle where they have none (Halkyon Learn's ADR 0020, D28); `beginChange`, `change`: a new password with the old one's proof and a code, every other session ending; `setRecoveryKeys`: a new recovery phrase's wrapping, with the old password's proof and a code | signed in |
|
||||
| `password` | `set`: the caller's password, with the address a mailed code proves unless they have one, and their first handle where they have none (Halkyon Learn's ADR 0020, D28); `beginChange`, `change`: a new password with the old one's proof and a code, every other session ending; `setRecoveryKeys`: a new recovery phrase's wrapping, with the old password's proof and a code; `setProofKey`: the keys' proof key the same password makes, with its proof and a code, so a device signed in with SimpleLogin opens the keys with the password (Halkyon Learn's ADR 0020, D34; a new password takes it away) | signed in |
|
||||
| `passwordSignIn` | `begin`, `complete`: SRP-6a's two steps with a code from an authenticator (or a recovery code; a setup never finished, a code by mail); a session. `checkHandle(request, handle)`: whether a sign-up could take a handle, while its mailed code waits (ADR 0020, D29); `signUp`: an account at the address a mailed code proves, with a password and a handle (an account there keeps its own where it has one); a session | without a session |
|
||||
| `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 (`identified` only, `wrongCaller` otherwise), SimpleLogin added to the caller's account; 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; `beginMend(attempt, handle)`, `mend(attempt, proof)`, without a session: a broken link mended with the handle and the recovery phrase, the new identity in the old one's place; a session | anyone |
|
||||
| `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 |
|
||||
| `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 (`identified` only, `wrongCaller` otherwise), SimpleLogin added to the caller's account; for an account, whether it has its keys, whether its password signs in too and has the keys' proof key (D34), when a reset of its keys takes effect (D31) 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; `beginMend(attempt, handle)`, `mend(attempt, proof)`, without a session: a broken link mended with the handle and the recovery phrase, the new identity in the old one's place; a session | anyone |
|
||||
| `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 password and the phrase lost (D31–D33), `startReset` with such a sign-in, `completeReset` 72 hours later with another (new keys, every other session ending), `callOffResets` with the password's proof, which any proof of the password or the phrase also does; only an account that signs in with SimpleLogin alone changes here, one whose password signs in only unlocking (D34); the tenth wrong proof in a row ends the session, as ADR 0017's do | 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` (with the password's proof where the account has one, `keysProof` for one that signs in with SimpleLogin), `signInWays` | signed in |
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ export 'src/password/key_proof.dart'
|
|||
newPasswordDigest,
|
||||
newPhraseDigest,
|
||||
passwordIdentity,
|
||||
proofKeyDigest,
|
||||
publicInstanceUrlOf;
|
||||
export 'src/password/password_config.dart';
|
||||
export 'src/password/passwords.dart'
|
||||
|
|
|
|||
|
|
@ -413,6 +413,69 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
recoveryProofKey: params['recoveryProofKey'],
|
||||
),
|
||||
),
|
||||
'startReset': _is.MethodConnector(
|
||||
name: 'startReset',
|
||||
params: {
|
||||
'attempt': _is.ParameterDescription(
|
||||
name: 'attempt',
|
||||
type: _is.getType<_is.UuidValue>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['keys'] as _icowoccn.KeysEndpoint).startReset(
|
||||
session,
|
||||
attempt: params['attempt'],
|
||||
),
|
||||
),
|
||||
'completeReset': _is.MethodConnector(
|
||||
name: 'completeReset',
|
||||
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,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['keys'] as _icowoccn.KeysEndpoint).completeReset(
|
||||
session,
|
||||
attempt: params['attempt'],
|
||||
setup: params['setup'],
|
||||
),
|
||||
),
|
||||
'callOffResets': _is.MethodConnector(
|
||||
name: 'callOffResets',
|
||||
params: {
|
||||
'proof': _is.ParameterDescription(
|
||||
name: 'proof',
|
||||
type: _is.getType<_iy592cuk.KeysProof>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['keys'] as _icowoccn.KeysEndpoint).callOffResets(
|
||||
session,
|
||||
proof: params['proof'],
|
||||
),
|
||||
),
|
||||
},
|
||||
);
|
||||
connectors['accountRecovery'] = _is.EndpointConnector(
|
||||
|
|
@ -879,6 +942,31 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
proof: params['proof'],
|
||||
),
|
||||
),
|
||||
'setProofKey': _is.MethodConnector(
|
||||
name: 'setProofKey',
|
||||
params: {
|
||||
'proof': _is.ParameterDescription(
|
||||
name: 'proof',
|
||||
type: _is.getType<_ifhod73s.OwnerProof>(),
|
||||
nullable: false,
|
||||
),
|
||||
'proofKey': _is.ParameterDescription(
|
||||
name: 'proofKey',
|
||||
type: _is.getType<_idt.ByteData>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async => (endpoints['password'] as _ig7fyg1b.PasswordEndpoint)
|
||||
.setProofKey(
|
||||
session,
|
||||
proof: params['proof'],
|
||||
proofKey: params['proofKey'],
|
||||
),
|
||||
),
|
||||
'setRecoveryKeys': _is.MethodConnector(
|
||||
name: 'setRecoveryKeys',
|
||||
params: {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ keys:
|
|||
- changePassword:
|
||||
- resetPassword:
|
||||
- setRecoveryKeys:
|
||||
- startReset:
|
||||
- completeReset:
|
||||
- callOffResets:
|
||||
accountRecovery:
|
||||
- resetAuthenticator:
|
||||
- beginKeyProof:
|
||||
|
|
@ -32,6 +35,7 @@ password:
|
|||
- beginChange:
|
||||
- change:
|
||||
- callOffResets:
|
||||
- setProofKey:
|
||||
- setRecoveryKeys:
|
||||
- dismissSessionEndedWarning:
|
||||
passwordSignIn:
|
||||
|
|
|
|||
|
|
@ -28,7 +28,12 @@ abstract class SimpleLoginCompleted
|
|||
this.suggestedHandle,
|
||||
bool? hasKeys,
|
||||
this.handle,
|
||||
}) : hasKeys = hasKeys ?? false;
|
||||
bool? passwordSignsIn,
|
||||
bool? hasProofKey,
|
||||
this.accountResetAt,
|
||||
}) : hasKeys = hasKeys ?? false,
|
||||
passwordSignsIn = passwordSignsIn ?? false,
|
||||
hasProofKey = hasProofKey ?? false;
|
||||
|
||||
factory SimpleLoginCompleted({
|
||||
required _i2lb2chw.SimpleLoginOutcome outcome,
|
||||
|
|
@ -36,6 +41,9 @@ abstract class SimpleLoginCompleted
|
|||
String? suggestedHandle,
|
||||
bool? hasKeys,
|
||||
String? handle,
|
||||
bool? passwordSignsIn,
|
||||
bool? hasProofKey,
|
||||
DateTime? accountResetAt,
|
||||
}) = _SimpleLoginCompletedImpl;
|
||||
|
||||
factory SimpleLoginCompleted.fromJson(
|
||||
|
|
@ -55,6 +63,19 @@ abstract class SimpleLoginCompleted
|
|||
? null
|
||||
: _is.BoolJsonExtension.fromJson(jsonSerialization['hasKeys']),
|
||||
handle: jsonSerialization['handle'] as String?,
|
||||
passwordSignsIn: jsonSerialization['passwordSignsIn'] == null
|
||||
? null
|
||||
: _is.BoolJsonExtension.fromJson(
|
||||
jsonSerialization['passwordSignsIn'],
|
||||
),
|
||||
hasProofKey: jsonSerialization['hasProofKey'] == null
|
||||
? null
|
||||
: _is.BoolJsonExtension.fromJson(jsonSerialization['hasProofKey']),
|
||||
accountResetAt: jsonSerialization['accountResetAt'] == null
|
||||
? null
|
||||
: _is.DateTimeJsonExtension.fromJson(
|
||||
jsonSerialization['accountResetAt'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -73,6 +94,22 @@ abstract class SimpleLoginCompleted
|
|||
/// keys).
|
||||
String? handle;
|
||||
|
||||
/// Whether the account's password signs in too (ADR 0017's, with SRP
|
||||
/// and a code): its changes and resets are the password sign-in's, never
|
||||
/// `keys`' (D34).
|
||||
bool passwordSignsIn;
|
||||
|
||||
/// Whether `keys.unlock` opens the keys: they have the password's proof
|
||||
/// key (D19). A password that signs in gets it once its password and a
|
||||
/// code set it (`password.setProofKey`, D34): on adding SimpleLogin, and
|
||||
/// again after a new password, as the key is made of the password.
|
||||
bool hasProofKey;
|
||||
|
||||
/// When a reset of the keys waiting (or due, not carried out) takes
|
||||
/// effect (D31): the password or the phrase opening them calls it off;
|
||||
/// due, `keys.completeReset` carries it out. Null for none.
|
||||
DateTime? accountResetAt;
|
||||
|
||||
/// Returns a shallow copy of this [SimpleLoginCompleted]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
|
|
@ -82,6 +119,9 @@ abstract class SimpleLoginCompleted
|
|||
String? suggestedHandle,
|
||||
bool? hasKeys,
|
||||
String? handle,
|
||||
bool? passwordSignsIn,
|
||||
bool? hasProofKey,
|
||||
DateTime? accountResetAt,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
|
|
@ -92,6 +132,9 @@ abstract class SimpleLoginCompleted
|
|||
if (suggestedHandle != null) 'suggestedHandle': suggestedHandle,
|
||||
'hasKeys': hasKeys,
|
||||
if (handle != null) 'handle': handle,
|
||||
'passwordSignsIn': passwordSignsIn,
|
||||
'hasProofKey': hasProofKey,
|
||||
if (accountResetAt != null) 'accountResetAt': accountResetAt?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -104,6 +147,9 @@ abstract class SimpleLoginCompleted
|
|||
if (suggestedHandle != null) 'suggestedHandle': suggestedHandle,
|
||||
'hasKeys': hasKeys,
|
||||
if (handle != null) 'handle': handle,
|
||||
'passwordSignsIn': passwordSignsIn,
|
||||
'hasProofKey': hasProofKey,
|
||||
if (accountResetAt != null) 'accountResetAt': accountResetAt?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -122,12 +168,18 @@ class _SimpleLoginCompletedImpl extends SimpleLoginCompleted {
|
|||
String? suggestedHandle,
|
||||
bool? hasKeys,
|
||||
String? handle,
|
||||
bool? passwordSignsIn,
|
||||
bool? hasProofKey,
|
||||
DateTime? accountResetAt,
|
||||
}) : super._(
|
||||
outcome: outcome,
|
||||
session: session,
|
||||
suggestedHandle: suggestedHandle,
|
||||
hasKeys: hasKeys,
|
||||
handle: handle,
|
||||
passwordSignsIn: passwordSignsIn,
|
||||
hasProofKey: hasProofKey,
|
||||
accountResetAt: accountResetAt,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [SimpleLoginCompleted]
|
||||
|
|
@ -140,6 +192,9 @@ class _SimpleLoginCompletedImpl extends SimpleLoginCompleted {
|
|||
Object? suggestedHandle = _Undefined,
|
||||
bool? hasKeys,
|
||||
Object? handle = _Undefined,
|
||||
bool? passwordSignsIn,
|
||||
bool? hasProofKey,
|
||||
Object? accountResetAt = _Undefined,
|
||||
}) {
|
||||
return SimpleLoginCompleted(
|
||||
outcome: outcome ?? this.outcome,
|
||||
|
|
@ -151,6 +206,11 @@ class _SimpleLoginCompletedImpl extends SimpleLoginCompleted {
|
|||
: this.suggestedHandle,
|
||||
hasKeys: hasKeys ?? this.hasKeys,
|
||||
handle: handle is String? ? handle : this.handle,
|
||||
passwordSignsIn: passwordSignsIn ?? this.passwordSignsIn,
|
||||
hasProofKey: hasProofKey ?? this.hasProofKey,
|
||||
accountResetAt: accountResetAt is DateTime?
|
||||
? accountResetAt
|
||||
: this.accountResetAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,8 +25,13 @@ abstract class SimpleLoginProof
|
|||
this.suggestedHandle,
|
||||
bool? hasKeys,
|
||||
this.handle,
|
||||
bool? passwordSignsIn,
|
||||
bool? hasProofKey,
|
||||
this.accountResetAt,
|
||||
this.endedBefore,
|
||||
}) : hasKeys = hasKeys ?? false;
|
||||
}) : hasKeys = hasKeys ?? false,
|
||||
passwordSignsIn = passwordSignsIn ?? false,
|
||||
hasProofKey = hasProofKey ?? false;
|
||||
|
||||
factory SimpleLoginProof({
|
||||
required _i2lb2chw.SimpleLoginOutcome outcome,
|
||||
|
|
@ -34,6 +39,9 @@ abstract class SimpleLoginProof
|
|||
String? suggestedHandle,
|
||||
bool? hasKeys,
|
||||
String? handle,
|
||||
bool? passwordSignsIn,
|
||||
bool? hasProofKey,
|
||||
DateTime? accountResetAt,
|
||||
DateTime? endedBefore,
|
||||
}) = _SimpleLoginProofImpl;
|
||||
|
||||
|
|
@ -50,6 +58,19 @@ abstract class SimpleLoginProof
|
|||
? null
|
||||
: _is.BoolJsonExtension.fromJson(jsonSerialization['hasKeys']),
|
||||
handle: jsonSerialization['handle'] as String?,
|
||||
passwordSignsIn: jsonSerialization['passwordSignsIn'] == null
|
||||
? null
|
||||
: _is.BoolJsonExtension.fromJson(
|
||||
jsonSerialization['passwordSignsIn'],
|
||||
),
|
||||
hasProofKey: jsonSerialization['hasProofKey'] == null
|
||||
? null
|
||||
: _is.BoolJsonExtension.fromJson(jsonSerialization['hasProofKey']),
|
||||
accountResetAt: jsonSerialization['accountResetAt'] == null
|
||||
? null
|
||||
: _is.DateTimeJsonExtension.fromJson(
|
||||
jsonSerialization['accountResetAt'],
|
||||
),
|
||||
endedBefore: jsonSerialization['endedBefore'] == null
|
||||
? null
|
||||
: _is.DateTimeJsonExtension.fromJson(
|
||||
|
|
@ -70,6 +91,15 @@ abstract class SimpleLoginProof
|
|||
/// The account's handle; null for none.
|
||||
String? handle;
|
||||
|
||||
/// Whether the account's password signs in too (ADR 0017's).
|
||||
bool passwordSignsIn;
|
||||
|
||||
/// Whether the keys have the password's proof key (D19, D34).
|
||||
bool hasProofKey;
|
||||
|
||||
/// When a reset of the keys waiting (or due) takes effect (D31).
|
||||
DateTime? accountResetAt;
|
||||
|
||||
/// For a link mended (8): the moment every session of the account made
|
||||
/// before ended, which the session issued for it comes after
|
||||
/// (`issueSessionAfter`); null otherwise.
|
||||
|
|
@ -84,6 +114,9 @@ abstract class SimpleLoginProof
|
|||
String? suggestedHandle,
|
||||
bool? hasKeys,
|
||||
String? handle,
|
||||
bool? passwordSignsIn,
|
||||
bool? hasProofKey,
|
||||
DateTime? accountResetAt,
|
||||
DateTime? endedBefore,
|
||||
});
|
||||
@override
|
||||
|
|
@ -95,6 +128,9 @@ abstract class SimpleLoginProof
|
|||
if (suggestedHandle != null) 'suggestedHandle': suggestedHandle,
|
||||
'hasKeys': hasKeys,
|
||||
if (handle != null) 'handle': handle,
|
||||
'passwordSignsIn': passwordSignsIn,
|
||||
'hasProofKey': hasProofKey,
|
||||
if (accountResetAt != null) 'accountResetAt': accountResetAt?.toJson(),
|
||||
if (endedBefore != null) 'endedBefore': endedBefore?.toJson(),
|
||||
};
|
||||
}
|
||||
|
|
@ -119,6 +155,9 @@ class _SimpleLoginProofImpl extends SimpleLoginProof {
|
|||
String? suggestedHandle,
|
||||
bool? hasKeys,
|
||||
String? handle,
|
||||
bool? passwordSignsIn,
|
||||
bool? hasProofKey,
|
||||
DateTime? accountResetAt,
|
||||
DateTime? endedBefore,
|
||||
}) : super._(
|
||||
outcome: outcome,
|
||||
|
|
@ -126,6 +165,9 @@ class _SimpleLoginProofImpl extends SimpleLoginProof {
|
|||
suggestedHandle: suggestedHandle,
|
||||
hasKeys: hasKeys,
|
||||
handle: handle,
|
||||
passwordSignsIn: passwordSignsIn,
|
||||
hasProofKey: hasProofKey,
|
||||
accountResetAt: accountResetAt,
|
||||
endedBefore: endedBefore,
|
||||
);
|
||||
|
||||
|
|
@ -139,6 +181,9 @@ class _SimpleLoginProofImpl extends SimpleLoginProof {
|
|||
Object? suggestedHandle = _Undefined,
|
||||
bool? hasKeys,
|
||||
Object? handle = _Undefined,
|
||||
bool? passwordSignsIn,
|
||||
bool? hasProofKey,
|
||||
Object? accountResetAt = _Undefined,
|
||||
Object? endedBefore = _Undefined,
|
||||
}) {
|
||||
return SimpleLoginProof(
|
||||
|
|
@ -149,6 +194,11 @@ class _SimpleLoginProofImpl extends SimpleLoginProof {
|
|||
: this.suggestedHandle,
|
||||
hasKeys: hasKeys ?? this.hasKeys,
|
||||
handle: handle is String? ? handle : this.handle,
|
||||
passwordSignsIn: passwordSignsIn ?? this.passwordSignsIn,
|
||||
hasProofKey: hasProofKey ?? this.hasProofKey,
|
||||
accountResetAt: accountResetAt is DateTime?
|
||||
? accountResetAt
|
||||
: this.accountResetAt,
|
||||
endedBefore: endedBefore is DateTime? ? endedBefore : this.endedBefore,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ 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/account_resets.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/profile/first_handle.dart';
|
||||
|
|
@ -14,8 +15,13 @@ import 'package:serverpod/serverpod.dart';
|
|||
// 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.
|
||||
// as ADR 0017's take the password and a code; and, the password and the
|
||||
// phrase lost, a reset to new keys after 72 hours (D31 to D33). Only an
|
||||
// account that signs in with SimpleLogin alone changes here (D34): one
|
||||
// whose password signs in too unlocks here, once it set the keys' proof
|
||||
// key, but changes as ADR 0017 has it, which keeps SRP's verifier in step.
|
||||
// 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
|
||||
|
|
@ -138,9 +144,11 @@ Future<void> setFirstKeys(
|
|||
/// [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.
|
||||
/// ([PasswordRefusal.invalidSetup]), an account whose password signs in
|
||||
/// too or that has no keys ([PasswordRefusal.noPassword], D34), 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,
|
||||
|
|
@ -202,6 +210,7 @@ Future<void> _newPassword(
|
|||
if (!isKeysPassword(password)) {
|
||||
throw refusedFor(PasswordRefusal.invalidSetup);
|
||||
}
|
||||
await _keysAloneOf(session, person);
|
||||
await requireSimpleLoginConfirmation(
|
||||
session,
|
||||
person: person,
|
||||
|
|
@ -274,9 +283,10 @@ Future<void> _newPassword(
|
|||
/// (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.
|
||||
/// of range ([PasswordRefusal.invalidSetup]), as [changeKeysPassword]
|
||||
/// refuses an account ([PasswordRefusal.noPassword]), and as
|
||||
/// `provePasswordKey` refuses; the sign-in's refusals as
|
||||
/// [changeKeysPassword]'s. Nothing changes then.
|
||||
Future<void> setKeysRecovery(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
|
|
@ -287,6 +297,7 @@ Future<void> setKeysRecovery(
|
|||
if (!isWrappedKeys(recoveryKeys) || !isPublicKey(recoveryProofKey)) {
|
||||
throw refusedFor(PasswordRefusal.invalidSetup);
|
||||
}
|
||||
await _keysAloneOf(session, person);
|
||||
await requireSimpleLoginConfirmation(
|
||||
session,
|
||||
person: person,
|
||||
|
|
@ -364,3 +375,187 @@ Future<void> requireKeysOwnerToDelete(
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Asks for the reset of [person]'s keys, their password and their recovery
|
||||
/// phrase lost (Halkyon Learn's ADR 0020, D31), with their SimpleLogin
|
||||
/// sign-in under [attempt] of the last ten minutes, taken (D20): it takes
|
||||
/// effect [resetDelay] later, when new keys made on a device take the old
|
||||
/// ones' place ([completeKeysReset]). There is no mail: their signed-in
|
||||
/// devices show it (`SignInWays.accountResetAt`), as the next SimpleLogin
|
||||
/// sign-in does, and any proof of the password or the phrase calls it off
|
||||
/// (`provePasswordKey`, [callOffKeysResets]; D32), so whoever took over the
|
||||
/// SimpleLogin account gets new keys only where its owner neither unlocked
|
||||
/// nor said no for 72 hours. Asked again while it waits, it keeps its time;
|
||||
/// once it lapsed (`resetLapsed`), it waits anew.
|
||||
///
|
||||
/// Refused ([PasswordRefusedException]) for an account without such keys
|
||||
/// ([PasswordRefusal.noPassword]: one whose password signs in resets as ADR
|
||||
/// 0017 has it, with a code by mail); the sign-in's refusals as
|
||||
/// [requireSimpleLoginConfirmation]'s and [takeSimpleLoginConfirmation]'s.
|
||||
/// Nothing is asked then.
|
||||
Future<ResetPending> startKeysReset(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required UuidValue attempt,
|
||||
}) async {
|
||||
await _keysAloneOf(session, person);
|
||||
await requireSimpleLoginConfirmation(
|
||||
session,
|
||||
person: person,
|
||||
attempt: attempt,
|
||||
);
|
||||
final now = clock.now().toUtc();
|
||||
return await session.db.transaction((transaction) async {
|
||||
await takeSimpleLoginConfirmation(
|
||||
session,
|
||||
person: person,
|
||||
attempt: attempt,
|
||||
transaction: transaction,
|
||||
);
|
||||
// SELECT * FROM "account_reset" WHERE "authUserId" = $1
|
||||
// AND "kind" = 'account'
|
||||
final waiting = await AccountReset.db.findFirstRow(
|
||||
session,
|
||||
where: (t) =>
|
||||
t.authUserId.equals(person) & t.kind.equals(AccountResetKind.account),
|
||||
transaction: transaction,
|
||||
);
|
||||
if (waiting != null) {
|
||||
if (!resetLapsed(waiting, now)) {
|
||||
return ResetPending(takesEffectAt: waiting.takesEffectAt);
|
||||
}
|
||||
// DELETE FROM "account_reset" WHERE "id" = $1
|
||||
await AccountReset.db.deleteRow(
|
||||
session,
|
||||
waiting,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
// INSERT INTO "account_reset" ("authUserId", "kind", "takesEffectAt",
|
||||
// "createdAt") VALUES ($1, 'account', $2, $3)
|
||||
final reset = await AccountReset.db.insertRow(
|
||||
session,
|
||||
AccountReset(
|
||||
authUserId: person,
|
||||
kind: AccountResetKind.account,
|
||||
takesEffectAt: now.add(resetDelay),
|
||||
createdAt: now,
|
||||
),
|
||||
transaction: transaction,
|
||||
);
|
||||
return ResetPending(takesEffectAt: reset.takesEffectAt);
|
||||
});
|
||||
}
|
||||
|
||||
/// Carries out [person]'s reset of keys, asked with [startKeysReset] and
|
||||
/// due now, with their SimpleLogin sign-in under [attempt] of the last ten
|
||||
/// minutes, taken: [setup] (a new password, keys and recovery phrase, all
|
||||
/// made on the device, which wraps the old keys anew where it still holds
|
||||
/// them, D33) replaces the old, and every other session of the account
|
||||
/// ends, in every service of the instance; [keepSession], the one asking,
|
||||
/// stays. What was encrypted under keys no device holds any more is lost;
|
||||
/// the handle, the profile and the SimpleLogin link stay.
|
||||
///
|
||||
/// Refused ([PasswordRefusedException]) for a setup out of range
|
||||
/// ([PasswordRefusal.invalidSetup]), as [startKeysReset] refuses an account
|
||||
/// ([PasswordRefusal.noPassword]), without such a reset or with one that
|
||||
/// lapsed ([PasswordRefusal.noReset]), and before its time
|
||||
/// ([PasswordRefusal.notDue], with when); the sign-in's refusals as
|
||||
/// [startKeysReset]'s. Nothing changes then.
|
||||
Future<void> completeKeysReset(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required UuidValue attempt,
|
||||
required KeysSetup setup,
|
||||
UuidValue? keepSession,
|
||||
}) async {
|
||||
if (!isKeysSetup(setup)) {
|
||||
throw refusedFor(PasswordRefusal.invalidSetup);
|
||||
}
|
||||
final row = await _keysAloneOf(session, person);
|
||||
await requireSimpleLoginConfirmation(
|
||||
session,
|
||||
person: person,
|
||||
attempt: attempt,
|
||||
);
|
||||
final now = clock.now().toUtc();
|
||||
await session.db.transaction((transaction) async {
|
||||
// DELETE FROM "account_reset" WHERE "authUserId" = $1
|
||||
// AND "kind" = 'account' RETURNING *
|
||||
final reset = (await AccountReset.db.deleteWhere(
|
||||
session,
|
||||
where: (t) =>
|
||||
t.authUserId.equals(person) & t.kind.equals(AccountResetKind.account),
|
||||
transaction: transaction,
|
||||
)).firstOrNull;
|
||||
// A week after it took effect, it lapsed: as never asked, its row left
|
||||
// to the pruning (the refusal rolls the deletion back).
|
||||
if (reset == null || resetLapsed(reset, now)) {
|
||||
throw refusedFor(PasswordRefusal.noReset);
|
||||
}
|
||||
if (reset.takesEffectAt.isAfter(now)) {
|
||||
throw PasswordRefusedException(
|
||||
reason: PasswordRefusal.notDue,
|
||||
takesEffectAt: reset.takesEffectAt,
|
||||
);
|
||||
}
|
||||
await takeSimpleLoginConfirmation(
|
||||
session,
|
||||
person: person,
|
||||
attempt: attempt,
|
||||
transaction: transaction,
|
||||
);
|
||||
// UPDATE "account_password" SET … WHERE "id" = $1 (the account's row,
|
||||
// when it was made kept)
|
||||
await AccountPassword.db.updateRow(
|
||||
session,
|
||||
keysRowOf(
|
||||
person,
|
||||
setup,
|
||||
now: now,
|
||||
).copyWith(id: row.id, createdAt: row.createdAt),
|
||||
transaction: transaction,
|
||||
);
|
||||
await endSessionsEverywhere(
|
||||
session,
|
||||
person,
|
||||
keepSession: keepSession,
|
||||
transaction: transaction,
|
||||
);
|
||||
});
|
||||
await announceSessionsEnded(session, person);
|
||||
}
|
||||
|
||||
/// Calls off [person]'s reset of keys waiting with [proof], their
|
||||
/// password's proof for it ([KeyProofPurpose.callOffResets]) from a
|
||||
/// signed-in device, which shows the reset (Halkyon Learn's ADR 0020, D32):
|
||||
/// whoever took over their SimpleLogin account asked for it, and the
|
||||
/// owner's password says no; a session alone, which a thief may hold too,
|
||||
/// says nothing. Refusals as `provePasswordKey`'s, which calls it off.
|
||||
Future<void> callOffKeysResets(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required KeysProof proof,
|
||||
}) async {
|
||||
await provePasswordKey(
|
||||
session,
|
||||
person: person,
|
||||
proof: proof,
|
||||
purpose: KeyProofPurpose.callOffResets,
|
||||
);
|
||||
}
|
||||
|
||||
/// [person]'s keys, refused ([PasswordRefusal.noPassword]) unless they are
|
||||
/// an account's that signs in with SimpleLogin alone (`opensKeysAlone`):
|
||||
/// only those change here (D34).
|
||||
Future<AccountPassword> _keysAloneOf(Session session, UuidValue person) async {
|
||||
// SELECT * FROM "account_password" WHERE "authUserId" = $1 LIMIT 1
|
||||
final row = await AccountPassword.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(person),
|
||||
);
|
||||
if (row == null || !opensKeysAlone(row)) {
|
||||
throw refusedFor(PasswordRefusal.noPassword);
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,11 @@ import 'package:serverpod_auth_idp_server/core.dart';
|
|||
/// The keys of an account that signs in with SimpleLogin (Halkyon Learn'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.
|
||||
/// 0016), every change with that proof and a SimpleLogin sign-in of the
|
||||
/// last ten minutes on the device (`KeysOwnerProof`), and, the password and
|
||||
/// the phrase lost, a reset to new keys 72 hours later (D31 to D33).
|
||||
/// Refusals are `PasswordRefusedException`s; the sign-in's,
|
||||
/// `OidcRefusedException`s.
|
||||
class KeysEndpoint extends Endpoint {
|
||||
@override
|
||||
bool get requireLogin => true;
|
||||
|
|
@ -157,6 +159,67 @@ class KeysEndpoint extends Endpoint {
|
|||
);
|
||||
}
|
||||
|
||||
/// Asks for the reset of your keys, your password and your recovery
|
||||
/// phrase lost (D31), with your SimpleLogin sign-in of the last ten
|
||||
/// minutes on this device, under [attempt]: it takes effect 72 hours
|
||||
/// later, which this answers, when [completeReset] gives you new keys.
|
||||
/// Your signed-in devices show it meanwhile, and your password or your
|
||||
/// phrase opening your keys anywhere calls it off, as [callOffResets]
|
||||
/// does. Asked again while it waits, it keeps its time. Refused for an
|
||||
/// account without such keys (`PasswordRefusal.noPassword`; one whose
|
||||
/// password signs in resets as `accountRecovery` has it) and without that
|
||||
/// sign-in (`OidcRefusal.unknownAttempt`).
|
||||
Future<ResetPending> startReset(
|
||||
Session session, {
|
||||
required UuidValue attempt,
|
||||
}) => AccountRegister.of(session.serverpod).startKeysReset(
|
||||
session,
|
||||
person: session.authenticated!.authUserId,
|
||||
attempt: attempt,
|
||||
);
|
||||
|
||||
/// Carries out the reset of your keys, due now, with your SimpleLogin
|
||||
/// sign-in of the last ten minutes on this device, under [attempt]:
|
||||
/// [setup], all made on your device (your old keys wrapped anew where it
|
||||
/// still holds them, D33), replaces your password, keys and phrase. What
|
||||
/// was encrypted under keys no device holds any more is lost; your
|
||||
/// handle, profile and SimpleLogin stay. Every other session of yours
|
||||
/// ends, in every service of the instance; this one stays. Refused before
|
||||
/// its time (`PasswordRefusal.notDue`, with when), without such a reset
|
||||
/// or once it lapsed (`PasswordRefusal.noReset`), for a setup out of range
|
||||
/// (`PasswordRefusal.invalidSetup`), and as [startReset] refuses.
|
||||
Future<void> completeReset(
|
||||
Session session, {
|
||||
required UuidValue attempt,
|
||||
required KeysSetup setup,
|
||||
}) => AccountRegister.of(session.serverpod).completeKeysReset(
|
||||
session,
|
||||
person: session.authenticated!.authUserId,
|
||||
attempt: attempt,
|
||||
setup: setup,
|
||||
keepSession: _asking(session),
|
||||
);
|
||||
|
||||
/// Calls off a reset of your keys waiting (D32), once [proof] proved your
|
||||
/// password for it, for a challenge of [begin]: whoever asked for it took
|
||||
/// over your SimpleLogin account. The proof counts against this session,
|
||||
/// as [unlock]'s. Refusals as [unlock]'s.
|
||||
Future<void> callOffResets(Session session, {required KeysProof proof}) {
|
||||
final pod = session.serverpod;
|
||||
final bound = proof.copyWith(
|
||||
instanceUrl: deviceInstanceUrlOn(pod, proof.instanceUrl),
|
||||
);
|
||||
return asOwnerProofTry(
|
||||
session,
|
||||
bound,
|
||||
() => AccountRegister.of(pod).callOffKeysResets(
|
||||
session,
|
||||
person: session.authenticated!.authUserId,
|
||||
proof: bound,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// [proof] bound to the base URL the device addressed here.
|
||||
static KeysOwnerProof _bound(Session session, KeysOwnerProof proof) =>
|
||||
proof.copyWith(
|
||||
|
|
|
|||
|
|
@ -334,21 +334,25 @@ Future<AccountSessionsEnded> completeAccountReset(
|
|||
return (account: account, endedBefore: endedBefore);
|
||||
}
|
||||
|
||||
/// The account whose address's hash is [hash], with a password; refused
|
||||
/// ([PasswordRefusal.noPassword]) for none.
|
||||
/// The account whose address's hash is [hash], with a password that signs
|
||||
/// in; refused ([PasswordRefusal.noPassword]) for none, and for one whose
|
||||
/// password only opens its keys, as SimpleLogin's do (Halkyon Learn's ADR
|
||||
/// 0020, D31): their SimpleLogin sign-in asks for their reset, which a
|
||||
/// mailbox alone mustn't, nor carry out with a password that signs in.
|
||||
Future<UuidValue> _accountWithPassword(Session session, String hash) async {
|
||||
// SELECT * FROM "email_identity" WHERE "addressHash" = $1
|
||||
final identity = await EmailIdentity.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.addressHash.equals(hash),
|
||||
);
|
||||
if (identity == null ||
|
||||
// SELECT count(*) FROM "account_password" WHERE "authUserId" = $1
|
||||
await AccountPassword.db.count(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(identity.authUserId),
|
||||
) ==
|
||||
0) {
|
||||
// SELECT * FROM "account_password" WHERE "authUserId" = $1 LIMIT 1
|
||||
final password = identity == null
|
||||
? null
|
||||
: await AccountPassword.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(identity.authUserId),
|
||||
);
|
||||
if (identity == null || password?.verifier == null) {
|
||||
throw refusedFor(PasswordRefusal.noPassword);
|
||||
}
|
||||
return identity.authUserId;
|
||||
|
|
|
|||
|
|
@ -50,7 +50,11 @@ enum PasswordProofPurpose {
|
|||
callOffResets('call off resets'),
|
||||
|
||||
/// Deleting the account.
|
||||
deleteAccount('delete account');
|
||||
deleteAccount('delete account'),
|
||||
|
||||
/// Setting the keys' proof key, made of the same password, for devices
|
||||
/// signed in with SimpleLogin (Halkyon Learn's ADR 0020, D34).
|
||||
proofKey('keys proof key');
|
||||
|
||||
PasswordProofPurpose(this.label);
|
||||
|
||||
|
|
@ -119,7 +123,11 @@ enum KeyProofPurpose {
|
|||
deleteAccount('delete account'),
|
||||
|
||||
/// Linking a new SimpleLogin ID in place of a lost one, by the phrase.
|
||||
mendLink('mend link');
|
||||
mendLink('mend link'),
|
||||
|
||||
/// Calling off a reset of the keys waiting, by the password (Halkyon
|
||||
/// Learn's ADR 0020, D32).
|
||||
callOffResets('call off resets');
|
||||
|
||||
KeyProofPurpose(this.label);
|
||||
|
||||
|
|
@ -198,6 +206,14 @@ Uint8List mendLinkDigest(UuidValue attempt) =>
|
|||
Uint8List authenticatorDigest(UuidValue id) =>
|
||||
_sha256([utf8.encode('halkyon authenticator v1'), id.toBytes()]);
|
||||
|
||||
/// The SHA-256 of [proofKey], the public half of the keys' proof key a
|
||||
/// password makes (Halkyon Learn's ADR 0020, D34), framed: what the owner's
|
||||
/// proof for setting it binds.
|
||||
Uint8List proofKeyDigest(ByteData proofKey) => _sha256([
|
||||
utf8.encode('halkyon keys proof key v1'),
|
||||
Uint8List.sublistView(proofKey),
|
||||
]);
|
||||
|
||||
Uint8List _sha256(List<List<int>> parts) =>
|
||||
Uint8List.fromList(hashes.sha256.convert(_framed(parts)).bytes);
|
||||
|
||||
|
|
|
|||
|
|
@ -135,6 +135,45 @@ Future<void> setRecoveryKeys(
|
|||
);
|
||||
}
|
||||
|
||||
/// Sets the keys' proof key of [person], whose password signs in, to
|
||||
/// [proofKey] (its public half, made on the device of that same password
|
||||
/// beside SRP's x), with the owner's [proof] ([proveOwner], binding the
|
||||
/// key): their keys then open with the password alone on a device signed in
|
||||
/// with SimpleLogin (`keys.unlock`; Halkyon Learn's ADR 0020, D34), as they
|
||||
/// open when the password signs in. A new password takes the key away
|
||||
/// (`writeNewPassword`), as it was made of the old one.
|
||||
///
|
||||
/// Refused ([PasswordRefusedException]) for a key out of range
|
||||
/// ([PasswordRefusal.invalidSetup]), and as [proveOwner]. Nothing changes
|
||||
/// then.
|
||||
Future<void> setPasswordProofKey(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required OwnerProof proof,
|
||||
required ByteData proofKey,
|
||||
}) async {
|
||||
final keys = requirePasswordKeys(session.serverpod);
|
||||
if (!isPublicKey(proofKey)) {
|
||||
throw refusedFor(PasswordRefusal.invalidSetup);
|
||||
}
|
||||
final row = await proveOwner(
|
||||
session,
|
||||
keys,
|
||||
person: person,
|
||||
proof: proof,
|
||||
purpose: PasswordProofPurpose.proofKey,
|
||||
digest: proofKeyDigest(proofKey),
|
||||
);
|
||||
// UPDATE "account_password" SET "passwordProofKey" = $1, "updatedAt" = $2
|
||||
// WHERE "id" = $3
|
||||
// (Those columns alone: a change of phrase meanwhile stays.)
|
||||
await AccountPassword.db.updateRow(
|
||||
session,
|
||||
row.copyWith(passwordProofKey: proofKey, updatedAt: clock.now().toUtc()),
|
||||
columns: (t) => [t.passwordProofKey, t.updatedAt],
|
||||
);
|
||||
}
|
||||
|
||||
/// Proves [person] may delete their account: one with a password only
|
||||
/// with [proof] (the password's proof bound to the base URL the device
|
||||
/// addressed and a code, as a change of password takes: a session alone,
|
||||
|
|
|
|||
|
|
@ -133,6 +133,33 @@ class PasswordEndpoint extends Endpoint {
|
|||
);
|
||||
}
|
||||
|
||||
/// Sets [proofKey], the public half of the keys' proof key the caller's
|
||||
/// password makes on the device, with the owner's [proof] binding it: the
|
||||
/// keys then open with the password alone on a device signed in with
|
||||
/// SimpleLogin (`keys.unlock`; Halkyon Learn's ADR 0020, D34). A new
|
||||
/// password takes it away, as it is made of the password. Counted against
|
||||
/// this session as every owner's proof is. Refusals as
|
||||
/// `setPasswordProofKey`'s.
|
||||
Future<void> setProofKey(
|
||||
Session session, {
|
||||
required OwnerProof proof,
|
||||
required ByteData proofKey,
|
||||
}) {
|
||||
final bound = proof.copyWith(
|
||||
instanceUrl: deviceInstanceUrlOn(session.serverpod, proof.instanceUrl),
|
||||
);
|
||||
return asOwnerProofTry(
|
||||
session,
|
||||
bound,
|
||||
() => AccountRegister.of(session.serverpod).setPasswordProofKey(
|
||||
session,
|
||||
person: session.authenticated!.authUserId,
|
||||
proof: bound,
|
||||
proofKey: proofKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// A new recovery phrase: [recoveryKeys], the caller's keys wrapped under
|
||||
/// it on the device, replace the old wrapping, and [recoveryProofKey], the
|
||||
/// public half of its proof key, the old one, with the owner's [proof], as
|
||||
|
|
|
|||
|
|
@ -807,8 +807,11 @@ Future<ProfileRefusal?> checkSignUpHandle(
|
|||
|
||||
/// Writes [password] (a change, or a reset keeping the keys) over [row]'s
|
||||
/// password alone: the salt, the costs, the verifier and the keys wrapped
|
||||
/// under it. Never the row's other columns, which [row] may hold as they
|
||||
/// were before a change made meanwhile (a new phrase) that must stay.
|
||||
/// under it, the keys' proof key going (Halkyon Learn's ADR 0020, D34: it
|
||||
/// was made of the old password; the owner sets the new one's with
|
||||
/// `setPasswordProofKey`). Never the row's other columns, which [row] may
|
||||
/// hold as they were before a change made meanwhile (a new phrase) that
|
||||
/// must stay.
|
||||
Future<void> writeNewPassword(
|
||||
Session session,
|
||||
AccountPassword row,
|
||||
|
|
@ -817,8 +820,9 @@ Future<void> writeNewPassword(
|
|||
}) =>
|
||||
// UPDATE "account_password" SET "salt" = $1, "kdfAlgorithm" = $2,
|
||||
// "kdfOpsLimit" = $3, "kdfMemLimit" = $4, "verifier" = $5,
|
||||
// "passwordKeysNonce" = $6, "passwordKeysCipherText" = $7,
|
||||
// "sessionEndedForProofsAt" = NULL, "updatedAt" = $8 WHERE "id" = $9
|
||||
// "passwordProofKey" = NULL, "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).
|
||||
AccountPassword.db.updateRow(
|
||||
|
|
@ -829,11 +833,14 @@ Future<void> writeNewPassword(
|
|||
kdfOpsLimit: password.costs.opsLimit,
|
||||
kdfMemLimit: password.costs.memLimit,
|
||||
verifier: password.verifier,
|
||||
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
|
||||
passwordProofKey: null,
|
||||
passwordKeysNonce: password.passwordKeys.nonce,
|
||||
passwordKeysCipherText: password.passwordKeys.cipherText,
|
||||
// As above.
|
||||
// ignore: avoid_redundant_argument_values
|
||||
sessionEndedForProofsAt: null,
|
||||
updatedAt: clock.now().toUtc(),
|
||||
),
|
||||
|
|
@ -843,6 +850,7 @@ Future<void> writeNewPassword(
|
|||
t.kdfOpsLimit,
|
||||
t.kdfMemLimit,
|
||||
t.verifier,
|
||||
t.passwordProofKey,
|
||||
t.passwordKeysNonce,
|
||||
t.passwordKeysCipherText,
|
||||
t.sessionEndedForProofsAt,
|
||||
|
|
@ -1473,7 +1481,9 @@ Future<DateTime> _carryOutAuthenticatorReset(
|
|||
/// Calls off the reset of [account] that a code by mail asked for: its
|
||||
/// owner signed in with the password alone, once a reset of the
|
||||
/// authenticators took effect. That reset's row stays: until an
|
||||
/// authenticator is confirmed, the password alone signs in.
|
||||
/// authenticator is confirmed, the password alone signs in. Also the reset
|
||||
/// of keys a SimpleLogin sign-in asked for, once the password or the phrase
|
||||
/// proved its owner (Halkyon Learn's ADR 0020, D31).
|
||||
Future<void> _callOffAccountReset(Session session, UuidValue account) =>
|
||||
// DELETE FROM "account_reset" WHERE "authUserId" = $1 AND "kind" = $2
|
||||
AccountReset.db.deleteWhere(
|
||||
|
|
@ -1668,13 +1678,13 @@ Future<AccountPassword> provePasswordKey(
|
|||
/// 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).
|
||||
/// password as ADR 0017 has it, its keys' proof key set or not, D34).
|
||||
Future<KeyProofChallenge> beginPhraseKeyProof(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
}) async {
|
||||
final password = await _passwordOf(session, person);
|
||||
if (password == null || password.passwordProofKey == null) {
|
||||
if (password == null || !opensKeysAlone(password)) {
|
||||
throw refusedFor(PasswordRefusal.noPassword);
|
||||
}
|
||||
final nonce = _randomBytes(32);
|
||||
|
|
@ -1717,7 +1727,7 @@ Future<AccountPassword> provePhraseKey(
|
|||
proof: proof,
|
||||
begunFor: SignInChallengePurpose.phraseProof,
|
||||
keyOf: (password) =>
|
||||
password.passwordProofKey == null ? null : password.recoveryProofKey,
|
||||
opensKeysAlone(password) ? password.recoveryProofKey : null,
|
||||
purpose: purpose,
|
||||
digest: digest,
|
||||
);
|
||||
|
|
@ -1725,7 +1735,8 @@ Future<AccountPassword> provePhraseKey(
|
|||
/// [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.
|
||||
/// check. Proved, it calls off a reset of the account waiting (Halkyon
|
||||
/// Learn's ADR 0020, D31): the owner knows the password or the phrase.
|
||||
Future<AccountPassword> _proveOwnKey(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
|
|
@ -1756,6 +1767,7 @@ Future<AccountPassword> _proveOwnKey(
|
|||
)) {
|
||||
throw refusedFor(PasswordRefusal.wrongCredentials);
|
||||
}
|
||||
await _callOffAccountReset(session, person);
|
||||
return password;
|
||||
}
|
||||
|
||||
|
|
@ -1809,7 +1821,7 @@ Future<KeyProofChallenge> beginMendKeyProof(
|
|||
/// Takes the challenge [proof] answers (once, one of [beginMendKeyProof])
|
||||
/// and checks its signature by the recovery phrase's proof key, for
|
||||
/// [KeyProofPurpose.mendLink] and [digest] (`mendLinkDigest`): the
|
||||
/// account's password row. Refused ([PasswordRefusedException]) for a
|
||||
/// account's password row, a reset of it waiting called off. Refused ([PasswordRefusedException]) for a
|
||||
/// challenge unknown, used or too old ([PasswordRefusal.challengeExpired]),
|
||||
/// and alike for a signature by another key and a handle nobody has, checked
|
||||
/// against the decoy's key so it takes as long
|
||||
|
|
@ -1844,6 +1856,8 @@ Future<AccountPassword> proveMendKeyProof(
|
|||
if (!signed || password == null || proofKey == null) {
|
||||
throw refusedFor(PasswordRefusal.wrongCredentials);
|
||||
}
|
||||
// The phrase proved its owner (Halkyon Learn's ADR 0020, D31).
|
||||
await _callOffAccountReset(session, password.authUserId);
|
||||
return password;
|
||||
}
|
||||
|
||||
|
|
@ -1854,6 +1868,14 @@ KeysProof keyProofOf(KeysOwnerProof proof) => KeysProof(
|
|||
instanceUrl: proof.instanceUrl,
|
||||
);
|
||||
|
||||
/// Whether [password], an account's row, is one of an account that signs
|
||||
/// in with SimpleLogin alone (Halkyon Learn's ADR 0020, D19): its password
|
||||
/// only opens its keys, proven with its own proof key, with no SRP
|
||||
/// verifier. Only such rows change through `keys` (D34): one whose password
|
||||
/// signs in changes as ADR 0017 has it, which keeps its verifier in step.
|
||||
bool opensKeysAlone(AccountPassword password) =>
|
||||
password.verifier == null && password.passwordProofKey != null;
|
||||
|
||||
/// Whether [password] is a new password for keys this register takes.
|
||||
bool isKeysPassword(KeysPassword password) =>
|
||||
password.salt.lengthInBytes == _saltLength &&
|
||||
|
|
|
|||
|
|
@ -268,6 +268,44 @@ abstract interface class AccountRegister {
|
|||
required ByteData recoveryProofKey,
|
||||
});
|
||||
|
||||
/// Asks for the reset of [person]'s keys, their password and phrase lost,
|
||||
/// with their SimpleLogin sign-in under [attempt]. Refusals as
|
||||
/// `startKeysReset`'s.
|
||||
Future<ResetPending> startKeysReset(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required UuidValue attempt,
|
||||
});
|
||||
|
||||
/// Carries out [person]'s reset of keys with [setup] and their SimpleLogin
|
||||
/// sign-in under [attempt]: every other session ends but [keepSession], in
|
||||
/// every service. Refusals as `completeKeysReset`'s.
|
||||
Future<void> completeKeysReset(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required UuidValue attempt,
|
||||
required KeysSetup setup,
|
||||
UuidValue? keepSession,
|
||||
});
|
||||
|
||||
/// Calls off [person]'s reset of keys waiting, with their password's
|
||||
/// [proof]. Refusals as `callOffKeysResets`'.
|
||||
Future<void> callOffKeysResets(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required KeysProof proof,
|
||||
});
|
||||
|
||||
/// Sets the keys' proof key of [person], whose password signs in, to
|
||||
/// [proofKey], with the owner's [proof]. Refusals as
|
||||
/// `setPasswordProofKey`'s.
|
||||
Future<void> setPasswordProofKey(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required OwnerProof proof,
|
||||
required ByteData proofKey,
|
||||
});
|
||||
|
||||
/// 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.
|
||||
|
|
|
|||
|
|
@ -297,6 +297,37 @@ Future<Map<String, Object?>?> _answer(
|
|||
recoveryProofKey: arguments.bytes('recoveryProofKey'),
|
||||
);
|
||||
return const {};
|
||||
case RegisterOperation.startKeysReset:
|
||||
final pending = await register.startKeysReset(
|
||||
session,
|
||||
person: arguments.id('person'),
|
||||
attempt: arguments.id('attempt'),
|
||||
);
|
||||
return {'pending': pending.toJson()};
|
||||
case RegisterOperation.completeKeysReset:
|
||||
await register.completeKeysReset(
|
||||
session,
|
||||
person: arguments.id('person'),
|
||||
attempt: arguments.id('attempt'),
|
||||
setup: arguments.model('setup', KeysSetup.fromJson),
|
||||
keepSession: arguments.optionalId('keepSession'),
|
||||
);
|
||||
return const {};
|
||||
case RegisterOperation.callOffKeysResets:
|
||||
await register.callOffKeysResets(
|
||||
session,
|
||||
person: arguments.id('person'),
|
||||
proof: arguments.model('proof', KeysProof.fromJson),
|
||||
);
|
||||
return const {};
|
||||
case RegisterOperation.setPasswordProofKey:
|
||||
await register.setPasswordProofKey(
|
||||
session,
|
||||
person: arguments.id('person'),
|
||||
proof: arguments.model('proof', OwnerProof.fromJson),
|
||||
proofKey: arguments.bytes('proofKey'),
|
||||
);
|
||||
return const {};
|
||||
case RegisterOperation.signInWaysOf:
|
||||
final ways = await register.signInWaysOf(
|
||||
session,
|
||||
|
|
|
|||
|
|
@ -545,6 +545,48 @@ final class LocalRegister implements AccountRegister {
|
|||
recoveryProofKey: recoveryProofKey,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<ResetPending> startKeysReset(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required UuidValue attempt,
|
||||
}) => keys.startKeysReset(session, person: person, attempt: attempt);
|
||||
|
||||
@override
|
||||
Future<void> completeKeysReset(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required UuidValue attempt,
|
||||
required KeysSetup setup,
|
||||
UuidValue? keepSession,
|
||||
}) => keys.completeKeysReset(
|
||||
session,
|
||||
person: person,
|
||||
attempt: attempt,
|
||||
setup: setup,
|
||||
keepSession: keepSession,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> callOffKeysResets(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required KeysProof proof,
|
||||
}) => keys.callOffKeysResets(session, person: person, proof: proof);
|
||||
|
||||
@override
|
||||
Future<void> setPasswordProofKey(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required OwnerProof proof,
|
||||
required ByteData proofKey,
|
||||
}) => change.setPasswordProofKey(
|
||||
session,
|
||||
person: person,
|
||||
proof: proof,
|
||||
proofKey: proofKey,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<SignInWays> signInWaysOf(Session session, UuidValue person) async {
|
||||
// SELECT * FROM "account_password" WHERE "authUserId" = $1
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue