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
36 changed files with 46254 additions and 78 deletions

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,

View file

@ -23,7 +23,13 @@ enum SignInChallengePurpose implements _is.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 _is.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 _is.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 _is.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

@ -29,6 +29,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_password.dart' as _ip37g5t3;
import 'password/account_reset.dart' as _i3fad0p6;
import 'password/account_reset_kind.dart' as _i91ka1it;
@ -74,6 +78,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_password.dart';
export 'password/account_reset.dart';
export 'password/account_reset_kind.dart';
@ -233,8 +241,14 @@ class Protocol extends _is.DatabaseSerializationManager {
_isp.ColumnDefinition(
name: 'verifier',
columnType: _isp.ColumnType.bytea,
isNullable: false,
dartType: 'dart:typed_data:ByteData',
isNullable: true,
dartType: 'dart:typed_data:ByteData?',
),
_isp.ColumnDefinition(
name: 'passwordProofKey',
columnType: _isp.ColumnType.bytea,
isNullable: true,
dartType: 'dart:typed_data:ByteData?',
),
_isp.ColumnDefinition(
name: 'encryptionPublicKey',
@ -1081,6 +1095,18 @@ class Protocol extends _is.DatabaseSerializationManager {
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 == _ip37g5t3.AccountPassword) {
return _ip37g5t3.AccountPassword.fromJson(data) as T;
}
@ -1230,6 +1256,19 @@ class Protocol extends _is.DatabaseSerializationManager {
return (data != null ? _i8jixqx6.InstanceBasics.fromJson(data) : null)
as T;
}
if (t == _is.getType<_iz50cwqw.DeviceKeys?>()) {
return (data != null ? _iz50cwqw.DeviceKeys.fromJson(data) : null) as T;
}
if (t == _is.getType<_itebbsfc.KeysChallenge?>()) {
return (data != null ? _itebbsfc.KeysChallenge.fromJson(data) : null)
as T;
}
if (t == _is.getType<_iviximyh.KeysProof?>()) {
return (data != null ? _iviximyh.KeysProof.fromJson(data) : null) as T;
}
if (t == _is.getType<_iqdhlx3t.KeysSetup?>()) {
return (data != null ? _iqdhlx3t.KeysSetup.fromJson(data) : null) as T;
}
if (t == _is.getType<_ip37g5t3.AccountPassword?>()) {
return (data != null ? _ip37g5t3.AccountPassword.fromJson(data) : null)
as T;
@ -1436,6 +1475,10 @@ class Protocol extends _is.DatabaseSerializationManager {
_i0xfcu5n.GitHubSignInRefusedException => 'GitHubSignInRefusedException',
_ie6tbh85.SignInWays => 'SignInWays',
_i8jixqx6.InstanceBasics => 'InstanceBasics',
_iz50cwqw.DeviceKeys => 'DeviceKeys',
_itebbsfc.KeysChallenge => 'KeysChallenge',
_iviximyh.KeysProof => 'KeysProof',
_iqdhlx3t.KeysSetup => 'KeysSetup',
_ip37g5t3.AccountPassword => 'AccountPassword',
_i3fad0p6.AccountReset => 'AccountReset',
_i91ka1it.AccountResetKind => 'AccountResetKind',
@ -1507,6 +1550,14 @@ class Protocol extends _is.DatabaseSerializationManager {
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 _ip37g5t3.AccountPassword():
return 'AccountPassword';
case _i3fad0p6.AccountReset():
@ -1620,6 +1671,18 @@ class Protocol extends _is.DatabaseSerializationManager {
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 == 'AccountPassword') {
return deserialize<_ip37g5t3.AccountPassword>(data['data']);
}

View file

@ -11,6 +11,9 @@ gitHubUpgrade:
- upgrade:
instance:
- basics:
keys:
- begin:
- unlock:
accountRecovery:
- resetAuthenticator:
- beginKeyProof:
@ -47,3 +50,5 @@ profiles:
simpleLogin:
- begin:
- complete:
- checkHandle:
- signUp:

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,
_iacs.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
: _is.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.
@_is.useResult
@ -63,6 +80,8 @@ abstract class SimpleLoginCompleted
_i2lb2chw.SimpleLoginOutcome? outcome,
_iacs.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,
_iacs.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

@ -23,12 +23,16 @@ abstract class SimpleLoginProof
required this.outcome,
this.account,
this.suggestedHandle,
});
bool? hasKeys,
this.handle,
}) : hasKeys = hasKeys ?? false;
factory SimpleLoginProof({
required _i2lb2chw.SimpleLoginOutcome outcome,
_is.UuidValue? account,
String? suggestedHandle,
bool? hasKeys,
String? handle,
}) = _SimpleLoginProofImpl;
factory SimpleLoginProof.fromJson(Map<String, dynamic> jsonSerialization) {
@ -40,6 +44,10 @@ abstract class SimpleLoginProof
? null
: _is.UuidValueJsonExtension.fromJson(jsonSerialization['account']),
suggestedHandle: jsonSerialization['suggestedHandle'] as String?,
hasKeys: jsonSerialization['hasKeys'] == null
? null
: _is.BoolJsonExtension.fromJson(jsonSerialization['hasKeys']),
handle: jsonSerialization['handle'] as String?,
);
}
@ -49,6 +57,12 @@ abstract class SimpleLoginProof
String? suggestedHandle;
/// Whether the account has its keys.
bool hasKeys;
/// The account's handle; null for none.
String? handle;
/// Returns a shallow copy of this [SimpleLoginProof]
/// with some or all fields replaced by the given arguments.
@_is.useResult
@ -56,6 +70,8 @@ abstract class SimpleLoginProof
_i2lb2chw.SimpleLoginOutcome? outcome,
_is.UuidValue? account,
String? suggestedHandle,
bool? hasKeys,
String? handle,
});
@override
Map<String, dynamic> toJson() {
@ -64,6 +80,8 @@ abstract class SimpleLoginProof
'outcome': outcome.toJson(),
if (account != null) 'account': account?.toJson(),
if (suggestedHandle != null) 'suggestedHandle': suggestedHandle,
'hasKeys': hasKeys,
if (handle != null) 'handle': handle,
};
}
@ -85,10 +103,14 @@ class _SimpleLoginProofImpl extends SimpleLoginProof {
required _i2lb2chw.SimpleLoginOutcome outcome,
_is.UuidValue? account,
String? suggestedHandle,
bool? hasKeys,
String? handle,
}) : super._(
outcome: outcome,
account: account,
suggestedHandle: suggestedHandle,
hasKeys: hasKeys,
handle: handle,
);
/// Returns a shallow copy of this [SimpleLoginProof]
@ -99,6 +121,8 @@ class _SimpleLoginProofImpl extends SimpleLoginProof {
_i2lb2chw.SimpleLoginOutcome? outcome,
Object? account = _Undefined,
Object? suggestedHandle = _Undefined,
bool? hasKeys,
Object? handle = _Undefined,
}) {
return SimpleLoginProof(
outcome: outcome ?? this.outcome,
@ -106,6 +130,8 @@ class _SimpleLoginProofImpl extends SimpleLoginProof {
suggestedHandle: suggestedHandle is String?
? suggestedHandle
: this.suggestedHandle,
hasKeys: hasKeys ?? this.hasKeys,
handle: handle is String? ? handle : this.handle,
);
}
}

View file

@ -0,0 +1,8 @@
### 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.
class: DeviceKeys
fields:
encryptionPublicKey: ByteData
signingPublicKey: ByteData
passwordKeys: WrappedKeys

View file

@ -0,0 +1,35 @@
import 'package:halkyon_base_server/src/generated/protocol.dart';
import 'package:halkyon_base_server/src/password/key_proof.dart';
import 'package:halkyon_base_server/src/password/passwords.dart';
import 'package:serverpod/serverpod.dart';
// The keys of an account that signs in with SimpleLogin, as the instance's
// register keeps them (Halkyon Learn's ADR 0020, 6, 7 and D19): ADR 0017's
// keys and their wrappings, the password proven with its own proof key in
// place of SRP. Refusals are `PasswordRefusedException`s.
/// [person]'s keys for a device, once [proof] proved their password for it
/// ([KeyProofPurpose.unlockKeys]): their public keys and their private keys
/// wrapped under the password's key, which the device opens. So someone who
/// took over the SimpleLogin account gets nothing to guess the password
/// against but the proofs, which count. Refusals as `provePasswordKey`'s.
Future<DeviceKeys> unlockKeys(
Session session, {
required UuidValue person,
required KeysProof proof,
}) async {
final password = await provePasswordKey(
session,
person: person,
proof: proof,
purpose: KeyProofPurpose.unlockKeys,
);
return DeviceKeys(
encryptionPublicKey: password.encryptionPublicKey,
signingPublicKey: password.signingPublicKey,
passwordKeys: WrappedKeys(
nonce: password.passwordKeysNonce,
cipherText: password.passwordKeysCipherText,
),
);
}

View file

@ -0,0 +1,10 @@
### 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.
class: KeysChallenge
fields:
challenge: UuidValue
nonce: ByteData
salt: ByteData
costs: PasswordCosts

View file

@ -0,0 +1,48 @@
import 'package:halkyon_base_server/src/generated/protocol.dart';
import 'package:halkyon_base_server/src/password/key_proof.dart';
import 'package:halkyon_base_server/src/password/owner_proof_tries.dart';
import 'package:halkyon_base_server/src/register/account_register.dart';
import 'package:serverpod/serverpod.dart';
import 'package:serverpod_auth_idp_server/core.dart';
/// The keys of an account that signs in with SimpleLogin (Halkyon Learn's
/// ADR 0020, 7 and D19), for its signed-in devices: the password proven
/// with its own proof key, which the instance's register checks (ADR 0016).
/// Refusals are `PasswordRefusedException`s.
class KeysEndpoint extends Endpoint {
@override
bool get requireLogin => true;
/// 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`).
Future<KeysChallenge> begin(Session session) =>
AccountRegister.of(session.serverpod).beginPasswordKeyProof(
session,
session.authenticated!.authUserId,
);
/// 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`).
Future<DeviceKeys> unlock(Session session, KeysProof proof) {
final pod = session.serverpod;
final bound = proof.copyWith(
instanceUrl: deviceInstanceUrlOn(pod, proof.instanceUrl),
);
return asOwnerProofTry(
session,
bound,
() => AccountRegister.of(pod).unlockKeys(
session,
session.authenticated!.authUserId,
bound,
),
);
}
}

View file

@ -0,0 +1,13 @@
### 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`).
class: KeysProof
fields:
challenge: UuidValue
### Ed25519's signature, 64 bytes.
signature: ByteData
### The instance's base URL the device addressed (scheme, host, port,
### path): the server it reached checks it is its own before the
### register binds the proof to it.
instanceUrl: String

View file

@ -0,0 +1,21 @@
### 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.
class: KeysSetup
fields:
salt: ByteData
costs: PasswordCosts
### The public half of the password's proof key (Ed25519).
passwordProofKey: ByteData
encryptionPublicKey: ByteData
signingPublicKey: ByteData
passwordKeys: WrappedKeys
recoveryKeys: WrappedKeys
### The public half of the recovery phrase's proof key (Ed25519, from the
### phrase alone).
recoveryProofKey: ByteData

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