fix(auth)!: proofs are bound to the instance the device addresses

Nothing tied a sign-in's proof to the instance it was made for: an
instance a person was lured into adding could pass A, M1 and the code on
to the real register and get a session there, and pass a phrase's proof
on with a password of its own in place of the person's (the second
reading's M6). SRP's identity is now a label, the challenge's ID and
the host the device addressed (passwordIdentity, in halkyon_base_client
and halkyon_base_server alike), and the phrase's proof signs that host,
what it is for (an authenticator's reset or a new password) and the new
password's digest (key proof v2). Every proving call carries the host;
in production it must be the server's apiServer.publicHost (otherwise
otherInstance, which the app shows as an instance it can't trust), in
development any; a member forwards its own. The tests prove that a
relayed sign-in, a proof for another host or purpose, and a swapped
password are refused, nothing changing.

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-20 21:20:12 +02:00
commit dab2c80524
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
15 changed files with 966 additions and 137 deletions

View file

@ -25,7 +25,8 @@ export 'src/password/authenticators.dart'
forgetUnconfirmedAuthenticators,
maxAuthenticators,
unconfirmedAuthenticatorLifetime;
export 'src/password/key_proof.dart' show keyProofMessage;
export 'src/password/key_proof.dart'
show KeyProofPurpose, keyProofMessage, newPasswordDigest, passwordIdentity;
export 'src/password/password_config.dart';
export 'src/password/passwords.dart'
show

View file

@ -339,6 +339,11 @@ class Endpoints extends _is.EndpointDispatch {
type: _is.getType<_idt.ByteData?>(),
nullable: true,
),
'host': _is.ParameterDescription(
name: 'host',
type: _is.getType<String>(),
nullable: false,
),
},
call:
(
@ -358,6 +363,7 @@ class Endpoints extends _is.EndpointDispatch {
code: params['code'],
keyChallenge: params['keyChallenge'],
signature: params['signature'],
host: params['host'],
),
),
'beginKeyProof': _is.MethodConnector(
@ -409,6 +415,11 @@ class Endpoints extends _is.EndpointDispatch {
type: _is.getType<String?>(),
nullable: true,
),
'host': _is.ParameterDescription(
name: 'host',
type: _is.getType<String>(),
nullable: false,
),
},
call:
(
@ -424,6 +435,7 @@ class Endpoints extends _is.EndpointDispatch {
password: params['password'],
totpCode: params['totpCode'],
recoveryCode: params['recoveryCode'],
host: params['host'],
),
),
'startAccountReset': _is.MethodConnector(
@ -710,6 +722,11 @@ class Endpoints extends _is.EndpointDispatch {
type: _is.getType<_ijye8mcv.NewPassword>(),
nullable: false,
),
'host': _is.ParameterDescription(
name: 'host',
type: _is.getType<String>(),
nullable: false,
),
},
call:
(
@ -724,6 +741,7 @@ class Endpoints extends _is.EndpointDispatch {
totpCode: params['totpCode'],
recoveryCode: params['recoveryCode'],
password: params['password'],
host: params['host'],
),
),
'setRecoveryKeys': _is.MethodConnector(
@ -764,6 +782,11 @@ class Endpoints extends _is.EndpointDispatch {
type: _is.getType<_idt.ByteData>(),
nullable: false,
),
'host': _is.ParameterDescription(
name: 'host',
type: _is.getType<String>(),
nullable: false,
),
},
call:
(
@ -779,6 +802,7 @@ class Endpoints extends _is.EndpointDispatch {
recoveryCode: params['recoveryCode'],
recoveryKeys: params['recoveryKeys'],
recoveryProofKey: params['recoveryProofKey'],
host: params['host'],
),
),
},
@ -846,6 +870,11 @@ class Endpoints extends _is.EndpointDispatch {
type: _is.getType<String?>(),
nullable: true,
),
'host': _is.ParameterDescription(
name: 'host',
type: _is.getType<String>(),
nullable: false,
),
},
call:
(
@ -863,6 +892,7 @@ class Endpoints extends _is.EndpointDispatch {
recoveryCode: params['recoveryCode'],
request: params['request'],
code: params['code'],
host: params['host'],
),
),
'signUp': _is.MethodConnector(

View file

@ -73,7 +73,12 @@ enum PasswordRefusal implements _is.SerializableModel {
/// Adding an authenticator to an account that has one confirmed takes a
/// code from one of those, which is missing or isn't right: a session
/// alone adds no second factor.
wrongExistingCode;
wrongExistingCode,
/// The device addressed this instance by another name than its own
/// (`apiServer.publicHost`): what it proves is bound to the name it
/// addressed, so the operator must configure the name devices use.
otherInstance;
static PasswordRefusal fromJson(String name) {
switch (name) {
@ -111,6 +116,8 @@ enum PasswordRefusal implements _is.SerializableModel {
return PasswordRefusal.noReset;
case 'wrongExistingCode':
return PasswordRefusal.wrongExistingCode;
case 'otherInstance':
return PasswordRefusal.otherInstance;
default:
throw ArgumentError(
'Value "$name" cannot be converted to "PasswordRefusal"',

View file

@ -3,6 +3,7 @@ import 'dart:typed_data';
import 'package:clock/clock.dart';
import 'package:halkyon_base_server/src/auth/email.dart';
import 'package:halkyon_base_server/src/generated/protocol.dart';
import 'package:halkyon_base_server/src/password/key_proof.dart';
import 'package:halkyon_base_server/src/password/passwords.dart';
import 'package:halkyon_base_server/src/register/sessions_ended.dart';
import 'package:serverpod/serverpod.dart';
@ -50,6 +51,7 @@ Future<ResetPending> resetAuthenticator(
String? code,
UuidValue? keyChallenge,
ByteData? signature,
required String host,
}) async {
final pod = session.serverpod;
final keys = requirePasswordKeys(pod);
@ -59,6 +61,7 @@ Future<ResetPending> resetAuthenticator(
challenge: challenge,
clientValue: clientValue,
clientProof: clientProof,
host: host,
);
final to = normalizedAddress(address);
if (to == null || addressHash(pod, to) != proved.addressHash) {
@ -80,6 +83,8 @@ Future<ResetPending> resetAuthenticator(
keys,
challenge: keyChallenge,
signature: signature,
host: host,
purpose: KeyProofPurpose.authenticatorReset,
);
if (owner.authUserId != account) {
throw refusedFor(PasswordRefusal.wrongCredentials);
@ -117,6 +122,7 @@ Future<AccountSessionsEnded> resetPasswordWithPhrase(
required NewPassword password,
String? totpCode,
String? recoveryCode,
required String host,
}) async {
final keys = requirePasswordKeys(session.serverpod);
if (!isNewPassword(password)) {
@ -127,6 +133,9 @@ Future<AccountSessionsEnded> resetPasswordWithPhrase(
keys,
challenge: challenge,
signature: signature,
host: host,
purpose: KeyProofPurpose.passwordReset,
digest: newPasswordDigest(password),
);
final account = row.authUserId;
await secondFactor(

View file

@ -2,6 +2,7 @@ import 'dart:typed_data';
import 'package:halkyon_base_server/src/common/network.dart';
import 'package:halkyon_base_server/src/generated/protocol.dart';
import 'package:halkyon_base_server/src/password/key_proof.dart';
import 'package:halkyon_base_server/src/password/passwords.dart';
import 'package:halkyon_base_server/src/register/account_register.dart';
import 'package:halkyon_base_server/src/register/people.dart';
@ -30,6 +31,7 @@ class AccountRecoveryEndpoint extends Endpoint {
String? code,
UuidValue? keyChallenge,
ByteData? signature,
required String host,
}) => AccountRegister.of(session.serverpod).resetAuthenticator(
session,
challenge: challenge,
@ -41,6 +43,7 @@ class AccountRecoveryEndpoint extends Endpoint {
code: code,
keyChallenge: keyChallenge,
signature: signature,
host: deviceHostOn(session.serverpod, host),
);
/// The first step of proving the recovery phrase for [address]: the
@ -64,6 +67,7 @@ class AccountRecoveryEndpoint extends Endpoint {
required NewPassword password,
String? totpCode,
String? recoveryCode,
required String host,
}) async {
final ended = await AccountRegister.of(session.serverpod)
.resetPasswordWithPhrase(
@ -73,6 +77,7 @@ class AccountRecoveryEndpoint extends Endpoint {
password: password,
totpCode: totpCode,
recoveryCode: recoveryCode,
host: deviceHostOn(session.serverpod, host),
);
return await issueSessionAfter(
session,

View file

@ -1,25 +1,125 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:crypto/crypto.dart' as hashes;
import 'package:cryptography/cryptography.dart';
import 'package:halkyon_base_server/src/generated/protocol.dart';
import 'package:serverpod/serverpod.dart';
// Proving the recovery phrase (ADR 0017): the device opens the account's
// private keys with the phrase and signs, with the Ed25519 key so opened,
// what [keyProofMessage] makes of the register's challenge; the register
// checks the signature against the account's public signing key. Only the
// account's own key signs so: the phrase never leaves the device.
// What a device proves and signs, bound to the instance it addresses (ADR
// 0017; the second reading's M6): SRP's identity and the recovery phrase's
// proof carry the host the device addressed, so a hostile instance a person
// was lured into adding can't relay them to the real one, which takes them
// only for its own host; and a phrase's proof for a new password signs that
// password, so nobody between can swap in another. The phrase's proof key
// is the phrase's own (`recoveryProofKey`), never the account's signing key.
// halkyon_base_client's `key_proof.dart` makes the same bytes.
/// What the device signs to prove the recovery phrase: a label naming the
/// purpose and its version, the challenge's ID (its 16 bytes) and its nonce,
/// so a signature made for one challenge, or for anything else the account
/// signs, never counts for another.
Uint8List keyProofMessage(UuidValue challenge, List<int> nonce) =>
Uint8List.fromList([
...utf8.encode('halkyon key proof v1|'),
...challenge.toBytes(),
...nonce,
]);
/// SRP's identity I for the sign-in [challenge] on [host] (in lower case):
/// x comes from Argon2id over the password and salt alone, so I only binds
/// the proof, to this sign-in and this instance.
Uint8List passwordIdentity(UuidValue challenge, String host) => _framed([
utf8.encode('halkyon srp identity v1'),
challenge.toBytes(),
utf8.encode(host.toLowerCase()),
]);
/// What proving the recovery phrase is for.
enum KeyProofPurpose {
/// Asking for the reset of a lost authenticator.
authenticatorReset('authenticator reset'),
/// Setting a new password, the keys staying.
passwordReset('password reset');
KeyProofPurpose(this.label);
/// The words the message carries.
final String label;
}
/// What the device signs with the phrase's proof key: a label naming the
/// format, the challenge's ID (its 16 bytes), its nonce, the [host] the
/// device addressed, the [purpose] and the [digest] of what it sets
/// ([newPasswordDigest]), each framed by its length; so a signature made
/// for one challenge, instance, purpose or password never counts for
/// another.
Uint8List keyProofMessage(
UuidValue challenge,
List<int> nonce, {
required String host,
required KeyProofPurpose purpose,
List<int> digest = const [],
}) => _framed([
utf8.encode('halkyon key proof v2'),
challenge.toBytes(),
nonce,
utf8.encode(host.toLowerCase()),
utf8.encode(purpose.label),
digest,
]);
/// The SHA-256 of [password] as the device sent it, framed: what a
/// phrase's proof for it signs.
Uint8List newPasswordDigest(NewPassword password) => Uint8List.fromList(
hashes.sha256
.convert(
_framed([
utf8.encode('halkyon new password v1'),
Uint8List.sublistView(password.salt),
utf8.encode(password.costs.algorithm),
utf8.encode('${password.costs.opsLimit}'),
utf8.encode('${password.costs.memLimit}'),
Uint8List.sublistView(password.verifier),
Uint8List.sublistView(password.passwordKeys.nonce),
Uint8List.sublistView(password.passwordKeys.cipherText),
]),
)
.bytes,
);
/// [parts], each after its length in four bytes, big-endian.
Uint8List _framed(List<List<int>> parts) {
final bytes = BytesBuilder(copy: false);
for (final part in parts) {
bytes
..addByte((part.length >> 24) & 0xff)
..addByte((part.length >> 16) & 0xff)
..addByte((part.length >> 8) & 0xff)
..addByte(part.length & 0xff)
..add(part);
}
return bytes.takeBytes();
}
/// The host a device addressed this server by ([claimed]), which its proofs
/// are bound to: where the server runs for real, only its own public host
/// ([publicHost], `apiServer.publicHost`), refused otherwise
/// ([PasswordRefusal.otherInstance]: the instance is configured under
/// another name than the device reaches it by); in development whatever
/// the device names, as an emulator reaches the developer's machine as
/// 10.0.2.2 and a phone on the network by its address. A relaying
/// instance gains nothing either way: the device's proof names the host it
/// addressed, and the real one checks it for its own.
String deviceHost({
required String runMode,
required String publicHost,
required String claimed,
}) {
final host = claimed.trim().toLowerCase();
if (runMode != ServerpodRunMode.development &&
host != publicHost.trim().toLowerCase()) {
throw PasswordRefusedException(reason: PasswordRefusal.otherInstance);
}
return host;
}
/// [deviceHost] on [pod].
String deviceHostOn(Serverpod pod, String claimed) => deviceHost(
runMode: pod.runMode,
publicHost: pod.config.apiServer.publicHost,
claimed: claimed,
);
/// Whether [signature] is the account's Ed25519 signature ([publicKey], 32
/// bytes) over [message]; false for anything malformed.

View file

@ -36,6 +36,7 @@ Future<void> changePassword(
required ByteData clientProof,
String? totpCode,
String? recoveryCode,
required String host,
required NewPassword password,
UuidValue? keepSession,
}) async {
@ -52,6 +53,7 @@ Future<void> changePassword(
clientProof: clientProof,
totpCode: totpCode,
recoveryCode: recoveryCode,
host: host,
);
await session.db.transaction((transaction) async {
// UPDATE "account_password" SET "salt" = $1, "kdfAlgorithm" = $2,
@ -101,6 +103,7 @@ Future<void> setRecoveryKeys(
required ByteData clientProof,
String? totpCode,
String? recoveryCode,
required String host,
required WrappedKeys recoveryKeys,
required ByteData recoveryProofKey,
}) async {
@ -117,6 +120,7 @@ Future<void> setRecoveryKeys(
clientProof: clientProof,
totpCode: totpCode,
recoveryCode: recoveryCode,
host: host,
);
// UPDATE "account_password" SET "recoveryKeysNonce" = $1,
// "recoveryKeysCipherText" = $2, "recoveryProofKey" = $3,
@ -150,6 +154,7 @@ Future<AccountPassword> _proveOwner(
required ByteData clientProof,
String? totpCode,
String? recoveryCode,
required String host,
}) async {
final proved = await provePassword(
session,
@ -157,6 +162,7 @@ Future<AccountPassword> _proveOwner(
challenge: challenge,
clientValue: clientValue,
clientProof: clientProof,
host: host,
);
final row = proved.password;
if (row.authUserId != person) {

View file

@ -2,6 +2,7 @@ import 'dart:typed_data';
import 'package:halkyon_base_server/src/auth/membership.dart';
import 'package:halkyon_base_server/src/generated/protocol.dart';
import 'package:halkyon_base_server/src/password/key_proof.dart';
import 'package:halkyon_base_server/src/register/account_register.dart';
import 'package:halkyon_base_server/src/register/people.dart';
import 'package:serverpod/serverpod.dart';
@ -60,6 +61,7 @@ class PasswordEndpoint extends Endpoint {
String? totpCode,
String? recoveryCode,
required NewPassword password,
required String host,
}) {
final authenticated = session.authenticated!;
return AccountRegister.of(session.serverpod).changePassword(
@ -75,6 +77,7 @@ class PasswordEndpoint extends Endpoint {
keepSession: Uuid.isValidUUID(fromString: authenticated.authId)
? UuidValue.fromString(authenticated.authId)
: null,
host: deviceHostOn(session.serverpod, host),
);
}
@ -92,6 +95,7 @@ class PasswordEndpoint extends Endpoint {
String? recoveryCode,
required WrappedKeys recoveryKeys,
required ByteData recoveryProofKey,
required String host,
}) => AccountRegister.of(session.serverpod).setRecoveryKeys(
session,
person: session.authenticated!.authUserId,
@ -102,5 +106,6 @@ class PasswordEndpoint extends Endpoint {
recoveryCode: recoveryCode,
recoveryKeys: recoveryKeys,
recoveryProofKey: recoveryProofKey,
host: deviceHostOn(session.serverpod, host),
);
}

View file

@ -46,3 +46,7 @@ values:
### code from one of those, which is missing or isn't right: a session
### alone adds no second factor.
- wrongExistingCode
### The device addressed this instance by another name than its own
### (`apiServer.publicHost`): what it proves is bound to the name it
### addressed, so the operator must configure the name devices use.
- otherInstance

View file

@ -2,6 +2,7 @@ import 'dart:typed_data';
import 'package:halkyon_base_server/src/common/network.dart';
import 'package:halkyon_base_server/src/generated/protocol.dart';
import 'package:halkyon_base_server/src/password/key_proof.dart';
import 'package:halkyon_base_server/src/password/passwords.dart';
import 'package:halkyon_base_server/src/register/account_register.dart';
import 'package:halkyon_base_server/src/register/people.dart';
@ -41,6 +42,7 @@ class PasswordSignInEndpoint extends Endpoint {
String? recoveryCode,
UuidValue? request,
String? code,
required String host,
}) async {
final proof = await AccountRegister.of(session.serverpod)
.completePasswordSignIn(
@ -52,6 +54,7 @@ class PasswordSignInEndpoint extends Endpoint {
recoveryCode: recoveryCode,
request: request,
code: code,
host: deviceHostOn(session.serverpod, host),
);
final signedIn = await issueSession(
session,

View file

@ -551,6 +551,7 @@ Future<PasswordProof> completePasswordSignIn(
String? recoveryCode,
UuidValue? request,
String? code,
required String host,
}) async {
final keys = requirePasswordKeys(session.serverpod);
final proved = await provePassword(
@ -559,6 +560,7 @@ Future<PasswordProof> completePasswordSignIn(
challenge: challenge,
clientValue: clientValue,
clientProof: clientProof,
host: host,
);
final account = proved.password.authUserId;
final bool needsAuthenticator;
@ -659,15 +661,18 @@ typedef PasswordProved = ({
/// Takes [challenge] (once) and checks the device's proof against it, as
/// [completePasswordSignIn] and the reset of a lost authenticator do; SRP's
/// identity is the challenge's ID (16 bytes), so nobody learns an account's
/// ID from its address. Refusals as [completePasswordSignIn]'s, but for the
/// second factor.
/// identity is [passwordIdentity] of the challenge's ID (so nobody learns an
/// account's ID from its address) and of the [host] the device addressed
/// (`deviceHost`, or the host a member names), so a proof a hostile
/// instance relays here never counts. Refusals as
/// [completePasswordSignIn]'s, but for the second factor.
Future<PasswordProved> provePassword(
Session session,
PasswordKeys keys, {
required UuidValue challenge,
required ByteData clientValue,
required ByteData clientProof,
required String host,
}) async {
final kept = await takeChallenge(
session,
@ -679,7 +684,7 @@ Future<PasswordProved> provePassword(
final clientBytes = Uint8List.sublistView(clientValue);
final verification = srpVerify(
passwordGroup,
identity: challenge.toBytes(),
identity: passwordIdentity(challenge, host),
salt: password == null ? decoy.salt : Uint8List.sublistView(password.salt),
verifier: password == null
? decoy.verifier
@ -972,7 +977,9 @@ Future<KeyProofChallenge> beginKeyProof(
}
/// Takes the key proof [challenge] (once) and checks [signature], the
/// signature over [keyProofMessage] by the recovery phrase's own proof key
/// signature over [keyProofMessage] (for the [host] the device addressed,
/// the [purpose] and the [digest] of what it sets) by the recovery
/// phrase's own proof key
/// (`recoveryProofKey`: derived from the phrase alone, so the account's
/// signing key, which every signed-in device keeps, proves nothing); the
/// account's password row. Refused ([PasswordRefusedException]) for a
@ -984,6 +991,9 @@ Future<AccountPassword> proveRecoveryPhrase(
PasswordKeys keys, {
required UuidValue challenge,
required ByteData signature,
required String host,
required KeyProofPurpose purpose,
List<int> digest = const [],
}) async {
final kept = await takeChallenge(
session,
@ -996,7 +1006,13 @@ Future<AccountPassword> proveRecoveryPhrase(
final password = await _passwordOf(session, kept.authUserId);
final proofKey = password?.recoveryProofKey;
final signed = await isAccountSignature(
keyProofMessage(challenge, _unhex(kept.secret)),
keyProofMessage(
challenge,
_unhex(kept.secret),
host: host,
purpose: purpose,
digest: digest,
),
signature: Uint8List.sublistView(signature),
publicKey: proofKey == null
? decoy.recoveryProofKey

View file

@ -170,6 +170,7 @@ abstract interface class AccountRegister {
String? recoveryCode,
UuidValue? request,
String? code,
required String host,
});
/// Signs up with the address [code] proves for [request] and the password
@ -236,6 +237,7 @@ abstract interface class AccountRegister {
String? code,
UuidValue? keyChallenge,
ByteData? signature,
required String host,
});
/// The first step of proving the recovery phrase for [address], the
@ -256,6 +258,7 @@ abstract interface class AccountRegister {
required NewPassword password,
String? totpCode,
String? recoveryCode,
required String host,
});
/// Asks for the reset of a whole account. Refusals as
@ -298,6 +301,7 @@ abstract interface class AccountRegister {
String? recoveryCode,
required NewPassword password,
UuidValue? keepSession,
required String host,
});
/// Replaces the wrapping of [person]'s keys under their recovery phrase
@ -313,6 +317,7 @@ abstract interface class AccountRegister {
String? recoveryCode,
required WrappedKeys recoveryKeys,
required ByteData recoveryProofKey,
required String host,
});
/// Deletes the account [person] with everything the register's server has

View file

@ -236,6 +236,7 @@ Future<Map<String, Object?>?> _answer(
recoveryCode: arguments.optionalString('recoveryCode'),
request: arguments.optionalId('request'),
code: arguments.optionalString('code'),
host: arguments.string('host'),
);
return {'proof': proof.toJson()};
case RegisterOperation.signUpWithCode:
@ -300,6 +301,7 @@ Future<Map<String, Object?>?> _answer(
code: arguments.optionalString('code'),
keyChallenge: arguments.optionalId('keyChallenge'),
signature: arguments.optionalBytes('signature'),
host: arguments.string('host'),
);
return {'pending': pending.toJson()};
case RegisterOperation.beginKeyProof:
@ -317,6 +319,7 @@ Future<Map<String, Object?>?> _answer(
password: arguments.model('password', NewPassword.fromJson),
totpCode: arguments.optionalString('totpCode'),
recoveryCode: arguments.optionalString('recoveryCode'),
host: arguments.string('host'),
);
return _sessionsEnded(ended);
case RegisterOperation.startAccountReset:
@ -354,6 +357,7 @@ Future<Map<String, Object?>?> _answer(
recoveryCode: arguments.optionalString('recoveryCode'),
password: arguments.model('password', NewPassword.fromJson),
keepSession: arguments.optionalId('keepSession'),
host: arguments.string('host'),
);
return const {};
case RegisterOperation.setRecoveryKeys:
@ -367,6 +371,7 @@ Future<Map<String, Object?>?> _answer(
recoveryCode: arguments.optionalString('recoveryCode'),
recoveryKeys: arguments.model('recoveryKeys', WrappedKeys.fromJson),
recoveryProofKey: arguments.bytes('recoveryProofKey'),
host: arguments.string('host'),
);
return const {};
}

View file

@ -430,6 +430,7 @@ final class LocalRegister implements AccountRegister {
String? recoveryCode,
UuidValue? request,
String? code,
required String host,
}) => passwords.completePasswordSignIn(
session,
challenge: challenge,
@ -439,6 +440,7 @@ final class LocalRegister implements AccountRegister {
recoveryCode: recoveryCode,
request: request,
code: code,
host: host,
);
@override
@ -520,6 +522,7 @@ final class LocalRegister implements AccountRegister {
String? code,
UuidValue? keyChallenge,
ByteData? signature,
required String host,
}) => recovery.resetAuthenticator(
session,
challenge: challenge,
@ -531,6 +534,7 @@ final class LocalRegister implements AccountRegister {
code: code,
keyChallenge: keyChallenge,
signature: signature,
host: host,
);
@override
@ -548,6 +552,7 @@ final class LocalRegister implements AccountRegister {
required NewPassword password,
String? totpCode,
String? recoveryCode,
required String host,
}) => recovery.resetPasswordWithPhrase(
session,
challenge: challenge,
@ -555,6 +560,7 @@ final class LocalRegister implements AccountRegister {
password: password,
totpCode: totpCode,
recoveryCode: recoveryCode,
host: host,
);
@override
@ -604,6 +610,7 @@ final class LocalRegister implements AccountRegister {
String? recoveryCode,
required NewPassword password,
UuidValue? keepSession,
required String host,
}) => change.changePassword(
session,
person: person,
@ -614,6 +621,7 @@ final class LocalRegister implements AccountRegister {
recoveryCode: recoveryCode,
password: password,
keepSession: keepSession,
host: host,
);
@override
@ -627,6 +635,7 @@ final class LocalRegister implements AccountRegister {
String? recoveryCode,
required WrappedKeys recoveryKeys,
required ByteData recoveryProofKey,
required String host,
}) => change.setRecoveryKeys(
session,
person: person,
@ -637,6 +646,7 @@ final class LocalRegister implements AccountRegister {
recoveryCode: recoveryCode,
recoveryKeys: recoveryKeys,
recoveryProofKey: recoveryProofKey,
host: host,
);
@override

View file

@ -321,9 +321,11 @@ final class RemoteRegister implements AccountRegister {
String? recoveryCode,
UuidValue? request,
String? code,
required String host,
}) async => _model<PasswordProof>(
RegisterOperation.completePasswordSignIn,
await _ask(RegisterOperation.completePasswordSignIn, {
'host': host,
'challenge': challenge.uuid,
'clientValue': _bytes(clientValue),
'clientProof': _bytes(clientProof),
@ -442,9 +444,11 @@ final class RemoteRegister implements AccountRegister {
String? code,
UuidValue? keyChallenge,
ByteData? signature,
required String host,
}) async => _model<ResetPending>(
RegisterOperation.resetAuthenticator,
await _ask(RegisterOperation.resetAuthenticator, {
'host': host,
'challenge': challenge.uuid,
'clientValue': _bytes(clientValue),
'clientProof': _bytes(clientProof),
@ -482,9 +486,11 @@ final class RemoteRegister implements AccountRegister {
required NewPassword password,
String? totpCode,
String? recoveryCode,
required String host,
}) async => _sessionsEnded(
RegisterOperation.resetPasswordWithPhrase,
await _ask(RegisterOperation.resetPasswordWithPhrase, {
'host': host,
'challenge': challenge.uuid,
'signature': _bytes(signature),
'password': password.toJson(),
@ -569,7 +575,9 @@ final class RemoteRegister implements AccountRegister {
String? recoveryCode,
required NewPassword password,
UuidValue? keepSession,
required String host,
}) => _ask(RegisterOperation.changePassword, {
'host': host,
'person': person.uuid,
'challenge': challenge.uuid,
'clientValue': _bytes(clientValue),
@ -591,7 +599,9 @@ final class RemoteRegister implements AccountRegister {
String? recoveryCode,
required WrappedKeys recoveryKeys,
required ByteData recoveryProofKey,
required String host,
}) => _ask(RegisterOperation.setRecoveryKeys, {
'host': host,
'person': person.uuid,
'challenge': challenge.uuid,
'clientValue': _bytes(clientValue),