fix(auth)!: the owner's proof is bound to what it approves
A signed-in change's proof covered only its challenge and the instance, so whoever could read and change the traffic (an http:// instance, a TLS-intercepting proxy, TLS broken: what SRP is for) could keep the owner's proof and code and swap in a new password, a recovery phrase or an authenticator of their own, or turn a proof made for new recovery codes into a deletion (the full reading's device medium 1). SRP's identity now also carries what the proof is for and the digest of what it sets, which the register works out from the call that brings it, never from the device: a new password (its digest), a new phrase (its wrapping and proof key), confirming the first or another authenticator and removing one (its ID), new recovery codes, calling resets off, deleting; the sessionless reset of an authenticator its own, apart from signing in. A proof made for one of them counts for no other. Tests made for each refusal fail with the binding taken out on both sides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
1ad5d84110
commit
92d2df4695
12 changed files with 467 additions and 77 deletions
|
|
@ -16,7 +16,10 @@ import 'package:serverpod/serverpod.dart' as _is;
|
|||
/// What a signed-in owner proves to change how their account is secured
|
||||
/// (ADR 0017; the third reading, as Proton has it): the password's SRP
|
||||
/// proof for a challenge `password.beginChange` gave, bound to the base URL
|
||||
/// the device addressed, and a TOTP code from one of the account's confirmed
|
||||
/// the device addressed and to what it approves (SRP's identity carries a
|
||||
/// purpose and the digest of what is set, which the register works out from
|
||||
/// the call that brings the proof, so a proof made for one change counts for
|
||||
/// no other), and a TOTP code from one of the account's confirmed
|
||||
/// authenticators or one of its recovery codes. Adding an authenticator once
|
||||
/// one is confirmed, removing a confirmed one, new recovery codes, a new
|
||||
/// recovery phrase and a new password take it: a session alone never gets a
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ Future<ResetPending> resetAuthenticator(
|
|||
clientValue: clientValue,
|
||||
clientProof: clientProof,
|
||||
instanceUrl: instanceUrl,
|
||||
proofPurpose: PasswordProofPurpose.authenticatorReset,
|
||||
);
|
||||
final to = normalizedAddress(address);
|
||||
if (to == null || addressHash(pod, to) != proved.addressHash) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:clock/clock.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/owner_proof_tries.dart';
|
||||
import 'package:halkyon_base_server/src/password/password_change.dart';
|
||||
import 'package:halkyon_base_server/src/password/password_config.dart';
|
||||
|
|
@ -166,11 +167,25 @@ Future<List<String>> confirmAuthenticator(
|
|||
throw refusedFor(PasswordRefusal.wrongCredentials);
|
||||
}
|
||||
if (await hasConfirmedAuthenticator(session, person)) {
|
||||
await proveOwner(session, keys, person: person, proof: proof);
|
||||
await proveOwner(
|
||||
session,
|
||||
keys,
|
||||
person: person,
|
||||
proof: proof,
|
||||
purpose: PasswordProofPurpose.addAuthenticator,
|
||||
digest: authenticatorDigest(authenticator),
|
||||
);
|
||||
} else {
|
||||
// The first: the password alone (the fourth reading), as no confirmed
|
||||
// authenticator gives a code yet.
|
||||
await proveOwnersPassword(session, keys, person: person, proof: proof);
|
||||
await proveOwnersPassword(
|
||||
session,
|
||||
keys,
|
||||
person: person,
|
||||
proof: proof,
|
||||
purpose: PasswordProofPurpose.firstAuthenticator,
|
||||
digest: authenticatorDigest(authenticator),
|
||||
);
|
||||
}
|
||||
return await session.db.transaction((transaction) async {
|
||||
// UPDATE "totp_authenticator" SET "lastStep" = $1, "confirmedAt" = $2
|
||||
|
|
@ -277,7 +292,14 @@ Future<void> removeAuthenticator(
|
|||
if (proof == null) {
|
||||
throw refusedFor(PasswordRefusal.wrongCredentials);
|
||||
}
|
||||
await proveOwner(session, keys, person: person, proof: proof);
|
||||
await proveOwner(
|
||||
session,
|
||||
keys,
|
||||
person: person,
|
||||
proof: proof,
|
||||
purpose: PasswordProofPurpose.removeAuthenticator,
|
||||
digest: authenticatorDigest(authenticator),
|
||||
);
|
||||
// Serializable: two removals at once must not take both of the last two.
|
||||
await session.db.transaction(
|
||||
(transaction) async {
|
||||
|
|
@ -317,7 +339,13 @@ Future<List<String>> newRecoveryCodes(
|
|||
required OwnerProof proof,
|
||||
}) async {
|
||||
final keys = requirePasswordKeys(session.serverpod);
|
||||
await proveOwner(session, keys, person: person, proof: proof);
|
||||
await proveOwner(
|
||||
session,
|
||||
keys,
|
||||
person: person,
|
||||
proof: proof,
|
||||
purpose: PasswordProofPurpose.newRecoveryCodes,
|
||||
);
|
||||
return await session.db.transaction(
|
||||
(transaction) =>
|
||||
_replaceRecoveryCodes(session, keys, person, transaction: transaction),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
### What a signed-in owner proves to change how their account is secured
|
||||
### (ADR 0017; the third reading, as Proton has it): the password's SRP
|
||||
### proof for a challenge `password.beginChange` gave, bound to the base URL
|
||||
### the device addressed, and a TOTP code from one of the account's confirmed
|
||||
### the device addressed and to what it approves (SRP's identity carries a
|
||||
### purpose and the digest of what is set, which the register works out from
|
||||
### the call that brings the proof, so a proof made for one change counts for
|
||||
### no other), and a TOTP code from one of the account's confirmed
|
||||
### authenticators or one of its recovery codes. Adding an authenticator once
|
||||
### one is confirmed, removing a confirmed one, new recovery codes, a new
|
||||
### recovery phrase and a new password take it: a session alone never gets a
|
||||
|
|
|
|||
|
|
@ -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/password_config.dart';
|
||||
import 'package:halkyon_base_server/src/password/passwords.dart';
|
||||
import 'package:halkyon_base_server/src/password/second_factor_waits.dart';
|
||||
|
|
@ -42,7 +43,14 @@ Future<void> changePassword(
|
|||
if (!isNewPassword(password)) {
|
||||
throw refusedFor(PasswordRefusal.invalidSetup);
|
||||
}
|
||||
final row = await proveOwner(session, keys, person: person, proof: proof);
|
||||
final row = await proveOwner(
|
||||
session,
|
||||
keys,
|
||||
person: person,
|
||||
proof: proof,
|
||||
purpose: PasswordProofPurpose.newPassword,
|
||||
digest: newPasswordDigest(password),
|
||||
);
|
||||
await session.db.transaction((transaction) async {
|
||||
await writeNewPassword(session, row, password, transaction: transaction);
|
||||
await endSessionsEverywhere(
|
||||
|
|
@ -68,7 +76,13 @@ Future<void> callOffResets(
|
|||
}) async {
|
||||
final keys = requirePasswordKeys(session.serverpod);
|
||||
// The owner's code, accepted, calls every reset off (`ownersCode`).
|
||||
await proveOwner(session, keys, person: person, proof: proof);
|
||||
await proveOwner(
|
||||
session,
|
||||
keys,
|
||||
person: person,
|
||||
proof: proof,
|
||||
purpose: PasswordProofPurpose.callOffResets,
|
||||
);
|
||||
}
|
||||
|
||||
/// Replaces the wrapping of [person]'s keys under their recovery phrase
|
||||
|
|
@ -91,7 +105,14 @@ Future<void> setRecoveryKeys(
|
|||
if (!isWrappedKeys(recoveryKeys) || !isPublicKey(recoveryProofKey)) {
|
||||
throw refusedFor(PasswordRefusal.invalidSetup);
|
||||
}
|
||||
final row = await proveOwner(session, keys, person: person, proof: proof);
|
||||
final row = await proveOwner(
|
||||
session,
|
||||
keys,
|
||||
person: person,
|
||||
proof: proof,
|
||||
purpose: PasswordProofPurpose.newPhrase,
|
||||
digest: newPhraseDigest(recoveryKeys, recoveryProofKey),
|
||||
);
|
||||
// UPDATE "account_password" SET "recoveryKeysNonce" = $1,
|
||||
// "recoveryKeysCipherText" = $2, "recoveryProofKey" = $3,
|
||||
// "updatedAt" = $4 WHERE "id" = $5
|
||||
|
|
@ -158,6 +179,7 @@ Future<void> requireOwnerToDelete(
|
|||
clientValue: proof.clientValue,
|
||||
clientProof: proof.clientProof,
|
||||
instanceUrl: proof.instanceUrl,
|
||||
purpose: PasswordProofPurpose.deleteAccount,
|
||||
);
|
||||
final totp = proof.totpCode?.trim() ?? '';
|
||||
final recovery = proof.recoveryCode?.trim() ?? '';
|
||||
|
|
@ -190,7 +212,8 @@ Future<void> requireOwnerToDelete(
|
|||
}
|
||||
|
||||
/// [person]'s password row, once [proof] (`OwnerProof`) proved their
|
||||
/// password alone, for a change's challenge of theirs, without a code: what
|
||||
/// password alone, for a change's challenge of theirs, made for [purpose]
|
||||
/// with the [digest] of what it sets, without a code: what
|
||||
/// confirming an account's first authenticator takes (the fourth reading:
|
||||
/// a session alone claims none; there is no confirmed authenticator yet to
|
||||
/// take a code from). Refused ([PasswordRefusedException]) for a proof
|
||||
|
|
@ -201,6 +224,8 @@ Future<AccountPassword> proveOwnersPassword(
|
|||
PasswordKeys keys, {
|
||||
required UuidValue person,
|
||||
required OwnerProof proof,
|
||||
required PasswordProofPurpose purpose,
|
||||
List<int> digest = const [],
|
||||
}) async {
|
||||
if ((proof.totpCode?.trim() ?? '').isNotEmpty ||
|
||||
(proof.recoveryCode?.trim() ?? '').isNotEmpty) {
|
||||
|
|
@ -214,13 +239,19 @@ Future<AccountPassword> proveOwnersPassword(
|
|||
clientValue: proof.clientValue,
|
||||
clientProof: proof.clientProof,
|
||||
instanceUrl: proof.instanceUrl,
|
||||
purpose: purpose,
|
||||
digest: digest,
|
||||
);
|
||||
return proved.password;
|
||||
}
|
||||
|
||||
/// [person]'s password row, once they proved owning the account with
|
||||
/// [proof] (`OwnerProof`): the password's proof for a change's challenge,
|
||||
/// [person]'s own, then a code ([ownersCode], which also calls off a reset
|
||||
/// [person]'s own, made for [purpose] with the [digest] of what it sets
|
||||
/// (the full reading's device medium 1: whoever stands between the device
|
||||
/// and the register can neither swap in a password, a phrase or an
|
||||
/// authenticator of their own, nor take a proof made for one change to
|
||||
/// make another), then a code ([ownersCode], which also calls off a reset
|
||||
/// waiting). Never counted toward the account's waits, nor refused while
|
||||
/// they run: the signed-in session's wrong proofs count against it where
|
||||
/// it is (`asOwnerProofTry`).
|
||||
|
|
@ -236,6 +267,8 @@ Future<AccountPassword> proveOwner(
|
|||
PasswordKeys keys, {
|
||||
required UuidValue person,
|
||||
required OwnerProof proof,
|
||||
required PasswordProofPurpose purpose,
|
||||
List<int> digest = const [],
|
||||
}) async {
|
||||
final proved = await _provePassword(
|
||||
session,
|
||||
|
|
@ -245,6 +278,8 @@ Future<AccountPassword> proveOwner(
|
|||
clientValue: proof.clientValue,
|
||||
clientProof: proof.clientProof,
|
||||
instanceUrl: proof.instanceUrl,
|
||||
purpose: purpose,
|
||||
digest: digest,
|
||||
);
|
||||
await ownersCode(
|
||||
session,
|
||||
|
|
@ -256,8 +291,8 @@ Future<AccountPassword> proveOwner(
|
|||
return proved.password;
|
||||
}
|
||||
|
||||
/// The password's proof for [challenge], [person]'s own and a change's;
|
||||
/// refused ([PasswordRefusal.wrongCredentials]) for a wrong password or a
|
||||
/// The password's proof for [challenge], [person]'s own and a change's,
|
||||
/// made for [purpose] with the [digest] of what it sets; refused ([PasswordRefusal.wrongCredentials]) for a wrong password or a
|
||||
/// challenge that isn't [person]'s.
|
||||
Future<PasswordProved> _provePassword(
|
||||
Session session,
|
||||
|
|
@ -267,6 +302,8 @@ Future<PasswordProved> _provePassword(
|
|||
required ByteData clientValue,
|
||||
required ByteData clientProof,
|
||||
required String instanceUrl,
|
||||
required PasswordProofPurpose purpose,
|
||||
List<int> digest = const [],
|
||||
}) async {
|
||||
final proved = await provePassword(
|
||||
session,
|
||||
|
|
@ -276,6 +313,8 @@ Future<PasswordProved> _provePassword(
|
|||
clientProof: clientProof,
|
||||
instanceUrl: instanceUrl,
|
||||
purpose: SignInChallengePurpose.change,
|
||||
proofPurpose: purpose,
|
||||
digest: digest,
|
||||
);
|
||||
if (proved.password.authUserId != person) {
|
||||
throw refusedFor(PasswordRefusal.wrongCredentials);
|
||||
|
|
|
|||
|
|
@ -890,9 +890,12 @@ typedef PasswordProved = ({
|
|||
/// [completePasswordSignIn] and the reset of a lost authenticator do; SRP's
|
||||
/// identity is [passwordIdentity] of the challenge's ID (so nobody learns an
|
||||
/// account's ID from its address) and of the [instanceUrl] the device
|
||||
/// addressed (`deviceInstanceUrl`, or the one a member names), so a proof a hostile
|
||||
/// instance relays here never counts. Refusals as
|
||||
/// [completePasswordSignIn]'s, but for the second factor.
|
||||
/// addressed (`deviceInstanceUrl`, or the one a member names), so a proof
|
||||
/// a hostile instance relays here never counts; and of what the proof is
|
||||
/// for ([proofPurpose]) with the [digest] of what it sets, which the caller
|
||||
/// works out from its own call, so a proof made for one purpose or one
|
||||
/// change never counts for another. Refusals as [completePasswordSignIn]'s,
|
||||
/// but for the second factor.
|
||||
Future<PasswordProved> provePassword(
|
||||
Session session,
|
||||
PasswordKeys keys, {
|
||||
|
|
@ -901,6 +904,8 @@ Future<PasswordProved> provePassword(
|
|||
required ByteData clientProof,
|
||||
required String instanceUrl,
|
||||
SignInChallengePurpose purpose = SignInChallengePurpose.password,
|
||||
PasswordProofPurpose proofPurpose = PasswordProofPurpose.signIn,
|
||||
List<int> digest = const [],
|
||||
}) async {
|
||||
final kept = await takeChallenge(session, challenge, purpose);
|
||||
// Counted before it is checked, whatever its outcome (a change's per
|
||||
|
|
@ -913,7 +918,12 @@ Future<PasswordProved> provePassword(
|
|||
final clientBytes = Uint8List.sublistView(clientValue);
|
||||
final verification = srpVerify(
|
||||
passwordGroup,
|
||||
identity: passwordIdentity(challenge, instanceUrl),
|
||||
identity: passwordIdentity(
|
||||
challenge,
|
||||
instanceUrl,
|
||||
purpose: proofPurpose,
|
||||
digest: digest,
|
||||
),
|
||||
salt: password == null ? decoy.salt : Uint8List.sublistView(password.salt),
|
||||
verifier: password == null
|
||||
? decoy.verifier
|
||||
|
|
|
|||
|
|
@ -123,30 +123,40 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
/// Changes Anna's password from her phone (or [from]), the old one
|
||||
/// proved by [privateValue] (hers by default) with [totpCode] (her
|
||||
/// authenticator's now by default); sent as [as] where given.
|
||||
/// Changes Anna's password from her phone (or [from]) to [password]
|
||||
/// (a new one by default), the old one proved by [privateValue] (hers
|
||||
/// by default) with [totpCode] (her authenticator's now by default),
|
||||
/// the proof made for [purpose] and for [provedFor] (the password
|
||||
/// sent, by default); sent as [as] where given.
|
||||
Future<void> change({
|
||||
BigInt? privateValue,
|
||||
String? totpCode,
|
||||
bool withoutCode = false,
|
||||
NewPassword? password,
|
||||
NewPassword? provedFor,
|
||||
PasswordProofPurpose purpose = PasswordProofPurpose.newPassword,
|
||||
TestSessionBuilder? from,
|
||||
TestSessionBuilder? as,
|
||||
}) => atNow(() async {
|
||||
final challenge = await endpoints.password.beginChange(
|
||||
from ?? onPhone(),
|
||||
);
|
||||
// The old password proves it: the device takes the new one's x as
|
||||
// it makes it.
|
||||
final old = privateValue ?? device.privateValue;
|
||||
final sent = password ?? device.newPassword();
|
||||
await endpoints.password.change(
|
||||
as ?? from ?? onPhone(),
|
||||
proof: device.ownerProof(
|
||||
challenge,
|
||||
privateValue: privateValue,
|
||||
purpose: purpose,
|
||||
digest: newPasswordDigest(provedFor ?? sent),
|
||||
privateValue: old,
|
||||
totpCode: withoutCode
|
||||
? null
|
||||
: totpCode ?? codeOf(enrolment, at: now),
|
||||
),
|
||||
password: password ?? device.newPassword(),
|
||||
password: sent,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -196,6 +206,8 @@ void main() {
|
|||
code: codeOf(enrolment, at: now),
|
||||
proof: device.ownerProof(
|
||||
await endpoints.password.beginChange(onPhone()),
|
||||
purpose: PasswordProofPurpose.firstAuthenticator,
|
||||
digest: authenticatorDigest(enrolment.authenticator),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -299,6 +311,20 @@ void main() {
|
|||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
device.privateValue = mine;
|
||||
// Her password and a right code, made for another new password
|
||||
// than the one sent, or for another change: whoever stands between
|
||||
// swaps in a password of their own nowhere.
|
||||
final another = (await PasswordDevice.make(seed: 51)).newPassword();
|
||||
await expectLater(
|
||||
change(provedFor: another),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
device.privateValue = mine;
|
||||
await expectLater(
|
||||
change(purpose: PasswordProofPurpose.newRecoveryCodes),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
device.privateValue = mine;
|
||||
await expectLater(
|
||||
change(
|
||||
password: device.newPassword().copyWith(salt: ByteData(3)),
|
||||
|
|
@ -373,6 +399,7 @@ void main() {
|
|||
onPhone(),
|
||||
proof: device.ownerProof(
|
||||
signInChallenge,
|
||||
purpose: PasswordProofPurpose.newPassword,
|
||||
totpCode: codeOf(enrolment, at: now),
|
||||
),
|
||||
password: device.newPassword(),
|
||||
|
|
@ -649,7 +676,9 @@ void main() {
|
|||
/// Gives Anna's keys a new phrase's wrapping ([fresh]) from her
|
||||
/// phone, the old password proved by [privateValue] (hers by
|
||||
/// default) or a made-up proof ([withoutProof]), with [totpCode] (her
|
||||
/// authenticator's now by default).
|
||||
/// authenticator's now by default), the proof made for [purpose]
|
||||
/// and for [provedFor] (the wrapping and proof key sent, by
|
||||
/// default).
|
||||
Future<void> newPhrase({
|
||||
BigInt? privateValue,
|
||||
bool withoutProof = false,
|
||||
|
|
@ -657,11 +686,20 @@ void main() {
|
|||
bool withoutCode = false,
|
||||
WrappedKeys? recoveryKeys,
|
||||
ByteData? recoveryProofKey,
|
||||
({WrappedKeys keys, ByteData proofKey})? provedFor,
|
||||
PasswordProofPurpose purpose = PasswordProofPurpose.newPhrase,
|
||||
TestSessionBuilder? as,
|
||||
}) => atNow(() async {
|
||||
final challenge = await endpoints.password.beginChange(onPhone());
|
||||
final keys = recoveryKeys ?? fresh;
|
||||
final proofKey = recoveryProofKey ?? freshProofKey;
|
||||
final proof = device.ownerProof(
|
||||
challenge,
|
||||
purpose: purpose,
|
||||
digest: newPhraseDigest(
|
||||
provedFor?.keys ?? keys,
|
||||
provedFor?.proofKey ?? proofKey,
|
||||
),
|
||||
privateValue: privateValue,
|
||||
totpCode: withoutCode
|
||||
? null
|
||||
|
|
@ -672,8 +710,8 @@ void main() {
|
|||
proof: withoutProof
|
||||
? proof.copyWith(clientProof: ByteData(32))
|
||||
: proof,
|
||||
recoveryKeys: recoveryKeys ?? fresh,
|
||||
recoveryProofKey: recoveryProofKey ?? freshProofKey,
|
||||
recoveryKeys: keys,
|
||||
recoveryProofKey: proofKey,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -783,6 +821,29 @@ void main() {
|
|||
newPhrase(totpCode: '000000'),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
// Her password and a right code, made for another wrapping or
|
||||
// proof key than the ones sent, or for another change: whoever
|
||||
// stands between swaps in a phrase of their own nowhere.
|
||||
await expectLater(
|
||||
newPhrase(
|
||||
provedFor: (
|
||||
keys: WrappedKeys(
|
||||
nonce: ByteData.sublistView(Uint8List(24)..[0] = 1),
|
||||
cipherText: ByteData(wrappedKeysLength),
|
||||
),
|
||||
proofKey: freshProofKey,
|
||||
),
|
||||
),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
await expectLater(
|
||||
newPhrase(provedFor: (keys: fresh, proofKey: ByteData(32))),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
await expectLater(
|
||||
newPhrase(purpose: PasswordProofPurpose.newPassword),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
final ben = await newAnonymousPerson(session);
|
||||
final bensLaptop = await signInNewDevice(session, ben);
|
||||
await expectLater(
|
||||
|
|
@ -822,13 +883,14 @@ void main() {
|
|||
/// Deletes Anna's account from her phone, with the password's proof
|
||||
/// (hers by default, by [privateValue] otherwise, none [withoutProof])
|
||||
/// and [totpCode] (her authenticator's now by default), bound to
|
||||
/// [instanceUrl].
|
||||
/// [instanceUrl] and made for [purpose].
|
||||
Future<void> delete({
|
||||
BigInt? privateValue,
|
||||
String? totpCode,
|
||||
bool withoutCode = false,
|
||||
bool withoutProof = false,
|
||||
String instanceUrl = testInstanceUrl,
|
||||
PasswordProofPurpose purpose = PasswordProofPurpose.deleteAccount,
|
||||
}) => atNow(() async {
|
||||
if (withoutProof) {
|
||||
return await endpoints.account.delete(onPhone());
|
||||
|
|
@ -838,6 +900,7 @@ void main() {
|
|||
challenge,
|
||||
privateValue: privateValue,
|
||||
instanceUrl: instanceUrl,
|
||||
purpose: purpose,
|
||||
);
|
||||
await endpoints.account.delete(
|
||||
onPhone(),
|
||||
|
|
@ -877,6 +940,19 @@ void main() {
|
|||
delete(instanceUrl: 'https://learn.evil.example/'),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
// Her password and a right code, made for new recovery codes
|
||||
// (or any other change): nobody between turns it into a
|
||||
// deletion.
|
||||
for (final purpose in [
|
||||
PasswordProofPurpose.newRecoveryCodes,
|
||||
PasswordProofPurpose.newPassword,
|
||||
]) {
|
||||
await expectLater(
|
||||
delete(purpose: purpose),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
reason: purpose.name,
|
||||
);
|
||||
}
|
||||
expect(await AuthUser.db.findById(session, anna), isNotNull);
|
||||
expect(await AccountPassword.db.count(session), 1);
|
||||
|
||||
|
|
@ -918,7 +994,10 @@ void main() {
|
|||
? null
|
||||
: await mailedCode('ben@example.org', purpose: mailedFor);
|
||||
final challenge = await endpoints.password.beginChange(asBen());
|
||||
final answer = bens.answer(challenge);
|
||||
final answer = bens.answer(
|
||||
challenge,
|
||||
purpose: PasswordProofPurpose.deleteAccount,
|
||||
);
|
||||
await endpoints.account.delete(
|
||||
asBen(),
|
||||
proof: DeletionProof(
|
||||
|
|
@ -963,7 +1042,10 @@ void main() {
|
|||
notSignedIn(),
|
||||
address: 'anna@example.org',
|
||||
);
|
||||
final answer = device.answer(challenge);
|
||||
final answer = device.answer(
|
||||
challenge,
|
||||
purpose: PasswordProofPurpose.authenticatorReset,
|
||||
);
|
||||
final code = await mailedCode(
|
||||
'anna@example.org',
|
||||
purpose: EmailCodePurpose.authenticatorReset,
|
||||
|
|
@ -996,7 +1078,10 @@ void main() {
|
|||
final challenge = await endpoints.password.beginChange(
|
||||
onPhone(),
|
||||
);
|
||||
final answer = device.answer(challenge);
|
||||
final answer = device.answer(
|
||||
challenge,
|
||||
purpose: PasswordProofPurpose.deleteAccount,
|
||||
);
|
||||
await endpoints.account.delete(
|
||||
onPhone(),
|
||||
proof: DeletionProof(
|
||||
|
|
|
|||
|
|
@ -76,15 +76,20 @@ void main() {
|
|||
);
|
||||
|
||||
/// Anna's proof from her phone (`OwnerProof`): her password (or
|
||||
/// [privateValue]'s) for a change's challenge, and [totpCode] or
|
||||
/// [privateValue]'s) for a change's challenge, made for [purpose]
|
||||
/// with the [digest] of what it sets, and [totpCode] or
|
||||
/// [recoveryCode].
|
||||
Future<OwnerProof> ownerProof({
|
||||
required PasswordProofPurpose purpose,
|
||||
List<int> digest = const [],
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
BigInt? privateValue,
|
||||
}) => atNow(
|
||||
() async => device.ownerProof(
|
||||
await endpoints.password.beginChange(asAnna()),
|
||||
purpose: purpose,
|
||||
digest: digest,
|
||||
totpCode: totpCode,
|
||||
recoveryCode: recoveryCode,
|
||||
privateValue: privateValue,
|
||||
|
|
@ -92,14 +97,17 @@ void main() {
|
|||
);
|
||||
|
||||
/// Anna's password alone, from [from] (her phone unless given), for a
|
||||
/// change's challenge: what her first authenticator takes (the fourth
|
||||
/// reading).
|
||||
Future<OwnerProof> passwordAlone({
|
||||
/// change's challenge, made for confirming [authenticator] as her
|
||||
/// first: what her first authenticator takes (the fourth reading).
|
||||
Future<OwnerProof> passwordAlone(
|
||||
UuidValue authenticator, {
|
||||
TestSessionBuilder? from,
|
||||
PasswordDevice? with_,
|
||||
}) => atNow(
|
||||
() async => (with_ ?? device).ownerProof(
|
||||
await endpoints.password.beginChange(from ?? asAnna()),
|
||||
purpose: PasswordProofPurpose.firstAuthenticator,
|
||||
digest: authenticatorDigest(authenticator),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -162,7 +170,7 @@ void main() {
|
|||
asAnna(),
|
||||
authenticator: enrolment.authenticator,
|
||||
code: codeOf(enrolment),
|
||||
proof: await passwordAlone(),
|
||||
proof: await passwordAlone(enrolment.authenticator),
|
||||
);
|
||||
nextStep();
|
||||
return (enrolment: enrolment, recoveryCodes: codes);
|
||||
|
|
@ -580,7 +588,10 @@ void main() {
|
|||
asAnna(),
|
||||
authenticator: enrolment.authenticator,
|
||||
code: codeOf(enrolment),
|
||||
proof: await passwordAlone(with_: other),
|
||||
proof: await passwordAlone(
|
||||
enrolment.authenticator,
|
||||
with_: other,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -1230,7 +1241,10 @@ void main() {
|
|||
'holds nobody out of their codes', () async {
|
||||
final complete = await annaComplete();
|
||||
for (var i = 0; i < 10; i++) {
|
||||
final proof = await ownerProof(totpCode: '000000');
|
||||
final proof = await ownerProof(
|
||||
purpose: PasswordProofPurpose.newRecoveryCodes,
|
||||
totpCode: '000000',
|
||||
);
|
||||
await expectLater(
|
||||
atNow(
|
||||
() => endpoints.authenticator.newRecoveryCodes(
|
||||
|
|
@ -1296,7 +1310,7 @@ void main() {
|
|||
asAnna(),
|
||||
authenticator: enrolment.authenticator,
|
||||
code: codeOf(enrolment),
|
||||
proof: await passwordAlone(),
|
||||
proof: await passwordAlone(enrolment.authenticator),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -1397,6 +1411,8 @@ void main() {
|
|||
proof: await atNow(
|
||||
() async => device.ownerProof(
|
||||
await endpoints.password.beginChange(asAnna()),
|
||||
purpose: PasswordProofPurpose.firstAuthenticator,
|
||||
digest: authenticatorDigest(enrolment.authenticator),
|
||||
privateValue: BigInt.from(12345),
|
||||
),
|
||||
),
|
||||
|
|
@ -1409,6 +1425,8 @@ void main() {
|
|||
proof: await atNow(
|
||||
() async => bensDevice.ownerProof(
|
||||
await endpoints.password.beginChange(asBen),
|
||||
purpose: PasswordProofPurpose.firstAuthenticator,
|
||||
digest: authenticatorDigest(enrolment.authenticator),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -1420,6 +1438,8 @@ void main() {
|
|||
proof: await atNow(
|
||||
() async => device.ownerProof(
|
||||
await endpoints.password.beginChange(asAnna()),
|
||||
purpose: PasswordProofPurpose.firstAuthenticator,
|
||||
digest: authenticatorDigest(enrolment.authenticator),
|
||||
totpCode: codeOf(enrolment, at: now),
|
||||
),
|
||||
),
|
||||
|
|
@ -1442,7 +1462,9 @@ void main() {
|
|||
2,
|
||||
);
|
||||
|
||||
final codes = await confirm(proof: await passwordAlone());
|
||||
final codes = await confirm(
|
||||
proof: await passwordAlone(enrolment.authenticator),
|
||||
);
|
||||
expect(codes, hasLength(recoveryCodeCount));
|
||||
expect(
|
||||
await SessionCodeFailures.db.count(
|
||||
|
|
@ -1466,7 +1488,10 @@ void main() {
|
|||
notSignedIn(),
|
||||
address: 'anna@example.org',
|
||||
);
|
||||
final answer = device.answer(challenge);
|
||||
final answer = device.answer(
|
||||
challenge,
|
||||
purpose: PasswordProofPurpose.authenticatorReset,
|
||||
);
|
||||
final code = await mailedCode(
|
||||
'anna@example.org',
|
||||
purpose: EmailCodePurpose.authenticatorReset,
|
||||
|
|
@ -1572,6 +1597,8 @@ void main() {
|
|||
refused(PasswordRefusal.wrongCode),
|
||||
);
|
||||
final proof = await ownerProof(
|
||||
purpose: PasswordProofPurpose.addAuthenticator,
|
||||
digest: authenticatorDigest(second.authenticator),
|
||||
totpCode: codeOf(complete.enrolment, at: now),
|
||||
);
|
||||
final codes = await atNow(
|
||||
|
|
@ -1615,30 +1642,67 @@ void main() {
|
|||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
await expectLater(
|
||||
confirm(proof: await ownerProof(totpCode: '000000')),
|
||||
confirm(
|
||||
proof: await ownerProof(
|
||||
purpose: PasswordProofPurpose.addAuthenticator,
|
||||
digest: authenticatorDigest(another.authenticator),
|
||||
totpCode: '000000',
|
||||
),
|
||||
),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
// Its own code is no code from one confirmed before.
|
||||
await expectLater(
|
||||
confirm(
|
||||
proof: await ownerProof(totpCode: codeOf(another, at: now)),
|
||||
proof: await ownerProof(
|
||||
purpose: PasswordProofPurpose.addAuthenticator,
|
||||
digest: authenticatorDigest(another.authenticator),
|
||||
totpCode: codeOf(another, at: now),
|
||||
),
|
||||
),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
await expectLater(
|
||||
confirm(
|
||||
proof: await ownerProof(
|
||||
purpose: PasswordProofPurpose.addAuthenticator,
|
||||
digest: authenticatorDigest(another.authenticator),
|
||||
totpCode: codeOf(complete.enrolment, at: now),
|
||||
privateValue: BigInt.from(4242),
|
||||
),
|
||||
),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
// Her password and a right code, made for adding another
|
||||
// authenticator, or for removing this one: whoever stands between
|
||||
// adds one of their own nowhere.
|
||||
await expectLater(
|
||||
confirm(
|
||||
proof: await ownerProof(
|
||||
purpose: PasswordProofPurpose.addAuthenticator,
|
||||
digest: authenticatorDigest(complete.enrolment.authenticator),
|
||||
totpCode: codeOf(complete.enrolment, at: now),
|
||||
),
|
||||
),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
await expectLater(
|
||||
confirm(
|
||||
proof: await ownerProof(
|
||||
purpose: PasswordProofPurpose.removeAuthenticator,
|
||||
digest: authenticatorDigest(another.authenticator),
|
||||
totpCode: codeOf(complete.enrolment, at: now),
|
||||
),
|
||||
),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
// A wrong code from the new one takes nothing of the proof's.
|
||||
await expectLater(
|
||||
confirm(
|
||||
code: '000000',
|
||||
proof: await ownerProof(
|
||||
purpose: PasswordProofPurpose.addAuthenticator,
|
||||
digest: authenticatorDigest(another.authenticator),
|
||||
totpCode: codeOf(complete.enrolment, at: now),
|
||||
),
|
||||
),
|
||||
|
|
@ -1654,6 +1718,8 @@ void main() {
|
|||
expect(
|
||||
await confirm(
|
||||
proof: await ownerProof(
|
||||
purpose: PasswordProofPurpose.addAuthenticator,
|
||||
digest: authenticatorDigest(another.authenticator),
|
||||
totpCode: codeOf(complete.enrolment, at: now),
|
||||
),
|
||||
),
|
||||
|
|
@ -1715,7 +1781,10 @@ void main() {
|
|||
authenticator: bens.authenticator,
|
||||
code: codeOf(bens, at: now),
|
||||
// Anna's password, for a challenge of hers.
|
||||
proof: await ownerProof(),
|
||||
proof: await ownerProof(
|
||||
purpose: PasswordProofPurpose.firstAuthenticator,
|
||||
digest: authenticatorDigest(bens.authenticator),
|
||||
),
|
||||
),
|
||||
),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
|
|
@ -1771,6 +1840,8 @@ void main() {
|
|||
asAnna(),
|
||||
authenticator: complete.enrolment.authenticator,
|
||||
proof: await ownerProof(
|
||||
purpose: PasswordProofPurpose.removeAuthenticator,
|
||||
digest: authenticatorDigest(complete.enrolment.authenticator),
|
||||
totpCode: codeOf(complete.enrolment, at: now),
|
||||
),
|
||||
),
|
||||
|
|
@ -1784,6 +1855,8 @@ void main() {
|
|||
label: '',
|
||||
);
|
||||
final proof = await ownerProof(
|
||||
purpose: PasswordProofPurpose.addAuthenticator,
|
||||
digest: authenticatorDigest(second.authenticator),
|
||||
totpCode: codeOf(complete.enrolment, at: now),
|
||||
);
|
||||
await atNow(
|
||||
|
|
@ -1807,7 +1880,11 @@ void main() {
|
|||
() async => await endpoints.authenticator.remove(
|
||||
asAnna(),
|
||||
authenticator: complete.enrolment.authenticator,
|
||||
proof: await ownerProof(totpCode: '123456'),
|
||||
proof: await ownerProof(
|
||||
purpose: PasswordProofPurpose.removeAuthenticator,
|
||||
digest: authenticatorDigest(complete.enrolment.authenticator),
|
||||
totpCode: '123456',
|
||||
),
|
||||
),
|
||||
),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
|
|
@ -1818,6 +1895,8 @@ void main() {
|
|||
asAnna(),
|
||||
authenticator: complete.enrolment.authenticator,
|
||||
proof: await ownerProof(
|
||||
purpose: PasswordProofPurpose.removeAuthenticator,
|
||||
digest: authenticatorDigest(complete.enrolment.authenticator),
|
||||
totpCode: codeOf(second, at: now),
|
||||
privateValue: BigInt.from(4242),
|
||||
),
|
||||
|
|
@ -1829,7 +1908,11 @@ void main() {
|
|||
() async => await endpoints.authenticator.remove(
|
||||
asAnna(),
|
||||
authenticator: complete.enrolment.authenticator,
|
||||
proof: await ownerProof(totpCode: codeOf(second, at: now)),
|
||||
proof: await ownerProof(
|
||||
purpose: PasswordProofPurpose.removeAuthenticator,
|
||||
digest: authenticatorDigest(complete.enrolment.authenticator),
|
||||
totpCode: codeOf(second, at: now),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -1846,7 +1929,10 @@ void main() {
|
|||
atNow(
|
||||
() async => await endpoints.authenticator.newRecoveryCodes(
|
||||
asAnna(),
|
||||
proof: await ownerProof(totpCode: '000000'),
|
||||
proof: await ownerProof(
|
||||
purpose: PasswordProofPurpose.newRecoveryCodes,
|
||||
totpCode: '000000',
|
||||
),
|
||||
),
|
||||
),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
|
|
@ -1856,6 +1942,7 @@ void main() {
|
|||
() async => await endpoints.authenticator.newRecoveryCodes(
|
||||
asAnna(),
|
||||
proof: await ownerProof(
|
||||
purpose: PasswordProofPurpose.newRecoveryCodes,
|
||||
totpCode: codeOf(complete.enrolment, at: now),
|
||||
privateValue: BigInt.from(4242),
|
||||
),
|
||||
|
|
@ -1868,6 +1955,7 @@ void main() {
|
|||
() async => await endpoints.authenticator.newRecoveryCodes(
|
||||
asAnna(),
|
||||
proof: await ownerProof(
|
||||
purpose: PasswordProofPurpose.newRecoveryCodes,
|
||||
totpCode: codeOf(complete.enrolment, at: now),
|
||||
),
|
||||
),
|
||||
|
|
@ -1890,7 +1978,10 @@ void main() {
|
|||
Future<List<String>> newCodes(String code) => atNow(
|
||||
() async => await endpoints.authenticator.newRecoveryCodes(
|
||||
asAnna(),
|
||||
proof: await ownerProof(totpCode: code),
|
||||
proof: await ownerProof(
|
||||
purpose: PasswordProofPurpose.newRecoveryCodes,
|
||||
totpCode: code,
|
||||
),
|
||||
),
|
||||
);
|
||||
for (var i = 0; i < 9; i++) {
|
||||
|
|
@ -1937,7 +2028,10 @@ void main() {
|
|||
// the session's 30 changes begun an hour.
|
||||
final proofs = [
|
||||
for (var i = 0; i < 29; i++)
|
||||
await ownerProof(totpCode: '${100000 + i}'),
|
||||
await ownerProof(
|
||||
purpose: PasswordProofPurpose.newRecoveryCodes,
|
||||
totpCode: '${100000 + i}',
|
||||
),
|
||||
];
|
||||
final reasons = await Future.wait([
|
||||
for (final proof in proofs)
|
||||
|
|
@ -1974,7 +2068,10 @@ void main() {
|
|||
atNow(
|
||||
() async => await endpoints.authenticator.newRecoveryCodes(
|
||||
asAnna(),
|
||||
proof: await ownerProof(totpCode: '000000'),
|
||||
proof: await ownerProof(
|
||||
purpose: PasswordProofPurpose.newRecoveryCodes,
|
||||
totpCode: '000000',
|
||||
),
|
||||
),
|
||||
),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
|
|
@ -1986,7 +2083,11 @@ void main() {
|
|||
() async => await endpoints.authenticator.remove(
|
||||
asAnna(),
|
||||
authenticator: complete.enrolment.authenticator,
|
||||
proof: await ownerProof(totpCode: '000000'),
|
||||
proof: await ownerProof(
|
||||
purpose: PasswordProofPurpose.removeAuthenticator,
|
||||
digest: authenticatorDigest(complete.enrolment.authenticator),
|
||||
totpCode: '000000',
|
||||
),
|
||||
),
|
||||
),
|
||||
refused(PasswordRefusal.lastAuthenticator),
|
||||
|
|
@ -2006,7 +2107,10 @@ void main() {
|
|||
await annaComplete();
|
||||
for (var round = 0; round < 2; round++) {
|
||||
for (var i = 0; i < (round == 0 ? 9 : 1); i++) {
|
||||
final proof = await ownerProof(totpCode: '000000');
|
||||
final proof = await ownerProof(
|
||||
purpose: PasswordProofPurpose.newRecoveryCodes,
|
||||
totpCode: '000000',
|
||||
);
|
||||
await expectLater(
|
||||
atNow(
|
||||
() => endpoints.authenticator.newRecoveryCodes(
|
||||
|
|
@ -2034,7 +2138,11 @@ void main() {
|
|||
endpoints.authenticator.remove(
|
||||
asAnna(),
|
||||
authenticator: own.authenticator,
|
||||
proof: await ownerProof(totpCode: '000000'),
|
||||
proof: await ownerProof(
|
||||
purpose: PasswordProofPurpose.removeAuthenticator,
|
||||
digest: authenticatorDigest(own.authenticator),
|
||||
totpCode: '000000',
|
||||
),
|
||||
),
|
||||
refused(PasswordRefusal.invalidSetup),
|
||||
);
|
||||
|
|
@ -2135,11 +2243,14 @@ void main() {
|
|||
});
|
||||
|
||||
group('a lost authenticator', () {
|
||||
/// Asks for the reset with the password and a code by mail.
|
||||
/// Asks for the reset with the password (its proof made for
|
||||
/// [purpose]) and a code by mail.
|
||||
Future<ResetPending> resetByMail({
|
||||
String address = 'anna@example.org',
|
||||
String codeFor = 'anna@example.org',
|
||||
BigInt? privateValue,
|
||||
PasswordProofPurpose purpose =
|
||||
PasswordProofPurpose.authenticatorReset,
|
||||
}) => atNow(() async {
|
||||
final challenge = await endpoints.passwordSignIn.begin(
|
||||
notSignedIn(),
|
||||
|
|
@ -2148,6 +2259,7 @@ void main() {
|
|||
final answer = device.answer(
|
||||
challenge,
|
||||
privateValue: privateValue,
|
||||
purpose: purpose,
|
||||
);
|
||||
final code = await mailedCode(
|
||||
codeFor,
|
||||
|
|
@ -2166,6 +2278,40 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test('is asked for only with a proof made for it: one made for '
|
||||
'signing in, which a hostile instance could have kept, asks '
|
||||
'nothing; nor does one made for it sign in', () async {
|
||||
final complete = await annaComplete();
|
||||
await expectLater(
|
||||
resetByMail(purpose: PasswordProofPurpose.signIn),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
expect(await AccountReset.db.count(session), 0);
|
||||
await expectLater(
|
||||
atNow(() async {
|
||||
final challenge = await endpoints.passwordSignIn.begin(
|
||||
notSignedIn(),
|
||||
address: 'anna@example.org',
|
||||
);
|
||||
final answer = device.answer(
|
||||
challenge,
|
||||
purpose: PasswordProofPurpose.authenticatorReset,
|
||||
);
|
||||
return await endpoints.passwordSignIn.complete(
|
||||
notSignedIn(),
|
||||
challenge: challenge.challenge,
|
||||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
totpCode: codeOf(complete.enrolment, at: now),
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
);
|
||||
}),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
});
|
||||
|
||||
test('carried out, forgets the codes refused before: the new '
|
||||
"authenticator's codes wait for nothing of the old", () async {
|
||||
await annaComplete();
|
||||
|
|
@ -2245,7 +2391,10 @@ void main() {
|
|||
asAnnaAgain,
|
||||
authenticator: fresh.authenticator,
|
||||
code: codeOf(fresh, at: now),
|
||||
proof: await passwordAlone(from: asAnnaAgain),
|
||||
proof: await passwordAlone(
|
||||
fresh.authenticator,
|
||||
from: asAnnaAgain,
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(codes, hasLength(recoveryCodeCount));
|
||||
|
|
@ -2323,7 +2472,10 @@ void main() {
|
|||
notSignedIn(),
|
||||
address: 'anna@example.org',
|
||||
);
|
||||
final answer = device.answer(challenge);
|
||||
final answer = device.answer(
|
||||
challenge,
|
||||
purpose: PasswordProofPurpose.authenticatorReset,
|
||||
);
|
||||
final keyChallenge = await endpoints.accountRecovery.beginKeyProof(
|
||||
notSignedIn(),
|
||||
address: 'anna@example.org',
|
||||
|
|
@ -2383,7 +2535,10 @@ void main() {
|
|||
notSignedIn(),
|
||||
address: 'anna@example.org',
|
||||
);
|
||||
final answer = device.answer(challenge);
|
||||
final answer = device.answer(
|
||||
challenge,
|
||||
purpose: PasswordProofPurpose.authenticatorReset,
|
||||
);
|
||||
final keyChallenge = await endpoints.accountRecovery
|
||||
.beginKeyProof(notSignedIn(), address: 'anna@example.org');
|
||||
return await endpoints.accountRecovery.resetAuthenticator(
|
||||
|
|
@ -2409,7 +2564,10 @@ void main() {
|
|||
notSignedIn(),
|
||||
address: 'anna@example.org',
|
||||
);
|
||||
final answer = device.answer(challenge);
|
||||
final answer = device.answer(
|
||||
challenge,
|
||||
purpose: PasswordProofPurpose.authenticatorReset,
|
||||
);
|
||||
return await endpoints.accountRecovery.resetAuthenticator(
|
||||
notSignedIn(),
|
||||
challenge: challenge.challenge,
|
||||
|
|
@ -2435,7 +2593,10 @@ void main() {
|
|||
address: 'anna@example.org',
|
||||
),
|
||||
);
|
||||
final answer = device.answer(challenge);
|
||||
final answer = device.answer(
|
||||
challenge,
|
||||
purpose: PasswordProofPurpose.authenticatorReset,
|
||||
);
|
||||
final code = await atNow(
|
||||
() => mailedCode(
|
||||
'anna@example.org',
|
||||
|
|
@ -3096,7 +3257,10 @@ void main() {
|
|||
notSignedIn(),
|
||||
address: 'anna@example.org',
|
||||
);
|
||||
final answer = device.answer(challenge);
|
||||
final answer = device.answer(
|
||||
challenge,
|
||||
purpose: PasswordProofPurpose.authenticatorReset,
|
||||
);
|
||||
final code = await mailedCode(
|
||||
'anna@example.org',
|
||||
purpose: EmailCodePurpose.authenticatorReset,
|
||||
|
|
|
|||
|
|
@ -85,10 +85,13 @@ void main() {
|
|||
return (request: request, code: mailbox.codeFor(address));
|
||||
});
|
||||
|
||||
/// Anna's password alone, for a change's challenge.
|
||||
Future<OwnerProof> passwordAlone() => atNow(
|
||||
/// Anna's password alone, for a change's challenge, made for
|
||||
/// confirming [authenticator] as her first.
|
||||
Future<OwnerProof> passwordAlone(UuidValue authenticator) => atNow(
|
||||
() async => device.ownerProof(
|
||||
await endpoints.password.beginChange(asAnna()),
|
||||
purpose: PasswordProofPurpose.firstAuthenticator,
|
||||
digest: authenticatorDigest(authenticator),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -110,7 +113,7 @@ void main() {
|
|||
asAnna(),
|
||||
authenticator: enrolment.authenticator,
|
||||
code: codeOf(enrolment),
|
||||
proof: await passwordAlone(),
|
||||
proof: await passwordAlone(enrolment.authenticator),
|
||||
);
|
||||
nextStep();
|
||||
return enrolment;
|
||||
|
|
@ -152,7 +155,10 @@ void main() {
|
|||
notSignedIn(),
|
||||
address: 'anna@example.org',
|
||||
);
|
||||
final answer = device.answer(challenge);
|
||||
final answer = device.answer(
|
||||
challenge,
|
||||
purpose: PasswordProofPurpose.authenticatorReset,
|
||||
);
|
||||
return await endpoints.accountRecovery.resetAuthenticator(
|
||||
notSignedIn(),
|
||||
challenge: challenge.challenge,
|
||||
|
|
@ -398,6 +404,7 @@ void main() {
|
|||
asAnna(),
|
||||
proof: device.ownerProof(
|
||||
await endpoints.password.beginChange(asAnna()),
|
||||
purpose: PasswordProofPurpose.callOffResets,
|
||||
totpCode: codeOf(enrolment, at: now),
|
||||
),
|
||||
),
|
||||
|
|
@ -415,15 +422,24 @@ void main() {
|
|||
for (final proof in <Future<OwnerProof> Function()>[
|
||||
() async => device.ownerProof(
|
||||
await endpoints.password.beginChange(asAnna()),
|
||||
purpose: PasswordProofPurpose.callOffResets,
|
||||
),
|
||||
() async => wrong.ownerProof(
|
||||
await endpoints.password.beginChange(asAnna()),
|
||||
purpose: PasswordProofPurpose.callOffResets,
|
||||
totpCode: codeOf(enrolment, at: now),
|
||||
),
|
||||
() async => device.ownerProof(
|
||||
await endpoints.password.beginChange(asAnna()),
|
||||
purpose: PasswordProofPurpose.callOffResets,
|
||||
totpCode: '000000',
|
||||
),
|
||||
// Her password and a right code, for another change.
|
||||
() async => device.ownerProof(
|
||||
await endpoints.password.beginChange(asAnna()),
|
||||
purpose: PasswordProofPurpose.newRecoveryCodes,
|
||||
totpCode: codeOf(enrolment, at: now),
|
||||
),
|
||||
]) {
|
||||
await expectLater(
|
||||
atNow(
|
||||
|
|
|
|||
|
|
@ -163,6 +163,8 @@ void main() {
|
|||
// The first takes the password too (the fourth reading).
|
||||
proof: device.ownerProof(
|
||||
await endpoints.password.beginChange(as(anna)),
|
||||
purpose: PasswordProofPurpose.firstAuthenticator,
|
||||
digest: authenticatorDigest(enrolment.authenticator),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -342,6 +344,8 @@ void main() {
|
|||
// The first takes the password too (the fourth reading).
|
||||
proof: device.ownerProof(
|
||||
await endpoints.password.beginChange(as(anna)),
|
||||
purpose: PasswordProofPurpose.firstAuthenticator,
|
||||
digest: authenticatorDigest(enrolment.authenticator),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -533,6 +533,8 @@ void main() {
|
|||
code: codeOf(enrolment),
|
||||
proof: device.ownerProof(
|
||||
await remote.beginPasswordChange(session, person: annaOnTodo),
|
||||
purpose: PasswordProofPurpose.firstAuthenticator,
|
||||
digest: authenticatorDigest(enrolment.authenticator),
|
||||
),
|
||||
);
|
||||
expect(codes, hasLength(recoveryCodeCount));
|
||||
|
|
@ -556,8 +558,13 @@ void main() {
|
|||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
now = now.add(totpPeriod);
|
||||
Future<OwnerProof> ownerProof() async => device.ownerProof(
|
||||
Future<OwnerProof> ownerProof(
|
||||
PasswordProofPurpose purpose, {
|
||||
List<int> digest = const [],
|
||||
}) async => device.ownerProof(
|
||||
await remote.beginPasswordChange(session, person: annaOnTodo),
|
||||
purpose: purpose,
|
||||
digest: digest,
|
||||
totpCode: codeOf(enrolment),
|
||||
);
|
||||
expect(
|
||||
|
|
@ -566,7 +573,10 @@ void main() {
|
|||
person: annaOnTodo,
|
||||
authenticator: spare.authenticator,
|
||||
code: codeOf(spare),
|
||||
proof: await ownerProof(),
|
||||
proof: await ownerProof(
|
||||
PasswordProofPurpose.addAuthenticator,
|
||||
digest: authenticatorDigest(spare.authenticator),
|
||||
),
|
||||
),
|
||||
isEmpty,
|
||||
);
|
||||
|
|
@ -575,7 +585,10 @@ void main() {
|
|||
session,
|
||||
person: annaOnTodo,
|
||||
authenticator: spare.authenticator,
|
||||
proof: await ownerProof(),
|
||||
proof: await ownerProof(
|
||||
PasswordProofPurpose.removeAuthenticator,
|
||||
digest: authenticatorDigest(spare.authenticator),
|
||||
),
|
||||
);
|
||||
final ways = await remote.signInWaysOf(session, annaOnTodo);
|
||||
expect(ways.password && ways.authenticator, isTrue);
|
||||
|
|
@ -660,7 +673,7 @@ void main() {
|
|||
await remote.newRecoveryCodes(
|
||||
session,
|
||||
person: annaOnTodo,
|
||||
proof: await ownerProof(),
|
||||
proof: await ownerProof(PasswordProofPurpose.newRecoveryCodes),
|
||||
),
|
||||
hasLength(recoveryCodeCount),
|
||||
);
|
||||
|
|
@ -701,7 +714,10 @@ void main() {
|
|||
address: 'anna@example.org',
|
||||
network: '203.0.113.7',
|
||||
);
|
||||
final resetAnswer = device.answer(forReset);
|
||||
final resetAnswer = device.answer(
|
||||
forReset,
|
||||
purpose: PasswordProofPurpose.authenticatorReset,
|
||||
);
|
||||
final resetRequest = await remote.sendSignInCode(
|
||||
session,
|
||||
address: 'anna@example.org',
|
||||
|
|
@ -808,6 +824,8 @@ void main() {
|
|||
session,
|
||||
person: annaOnTodo,
|
||||
),
|
||||
purpose: PasswordProofPurpose.firstAuthenticator,
|
||||
digest: authenticatorDigest(enrolment.authenticator),
|
||||
),
|
||||
);
|
||||
now = now.add(totpPeriod);
|
||||
|
|
@ -816,14 +834,21 @@ void main() {
|
|||
session,
|
||||
person: annaOnTodo,
|
||||
);
|
||||
// The old password proves it: the device takes the new one's
|
||||
// x as it makes it.
|
||||
final old = device.privateValue;
|
||||
final newPassword = device.newPassword();
|
||||
await remote.changePassword(
|
||||
session,
|
||||
person: annaOnTodo,
|
||||
proof: device.ownerProof(
|
||||
challenge,
|
||||
purpose: PasswordProofPurpose.newPassword,
|
||||
digest: newPasswordDigest(newPassword),
|
||||
privateValue: old,
|
||||
totpCode: codeOf(enrolment),
|
||||
),
|
||||
password: device.newPassword(),
|
||||
password: newPassword,
|
||||
keepSession: annasPhone,
|
||||
);
|
||||
await sessionNoticesDelivered();
|
||||
|
|
@ -844,17 +869,18 @@ void main() {
|
|||
session,
|
||||
person: annaOnTodo,
|
||||
);
|
||||
final proofKey = ByteData.sublistView(device.recoveryProofKey);
|
||||
await remote.setRecoveryKeys(
|
||||
session,
|
||||
person: annaOnTodo,
|
||||
proof: device.ownerProof(
|
||||
challenge,
|
||||
purpose: PasswordProofPurpose.newPhrase,
|
||||
digest: newPhraseDigest(fresh, proofKey),
|
||||
totpCode: codeOf(enrolment),
|
||||
),
|
||||
recoveryKeys: fresh,
|
||||
recoveryProofKey: ByteData.sublistView(
|
||||
device.recoveryProofKey,
|
||||
),
|
||||
recoveryProofKey: proofKey,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -918,6 +944,7 @@ void main() {
|
|||
session,
|
||||
person: annaOnTodo,
|
||||
),
|
||||
purpose: PasswordProofPurpose.callOffResets,
|
||||
totpCode: codeOf(enrolment),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ final class PasswordDevice {
|
|||
PasswordChallenge challenge, {
|
||||
BigInt? privateValue,
|
||||
String instanceUrl = testInstanceUrl,
|
||||
srp.PasswordProofPurpose purpose = srp.PasswordProofPurpose.signIn,
|
||||
PasswordProofPurpose purpose = PasswordProofPurpose.signIn,
|
||||
List<int> digest = const [],
|
||||
}) {
|
||||
final client = srp.SrpClient(_clientGroup, random: _random);
|
||||
|
|
@ -145,7 +145,9 @@ final class PasswordDevice {
|
|||
identity: srp.passwordIdentity(
|
||||
challenge.challenge.toBytes(),
|
||||
instanceUrl,
|
||||
purpose: purpose,
|
||||
// As the device names it: the register's and the device's purposes
|
||||
// are one list (key_proof_test).
|
||||
purpose: srp.PasswordProofPurpose.values.byName(purpose.name),
|
||||
digest: digest,
|
||||
),
|
||||
salt: Uint8List.sublistView(challenge.salt),
|
||||
|
|
@ -163,11 +165,14 @@ final class PasswordDevice {
|
|||
);
|
||||
}
|
||||
|
||||
/// The owner's proof for [challenge] (a change's): the device's answer
|
||||
/// with its password, or the [privateValue] of another, and a
|
||||
/// [totpCode] or [recoveryCode].
|
||||
/// The owner's proof for [challenge] (a change's), made for [purpose]
|
||||
/// with the [digest] of what it sets: the device's answer with its
|
||||
/// password, or the [privateValue] of another, and a [totpCode] or
|
||||
/// [recoveryCode].
|
||||
OwnerProof ownerProof(
|
||||
PasswordChallenge challenge, {
|
||||
required PasswordProofPurpose purpose,
|
||||
List<int> digest = const [],
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
BigInt? privateValue,
|
||||
|
|
@ -177,6 +182,8 @@ final class PasswordDevice {
|
|||
challenge,
|
||||
privateValue: privateValue,
|
||||
instanceUrl: instanceUrl,
|
||||
purpose: purpose,
|
||||
digest: digest,
|
||||
);
|
||||
return OwnerProof(
|
||||
challenge: challenge.challenge,
|
||||
|
|
|
|||
Loading…
Reference in a new issue