fix(app)!: every owner's proof binds the change it approves
The device's answer takes what it is made for and the digest of what it sets, as the register now requires: the new password (made before the old one's proof, which binds it), the new phrase's wrapping and proof key, the authenticator confirmed (the first or another) or removed, new recovery codes, calling resets off, deleting, and asking for an authenticator's reset apart from signing in. proof_digests.dart maps the protocol's objects onto the shared framing; the key proof's password digest uses it too. The fake gateway refuses a proof made for anything but the call's own change, as the register does, so every flow's tests hold the repository to it; the fake's authenticators have UUIDs, as the register's do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
92d2df4695
commit
0a7679396b
6 changed files with 286 additions and 40 deletions
|
|
@ -2,6 +2,7 @@ import 'dart:typed_data';
|
|||
|
||||
import 'package:halkyon_app/src/password/model/unlocked_account_keys.dart';
|
||||
import 'package:halkyon_base_client/halkyon_base_client.dart' as protocol;
|
||||
import 'package:halkyon_base_client/srp.dart' as srp;
|
||||
|
||||
/// What a password unlocks on the device (ADR 0017), for one account's salt
|
||||
/// and costs: SRP's private value and the key that wraps the account's
|
||||
|
|
@ -97,11 +98,16 @@ abstract interface class AccountCrypto() {
|
|||
|
||||
/// The answer to [challenge] (SRP's B and the account's salt) with what
|
||||
/// [unlock] holds, its identity bound to [instanceUrl], the instance's
|
||||
/// base URL as the device addresses it.
|
||||
/// base URL as the device addresses it, and to what it is made for
|
||||
/// ([purpose]) with the [digest] of what it sets
|
||||
/// (`proof_digests.dart`), so nobody between the device and the register
|
||||
/// can take it for another change.
|
||||
Future<SrpAnswer> answer(
|
||||
PasswordUnlock unlock,
|
||||
protocol.PasswordChallenge challenge, {
|
||||
required String instanceUrl,
|
||||
srp.PasswordProofPurpose purpose = srp.PasswordProofPurpose.signIn,
|
||||
List<int> digest = const [],
|
||||
});
|
||||
|
||||
/// The account's keys [wrapped] under the password, opened with
|
||||
|
|
|
|||
|
|
@ -9,12 +9,14 @@ import 'package:halkyon_app/src/auth/model/session_grant.dart';
|
|||
import 'package:halkyon_app/src/password/data/account_crypto.dart';
|
||||
import 'package:halkyon_app/src/password/data/account_key_store.dart';
|
||||
import 'package:halkyon_app/src/password/data/password_gateway.dart';
|
||||
import 'package:halkyon_app/src/password/data/proof_digests.dart';
|
||||
import 'package:halkyon_app/src/password/model/account_authenticator.dart';
|
||||
import 'package:halkyon_app/src/password/model/account_security.dart';
|
||||
import 'package:halkyon_app/src/password/model/mail_code_purpose.dart';
|
||||
import 'package:halkyon_app/src/password/model/password_failed.dart';
|
||||
import 'package:halkyon_app/src/password/model/unlocked_account_keys.dart';
|
||||
import 'package:halkyon_base_client/halkyon_base_client.dart' as protocol;
|
||||
import 'package:halkyon_base_client/srp.dart' as srp;
|
||||
|
||||
/// A sign-in under way: the address typed, and what the password unlocked
|
||||
/// for the account there (or for a decoy, should nobody have it: the
|
||||
|
|
@ -295,6 +297,12 @@ class PasswordRepository(
|
|||
: await _ownerAnswer(
|
||||
password,
|
||||
unlocked: unlocked,
|
||||
// Another one comes with a code from one confirmed before;
|
||||
// the first with the password alone.
|
||||
purpose: existingCode == null
|
||||
? srp.PasswordProofPurpose.firstAuthenticator
|
||||
: srp.PasswordProofPurpose.addAuthenticator,
|
||||
digest: authenticatorDigestOf(id),
|
||||
totpCode: existingCode,
|
||||
as: pending?.grant,
|
||||
),
|
||||
|
|
@ -316,7 +324,12 @@ class PasswordRepository(
|
|||
id,
|
||||
owner: password == null
|
||||
? null
|
||||
: await _ownerAnswer(password, totpCode: code),
|
||||
: await _ownerAnswer(
|
||||
password,
|
||||
purpose: srp.PasswordProofPurpose.removeAuthenticator,
|
||||
digest: authenticatorDigestOf(id),
|
||||
totpCode: code,
|
||||
),
|
||||
);
|
||||
_changed();
|
||||
}
|
||||
|
|
@ -327,7 +340,11 @@ class PasswordRepository(
|
|||
required String password,
|
||||
required String code,
|
||||
}) async => await _gateway.newRecoveryCodes(
|
||||
await _ownerAnswer(password, totpCode: code),
|
||||
await _ownerAnswer(
|
||||
password,
|
||||
purpose: srp.PasswordProofPurpose.newRecoveryCodes,
|
||||
totpCode: code,
|
||||
),
|
||||
);
|
||||
|
||||
/// Calls off every reset of the signed-in person's account waiting, for
|
||||
|
|
@ -337,15 +354,26 @@ class PasswordRepository(
|
|||
required String password,
|
||||
required String code,
|
||||
}) async {
|
||||
await _gateway.callOffResets(await _ownerAnswer(password, totpCode: code));
|
||||
await _gateway.callOffResets(
|
||||
await _ownerAnswer(
|
||||
password,
|
||||
purpose: srp.PasswordProofPurpose.callOffResets,
|
||||
totpCode: code,
|
||||
),
|
||||
);
|
||||
_changed();
|
||||
}
|
||||
|
||||
/// The owner's proof (`OwnerProof`): [password] (or [unlocked], unlocked
|
||||
/// at a sign-in already) answering a change's challenge, of the signed-in
|
||||
/// session or [as], with a [totpCode] or [recoveryCode] where given.
|
||||
/// session or [as], made for [purpose] with the [digest] of what it sets
|
||||
/// (`proof_digests.dart`: nobody between swaps anything in around it, nor
|
||||
/// takes it for another change), with a [totpCode] or [recoveryCode]
|
||||
/// where given.
|
||||
Future<OwnerAnswer> _ownerAnswer(
|
||||
String? password, {
|
||||
required srp.PasswordProofPurpose purpose,
|
||||
List<int> digest = const [],
|
||||
PasswordUnlocked? unlocked,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
|
|
@ -369,6 +397,8 @@ class PasswordRepository(
|
|||
unlock,
|
||||
challenge,
|
||||
instanceUrl: _gateway.instanceUrl,
|
||||
purpose: purpose,
|
||||
digest: digest,
|
||||
),
|
||||
totpCode: totpCode?.replaceAll(' ', ''),
|
||||
recoveryCode: recoveryCode,
|
||||
|
|
@ -422,13 +452,17 @@ class PasswordRepository(
|
|||
String? recoveryCode,
|
||||
}) async {
|
||||
final keys = await _keysHere();
|
||||
// Made first: the old password's proof binds it.
|
||||
final password = await _crypto.newPassword(newPassword, keys);
|
||||
await _gateway.change(
|
||||
await _ownerAnswer(
|
||||
oldPassword,
|
||||
purpose: srp.PasswordProofPurpose.newPassword,
|
||||
digest: newPasswordDigestOf(password),
|
||||
totpCode: totpCode,
|
||||
recoveryCode: recoveryCode,
|
||||
),
|
||||
password: await _crypto.newPassword(newPassword, keys),
|
||||
password: password,
|
||||
);
|
||||
_changed();
|
||||
}
|
||||
|
|
@ -458,6 +492,7 @@ class PasswordRepository(
|
|||
unlock,
|
||||
challenge,
|
||||
instanceUrl: _gateway.instanceUrl,
|
||||
purpose: srp.PasswordProofPurpose.deleteAccount,
|
||||
),
|
||||
totpCode: totpCode?.replaceAll(' ', ''),
|
||||
recoveryCode: recoveryCode,
|
||||
|
|
@ -507,7 +542,15 @@ class PasswordRepository(
|
|||
required String code,
|
||||
}) async {
|
||||
await _gateway.setRecoveryKeys(
|
||||
await _ownerAnswer(password, totpCode: code),
|
||||
await _ownerAnswer(
|
||||
password,
|
||||
purpose: srp.PasswordProofPurpose.newPhrase,
|
||||
digest: newPhraseDigestOf(
|
||||
phrase._recoveryKeys,
|
||||
phrase._recoveryProofKey,
|
||||
),
|
||||
totpCode: code,
|
||||
),
|
||||
keys: phrase._recoveryKeys,
|
||||
proofKey: phrase._recoveryProofKey,
|
||||
);
|
||||
|
|
@ -545,6 +588,7 @@ class PasswordRepository(
|
|||
unlocked._unlock,
|
||||
challenge,
|
||||
instanceUrl: _gateway.instanceUrl,
|
||||
purpose: srp.PasswordProofPurpose.authenticatorReset,
|
||||
);
|
||||
return await _gateway.resetAuthenticator(
|
||||
challenge,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_base_client/halkyon_base_client.dart' as protocol;
|
||||
import 'package:halkyon_base_client/srp.dart' as srp;
|
||||
|
||||
// The digests of what the owner's proofs set (ADR 0017; the full reading's
|
||||
// device medium 1), from the protocol's objects as the device sends them:
|
||||
// SRP's identity carries them, so whoever stands between the device and
|
||||
// the register can swap nothing in around a proof. halkyon_base_server's
|
||||
// `key_proof.dart` makes the same bytes from what it receives.
|
||||
|
||||
/// The digest of a new [password] (its salt, costs, verifier and wrapping).
|
||||
Uint8List newPasswordDigestOf(protocol.NewPassword password) =>
|
||||
srp.newPasswordDigest(
|
||||
salt: Uint8List.sublistView(password.salt),
|
||||
algorithm: password.costs.algorithm,
|
||||
opsLimit: password.costs.opsLimit,
|
||||
memLimit: password.costs.memLimit,
|
||||
verifier: Uint8List.sublistView(password.verifier),
|
||||
keysNonce: Uint8List.sublistView(password.passwordKeys.nonce),
|
||||
keysCipherText: Uint8List.sublistView(password.passwordKeys.cipherText),
|
||||
);
|
||||
|
||||
/// The digest of a new recovery phrase: the keys wrapped under it
|
||||
/// ([recoveryKeys]) and its proof key's public half ([proofKey]).
|
||||
Uint8List newPhraseDigestOf(
|
||||
protocol.WrappedKeys recoveryKeys,
|
||||
ByteData proofKey,
|
||||
) => srp.newPhraseDigest(
|
||||
keysNonce: Uint8List.sublistView(recoveryKeys.nonce),
|
||||
keysCipherText: Uint8List.sublistView(recoveryKeys.cipherText),
|
||||
proofKey: Uint8List.sublistView(proofKey),
|
||||
);
|
||||
|
||||
/// The digest of the authenticator whose ID is [id].
|
||||
Uint8List authenticatorDigestOf(String id) =>
|
||||
srp.authenticatorDigest(protocol.UuidValue.fromString(id).toBytes());
|
||||
|
|
@ -2,6 +2,7 @@ import 'dart:isolate';
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_app/src/password/data/account_crypto.dart';
|
||||
import 'package:halkyon_app/src/password/data/proof_digests.dart';
|
||||
import 'package:halkyon_app/src/password/model/password_failed.dart';
|
||||
import 'package:halkyon_app/src/password/model/unlocked_account_keys.dart';
|
||||
import 'package:halkyon_base_client/halkyon_base_client.dart' as protocol;
|
||||
|
|
@ -87,6 +88,8 @@ class SodiumAccountCrypto() implements AccountCrypto {
|
|||
PasswordUnlock unlock,
|
||||
protocol.PasswordChallenge challenge, {
|
||||
required String instanceUrl,
|
||||
srp.PasswordProofPurpose purpose = srp.PasswordProofPurpose.signIn,
|
||||
List<int> digest = const [],
|
||||
}) async {
|
||||
final group = srp.SrpGroup.rfc5054Group3072;
|
||||
final (:clientValue, :proof) = await Isolate.run(() {
|
||||
|
|
@ -95,6 +98,8 @@ class SodiumAccountCrypto() implements AccountCrypto {
|
|||
identity: srp.passwordIdentity(
|
||||
challenge.challenge.toBytes(),
|
||||
instanceUrl,
|
||||
purpose: purpose,
|
||||
digest: digest,
|
||||
),
|
||||
salt: Uint8List.sublistView(challenge.salt),
|
||||
privateValue: _numberOf(unlock.srpSecret),
|
||||
|
|
@ -233,7 +238,9 @@ class SodiumAccountCrypto() implements AccountCrypto {
|
|||
purpose: password == null
|
||||
? srp.KeyProofPurpose.authenticatorReset
|
||||
: srp.KeyProofPurpose.passwordReset,
|
||||
digest: password == null ? const [] : _digestOf(password),
|
||||
digest: password == null
|
||||
? const []
|
||||
: newPasswordDigestOf(password),
|
||||
),
|
||||
secretKey: proof.secretKey,
|
||||
),
|
||||
|
|
@ -268,17 +275,6 @@ class SodiumAccountCrypto() implements AccountCrypto {
|
|||
);
|
||||
}
|
||||
|
||||
/// [password]'s digest, which a phrase's proof for it signs.
|
||||
Uint8List _digestOf(protocol.NewPassword password) => srp.newPasswordDigest(
|
||||
salt: Uint8List.sublistView(password.salt),
|
||||
algorithm: password.costs.algorithm,
|
||||
opsLimit: password.costs.opsLimit,
|
||||
memLimit: password.costs.memLimit,
|
||||
verifier: Uint8List.sublistView(password.verifier),
|
||||
keysNonce: Uint8List.sublistView(password.passwordKeys.nonce),
|
||||
keysCipherText: Uint8List.sublistView(password.passwordKeys.cipherText),
|
||||
);
|
||||
|
||||
/// Argon2id's [costs] as an instance named them, refused
|
||||
/// ([PasswordProblem.unsafeCosts]) outside the bounds every account is
|
||||
/// within, before anything is derived: below them guessing the password
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:halkyon_app/src/password/data/account_crypto.dart';
|
|||
import 'package:halkyon_app/src/password/model/password_failed.dart';
|
||||
import 'package:halkyon_app/src/password/model/unlocked_account_keys.dart';
|
||||
import 'package:halkyon_base_client/halkyon_base_client.dart' as protocol;
|
||||
import 'package:halkyon_base_client/srp.dart' as srp;
|
||||
|
||||
/// The recovery phrase [FakeAccountCrypto] makes first, of 24 words.
|
||||
const fakePhrase =
|
||||
|
|
@ -155,13 +156,34 @@ class FakeAccountCrypto() implements AccountCrypto {
|
|||
PasswordUnlock unlock,
|
||||
protocol.PasswordChallenge challenge, {
|
||||
required String instanceUrl,
|
||||
srp.PasswordProofPurpose purpose = srp.PasswordProofPurpose.signIn,
|
||||
List<int> digest = const [],
|
||||
}) async => SrpAnswer(
|
||||
clientValue: ByteData(384),
|
||||
clientValue: ByteData.sublistView(bindingOf(purpose, digest)),
|
||||
clientProof: ByteData.sublistView(unlock.srpSecret),
|
||||
verifies: (serverProof) =>
|
||||
utf8.decode(Uint8List.sublistView(serverProof)) == 'M2',
|
||||
);
|
||||
|
||||
/// What a fake answer carries in place of SRP's A: what it was made for
|
||||
/// ([purpose]) and the [digest] of what it sets, as SRP's identity binds
|
||||
/// them, for `FakePasswordGateway` to check as the register does.
|
||||
static Uint8List bindingOf(
|
||||
srp.PasswordProofPurpose purpose,
|
||||
List<int> digest,
|
||||
) => utf8.encode(
|
||||
'${purpose.name}:${[for (final byte in digest) byte.toRadixString(16).padLeft(2, '0')].join()}',
|
||||
);
|
||||
|
||||
/// Whether [answer] was made for [purpose] with [digest] ([bindingOf]).
|
||||
static bool binds(
|
||||
SrpAnswer answer, {
|
||||
required srp.PasswordProofPurpose purpose,
|
||||
required List<int> digest,
|
||||
}) =>
|
||||
utf8.decode(Uint8List.sublistView(answer.clientValue)) ==
|
||||
utf8.decode(bindingOf(purpose, digest));
|
||||
|
||||
@override
|
||||
Future<UnlockedAccountKeys> openKeys(
|
||||
PasswordUnlock unlock,
|
||||
|
|
|
|||
|
|
@ -7,11 +7,13 @@ import 'package:halkyon_app/src/auth/model/auth_status.dart';
|
|||
import 'package:halkyon_app/src/auth/model/session_grant.dart';
|
||||
import 'package:halkyon_app/src/password/data/account_crypto.dart';
|
||||
import 'package:halkyon_app/src/password/data/password_gateway.dart';
|
||||
import 'package:halkyon_app/src/password/data/proof_digests.dart';
|
||||
import 'package:halkyon_app/src/password/model/account_authenticator.dart';
|
||||
import 'package:halkyon_app/src/password/model/mail_code_purpose.dart';
|
||||
import 'package:halkyon_app/src/password/model/password_failed.dart';
|
||||
import 'package:halkyon_app/src/testing/fake_account_crypto.dart';
|
||||
import 'package:halkyon_base_client/halkyon_base_client.dart' as protocol;
|
||||
import 'package:halkyon_base_client/srp.dart' as srp;
|
||||
|
||||
/// The code every authenticator of [FakePasswordGateway] shows.
|
||||
const fakeTotpCode = '123456';
|
||||
|
|
@ -114,6 +116,10 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
|||
var _people = 0;
|
||||
var _sessions = 0;
|
||||
var _authenticators = 0;
|
||||
|
||||
/// The [n]th authenticator's ID: a UUID, as the register's are.
|
||||
static String _authenticatorId(int n) =>
|
||||
'00000000-0000-7000-8000-${n.toRadixString(16).padLeft(12, '0')}';
|
||||
final _challenges = <String, String>{};
|
||||
final _keyProofs = <String, String>{};
|
||||
|
||||
|
|
@ -143,7 +149,7 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
|||
if (finished) {
|
||||
account.authenticators.add(
|
||||
AccountAuthenticator(
|
||||
id: 'authenticator-${++_authenticators}',
|
||||
id: _authenticatorId(++_authenticators),
|
||||
name: 'Phone',
|
||||
confirmed: true,
|
||||
addedAt: clock.now(),
|
||||
|
|
@ -247,19 +253,25 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
|||
Future<protocol.PasswordChallenge> begin(String address) =>
|
||||
_call('begin', () => _challengeFor(address));
|
||||
|
||||
/// The account [challenge] (taken) and [answer] prove the password of;
|
||||
/// refused alike for none.
|
||||
/// The account [challenge] (taken) and [answer] prove the password of,
|
||||
/// the answer made for [purpose] with the [digest] of what it sets
|
||||
/// ([FakeAccountCrypto.bindingOf], as the register works them out from
|
||||
/// the call); refused alike for none.
|
||||
FakePasswordAccount _proved(
|
||||
protocol.PasswordChallenge challenge,
|
||||
SrpAnswer answer,
|
||||
) {
|
||||
SrpAnswer answer, {
|
||||
srp.PasswordProofPurpose purpose = srp.PasswordProofPurpose.signIn,
|
||||
List<int> digest = const [],
|
||||
}) {
|
||||
final address = _challenges.remove(challenge.challenge.uuid);
|
||||
if (address == null) {
|
||||
throw const PasswordFailed(PasswordProblem.challengeExpired);
|
||||
}
|
||||
final account = accounts[address];
|
||||
final typed = utf8.decode(Uint8List.sublistView(answer.clientProof));
|
||||
if (account?.password == null || account!.password != typed) {
|
||||
if (account?.password == null ||
|
||||
account!.password != typed ||
|
||||
!FakeAccountCrypto.binds(answer, purpose: purpose, digest: digest)) {
|
||||
throw const PasswordFailed(PasswordProblem.wrongCredentials);
|
||||
}
|
||||
return account;
|
||||
|
|
@ -452,7 +464,7 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
|||
SessionGrant? as,
|
||||
}) => _call('enrol', () {
|
||||
final account = _accountAs(as);
|
||||
final id = 'authenticator-${++_authenticators}';
|
||||
final id = _authenticatorId(++_authenticators);
|
||||
account.authenticators.add(
|
||||
AccountAuthenticator(
|
||||
id: id,
|
||||
|
|
@ -489,10 +501,19 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
|||
throw const PasswordFailed(PasswordProblem.wrongCredentials);
|
||||
}
|
||||
if (!first) {
|
||||
_ownerProved(owner);
|
||||
_ownerProved(
|
||||
owner,
|
||||
purpose: srp.PasswordProofPurpose.addAuthenticator,
|
||||
digest: authenticatorDigestOf(id),
|
||||
);
|
||||
} else {
|
||||
// The first takes the password alone (the fourth reading).
|
||||
final proved = _proved(owner.challenge, owner.answer);
|
||||
final proved = _proved(
|
||||
owner.challenge,
|
||||
owner.answer,
|
||||
purpose: srp.PasswordProofPurpose.firstAuthenticator,
|
||||
digest: authenticatorDigestOf(id),
|
||||
);
|
||||
if (proved.person != account.person) {
|
||||
throw const PasswordFailed(PasswordProblem.wrongCredentials);
|
||||
}
|
||||
|
|
@ -538,7 +559,11 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
|||
if (owner == null) {
|
||||
throw const PasswordFailed(PasswordProblem.wrongCredentials);
|
||||
}
|
||||
_ownerProved(owner);
|
||||
_ownerProved(
|
||||
owner,
|
||||
purpose: srp.PasswordProofPurpose.removeAuthenticator,
|
||||
digest: authenticatorDigestOf(id),
|
||||
);
|
||||
} else if (owner != null) {
|
||||
throw const PasswordFailed(PasswordProblem.invalidSetup);
|
||||
}
|
||||
|
|
@ -549,7 +574,10 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
|||
Future<List<String>> newRecoveryCodes(OwnerAnswer owner) => _call(
|
||||
'newRecoveryCodes',
|
||||
() {
|
||||
final account = _ownerProved(owner);
|
||||
final account = _ownerProved(
|
||||
owner,
|
||||
purpose: srp.PasswordProofPurpose.newRecoveryCodes,
|
||||
);
|
||||
account.recoveryCodes
|
||||
..clear()
|
||||
..addAll([
|
||||
|
|
@ -560,8 +588,13 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
|||
);
|
||||
|
||||
@override
|
||||
Future<void> callOffResets(OwnerAnswer owner) =>
|
||||
_call('callOffResets', () => _ownerProved(owner));
|
||||
Future<void> callOffResets(OwnerAnswer owner) => _call(
|
||||
'callOffResets',
|
||||
() => _ownerProved(
|
||||
owner,
|
||||
purpose: srp.PasswordProofPurpose.callOffResets,
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<PasswordWays> ways() => _call('ways', () {
|
||||
|
|
@ -610,7 +643,11 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
|||
String? code,
|
||||
}) async {
|
||||
final account = await _call('deleteAccount', () {
|
||||
final proved = _proved(challenge, answer);
|
||||
final proved = _proved(
|
||||
challenge,
|
||||
answer,
|
||||
purpose: srp.PasswordProofPurpose.deleteAccount,
|
||||
);
|
||||
if (proved.person != _signedIn()) {
|
||||
throw const PasswordFailed(PasswordProblem.wrongCredentials);
|
||||
}
|
||||
|
|
@ -639,7 +676,11 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
|||
OwnerAnswer owner, {
|
||||
required protocol.NewPassword password,
|
||||
}) => _call('change', () {
|
||||
_ownerProved(owner)
|
||||
_ownerProved(
|
||||
owner,
|
||||
purpose: srp.PasswordProofPurpose.newPassword,
|
||||
digest: newPasswordDigestOf(password),
|
||||
)
|
||||
..password = utf8.decode(Uint8List.sublistView(password.verifier))
|
||||
..passwordKeys = password.passwordKeys
|
||||
..codesWaitUntil = null
|
||||
|
|
@ -647,9 +688,14 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
|||
});
|
||||
|
||||
/// The signed-in person's account, once the [owner]'s proof proved its
|
||||
/// password and a code came: refused as the register refuses a change.
|
||||
FakePasswordAccount _ownerProved(OwnerAnswer owner) => _ownerCoded(
|
||||
_proved(owner.challenge, owner.answer),
|
||||
/// password, made for [purpose] with the [digest] of what it sets, and a
|
||||
/// code came: refused as the register refuses a change.
|
||||
FakePasswordAccount _ownerProved(
|
||||
OwnerAnswer owner, {
|
||||
required srp.PasswordProofPurpose purpose,
|
||||
List<int> digest = const [],
|
||||
}) => _ownerCoded(
|
||||
_proved(owner.challenge, owner.answer, purpose: purpose, digest: digest),
|
||||
owner.totpCode,
|
||||
owner.recoveryCode,
|
||||
);
|
||||
|
|
@ -688,7 +734,11 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
|||
required protocol.WrappedKeys keys,
|
||||
required ByteData proofKey,
|
||||
}) => _call('setRecoveryKeys', () {
|
||||
_ownerProved(owner)
|
||||
_ownerProved(
|
||||
owner,
|
||||
purpose: srp.PasswordProofPurpose.newPhrase,
|
||||
digest: newPhraseDigestOf(keys, proofKey),
|
||||
)
|
||||
..recoveryKeys = keys
|
||||
..recoveryProofKey = proofKey;
|
||||
});
|
||||
|
|
@ -704,7 +754,11 @@ class FakePasswordGateway(final AuthGateway _auth) implements PasswordGateway {
|
|||
protocol.KeyProofChallenge? keyChallenge,
|
||||
ByteData? signature,
|
||||
}) => _call('resetAuthenticator', () {
|
||||
final account = _proved(challenge, answer);
|
||||
final account = _proved(
|
||||
challenge,
|
||||
answer,
|
||||
purpose: srp.PasswordProofPurpose.authenticatorReset,
|
||||
);
|
||||
if (request != null && code != null) {
|
||||
if (_proven(request, code, MailCodePurpose.authenticatorReset) !=
|
||||
account.address) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue