feat(base): SimpleLogin accounts' keys without SRP, signing up in one call (ADR 0020, step 3)

An account that signs in with SimpleLogin keeps its keys under a password
that signs nobody in (D19): `account_password` keeps the password's own
proof key (Ed25519, `passwordProofKey`) in place of SRP's verifier, which
becomes nullable. The migration adds the column and drops the NOT NULL,
nothing else.

- `simpleLogin.signUp(attempt, handle, setup)`: someone new whose identity
  waits under the attempt gets, in one transaction, an account
  (identified), the link, a profile with the handle and the keys; then a
  session (D18). Refused, the identity still waiting, for a setup out of
  range and a handle refused as saving a profile refuses it.
- `simpleLogin.checkHandle(attempt, handle)`: whether someone new could
  take a handle, at most 30 an hour per attempt and 60 per network.
- `simpleLogin.complete` says, for an account, whether it has its keys and
  its handle, and keeps the sign-in ten minutes under its attempt, the
  account as its caller: D20's confirmation for the changes to come.
- `keys.begin`, `keys.unlock`: the keys for a signed-in device once the
  password is proven with its proof key, for a nonce, bound to the base
  URL the device addressed; wrong proofs counted per session as ADR
  0017's.
- An authenticator is enrolled only for a password that signs in.
- Members ask the register for all of it; `RemoteRegister` and the
  instance API carry the four new operations.

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

View file

@ -20,6 +20,7 @@ abstract class SignInWays
required this.email,
bool? simpleLogin,
bool? password,
bool? keys,
bool? authenticator,
bool? passwordOnly,
bool? secondFactorWorks,
@ -28,6 +29,7 @@ abstract class SignInWays
this.accountResetAt,
}) : simpleLogin = simpleLogin ?? false,
password = password ?? false,
keys = keys ?? false,
authenticator = authenticator ?? false,
passwordOnly = passwordOnly ?? false,
secondFactorWorks = secondFactorWorks ?? false;
@ -37,6 +39,7 @@ abstract class SignInWays
required bool email,
bool? simpleLogin,
bool? password,
bool? keys,
bool? authenticator,
bool? passwordOnly,
bool? secondFactorWorks,
@ -55,6 +58,9 @@ abstract class SignInWays
password: jsonSerialization['password'] == null
? null
: _isc.BoolJsonExtension.fromJson(jsonSerialization['password']),
keys: jsonSerialization['keys'] == null
? null
: _isc.BoolJsonExtension.fromJson(jsonSerialization['keys']),
authenticator: jsonSerialization['authenticator'] == null
? null
: _isc.BoolJsonExtension.fromJson(jsonSerialization['authenticator']),
@ -93,9 +99,14 @@ abstract class SignInWays
/// instance's SimpleLogin app.
bool simpleLogin;
/// A password (ADR 0017), which an address needs for resets.
/// A password that signs in (ADR 0017, SRP's), which an address needs
/// for resets.
bool password;
/// The account's keys, under a password that signs in (ADR 0017) or one
/// that only opens them, as for SimpleLogin (ADR 0020, D19).
bool keys;
/// A confirmed authenticator: the password signs in only with one.
bool authenticator;
@ -130,6 +141,7 @@ abstract class SignInWays
bool? email,
bool? simpleLogin,
bool? password,
bool? keys,
bool? authenticator,
bool? passwordOnly,
bool? secondFactorWorks,
@ -145,6 +157,7 @@ abstract class SignInWays
'email': email,
'simpleLogin': simpleLogin,
'password': password,
'keys': keys,
'authenticator': authenticator,
'passwordOnly': passwordOnly,
'secondFactorWorks': secondFactorWorks,
@ -164,6 +177,7 @@ abstract class SignInWays
'email': email,
'simpleLogin': simpleLogin,
'password': password,
'keys': keys,
'authenticator': authenticator,
'passwordOnly': passwordOnly,
'secondFactorWorks': secondFactorWorks,
@ -189,6 +203,7 @@ class _SignInWaysImpl extends SignInWays {
required bool email,
bool? simpleLogin,
bool? password,
bool? keys,
bool? authenticator,
bool? passwordOnly,
bool? secondFactorWorks,
@ -200,6 +215,7 @@ class _SignInWaysImpl extends SignInWays {
email: email,
simpleLogin: simpleLogin,
password: password,
keys: keys,
authenticator: authenticator,
passwordOnly: passwordOnly,
secondFactorWorks: secondFactorWorks,
@ -217,6 +233,7 @@ class _SignInWaysImpl extends SignInWays {
bool? email,
bool? simpleLogin,
bool? password,
bool? keys,
bool? authenticator,
bool? passwordOnly,
bool? secondFactorWorks,
@ -229,6 +246,7 @@ class _SignInWaysImpl extends SignInWays {
email: email ?? this.email,
simpleLogin: simpleLogin ?? this.simpleLogin,
password: password ?? this.password,
keys: keys ?? this.keys,
authenticator: authenticator ?? this.authenticator,
passwordOnly: passwordOnly ?? this.passwordOnly,
secondFactorWorks: secondFactorWorks ?? this.secondFactorWorks,

View file

@ -18,6 +18,14 @@ import 'package:halkyon_base_client/src/protocol/auth/sign_in_ways.dart'
as _ifld8s4r;
import 'package:halkyon_base_client/src/protocol/instance/instance_basics.dart'
as _isnyur2i;
import 'package:halkyon_base_client/src/protocol/keys/device_keys.dart'
as _invv3u3l;
import 'package:halkyon_base_client/src/protocol/keys/keys_challenge.dart'
as _ivkzjgkp;
import 'package:halkyon_base_client/src/protocol/keys/keys_proof.dart'
as _iznbn55x;
import 'package:halkyon_base_client/src/protocol/keys/keys_setup.dart'
as _iec2fqr0;
import 'package:halkyon_base_client/src/protocol/password/authenticator.dart'
as _ivfmflch;
import 'package:halkyon_base_client/src/protocol/password/authenticator_enrolment.dart'
@ -222,6 +230,43 @@ class EndpointInstance extends _isc.EndpointRef {
);
}
/// 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.
/// {@category Endpoint}
class EndpointKeys extends _isc.EndpointRef {
EndpointKeys(_isc.EndpointCaller caller) : super(caller);
@override
String get name => 'halkyon_base.keys';
/// The first step of proving your password: the salt and costs your
/// device derives the password's keys with, and a nonce to sign with its
/// proof key, within two minutes, once. Refused for an account without
/// such keys (`PasswordRefusal.noPassword`).
_ida.Future<_ivkzjgkp.KeysChallenge> begin() =>
caller.callServerEndpoint<_ivkzjgkp.KeysChallenge>(
'halkyon_base.keys',
'begin',
{},
);
/// Your keys for this device, once [proof] proved your password: your
/// public keys and your private keys wrapped under the password's key,
/// which the device opens. The proof is bound to the base URL the device
/// addressed here, and counts against this session: the tenth wrong in a
/// row ends it (`PasswordRefusal.sessionEnded`), and you are warned.
/// Refused for a wrong proof (`PasswordRefusal.wrongCredentials`) and a
/// challenge unknown, used or too old (`PasswordRefusal.challengeExpired`).
_ida.Future<_invv3u3l.DeviceKeys> unlock(_iznbn55x.KeysProof proof) =>
caller.callServerEndpoint<_invv3u3l.DeviceKeys>(
'halkyon_base.keys',
'unlock',
{'proof': proof},
);
}
/// Getting back into an account (ADR 0017): a lost authenticator, a lost
/// password with the recovery phrase, a lost password without it. Called
/// without a session, as the person can't sign in. The instance's register
@ -761,10 +806,14 @@ class EndpointSimpleLogin extends _isc.EndpointRef {
/// Ends [attempt] with SimpleLogin's [code], as the caller began it:
/// signed out, a session for the account with that identity, or for
/// someone new the handle suggested (their account comes next); 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. Refusals as `completeSimpleLogin`'s.
/// someone new the handle suggested (their account comes with [signUp]);
/// 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
/// for ten minutes the attempt's ID stands for this sign-in, for a change
/// the device asks for (ADR 0020, D20). Refusals as
/// `completeSimpleLogin`'s.
_ida.Future<_ianfx7qj.SimpleLoginCompleted> complete({
required _isc.UuidValue attempt,
required String code,
@ -776,6 +825,48 @@ class EndpointSimpleLogin extends _isc.EndpointRef {
'code': code,
},
);
/// Whether someone new, whose identity waits under [attempt] ([complete]
/// said `newcomer`), could take [handle]: null where they could, else why
/// not, as saving a profile would say. For the app to say so while it is
/// typed; [signUp] checks again. At most 30 for an attempt and 60 from a
/// network in an hour (`ProfileRefusal.tooManyHandleChecks`, thrown):
/// each answer tells which handles exist. Refused
/// (`OidcRefusal.unknownAttempt`) where no identity waits.
_ida.Future<_ih722tf9.ProfileRefusal?> checkHandle({
required _isc.UuidValue attempt,
required String handle,
}) => caller.callServerEndpoint<_ih722tf9.ProfileRefusal?>(
'halkyon_base.simpleLogin',
'checkHandle',
{
'attempt': attempt,
'handle': handle,
},
);
/// Signs up someone new, whose identity waits under [attempt] for half an
/// hour after [complete] said `newcomer` (ADR 0020, D16 and D18): an
/// account with [handle], private until its person goes public, and the
/// keys [setup] describes, all made on the device beforehand; a session
/// for it here. Refused, the identity still waiting: for a setup out of
/// range (`PasswordRefusal.invalidSetup`) and a handle refused
/// (`ProfileRefusedException`, as saving a profile refuses it); for an
/// attempt unknown, taken or past its time (`OidcRefusal.unknownAttempt`),
/// and called signed in (`OidcRefusal.wrongCaller`).
_ida.Future<_iacc.AuthSuccess> signUp({
required _isc.UuidValue attempt,
required String handle,
required _iec2fqr0.KeysSetup setup,
}) => caller.callServerEndpoint<_iacc.AuthSuccess>(
'halkyon_base.simpleLogin',
'signUp',
{
'attempt': attempt,
'handle': handle,
'setup': setup,
},
);
}
class Caller extends _isc.ModuleEndpointCaller {
@ -786,6 +877,7 @@ class Caller extends _isc.ModuleEndpointCaller {
gitHubSignIn = EndpointGitHubSignIn(this);
gitHubUpgrade = EndpointGitHubUpgrade(this);
instance = EndpointInstance(this);
keys = EndpointKeys(this);
accountRecovery = EndpointAccountRecovery(this);
authenticator = EndpointAuthenticator(this);
password = EndpointPassword(this);
@ -807,6 +899,8 @@ class Caller extends _isc.ModuleEndpointCaller {
late final EndpointInstance instance;
late final EndpointKeys keys;
late final EndpointAccountRecovery accountRecovery;
late final EndpointAuthenticator authenticator;
@ -829,6 +923,7 @@ class Caller extends _isc.ModuleEndpointCaller {
'halkyon_base.gitHubSignIn': gitHubSignIn,
'halkyon_base.gitHubUpgrade': gitHubUpgrade,
'halkyon_base.instance': instance,
'halkyon_base.keys': keys,
'halkyon_base.accountRecovery': accountRecovery,
'halkyon_base.authenticator': authenticator,
'halkyon_base.password': password,

View file

@ -0,0 +1,116 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:typed_data' as _idt;
import 'package:halkyon_base_client/src/protocol/protocol.dart' as _iwgbpyo5;
import 'package:serverpod_client/serverpod_client.dart' as _isc;
import '../password/wrapped_keys.dart' as _i2rxumte;
/// The account's keys as a device gets them once it proved the password
/// (Halkyon Learn's ADR 0020, 7 and D19): its public keys, and its private
/// keys wrapped under the password's key, which the device opens.
abstract class DeviceKeys
implements _isc.SerializableModel, _isc.ProtocolSerialization {
DeviceKeys._({
required this.encryptionPublicKey,
required this.signingPublicKey,
required this.passwordKeys,
});
factory DeviceKeys({
required _idt.ByteData encryptionPublicKey,
required _idt.ByteData signingPublicKey,
required _i2rxumte.WrappedKeys passwordKeys,
}) = _DeviceKeysImpl;
factory DeviceKeys.fromJson(Map<String, dynamic> jsonSerialization) {
return DeviceKeys(
encryptionPublicKey: _isc.ByteDataJsonExtension.fromJson(
jsonSerialization['encryptionPublicKey'],
),
signingPublicKey: _isc.ByteDataJsonExtension.fromJson(
jsonSerialization['signingPublicKey'],
),
passwordKeys: _iwgbpyo5.Protocol().deserialize<_i2rxumte.WrappedKeys>(
jsonSerialization['passwordKeys'],
),
);
}
_idt.ByteData encryptionPublicKey;
_idt.ByteData signingPublicKey;
_i2rxumte.WrappedKeys passwordKeys;
/// Returns a shallow copy of this [DeviceKeys]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
DeviceKeys copyWith({
_idt.ByteData? encryptionPublicKey,
_idt.ByteData? signingPublicKey,
_i2rxumte.WrappedKeys? passwordKeys,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'halkyon_base.DeviceKeys',
'encryptionPublicKey': encryptionPublicKey.toJson(),
'signingPublicKey': signingPublicKey.toJson(),
'passwordKeys': passwordKeys.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'halkyon_base.DeviceKeys',
'encryptionPublicKey': encryptionPublicKey.toJson(),
'signingPublicKey': signingPublicKey.toJson(),
'passwordKeys': passwordKeys.toJsonForProtocol(),
};
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _DeviceKeysImpl extends DeviceKeys {
_DeviceKeysImpl({
required _idt.ByteData encryptionPublicKey,
required _idt.ByteData signingPublicKey,
required _i2rxumte.WrappedKeys passwordKeys,
}) : super._(
encryptionPublicKey: encryptionPublicKey,
signingPublicKey: signingPublicKey,
passwordKeys: passwordKeys,
);
/// Returns a shallow copy of this [DeviceKeys]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
DeviceKeys copyWith({
_idt.ByteData? encryptionPublicKey,
_idt.ByteData? signingPublicKey,
_i2rxumte.WrappedKeys? passwordKeys,
}) {
return DeviceKeys(
encryptionPublicKey:
encryptionPublicKey ?? this.encryptionPublicKey.clone(),
signingPublicKey: signingPublicKey ?? this.signingPublicKey.clone(),
passwordKeys: passwordKeys ?? this.passwordKeys.copyWith(),
);
}
}

View file

@ -0,0 +1,126 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:typed_data' as _idt;
import 'package:halkyon_base_client/src/protocol/protocol.dart' as _iwgbpyo5;
import 'package:serverpod_client/serverpod_client.dart' as _isc;
import '../password/password_costs.dart' as _ioe6tior;
/// What a signed-in device needs to prove the password of an account that
/// signs in with SimpleLogin (Halkyon Learn's ADR 0020, D19): the salt and
/// costs it derives the password's keys with, and a nonce it signs with the
/// password's proof key, within two minutes, once.
abstract class KeysChallenge
implements _isc.SerializableModel, _isc.ProtocolSerialization {
KeysChallenge._({
required this.challenge,
required this.nonce,
required this.salt,
required this.costs,
});
factory KeysChallenge({
required _isc.UuidValue challenge,
required _idt.ByteData nonce,
required _idt.ByteData salt,
required _ioe6tior.PasswordCosts costs,
}) = _KeysChallengeImpl;
factory KeysChallenge.fromJson(Map<String, dynamic> jsonSerialization) {
return KeysChallenge(
challenge: _isc.UuidValueJsonExtension.fromJson(
jsonSerialization['challenge'],
),
nonce: _isc.ByteDataJsonExtension.fromJson(jsonSerialization['nonce']),
salt: _isc.ByteDataJsonExtension.fromJson(jsonSerialization['salt']),
costs: _iwgbpyo5.Protocol().deserialize<_ioe6tior.PasswordCosts>(
jsonSerialization['costs'],
),
);
}
_isc.UuidValue challenge;
_idt.ByteData nonce;
_idt.ByteData salt;
_ioe6tior.PasswordCosts costs;
/// Returns a shallow copy of this [KeysChallenge]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
KeysChallenge copyWith({
_isc.UuidValue? challenge,
_idt.ByteData? nonce,
_idt.ByteData? salt,
_ioe6tior.PasswordCosts? costs,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'halkyon_base.KeysChallenge',
'challenge': challenge.toJson(),
'nonce': nonce.toJson(),
'salt': salt.toJson(),
'costs': costs.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'halkyon_base.KeysChallenge',
'challenge': challenge.toJson(),
'nonce': nonce.toJson(),
'salt': salt.toJson(),
'costs': costs.toJsonForProtocol(),
};
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _KeysChallengeImpl extends KeysChallenge {
_KeysChallengeImpl({
required _isc.UuidValue challenge,
required _idt.ByteData nonce,
required _idt.ByteData salt,
required _ioe6tior.PasswordCosts costs,
}) : super._(
challenge: challenge,
nonce: nonce,
salt: salt,
costs: costs,
);
/// Returns a shallow copy of this [KeysChallenge]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
KeysChallenge copyWith({
_isc.UuidValue? challenge,
_idt.ByteData? nonce,
_idt.ByteData? salt,
_ioe6tior.PasswordCosts? costs,
}) {
return KeysChallenge(
challenge: challenge ?? this.challenge,
nonce: nonce ?? this.nonce.clone(),
salt: salt ?? this.salt.clone(),
costs: costs ?? this.costs.copyWith(),
);
}
}

View file

@ -0,0 +1,116 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:typed_data' as _idt;
import 'package:serverpod_client/serverpod_client.dart' as _isc;
/// A proof of the password of an account that signs in with SimpleLogin
/// (Halkyon Learn's ADR 0020, D19): the signature by the password's proof
/// key over the challenge's nonce, bound to the base URL the device
/// addressed and to what it is for (`keyProofMessage`).
abstract class KeysProof
implements _isc.SerializableModel, _isc.ProtocolSerialization {
KeysProof._({
required this.challenge,
required this.signature,
required this.instanceUrl,
});
factory KeysProof({
required _isc.UuidValue challenge,
required _idt.ByteData signature,
required String instanceUrl,
}) = _KeysProofImpl;
factory KeysProof.fromJson(Map<String, dynamic> jsonSerialization) {
return KeysProof(
challenge: _isc.UuidValueJsonExtension.fromJson(
jsonSerialization['challenge'],
),
signature: _isc.ByteDataJsonExtension.fromJson(
jsonSerialization['signature'],
),
instanceUrl: jsonSerialization['instanceUrl'] as String,
);
}
_isc.UuidValue challenge;
/// Ed25519's signature, 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;
/// Returns a shallow copy of this [KeysProof]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
KeysProof copyWith({
_isc.UuidValue? challenge,
_idt.ByteData? signature,
String? instanceUrl,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'halkyon_base.KeysProof',
'challenge': challenge.toJson(),
'signature': signature.toJson(),
'instanceUrl': instanceUrl,
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'halkyon_base.KeysProof',
'challenge': challenge.toJson(),
'signature': signature.toJson(),
'instanceUrl': instanceUrl,
};
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _KeysProofImpl extends KeysProof {
_KeysProofImpl({
required _isc.UuidValue challenge,
required _idt.ByteData signature,
required String instanceUrl,
}) : super._(
challenge: challenge,
signature: signature,
instanceUrl: instanceUrl,
);
/// Returns a shallow copy of this [KeysProof]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
KeysProof copyWith({
_isc.UuidValue? challenge,
_idt.ByteData? signature,
String? instanceUrl,
}) {
return KeysProof(
challenge: challenge ?? this.challenge,
signature: signature ?? this.signature.clone(),
instanceUrl: instanceUrl ?? this.instanceUrl,
);
}
}

View file

@ -0,0 +1,193 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:typed_data' as _idt;
import 'package:halkyon_base_client/src/protocol/protocol.dart' as _iwgbpyo5;
import 'package:serverpod_client/serverpod_client.dart' as _isc;
import '../password/password_costs.dart' as _ioe6tior;
import '../password/wrapped_keys.dart' as _i2rxumte;
/// What a device sends to give an account that signs in with SimpleLogin
/// its keys (Halkyon Learn's ADR 0020, 6 and D19), all made on the device:
/// the salt and costs it derived the password's keys with, the public half
/// of the password's proof key, the account's public keys, and its private
/// keys wrapped under the password's key and the recovery phrase's, with
/// the phrase's proof key. As ADR 0017's `PasswordSetup`, without SRP's
/// verifier: nothing of it signs in, and nothing decrypts without the
/// password or the phrase.
abstract class KeysSetup
implements _isc.SerializableModel, _isc.ProtocolSerialization {
KeysSetup._({
required this.salt,
required this.costs,
required this.passwordProofKey,
required this.encryptionPublicKey,
required this.signingPublicKey,
required this.passwordKeys,
required this.recoveryKeys,
required this.recoveryProofKey,
});
factory KeysSetup({
required _idt.ByteData salt,
required _ioe6tior.PasswordCosts costs,
required _idt.ByteData passwordProofKey,
required _idt.ByteData encryptionPublicKey,
required _idt.ByteData signingPublicKey,
required _i2rxumte.WrappedKeys passwordKeys,
required _i2rxumte.WrappedKeys recoveryKeys,
required _idt.ByteData recoveryProofKey,
}) = _KeysSetupImpl;
factory KeysSetup.fromJson(Map<String, dynamic> jsonSerialization) {
return KeysSetup(
salt: _isc.ByteDataJsonExtension.fromJson(jsonSerialization['salt']),
costs: _iwgbpyo5.Protocol().deserialize<_ioe6tior.PasswordCosts>(
jsonSerialization['costs'],
),
passwordProofKey: _isc.ByteDataJsonExtension.fromJson(
jsonSerialization['passwordProofKey'],
),
encryptionPublicKey: _isc.ByteDataJsonExtension.fromJson(
jsonSerialization['encryptionPublicKey'],
),
signingPublicKey: _isc.ByteDataJsonExtension.fromJson(
jsonSerialization['signingPublicKey'],
),
passwordKeys: _iwgbpyo5.Protocol().deserialize<_i2rxumte.WrappedKeys>(
jsonSerialization['passwordKeys'],
),
recoveryKeys: _iwgbpyo5.Protocol().deserialize<_i2rxumte.WrappedKeys>(
jsonSerialization['recoveryKeys'],
),
recoveryProofKey: _isc.ByteDataJsonExtension.fromJson(
jsonSerialization['recoveryProofKey'],
),
);
}
_idt.ByteData salt;
_ioe6tior.PasswordCosts costs;
/// The public half of the password's proof key (Ed25519).
_idt.ByteData passwordProofKey;
_idt.ByteData encryptionPublicKey;
_idt.ByteData signingPublicKey;
_i2rxumte.WrappedKeys passwordKeys;
_i2rxumte.WrappedKeys recoveryKeys;
/// The public half of the recovery phrase's proof key (Ed25519, from the
/// phrase alone).
_idt.ByteData recoveryProofKey;
/// Returns a shallow copy of this [KeysSetup]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
KeysSetup copyWith({
_idt.ByteData? salt,
_ioe6tior.PasswordCosts? costs,
_idt.ByteData? passwordProofKey,
_idt.ByteData? encryptionPublicKey,
_idt.ByteData? signingPublicKey,
_i2rxumte.WrappedKeys? passwordKeys,
_i2rxumte.WrappedKeys? recoveryKeys,
_idt.ByteData? recoveryProofKey,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'halkyon_base.KeysSetup',
'salt': salt.toJson(),
'costs': costs.toJson(),
'passwordProofKey': passwordProofKey.toJson(),
'encryptionPublicKey': encryptionPublicKey.toJson(),
'signingPublicKey': signingPublicKey.toJson(),
'passwordKeys': passwordKeys.toJson(),
'recoveryKeys': recoveryKeys.toJson(),
'recoveryProofKey': recoveryProofKey.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'halkyon_base.KeysSetup',
'salt': salt.toJson(),
'costs': costs.toJsonForProtocol(),
'passwordProofKey': passwordProofKey.toJson(),
'encryptionPublicKey': encryptionPublicKey.toJson(),
'signingPublicKey': signingPublicKey.toJson(),
'passwordKeys': passwordKeys.toJsonForProtocol(),
'recoveryKeys': recoveryKeys.toJsonForProtocol(),
'recoveryProofKey': recoveryProofKey.toJson(),
};
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _KeysSetupImpl extends KeysSetup {
_KeysSetupImpl({
required _idt.ByteData salt,
required _ioe6tior.PasswordCosts costs,
required _idt.ByteData passwordProofKey,
required _idt.ByteData encryptionPublicKey,
required _idt.ByteData signingPublicKey,
required _i2rxumte.WrappedKeys passwordKeys,
required _i2rxumte.WrappedKeys recoveryKeys,
required _idt.ByteData recoveryProofKey,
}) : super._(
salt: salt,
costs: costs,
passwordProofKey: passwordProofKey,
encryptionPublicKey: encryptionPublicKey,
signingPublicKey: signingPublicKey,
passwordKeys: passwordKeys,
recoveryKeys: recoveryKeys,
recoveryProofKey: recoveryProofKey,
);
/// Returns a shallow copy of this [KeysSetup]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
KeysSetup copyWith({
_idt.ByteData? salt,
_ioe6tior.PasswordCosts? costs,
_idt.ByteData? passwordProofKey,
_idt.ByteData? encryptionPublicKey,
_idt.ByteData? signingPublicKey,
_i2rxumte.WrappedKeys? passwordKeys,
_i2rxumte.WrappedKeys? recoveryKeys,
_idt.ByteData? recoveryProofKey,
}) {
return KeysSetup(
salt: salt ?? this.salt.clone(),
costs: costs ?? this.costs.copyWith(),
passwordProofKey: passwordProofKey ?? this.passwordProofKey.clone(),
encryptionPublicKey:
encryptionPublicKey ?? this.encryptionPublicKey.clone(),
signingPublicKey: signingPublicKey ?? this.signingPublicKey.clone(),
passwordKeys: passwordKeys ?? this.passwordKeys.copyWith(),
recoveryKeys: recoveryKeys ?? this.recoveryKeys.copyWith(),
recoveryProofKey: recoveryProofKey ?? this.recoveryProofKey.clone(),
);
}
}

View file

@ -23,7 +23,13 @@ enum SignInChallengePurpose implements _isc.SerializableModel {
/// Proving the password while signed in, to change it, set a new phrase
/// or delete the account: counted per account, never toward the
/// address's proofs, so strangers' guesses hold no signed-in owner back.
change;
change,
/// Proving the password of an account without SRP's verifier, signed in,
/// 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;
static SignInChallengePurpose fromJson(String name) {
switch (name) {
@ -33,6 +39,8 @@ enum SignInChallengePurpose implements _isc.SerializableModel {
return SignInChallengePurpose.keyProof;
case 'change':
return SignInChallengePurpose.change;
case 'passwordProof':
return SignInChallengePurpose.passwordProof;
default:
throw ArgumentError(
'Value "$name" cannot be converted to "SignInChallengePurpose"',

View file

@ -43,7 +43,11 @@ enum ProfileRefusal implements _isc.SerializableModel {
avatarInvalid,
/// The links were checked too often in the last hour.
tooManyLinkChecks;
tooManyLinkChecks,
/// The handles were checked too often in the last hour: their answers
/// tell which handles exist.
tooManyHandleChecks;
static ProfileRefusal fromJson(String name) {
switch (name) {
@ -67,6 +71,8 @@ enum ProfileRefusal implements _isc.SerializableModel {
return ProfileRefusal.avatarInvalid;
case 'tooManyLinkChecks':
return ProfileRefusal.tooManyLinkChecks;
case 'tooManyHandleChecks':
return ProfileRefusal.tooManyHandleChecks;
default:
throw ArgumentError(
'Value "$name" cannot be converted to "ProfileRefusal"',

View file

@ -26,6 +26,10 @@ import 'auth/github_sign_in_refusal.dart' as _ir1gd9p5;
import 'auth/github_sign_in_refused_exception.dart' as _i0xfcu5n;
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_proof.dart' as _iviximyh;
import 'keys/keys_setup.dart' as _iqdhlx3t;
import 'password/account_reset_kind.dart' as _i91ka1it;
import 'password/authenticator.dart' as _icpzuvvi;
import 'password/authenticator_enrolment.dart' as _iv7tra7v;
@ -57,6 +61,10 @@ export 'auth/github_sign_in_refusal.dart';
export 'auth/github_sign_in_refused_exception.dart';
export 'auth/sign_in_ways.dart';
export 'instance/instance_basics.dart';
export 'keys/device_keys.dart';
export 'keys/keys_challenge.dart';
export 'keys/keys_proof.dart';
export 'keys/keys_setup.dart';
export 'password/account_reset_kind.dart';
export 'password/authenticator.dart';
export 'password/authenticator_enrolment.dart';
@ -149,6 +157,18 @@ class Protocol extends _isc.SerializationManager {
if (t == _i8jixqx6.InstanceBasics) {
return _i8jixqx6.InstanceBasics.fromJson(data) as T;
}
if (t == _iz50cwqw.DeviceKeys) {
return _iz50cwqw.DeviceKeys.fromJson(data) as T;
}
if (t == _itebbsfc.KeysChallenge) {
return _itebbsfc.KeysChallenge.fromJson(data) as T;
}
if (t == _iviximyh.KeysProof) {
return _iviximyh.KeysProof.fromJson(data) as T;
}
if (t == _iqdhlx3t.KeysSetup) {
return _iqdhlx3t.KeysSetup.fromJson(data) as T;
}
if (t == _i91ka1it.AccountResetKind) {
return _i91ka1it.AccountResetKind.fromJson(data) as T;
}
@ -254,6 +274,19 @@ class Protocol extends _isc.SerializationManager {
return (data != null ? _i8jixqx6.InstanceBasics.fromJson(data) : null)
as T;
}
if (t == _isc.getType<_iz50cwqw.DeviceKeys?>()) {
return (data != null ? _iz50cwqw.DeviceKeys.fromJson(data) : null) as T;
}
if (t == _isc.getType<_itebbsfc.KeysChallenge?>()) {
return (data != null ? _itebbsfc.KeysChallenge.fromJson(data) : null)
as T;
}
if (t == _isc.getType<_iviximyh.KeysProof?>()) {
return (data != null ? _iviximyh.KeysProof.fromJson(data) : null) as T;
}
if (t == _isc.getType<_iqdhlx3t.KeysSetup?>()) {
return (data != null ? _iqdhlx3t.KeysSetup.fromJson(data) : null) as T;
}
if (t == _isc.getType<_i91ka1it.AccountResetKind?>()) {
return (data != null ? _i91ka1it.AccountResetKind.fromJson(data) : null)
as T;
@ -401,6 +434,10 @@ class Protocol extends _isc.SerializationManager {
_i0xfcu5n.GitHubSignInRefusedException => 'GitHubSignInRefusedException',
_ie6tbh85.SignInWays => 'SignInWays',
_i8jixqx6.InstanceBasics => 'InstanceBasics',
_iz50cwqw.DeviceKeys => 'DeviceKeys',
_itebbsfc.KeysChallenge => 'KeysChallenge',
_iviximyh.KeysProof => 'KeysProof',
_iqdhlx3t.KeysSetup => 'KeysSetup',
_i91ka1it.AccountResetKind => 'AccountResetKind',
_icpzuvvi.Authenticator => 'Authenticator',
_iv7tra7v.AuthenticatorEnrolment => 'AuthenticatorEnrolment',
@ -456,6 +493,14 @@ class Protocol extends _isc.SerializationManager {
return 'SignInWays';
case _i8jixqx6.InstanceBasics():
return 'InstanceBasics';
case _iz50cwqw.DeviceKeys():
return 'DeviceKeys';
case _itebbsfc.KeysChallenge():
return 'KeysChallenge';
case _iviximyh.KeysProof():
return 'KeysProof';
case _iqdhlx3t.KeysSetup():
return 'KeysSetup';
case _i91ka1it.AccountResetKind():
return 'AccountResetKind';
case _icpzuvvi.Authenticator():
@ -535,6 +580,18 @@ class Protocol extends _isc.SerializationManager {
if (dataClassName == 'InstanceBasics') {
return deserialize<_i8jixqx6.InstanceBasics>(data['data']);
}
if (dataClassName == 'DeviceKeys') {
return deserialize<_iz50cwqw.DeviceKeys>(data['data']);
}
if (dataClassName == 'KeysChallenge') {
return deserialize<_itebbsfc.KeysChallenge>(data['data']);
}
if (dataClassName == 'KeysProof') {
return deserialize<_iviximyh.KeysProof>(data['data']);
}
if (dataClassName == 'KeysSetup') {
return deserialize<_iqdhlx3t.KeysSetup>(data['data']);
}
if (dataClassName == 'AccountResetKind') {
return deserialize<_i91ka1it.AccountResetKind>(data['data']);
}

View file

@ -26,12 +26,16 @@ abstract class SimpleLoginCompleted
required this.outcome,
this.session,
this.suggestedHandle,
});
bool? hasKeys,
this.handle,
}) : hasKeys = hasKeys ?? false;
factory SimpleLoginCompleted({
required _i2lb2chw.SimpleLoginOutcome outcome,
_iacc.AuthSuccess? session,
String? suggestedHandle,
bool? hasKeys,
String? handle,
}) = _SimpleLoginCompletedImpl;
factory SimpleLoginCompleted.fromJson(
@ -47,6 +51,10 @@ abstract class SimpleLoginCompleted
jsonSerialization['session'],
),
suggestedHandle: jsonSerialization['suggestedHandle'] as String?,
hasKeys: jsonSerialization['hasKeys'] == null
? null
: _isc.BoolJsonExtension.fromJson(jsonSerialization['hasKeys']),
handle: jsonSerialization['handle'] as String?,
);
}
@ -56,6 +64,15 @@ abstract class SimpleLoginCompleted
String? suggestedHandle;
/// 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.
bool hasKeys;
/// The account's handle; null for one without (it chooses one with its
/// keys).
String? handle;
/// Returns a shallow copy of this [SimpleLoginCompleted]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@ -63,6 +80,8 @@ abstract class SimpleLoginCompleted
_i2lb2chw.SimpleLoginOutcome? outcome,
_iacc.AuthSuccess? session,
String? suggestedHandle,
bool? hasKeys,
String? handle,
});
@override
Map<String, dynamic> toJson() {
@ -71,6 +90,8 @@ abstract class SimpleLoginCompleted
'outcome': outcome.toJson(),
if (session != null) 'session': session?.toJson(),
if (suggestedHandle != null) 'suggestedHandle': suggestedHandle,
'hasKeys': hasKeys,
if (handle != null) 'handle': handle,
};
}
@ -81,6 +102,8 @@ abstract class SimpleLoginCompleted
'outcome': outcome.toJson(),
if (session != null) 'session': session?.toJson(),
if (suggestedHandle != null) 'suggestedHandle': suggestedHandle,
'hasKeys': hasKeys,
if (handle != null) 'handle': handle,
};
}
@ -97,10 +120,14 @@ class _SimpleLoginCompletedImpl extends SimpleLoginCompleted {
required _i2lb2chw.SimpleLoginOutcome outcome,
_iacc.AuthSuccess? session,
String? suggestedHandle,
bool? hasKeys,
String? handle,
}) : super._(
outcome: outcome,
session: session,
suggestedHandle: suggestedHandle,
hasKeys: hasKeys,
handle: handle,
);
/// Returns a shallow copy of this [SimpleLoginCompleted]
@ -111,6 +138,8 @@ class _SimpleLoginCompletedImpl extends SimpleLoginCompleted {
_i2lb2chw.SimpleLoginOutcome? outcome,
Object? session = _Undefined,
Object? suggestedHandle = _Undefined,
bool? hasKeys,
Object? handle = _Undefined,
}) {
return SimpleLoginCompleted(
outcome: outcome ?? this.outcome,
@ -120,6 +149,8 @@ class _SimpleLoginCompletedImpl extends SimpleLoginCompleted {
suggestedHandle: suggestedHandle is String?
? suggestedHandle
: this.suggestedHandle,
hasKeys: hasKeys ?? this.hasKeys,
handle: handle is String? ? handle : this.handle,
);
}
}

View file

@ -88,7 +88,8 @@ String canonicalInstanceUrl(String url) {
return '$scheme://$shownHost$port$path';
}
/// What proving the recovery phrase is for, by the words the message
/// What proving the recovery phrase, or the password with its own proof key
/// (Halkyon Learn's ADR 0020, D19), is for, by the words the message
/// carries ([label]).
enum KeyProofPurpose(final String label) {
/// Asking for the reset of a lost authenticator.
@ -96,6 +97,21 @@ enum KeyProofPurpose(final String label) {
/// Setting a new password, the keys staying.
passwordReset('password reset'),
/// Getting the account's keys on a device, by the password.
unlockKeys('unlock keys'),
/// A new password while signed in, by the old one.
newPassword('new password'),
/// A new recovery phrase while signed in, by the password.
newPhrase('new phrase'),
/// Deleting the account, by the password.
deleteAccount('delete account'),
/// Linking a new SimpleLogin ID in place of a lost one, by the phrase.
mendLink('mend link'),
}
/// What the device signs with the recovery phrase's proof key to prove the