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
63 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

View file

@ -19,7 +19,8 @@ Endpoints, under the nickname `base` in a host's client (`client.modules.base.em
| `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. `signUp`: an account at the address a mailed code proves, with a password; 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 (their account comes with step 3); signed in, SimpleLogin added to the caller's account, who becomes `identified` | anyone |
| `simpleLogin` | `begin(platform)`: an attempt and SimpleLogin's address to open, at most 30 an hour per network; `complete(attempt, code)`: signed out, a session for the account with that SimpleLogin ID, or for someone new the handle suggested from their name; signed in, SimpleLogin added to the caller's account, who becomes `identified`; for an account, whether it has its keys and its handle, the attempt standing ten minutes for the sign-in; `checkHandle(attempt, handle)`: whether someone new could take a handle, at most 30 an hour per attempt and 60 per network; `signUp(attempt, handle, setup)`, without a session: someone new's account, `identified`, with the handle and the keys the device made; a session | anyone |
| `keys` | `begin`, `unlock`: the caller's keys for their device, once the password is proven with its own proof key (an account that signs in with SimpleLogin, Halkyon Learn's ADR 0020, D19); the tenth wrong proof in a row ends the session, as ADR 0017's do | signed in |
| `gitHubUpgrade` | `upgrade`: links a GitHub account to the caller, who becomes `identified`; refused where passwords or SimpleLogin are offered (deprecated) | signed in |
| `gitHubSignIn` | `signIn`: a session for whoever has that GitHub account, unless they sign in with a password; none once `GITHUB_SIGN_IN_ENDS` has come | without a session |
| `emailCode` | `sendCode`: a code by mail, which proves the address and signs nobody in | anyone |
@ -53,7 +54,13 @@ Endpoints, under the nickname `base` in a host's client (`client.modules.base.em
(`halkyon_oidc_identity`, the provider `simplelogin`): one per account, each an
account's. Someone new's identity waits 30 minutes under the attempt's ID, with a
handle suggested from the name SimpleLogin shared (`handleFromName`: `anna_mueller`,
a number added where taken), which nothing keeps.
a number added where taken), which nothing keeps. Signing up (`signUp`, D18) takes
the identity and makes the account in one transaction: `identified`, linked, a
profile with the handle (refused as saving a profile refuses it) and the keys, the
password's own proof key (Ed25519) in place of SRP's verifier (D19). The password
opens the keys and signs nobody in, so such an account has no authenticator (`enrol`
refuses it `noPassword`). A sign-in into an account keeps its attempt ten minutes
more, the account its caller: it confirms a change the device asks for next (D20).
- SimpleLogin's forwarding page (Halkyon Learn's ADR 0020; `addSimpleLoginCallbackRoute`,
on the register, on halkyon_oidc's `addOidcCallbackForwarding`, ADR 0023):
`/simplelogin-callback`, the one address the instance's SimpleLogin
@ -198,7 +205,8 @@ Built on the three parts above, on the register (members forward every step thro
- **Tables** (`serverOnly`, each row deleted with its person by `deletePerson`):
`account_password` (one per account: Argon2id's salt and costs, SRP's verifier padded
to N, the public keys, the private keys wrapped under the password and under the
to N, or for an account that signs in with SimpleLogin the password's proof key in its
place, the public keys, the private keys wrapped under the password and under the
recovery phrase), `totp_authenticator` (the secret sealed with XChaCha20-Poly1305 under
a key derived from `totpKey`, bound to the row's account and ID; `confirmedAt`;
`lastStep`), `recovery_code` (keyed hashes), `sign_in_challenge` (a step begun: SRP's b

View file

@ -6,8 +6,12 @@ fields:
### SimpleLogin (ADR 0020): the instance's register keeps its ID for the
### instance's SimpleLogin app.
simpleLogin: bool, default=false
### 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.
password: bool, default=false
### The account's keys, under a password that signs in (ADR 0017) or one
### that only opens them, as for SimpleLogin (ADR 0020, D19).
keys: bool, default=false
### A confirmed authenticator: the password signs in only with one.
authenticator: bool, default=false
### The account signs in with its password alone: it had a confirmed

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
: _is.BoolJsonExtension.fromJson(jsonSerialization['password']),
keys: jsonSerialization['keys'] == null
? null
: _is.BoolJsonExtension.fromJson(jsonSerialization['keys']),
authenticator: jsonSerialization['authenticator'] == null
? null
: _is.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

@ -13,6 +13,10 @@
import 'dart:typed_data' as _idt;
import 'package:halkyon_base_server/src/generated/auth/email_code_purpose.dart'
as _i5775c0l;
import 'package:halkyon_base_server/src/generated/keys/keys_proof.dart'
as _iy592cuk;
import 'package:halkyon_base_server/src/generated/keys/keys_setup.dart'
as _ii64ejy6;
import 'package:halkyon_base_server/src/generated/password/deletion_proof.dart'
as _i619l6vv;
import 'package:halkyon_base_server/src/generated/password/new_password.dart'
@ -37,6 +41,7 @@ import '../auth/email_code_endpoint.dart' as _in3cimlf;
import '../auth/github_sign_in_endpoint.dart' as _iasw79m5;
import '../auth/github_upgrade_endpoint.dart' as _ivnugbf2;
import '../instance/instance_endpoint.dart' as _i3g8bfc2;
import '../keys/keys_endpoint.dart' as _icowoccn;
import '../password/account_recovery_endpoint.dart' as _ivnbjhab;
import '../password/authenticator_endpoint.dart' as _i3xqawxj;
import '../password/password_endpoint.dart' as _ig7fyg1b;
@ -84,6 +89,12 @@ class Endpoints extends _is.EndpointDispatch {
'instance',
'halkyon_base',
),
'keys': _icowoccn.KeysEndpoint()
..initialize(
server,
'keys',
'halkyon_base',
),
'accountRecovery': _ivnbjhab.AccountRecoveryEndpoint()
..initialize(
server,
@ -318,6 +329,40 @@ class Endpoints extends _is.EndpointDispatch {
),
},
);
connectors['keys'] = _is.EndpointConnector(
name: 'keys',
endpoint: endpoints['keys']!,
methodConnectors: {
'begin': _is.MethodConnector(
name: 'begin',
params: {},
call:
(
_is.Session session,
Map<String, dynamic> params,
) async =>
(endpoints['keys'] as _icowoccn.KeysEndpoint).begin(session),
),
'unlock': _is.MethodConnector(
name: 'unlock',
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).unlock(
session,
params['proof'],
),
),
},
);
connectors['accountRecovery'] = _is.EndpointConnector(
name: 'accountRecovery',
endpoint: endpoints['accountRecovery']!,
@ -1150,6 +1195,64 @@ class Endpoints extends _is.EndpointDispatch {
code: params['code'],
),
),
'checkHandle': _is.MethodConnector(
name: 'checkHandle',
params: {
'attempt': _is.ParameterDescription(
name: 'attempt',
type: _is.getType<_is.UuidValue>(),
nullable: false,
),
'handle': _is.ParameterDescription(
name: 'handle',
type: _is.getType<String>(),
nullable: false,
),
},
call:
(
_is.Session session,
Map<String, dynamic> params,
) async =>
(endpoints['simpleLogin'] as _ia8c9alv.SimpleLoginEndpoint)
.checkHandle(
session,
attempt: params['attempt'],
handle: params['handle'],
),
),
'signUp': _is.MethodConnector(
name: 'signUp',
params: {
'attempt': _is.ParameterDescription(
name: 'attempt',
type: _is.getType<_is.UuidValue>(),
nullable: false,
),
'handle': _is.ParameterDescription(
name: 'handle',
type: _is.getType<String>(),
nullable: false,
),
'setup': _is.ParameterDescription(
name: 'setup',
type: _is.getType<_ii64ejy6.KeysSetup>(),
nullable: false,
),
},
call:
(
_is.Session session,
Map<String, dynamic> params,
) async =>
(endpoints['simpleLogin'] as _ia8c9alv.SimpleLoginEndpoint)
.signUp(
session,
attempt: params['attempt'],
handle: params['handle'],
setup: params['setup'],
),
),
},
);
modules['halkyon_oidc'] = _ipoqfiqr.Endpoints()

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_server/src/generated/protocol.dart' as _ibewtqgw;
import 'package:serverpod/serverpod.dart' as _is;
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 _is.SerializableModel, _is.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: _is.ByteDataJsonExtension.fromJson(
jsonSerialization['encryptionPublicKey'],
),
signingPublicKey: _is.ByteDataJsonExtension.fromJson(
jsonSerialization['signingPublicKey'],
),
passwordKeys: _ibewtqgw.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.
@_is.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 _is.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.
@_is.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_server/src/generated/protocol.dart' as _ibewtqgw;
import 'package:serverpod/serverpod.dart' as _is;
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 _is.SerializableModel, _is.ProtocolSerialization {
KeysChallenge._({
required this.challenge,
required this.nonce,
required this.salt,
required this.costs,
});
factory KeysChallenge({
required _is.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: _is.UuidValueJsonExtension.fromJson(
jsonSerialization['challenge'],
),
nonce: _is.ByteDataJsonExtension.fromJson(jsonSerialization['nonce']),
salt: _is.ByteDataJsonExtension.fromJson(jsonSerialization['salt']),
costs: _ibewtqgw.Protocol().deserialize<_ioe6tior.PasswordCosts>(
jsonSerialization['costs'],
),
);
}
_is.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.
@_is.useResult
KeysChallenge copyWith({
_is.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 _is.SerializationManager.encode(this);
}
}
class _KeysChallengeImpl extends KeysChallenge {
_KeysChallengeImpl({
required _is.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.
@_is.useResult
@override
KeysChallenge copyWith({
_is.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/serverpod.dart' as _is;
/// 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 _is.SerializableModel, _is.ProtocolSerialization {
KeysProof._({
required this.challenge,
required this.signature,
required this.instanceUrl,
});
factory KeysProof({
required _is.UuidValue challenge,
required _idt.ByteData signature,
required String instanceUrl,
}) = _KeysProofImpl;
factory KeysProof.fromJson(Map<String, dynamic> jsonSerialization) {
return KeysProof(
challenge: _is.UuidValueJsonExtension.fromJson(
jsonSerialization['challenge'],
),
signature: _is.ByteDataJsonExtension.fromJson(
jsonSerialization['signature'],
),
instanceUrl: jsonSerialization['instanceUrl'] as String,
);
}
_is.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.
@_is.useResult
KeysProof copyWith({
_is.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 _is.SerializationManager.encode(this);
}
}
class _KeysProofImpl extends KeysProof {
_KeysProofImpl({
required _is.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.
@_is.useResult
@override
KeysProof copyWith({
_is.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_server/src/generated/protocol.dart' as _ibewtqgw;
import 'package:serverpod/serverpod.dart' as _is;
import '../password/password_costs.dart' as _ioe6tior;
import '../password/wrapped_keys.dart' as _i2rxumte;
/// 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 _is.SerializableModel, _is.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: _is.ByteDataJsonExtension.fromJson(jsonSerialization['salt']),
costs: _ibewtqgw.Protocol().deserialize<_ioe6tior.PasswordCosts>(
jsonSerialization['costs'],
),
passwordProofKey: _is.ByteDataJsonExtension.fromJson(
jsonSerialization['passwordProofKey'],
),
encryptionPublicKey: _is.ByteDataJsonExtension.fromJson(
jsonSerialization['encryptionPublicKey'],
),
signingPublicKey: _is.ByteDataJsonExtension.fromJson(
jsonSerialization['signingPublicKey'],
),
passwordKeys: _ibewtqgw.Protocol().deserialize<_i2rxumte.WrappedKeys>(
jsonSerialization['passwordKeys'],
),
recoveryKeys: _ibewtqgw.Protocol().deserialize<_i2rxumte.WrappedKeys>(
jsonSerialization['recoveryKeys'],
),
recoveryProofKey: _is.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.
@_is.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 _is.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.
@_is.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

@ -16,9 +16,11 @@ import 'package:serverpod/serverpod.dart' as _is;
/// An account's password as the register keeps it (ADR 0017): nothing that
/// signs in or decrypts in the password's place. The salt and Argon2id's
/// costs the device derives the password's keys with, SRP's verifier
/// (v = g^x mod N, PAD to N's length), the account's public keys, and its
/// private keys wrapped under the password's key and again under the
/// recovery phrase's, which only a device can open.
/// (v = g^x mod N, PAD to N's length) or, for an account that signs in with
/// SimpleLogin, the password's proof key (Halkyon Learn's ADR 0020, D19),
/// the account's public keys, and its private keys wrapped under the
/// password's key and again under the recovery phrase's, which only a
/// device can open.
abstract class AccountPassword
implements _is.TableRow<int?>, _is.ProtocolSerialization {
AccountPassword._({
@ -28,7 +30,8 @@ abstract class AccountPassword
required this.kdfAlgorithm,
required this.kdfOpsLimit,
required this.kdfMemLimit,
required this.verifier,
this.verifier,
this.passwordProofKey,
required this.encryptionPublicKey,
required this.signingPublicKey,
required this.passwordKeysNonce,
@ -48,7 +51,8 @@ abstract class AccountPassword
required String kdfAlgorithm,
required int kdfOpsLimit,
required int kdfMemLimit,
required _idt.ByteData verifier,
_idt.ByteData? verifier,
_idt.ByteData? passwordProofKey,
required _idt.ByteData encryptionPublicKey,
required _idt.ByteData signingPublicKey,
required _idt.ByteData passwordKeysNonce,
@ -71,9 +75,14 @@ abstract class AccountPassword
kdfAlgorithm: jsonSerialization['kdfAlgorithm'] as String,
kdfOpsLimit: jsonSerialization['kdfOpsLimit'] as int,
kdfMemLimit: jsonSerialization['kdfMemLimit'] as int,
verifier: _is.ByteDataJsonExtension.fromJson(
jsonSerialization['verifier'],
),
verifier: jsonSerialization['verifier'] == null
? null
: _is.ByteDataJsonExtension.fromJson(jsonSerialization['verifier']),
passwordProofKey: jsonSerialization['passwordProofKey'] == null
? null
: _is.ByteDataJsonExtension.fromJson(
jsonSerialization['passwordProofKey'],
),
encryptionPublicKey: _is.ByteDataJsonExtension.fromJson(
jsonSerialization['encryptionPublicKey'],
),
@ -134,8 +143,16 @@ abstract class AccountPassword
/// Argon2id's memory, in bytes.
int kdfMemLimit;
/// SRP's verifier, big-endian, as long as N.
_idt.ByteData verifier;
/// SRP's verifier, big-endian, as long as N; null for an account that
/// signs in with SimpleLogin, which proves the password with
/// [passwordProofKey] instead.
_idt.ByteData? verifier;
/// The public half of the password's own Ed25519 proof key, derived from
/// the password alone (halkyon_keys' `PasswordKeys.proofPublicKey`): what
/// proves the password of an account without SRP's verifier. Null for
/// one with it.
_idt.ByteData? passwordProofKey;
/// The account's X25519 public key, which data keys are sealed to.
_idt.ByteData encryptionPublicKey;
@ -187,6 +204,7 @@ abstract class AccountPassword
int? kdfOpsLimit,
int? kdfMemLimit,
_idt.ByteData? verifier,
_idt.ByteData? passwordProofKey,
_idt.ByteData? encryptionPublicKey,
_idt.ByteData? signingPublicKey,
_idt.ByteData? passwordKeysNonce,
@ -208,7 +226,9 @@ abstract class AccountPassword
'kdfAlgorithm': kdfAlgorithm,
'kdfOpsLimit': kdfOpsLimit,
'kdfMemLimit': kdfMemLimit,
'verifier': verifier.toJson(),
if (verifier != null) 'verifier': verifier?.toJson(),
if (passwordProofKey != null)
'passwordProofKey': passwordProofKey?.toJson(),
'encryptionPublicKey': encryptionPublicKey.toJson(),
'signingPublicKey': signingPublicKey.toJson(),
'passwordKeysNonce': passwordKeysNonce.toJson(),
@ -267,7 +287,8 @@ class _AccountPasswordImpl extends AccountPassword {
required String kdfAlgorithm,
required int kdfOpsLimit,
required int kdfMemLimit,
required _idt.ByteData verifier,
_idt.ByteData? verifier,
_idt.ByteData? passwordProofKey,
required _idt.ByteData encryptionPublicKey,
required _idt.ByteData signingPublicKey,
required _idt.ByteData passwordKeysNonce,
@ -286,6 +307,7 @@ class _AccountPasswordImpl extends AccountPassword {
kdfOpsLimit: kdfOpsLimit,
kdfMemLimit: kdfMemLimit,
verifier: verifier,
passwordProofKey: passwordProofKey,
encryptionPublicKey: encryptionPublicKey,
signingPublicKey: signingPublicKey,
passwordKeysNonce: passwordKeysNonce,
@ -309,7 +331,8 @@ class _AccountPasswordImpl extends AccountPassword {
String? kdfAlgorithm,
int? kdfOpsLimit,
int? kdfMemLimit,
_idt.ByteData? verifier,
Object? verifier = _Undefined,
Object? passwordProofKey = _Undefined,
_idt.ByteData? encryptionPublicKey,
_idt.ByteData? signingPublicKey,
_idt.ByteData? passwordKeysNonce,
@ -328,7 +351,10 @@ class _AccountPasswordImpl extends AccountPassword {
kdfAlgorithm: kdfAlgorithm ?? this.kdfAlgorithm,
kdfOpsLimit: kdfOpsLimit ?? this.kdfOpsLimit,
kdfMemLimit: kdfMemLimit ?? this.kdfMemLimit,
verifier: verifier ?? this.verifier.clone(),
verifier: verifier is _idt.ByteData? ? verifier : this.verifier?.clone(),
passwordProofKey: passwordProofKey is _idt.ByteData?
? passwordProofKey
: this.passwordProofKey?.clone(),
encryptionPublicKey:
encryptionPublicKey ?? this.encryptionPublicKey.clone(),
signingPublicKey: signingPublicKey ?? this.signingPublicKey.clone(),
@ -381,11 +407,19 @@ class AccountPasswordUpdateTable extends _is.UpdateTable<AccountPasswordTable> {
value,
);
_is.ColumnValue<_idt.ByteData, _idt.ByteData> verifier(_idt.ByteData value) =>
_is.ColumnValue(
table.verifier,
value,
);
_is.ColumnValue<_idt.ByteData, _idt.ByteData> verifier(
_idt.ByteData? value,
) => _is.ColumnValue(
table.verifier,
value,
);
_is.ColumnValue<_idt.ByteData, _idt.ByteData> passwordProofKey(
_idt.ByteData? value,
) => _is.ColumnValue(
table.passwordProofKey,
value,
);
_is.ColumnValue<_idt.ByteData, _idt.ByteData> encryptionPublicKey(
_idt.ByteData value,
@ -484,6 +518,10 @@ class AccountPasswordTable extends _is.Table<int?> {
'verifier',
this,
);
passwordProofKey = _is.ColumnByteData(
'passwordProofKey',
this,
);
encryptionPublicKey = _is.ColumnByteData(
'encryptionPublicKey',
this,
@ -543,9 +581,17 @@ class AccountPasswordTable extends _is.Table<int?> {
/// Argon2id's memory, in bytes.
late final _is.ColumnInt kdfMemLimit;
/// SRP's verifier, big-endian, as long as N.
/// SRP's verifier, big-endian, as long as N; null for an account that
/// signs in with SimpleLogin, which proves the password with
/// [passwordProofKey] instead.
late final _is.ColumnByteData verifier;
/// The public half of the password's own Ed25519 proof key, derived from
/// the password alone (halkyon_keys' `PasswordKeys.proofPublicKey`): what
/// proves the password of an account without SRP's verifier. Null for
/// one with it.
late final _is.ColumnByteData passwordProofKey;
/// The account's X25519 public key, which data keys are sealed to.
late final _is.ColumnByteData encryptionPublicKey;
@ -591,6 +637,7 @@ class AccountPasswordTable extends _is.Table<int?> {
kdfOpsLimit,
kdfMemLimit,
verifier,
passwordProofKey,
encryptionPublicKey,
signingPublicKey,
passwordKeysNonce,

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