feat(base): the changes of SimpleLogin accounts' keys, each with the password and a fresh sign-in (ADR 0020, D20)
Every change takes the password's proof by its own proof key (the recovery phrase's for a lost password) and a SimpleLogin sign-in of the last ten minutes on the device (`KeysOwnerProof`, its attempt), as ADR 0017's take the password and a code. The sign-in is checked before the proof and taken with the change, so a wrong proof leaves it standing and each sign-in serves one change. - `keys.set(attempt, setup, handle?)`: the first keys of an account that SimpleLogin was added to (anonymous before, or signing in with GitHub or a code), with the handle where it has none; the sign-in alone, as there is no password to prove yet. - `keys.changePassword`, `keys.beginPhrase` and `keys.resetPassword` (a lost password, with the phrase): every other session ends, in every service, as ADR 0017's change has it. - `keys.setRecoveryKeys`: a new phrase. - `account.delete(keysProof)`: an account without SRP's verifier deletes itself with the proof and the sign-in; `requireOwnerToDelete` no longer asks such an account for SRP. - `newKeysPasswordDigest` on both sides, held to each other by a test; a challenge purpose of its own for the phrase's proof while signed in. - Members ask the register for all of it; `RemoteRegister` and the instance API carry the five new operations and the deletion's proof. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
090d901e92
commit
7c58d23424
6 changed files with 3163 additions and 78 deletions
|
|
@ -1,8 +1,12 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:cryptography/cryptography.dart';
|
||||
import 'package:halkyon_base_server/base.dart';
|
||||
import 'package:halkyon_base_server/src/password/passwords.dart'
|
||||
show passwordRowOf;
|
||||
import 'package:halkyon_oidc_server/oidc.dart';
|
||||
import 'package:halkyon_oidc_server/testing.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
|
@ -15,12 +19,19 @@ import '../support/password_support.dart';
|
|||
import '../test_tools/serverpod_test_tools.dart';
|
||||
|
||||
// The keys of an account that signs in with SimpleLogin on a signed-in
|
||||
// device (Halkyon Learn's ADR 0020, 7 and D19): the password proven with
|
||||
// its own proof key, a signature over the register's nonce, before the
|
||||
// device gets the keys wrapped under it. Every way a proof is refused, and
|
||||
// a session's wrong proofs ending it as ADR 0017's do.
|
||||
// device (Halkyon Learn's ADR 0020, 7, D19 and D20): the password proven
|
||||
// with its own proof key, a signature over the register's nonce, before the
|
||||
// device gets the keys wrapped under it; the changes, each with that proof
|
||||
// (the phrase's for a lost password) and a SimpleLogin sign-in of the last
|
||||
// ten minutes, against a fake SimpleLogin (halkyon_oidc's testing.dart).
|
||||
// Every way a proof is refused, and a session's wrong proofs ending it as
|
||||
// ADR 0017's do.
|
||||
|
||||
void main() {
|
||||
late TestKey key;
|
||||
|
||||
setUpAll(() => key = TestKey.generate('sl', seed: 13));
|
||||
|
||||
withServerpod(
|
||||
'The keys of an account that signs in with SimpleLogin',
|
||||
// Sessions ended for wrong proofs, in transactions of their own: these
|
||||
|
|
@ -29,20 +40,29 @@ void main() {
|
|||
(sessionBuilder, endpoints) {
|
||||
late Session session;
|
||||
late KeysDevice device;
|
||||
late FakeProvider simpleLogin;
|
||||
|
||||
/// With keys, signed in on her phone and her laptop.
|
||||
/// With keys, signed in on her phone and her laptop; SimpleLogin's
|
||||
/// `sl-anna`.
|
||||
late UuidValue anna;
|
||||
late UuidValue annasPhone;
|
||||
late UuidValue annasLaptop;
|
||||
|
||||
/// Without keys, signed in on his laptop.
|
||||
/// Without keys, signed in on his laptop; SimpleLogin's `sl-ben`.
|
||||
late UuidValue ben;
|
||||
late UuidValue bensLaptop;
|
||||
|
||||
setUp(() async {
|
||||
useBase(sessionBuilder);
|
||||
useBase(sessionBuilder, app: 'learn');
|
||||
session = sessionBuilder.build();
|
||||
await deleteAll(session);
|
||||
await _deleteAll(session);
|
||||
simpleLogin = FakeProvider.simpleLogin(
|
||||
key: key,
|
||||
clientId: 'halkyon-learn',
|
||||
clientSecret: 'a secret of the app',
|
||||
callbackUrl: Uri.parse('https://learn.test/simplelogin-callback'),
|
||||
);
|
||||
useSimpleLogin(sessionBuilder, simpleLogin);
|
||||
device = await KeysDevice.make();
|
||||
anna = (await AuthServices.instance.authUsers.create(
|
||||
session,
|
||||
|
|
@ -64,8 +84,21 @@ void main() {
|
|||
scopes: {identified},
|
||||
)).id;
|
||||
bensLaptop = await signInNewDevice(session, ben, method: 'simplelogin');
|
||||
for (final (person, subject) in [(anna, 'sl-anna'), (ben, 'sl-ben')]) {
|
||||
await OidcIdentity.db.insertRow(
|
||||
session,
|
||||
OidcIdentity(
|
||||
authUserId: person,
|
||||
provider: simpleLoginProvider,
|
||||
subject: subject,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
tearDown(() async {
|
||||
useSimpleLoginApp(session.serverpod, null);
|
||||
await _deleteAll(sessionBuilder.build());
|
||||
});
|
||||
tearDown(() => deleteAll(sessionBuilder.build()));
|
||||
|
||||
TestSessionBuilder signedIn(UuidValue person, UuidValue on) =>
|
||||
sessionBuilder.copyWith(
|
||||
|
|
@ -77,6 +110,37 @@ void main() {
|
|||
);
|
||||
TestSessionBuilder onPhone() => signedIn(anna, annasPhone);
|
||||
|
||||
OidcIdp idp() => AuthServices.instance.oidcIdp;
|
||||
|
||||
Matcher refusedAsOidc(OidcRefusal reason) => throwsA(
|
||||
isA<OidcRefusedException>().having((e) => e.reason, 'reason', reason),
|
||||
);
|
||||
Matcher refusedAsProfile(ProfileRefusal reason) => throwsA(
|
||||
isA<ProfileRefusedException>().having(
|
||||
(e) => e.reason,
|
||||
'reason',
|
||||
reason,
|
||||
),
|
||||
);
|
||||
|
||||
/// A SimpleLogin sign-in on [as], as [subject], just now: its attempt,
|
||||
/// which stands for it ten minutes (D20).
|
||||
Future<UuidValue> signInAgain(
|
||||
TestSessionBuilder as,
|
||||
String subject,
|
||||
) async {
|
||||
final start = await endpoints.simpleLogin.begin(
|
||||
as,
|
||||
platform: SimpleLoginPlatform.desktop,
|
||||
);
|
||||
await endpoints.simpleLogin.complete(
|
||||
as,
|
||||
attempt: start.attempt,
|
||||
code: simpleLogin.authorize(start.authorizationUrl, subject: subject),
|
||||
);
|
||||
return start.attempt;
|
||||
}
|
||||
|
||||
Future<Set<UuidValue>> sessionsOf(UuidValue person) async => {
|
||||
for (final row in await ServerSideSession.db.find(
|
||||
session,
|
||||
|
|
@ -279,6 +343,566 @@ void main() {
|
|||
});
|
||||
});
|
||||
|
||||
group('first keys, for an account SimpleLogin was added to (D20)', () {
|
||||
late KeysDevice bens;
|
||||
|
||||
setUp(() async => bens = await KeysDevice.make(seed: 3));
|
||||
|
||||
TestSessionBuilder onBensLaptop() => signedIn(ben, bensLaptop);
|
||||
|
||||
Future<void> setWith(
|
||||
UuidValue attempt, {
|
||||
KeysSetup? setup,
|
||||
String? handle = 'ben',
|
||||
}) => endpoints.keys.set(
|
||||
onBensLaptop(),
|
||||
attempt: attempt,
|
||||
setup: setup ?? bens.setup(),
|
||||
handle: handle,
|
||||
);
|
||||
|
||||
test('are set with a SimpleLogin sign-in of the last ten minutes, '
|
||||
'taken, with a handle for an account without one, and then '
|
||||
'unlock', () async {
|
||||
final attempt = await signInAgain(onBensLaptop(), 'sl-ben');
|
||||
|
||||
await setWith(attempt, handle: 'Ben');
|
||||
|
||||
final row = (await AccountPassword.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(ben),
|
||||
))!;
|
||||
expect(row.verifier, isNull);
|
||||
expect(
|
||||
Uint8List.sublistView(row.passwordProofKey!),
|
||||
bens.passwordProofKey,
|
||||
);
|
||||
expect(
|
||||
(await PublicProfile.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(ben),
|
||||
))!.handle,
|
||||
'Ben',
|
||||
);
|
||||
expect(
|
||||
(await endpoints.account.signInWays(onBensLaptop())).keys,
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
await idp().isKept(session, attempt: attempt, caller: ben),
|
||||
isFalse,
|
||||
);
|
||||
await endpoints.keys.unlock(
|
||||
onBensLaptop(),
|
||||
await bens.prove(await endpoints.keys.begin(onBensLaptop())),
|
||||
);
|
||||
});
|
||||
|
||||
test('keep the handle an account has, refusing another', () async {
|
||||
await AccountRegister.of(session.serverpod).saveProfile(
|
||||
session,
|
||||
ben,
|
||||
handle: 'Ben',
|
||||
displayName: '',
|
||||
bio: '',
|
||||
links: const [],
|
||||
);
|
||||
final attempt = await signInAgain(onBensLaptop(), 'sl-ben');
|
||||
|
||||
await expectLater(
|
||||
setWith(attempt, handle: 'Benjamin'),
|
||||
refusedAsProfile(ProfileRefusal.handleInvalid),
|
||||
);
|
||||
await setWith(attempt, handle: null);
|
||||
expect(
|
||||
(await PublicProfile.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(ben),
|
||||
))!.handle,
|
||||
'Ben',
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'refuse a handle missing or refused as saving a profile refuses '
|
||||
'it, and a setup out of range, the sign-in still standing',
|
||||
() async {
|
||||
await AccountRegister.of(session.serverpod).saveProfile(
|
||||
session,
|
||||
anna,
|
||||
handle: 'Anna',
|
||||
displayName: '',
|
||||
bio: '',
|
||||
links: const [],
|
||||
);
|
||||
final attempt = await signInAgain(onBensLaptop(), 'sl-ben');
|
||||
|
||||
for (final (handle, reason) in [
|
||||
(null, ProfileRefusal.handleInvalid),
|
||||
('ANNA', ProfileRefusal.handleTaken),
|
||||
('admin', ProfileRefusal.handleReserved),
|
||||
('ben!', ProfileRefusal.handleInvalid),
|
||||
]) {
|
||||
await expectLater(
|
||||
setWith(attempt, handle: handle),
|
||||
refusedAsProfile(reason),
|
||||
reason: '$handle',
|
||||
);
|
||||
}
|
||||
await expectLater(
|
||||
setWith(
|
||||
attempt,
|
||||
setup: bens.setup().copyWith(salt: ByteData(15)),
|
||||
),
|
||||
refused(PasswordRefusal.invalidSetup),
|
||||
);
|
||||
await setWith(attempt);
|
||||
},
|
||||
);
|
||||
|
||||
test('refuse an account with keys already, under either kind of '
|
||||
'password', () async {
|
||||
final annas = await signInAgain(onPhone(), 'sl-anna');
|
||||
await expectLater(
|
||||
endpoints.keys.set(onPhone(), attempt: annas, setup: bens.setup()),
|
||||
refused(PasswordRefusal.passwordExists),
|
||||
);
|
||||
await AccountPassword.db.insertRow(
|
||||
session,
|
||||
passwordRowOf(
|
||||
ben,
|
||||
(await PasswordDevice.make()).setup(),
|
||||
now: DateTime.now().toUtc(),
|
||||
),
|
||||
);
|
||||
await expectLater(
|
||||
setWith(await signInAgain(onBensLaptop(), 'sl-ben')),
|
||||
refused(PasswordRefusal.passwordExists),
|
||||
);
|
||||
});
|
||||
|
||||
test("refuse without a SimpleLogin sign-in of the account's: none, "
|
||||
"past its ten minutes, or another account's, which stands for "
|
||||
'it still', () async {
|
||||
final annas = await signInAgain(onPhone(), 'sl-anna');
|
||||
final attempt = await signInAgain(onBensLaptop(), 'sl-ben');
|
||||
|
||||
await expectLater(
|
||||
setWith(const Uuid().v4obj()),
|
||||
refusedAsOidc(OidcRefusal.unknownAttempt),
|
||||
);
|
||||
await expectLater(
|
||||
withClock(
|
||||
Clock.fixed(DateTime.now().add(const Duration(minutes: 11))),
|
||||
() => setWith(attempt),
|
||||
),
|
||||
refusedAsOidc(OidcRefusal.unknownAttempt),
|
||||
);
|
||||
await expectLater(
|
||||
setWith(annas),
|
||||
refusedAsOidc(OidcRefusal.wrongCaller),
|
||||
);
|
||||
expect(
|
||||
await idp().isKept(session, attempt: annas, caller: anna),
|
||||
isTrue,
|
||||
);
|
||||
await setWith(attempt);
|
||||
});
|
||||
});
|
||||
|
||||
group('a new password (D20)', () {
|
||||
late KeysDevice next;
|
||||
|
||||
setUp(() async => next = await KeysDevice.make(seed: 7));
|
||||
|
||||
/// The proof for a new password ([binding], else [next]'s) by
|
||||
/// [keys] (the password's proof key unless given), with the
|
||||
/// sign-in under [attempt].
|
||||
Future<KeysOwnerProof> changeProof(
|
||||
UuidValue attempt, {
|
||||
KeysPassword? binding,
|
||||
SimpleKeyPair? keys,
|
||||
KeyProofPurpose purpose = KeyProofPurpose.newPassword,
|
||||
KeysChallenge? challenge,
|
||||
}) async => await device.ownerProof(
|
||||
attempt,
|
||||
challenge: challenge ?? await endpoints.keys.begin(onPhone()),
|
||||
purpose: purpose,
|
||||
digest: newKeysPasswordDigest(binding ?? next.password()),
|
||||
keys: keys,
|
||||
);
|
||||
|
||||
Future<void> changeWith(KeysOwnerProof proof, {KeysPassword? to}) =>
|
||||
endpoints.keys.changePassword(
|
||||
onPhone(),
|
||||
proof: proof,
|
||||
password: to ?? next.password(),
|
||||
);
|
||||
|
||||
test("takes the old password's proof and a SimpleLogin sign-in: the "
|
||||
'keys open with the new one alone, and every other session '
|
||||
'ends', () async {
|
||||
final attempt = await signInAgain(onPhone(), 'sl-anna');
|
||||
|
||||
await changeWith(await changeProof(attempt));
|
||||
|
||||
expect(await sessionsOf(anna), {annasPhone});
|
||||
final challenge = await endpoints.keys.begin(onPhone());
|
||||
expect(Uint8List.sublistView(challenge.salt), next.salt);
|
||||
final keys = await endpoints.keys.unlock(
|
||||
onPhone(),
|
||||
await next.prove(challenge),
|
||||
);
|
||||
expect(keys.passwordKeys.toJson(), next.passwordKeys.toJson());
|
||||
await expectLater(
|
||||
endpoints.keys.unlock(
|
||||
onPhone(),
|
||||
await device.prove(await endpoints.keys.begin(onPhone())),
|
||||
),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
});
|
||||
|
||||
test('refuses a proof by another key, for another password or '
|
||||
'purpose, and a password out of range, the sign-in still '
|
||||
'standing', () async {
|
||||
final attempt = await signInAgain(onPhone(), 'sl-anna');
|
||||
|
||||
for (final proof in [
|
||||
await changeProof(attempt, keys: device.recoveryProofKeys),
|
||||
await changeProof(
|
||||
attempt,
|
||||
binding: (await KeysDevice.make(seed: 9)).password(),
|
||||
),
|
||||
await changeProof(attempt, purpose: KeyProofPurpose.newPhrase),
|
||||
]) {
|
||||
await expectLater(
|
||||
changeWith(proof),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
}
|
||||
await expectLater(
|
||||
changeWith(
|
||||
await changeProof(attempt),
|
||||
to: next.password().copyWith(passwordProofKey: ByteData(31)),
|
||||
),
|
||||
refused(PasswordRefusal.invalidSetup),
|
||||
);
|
||||
expect(await sessionsOf(anna), {annasPhone, annasLaptop});
|
||||
await changeWith(await changeProof(attempt));
|
||||
});
|
||||
|
||||
test('refuses without a SimpleLogin sign-in of hers, checking no '
|
||||
"proof: none, another account's, past its ten minutes, or one a "
|
||||
'change took', () async {
|
||||
final bens = await signInAgain(signedIn(ben, bensLaptop), 'sl-ben');
|
||||
final challenge = await endpoints.keys.begin(onPhone());
|
||||
|
||||
for (final attempt in [const Uuid().v4obj(), bens]) {
|
||||
await expectLater(
|
||||
changeWith(await changeProof(attempt, challenge: challenge)),
|
||||
refusedAsOidc(OidcRefusal.unknownAttempt),
|
||||
);
|
||||
}
|
||||
final annas = await signInAgain(onPhone(), 'sl-anna');
|
||||
await expectLater(
|
||||
withClock(
|
||||
Clock.fixed(DateTime.now().add(const Duration(minutes: 11))),
|
||||
() async => await changeWith(
|
||||
await changeProof(annas, challenge: challenge),
|
||||
),
|
||||
),
|
||||
refusedAsOidc(OidcRefusal.unknownAttempt),
|
||||
);
|
||||
// The challenge still answers: no proof was taken.
|
||||
await changeWith(await changeProof(annas, challenge: challenge));
|
||||
// One change for each sign-in.
|
||||
await expectLater(
|
||||
changeWith(
|
||||
await next.ownerProof(
|
||||
annas,
|
||||
challenge: await endpoints.keys.begin(onPhone()),
|
||||
purpose: KeyProofPurpose.newPassword,
|
||||
digest: newKeysPasswordDigest(device.password()),
|
||||
),
|
||||
to: device.password(),
|
||||
),
|
||||
refusedAsOidc(OidcRefusal.unknownAttempt),
|
||||
);
|
||||
});
|
||||
|
||||
test("counts the session's wrong proofs, as unlocking does: the "
|
||||
'tenth in a row ends it', () async {
|
||||
final attempt = await signInAgain(onPhone(), 'sl-anna');
|
||||
|
||||
for (var i = 0; i < 10; i++) {
|
||||
await expectLater(
|
||||
changeWith(
|
||||
await changeProof(attempt, keys: device.recoveryProofKeys),
|
||||
),
|
||||
refused(
|
||||
i < 9
|
||||
? PasswordRefusal.wrongCredentials
|
||||
: PasswordRefusal.sessionEnded,
|
||||
),
|
||||
);
|
||||
}
|
||||
expect(await sessionsOf(anna), {annasLaptop});
|
||||
});
|
||||
});
|
||||
|
||||
group('a lost password (D20)', () {
|
||||
late KeysDevice next;
|
||||
|
||||
setUp(() async => next = await KeysDevice.make(seed: 7));
|
||||
|
||||
Future<void> resetWith(KeysOwnerProof proof) => endpoints.keys
|
||||
.resetPassword(onPhone(), proof: proof, password: next.password());
|
||||
|
||||
test("is set anew with the recovery phrase's proof and a SimpleLogin "
|
||||
'sign-in, every other session ending', () async {
|
||||
final phrase = await endpoints.keys.beginPhrase(onPhone());
|
||||
expect(phrase.recoveryKeys.toJson(), device.recoveryKeys.toJson());
|
||||
expect(
|
||||
Uint8List.sublistView(phrase.signingPublicKey),
|
||||
device.signingPublicKey,
|
||||
);
|
||||
expect(phrase.nonce.lengthInBytes, 32);
|
||||
final attempt = await signInAgain(onPhone(), 'sl-anna');
|
||||
|
||||
await resetWith(
|
||||
await device.ownerProof(
|
||||
attempt,
|
||||
phrase: phrase,
|
||||
purpose: KeyProofPurpose.passwordReset,
|
||||
digest: newKeysPasswordDigest(next.password()),
|
||||
),
|
||||
);
|
||||
|
||||
expect(await sessionsOf(anna), {annasPhone});
|
||||
await endpoints.keys.unlock(
|
||||
onPhone(),
|
||||
await next.prove(await endpoints.keys.begin(onPhone())),
|
||||
);
|
||||
});
|
||||
|
||||
test("refuses the password's proof key, a challenge of keys.begin, "
|
||||
'and a proof for another purpose', () async {
|
||||
final attempt = await signInAgain(onPhone(), 'sl-anna');
|
||||
final digest = newKeysPasswordDigest(next.password());
|
||||
|
||||
await expectLater(
|
||||
resetWith(
|
||||
await device.ownerProof(
|
||||
attempt,
|
||||
phrase: await endpoints.keys.beginPhrase(onPhone()),
|
||||
keys: device.passwordProofKeys,
|
||||
purpose: KeyProofPurpose.passwordReset,
|
||||
digest: digest,
|
||||
),
|
||||
),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
await expectLater(
|
||||
resetWith(
|
||||
await device.ownerProof(
|
||||
attempt,
|
||||
challenge: await endpoints.keys.begin(onPhone()),
|
||||
keys: device.recoveryProofKeys,
|
||||
purpose: KeyProofPurpose.passwordReset,
|
||||
digest: digest,
|
||||
),
|
||||
),
|
||||
refused(PasswordRefusal.challengeExpired),
|
||||
);
|
||||
await expectLater(
|
||||
resetWith(
|
||||
await device.ownerProof(
|
||||
attempt,
|
||||
phrase: await endpoints.keys.beginPhrase(onPhone()),
|
||||
purpose: KeyProofPurpose.newPassword,
|
||||
digest: digest,
|
||||
),
|
||||
),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
await resetWith(
|
||||
await device.ownerProof(
|
||||
attempt,
|
||||
phrase: await endpoints.keys.beginPhrase(onPhone()),
|
||||
purpose: KeyProofPurpose.passwordReset,
|
||||
digest: digest,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('is for an account with such keys alone', () async {
|
||||
final bens = signedIn(ben, bensLaptop);
|
||||
await expectLater(
|
||||
endpoints.keys.beginPhrase(bens),
|
||||
refused(PasswordRefusal.noPassword),
|
||||
);
|
||||
await AccountPassword.db.insertRow(
|
||||
session,
|
||||
passwordRowOf(
|
||||
ben,
|
||||
(await PasswordDevice.make()).setup(),
|
||||
now: DateTime.now().toUtc(),
|
||||
),
|
||||
);
|
||||
await expectLater(
|
||||
endpoints.keys.beginPhrase(bens),
|
||||
refused(PasswordRefusal.noPassword),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('a new recovery phrase (D20)', () {
|
||||
late KeysDevice next;
|
||||
|
||||
setUp(() async => next = await KeysDevice.make(seed: 7));
|
||||
|
||||
Future<void> setPhrase(
|
||||
KeysOwnerProof proof, {
|
||||
WrappedKeys? recoveryKeys,
|
||||
}) => endpoints.keys.setRecoveryKeys(
|
||||
onPhone(),
|
||||
proof: proof,
|
||||
recoveryKeys: recoveryKeys ?? next.recoveryKeys,
|
||||
recoveryProofKey: ByteData.sublistView(next.recoveryProofKey),
|
||||
);
|
||||
|
||||
Future<KeysOwnerProof> phraseProof(UuidValue attempt) async =>
|
||||
await device.ownerProof(
|
||||
attempt,
|
||||
challenge: await endpoints.keys.begin(onPhone()),
|
||||
purpose: KeyProofPurpose.newPhrase,
|
||||
digest: newPhraseDigest(
|
||||
next.recoveryKeys,
|
||||
ByteData.sublistView(next.recoveryProofKey),
|
||||
),
|
||||
);
|
||||
|
||||
test("takes the password's proof and a SimpleLogin sign-in, the old "
|
||||
'phrase proving nothing then, the sessions staying', () async {
|
||||
await setPhrase(
|
||||
await phraseProof(await signInAgain(onPhone(), 'sl-anna')),
|
||||
);
|
||||
|
||||
final phrase = await endpoints.keys.beginPhrase(onPhone());
|
||||
expect(phrase.recoveryKeys.toJson(), next.recoveryKeys.toJson());
|
||||
expect(await sessionsOf(anna), {annasPhone, annasLaptop});
|
||||
final again = await signInAgain(onPhone(), 'sl-anna');
|
||||
final digest = newKeysPasswordDigest(next.password());
|
||||
await expectLater(
|
||||
endpoints.keys.resetPassword(
|
||||
onPhone(),
|
||||
proof: await device.ownerProof(
|
||||
again,
|
||||
phrase: phrase,
|
||||
purpose: KeyProofPurpose.passwordReset,
|
||||
digest: digest,
|
||||
),
|
||||
password: next.password(),
|
||||
),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
await endpoints.keys.resetPassword(
|
||||
onPhone(),
|
||||
proof: await next.ownerProof(
|
||||
again,
|
||||
phrase: await endpoints.keys.beginPhrase(onPhone()),
|
||||
purpose: KeyProofPurpose.passwordReset,
|
||||
digest: digest,
|
||||
),
|
||||
password: next.password(),
|
||||
);
|
||||
});
|
||||
|
||||
test('refuses a phrase out of range, or a proof for another '
|
||||
'one', () async {
|
||||
final attempt = await signInAgain(onPhone(), 'sl-anna');
|
||||
|
||||
await expectLater(
|
||||
setPhrase(
|
||||
await phraseProof(attempt),
|
||||
recoveryKeys: next.recoveryKeys.copyWith(nonce: ByteData(12)),
|
||||
),
|
||||
refused(PasswordRefusal.invalidSetup),
|
||||
);
|
||||
await expectLater(
|
||||
setPhrase(
|
||||
await phraseProof(attempt),
|
||||
recoveryKeys: device.recoveryKeys,
|
||||
),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
await setPhrase(await phraseProof(attempt));
|
||||
});
|
||||
});
|
||||
|
||||
group('deleting the account (D20)', () {
|
||||
Future<KeysOwnerProof> deletionProof(
|
||||
UuidValue attempt, {
|
||||
KeyProofPurpose purpose = KeyProofPurpose.deleteAccount,
|
||||
}) async => await device.ownerProof(
|
||||
attempt,
|
||||
challenge: await endpoints.keys.begin(onPhone()),
|
||||
purpose: purpose,
|
||||
);
|
||||
|
||||
test("takes the password's proof and a SimpleLogin sign-in", () async {
|
||||
await endpoints.account.delete(
|
||||
onPhone(),
|
||||
keysProof: await deletionProof(
|
||||
await signInAgain(onPhone(), 'sl-anna'),
|
||||
),
|
||||
);
|
||||
|
||||
expect(await AuthUser.db.findById(session, anna), isNull);
|
||||
});
|
||||
|
||||
test('refuses a session alone, the proof of a password that signs '
|
||||
'in, a proof for another purpose, and one without a sign-in, '
|
||||
'keeping the account', () async {
|
||||
await expectLater(
|
||||
endpoints.account.delete(onPhone()),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
await expectLater(
|
||||
endpoints.account.delete(
|
||||
onPhone(),
|
||||
proof: DeletionProof(
|
||||
challenge: const Uuid().v4obj(),
|
||||
clientValue: ByteData(384),
|
||||
clientProof: ByteData(32),
|
||||
totpCode: '000000',
|
||||
instanceUrl: testInstanceUrl,
|
||||
),
|
||||
),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
await expectLater(
|
||||
endpoints.account.delete(
|
||||
onPhone(),
|
||||
keysProof: await deletionProof(const Uuid().v4obj()),
|
||||
),
|
||||
refusedAsOidc(OidcRefusal.unknownAttempt),
|
||||
);
|
||||
await expectLater(
|
||||
endpoints.account.delete(
|
||||
onPhone(),
|
||||
keysProof: await deletionProof(
|
||||
await signInAgain(onPhone(), 'sl-anna'),
|
||||
purpose: KeyProofPurpose.newPassword,
|
||||
),
|
||||
),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
expect(await AuthUser.db.findById(session, anna), isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('the ways in', () {
|
||||
test('say the account has keys, and no password that signs in, nor '
|
||||
'an authenticator to add', () async {
|
||||
|
|
@ -305,3 +929,9 @@ void main() {
|
|||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _deleteAll(Session session) async {
|
||||
await OidcAttempt.db.deleteWhere(session, where: (t) => t.id.notEquals(null));
|
||||
await PublicProfile.db.deleteWhere(session, where: (t) => t.id > 0);
|
||||
await deleteAll(session);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -321,6 +321,76 @@ final class _Register implements AccountRegister {
|
|||
return account;
|
||||
});
|
||||
|
||||
/// The first keys set, by person, with the sign-in and handle they
|
||||
/// came with; the changes of keys asked, by name, with the proof (as the
|
||||
/// member bound it) and the session kept.
|
||||
final firstKeys = <UuidValue, ({UuidValue attempt, String? handle})>{};
|
||||
final keysChanges =
|
||||
<({String change, KeysOwnerProof proof, UuidValue? keepSession})>[];
|
||||
|
||||
@override
|
||||
Future<void> setFirstKeys(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required UuidValue attempt,
|
||||
required KeysSetup setup,
|
||||
String? handle,
|
||||
}) => _answer('setFirstKeys', () {
|
||||
firstKeys[person] = (attempt: attempt, handle: handle);
|
||||
});
|
||||
|
||||
@override
|
||||
Future<KeyProofChallenge> beginPhraseKeyProof(
|
||||
Session session,
|
||||
UuidValue person,
|
||||
) => _answer(
|
||||
'beginPhraseKeyProof',
|
||||
() => KeyProofChallenge(
|
||||
challenge: const Uuid().v7obj(),
|
||||
nonce: ByteData(32),
|
||||
encryptionPublicKey: ByteData(32),
|
||||
signingPublicKey: ByteData(32),
|
||||
recoveryKeys: WrappedKeys(nonce: ByteData(24), cipherText: ByteData(112)),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> changeKeysPassword(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required KeysOwnerProof proof,
|
||||
required KeysPassword password,
|
||||
UuidValue? keepSession,
|
||||
}) => _answer('changeKeysPassword', () {
|
||||
keysChanges.add((
|
||||
change: 'password',
|
||||
proof: proof,
|
||||
keepSession: keepSession,
|
||||
));
|
||||
});
|
||||
|
||||
@override
|
||||
Future<void> resetKeysPassword(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required KeysOwnerProof proof,
|
||||
required KeysPassword password,
|
||||
UuidValue? keepSession,
|
||||
}) => _answer('resetKeysPassword', () {
|
||||
keysChanges.add((change: 'reset', proof: proof, keepSession: keepSession));
|
||||
});
|
||||
|
||||
@override
|
||||
Future<void> setKeysRecovery(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required KeysOwnerProof proof,
|
||||
required WrappedKeys recoveryKeys,
|
||||
required ByteData recoveryProofKey,
|
||||
}) => _answer('setKeysRecovery', () {
|
||||
keysChanges.add((change: 'phrase', proof: proof, keepSession: null));
|
||||
});
|
||||
|
||||
@override
|
||||
Future<KeysChallenge> beginPasswordKeyProof(
|
||||
Session session,
|
||||
|
|
@ -602,14 +672,17 @@ final class _Register implements AccountRegister {
|
|||
Session session,
|
||||
UuidValue person, {
|
||||
DeletionProof? proof,
|
||||
KeysOwnerProof? keysProof,
|
||||
}) => _answer('deleteAccount', () {
|
||||
deleted.add(person);
|
||||
deletedWith.add(proof);
|
||||
deletedWithKeys.add(keysProof);
|
||||
accounts.remove(person);
|
||||
});
|
||||
|
||||
/// The proofs deletions came with, as the member bound them.
|
||||
final deletedWith = <DeletionProof?>[];
|
||||
final deletedWithKeys = <KeysOwnerProof?>[];
|
||||
final changedWith = <OwnerProof>[];
|
||||
|
||||
@override
|
||||
|
|
@ -1348,6 +1421,99 @@ void main() {
|
|||
expect(register.asked, isEmpty);
|
||||
});
|
||||
|
||||
test('are set first at the register with the sign-in and handle, '
|
||||
'for the caller', () async {
|
||||
final attempt = const Uuid().v7obj();
|
||||
|
||||
await endpoints.keys.set(
|
||||
asBen(),
|
||||
attempt: attempt,
|
||||
setup: (await KeysDevice.make()).setup(),
|
||||
handle: 'ben',
|
||||
);
|
||||
|
||||
expect(register.asked, ['setFirstKeys']);
|
||||
expect(register.firstKeys, {ben: (attempt: attempt, handle: 'ben')});
|
||||
});
|
||||
|
||||
test('are changed at the register, each proof bound to the base URL '
|
||||
'the device addressed here, this session kept where every other '
|
||||
'ends', () async {
|
||||
final proof = KeysOwnerProof(
|
||||
challenge: const Uuid().v7obj(),
|
||||
signature: ByteData(64),
|
||||
instanceUrl: testInstanceUrl,
|
||||
attempt: const Uuid().v7obj(),
|
||||
);
|
||||
final password = KeysPassword(
|
||||
salt: ByteData(16),
|
||||
costs: newPasswordCosts,
|
||||
passwordProofKey: ByteData(32),
|
||||
passwordKeys: WrappedKeys(
|
||||
nonce: ByteData(24),
|
||||
cipherText: ByteData(112),
|
||||
),
|
||||
);
|
||||
|
||||
await endpoints.keys.changePassword(
|
||||
asBen(),
|
||||
proof: proof,
|
||||
password: password,
|
||||
);
|
||||
await endpoints.keys.beginPhrase(asBen());
|
||||
await endpoints.keys.resetPassword(
|
||||
asBen(),
|
||||
proof: proof,
|
||||
password: password,
|
||||
);
|
||||
await endpoints.keys.setRecoveryKeys(
|
||||
asBen(),
|
||||
proof: proof,
|
||||
recoveryKeys: WrappedKeys(
|
||||
nonce: ByteData(24),
|
||||
cipherText: ByteData(112),
|
||||
),
|
||||
recoveryProofKey: ByteData(32),
|
||||
);
|
||||
|
||||
expect(register.asked, [
|
||||
'changeKeysPassword',
|
||||
'beginPhraseKeyProof',
|
||||
'resetKeysPassword',
|
||||
'setKeysRecovery',
|
||||
]);
|
||||
expect(
|
||||
[
|
||||
for (final change in register.keysChanges)
|
||||
(change.change, change.keepSession),
|
||||
],
|
||||
[
|
||||
('password', bensLaptop),
|
||||
('reset', bensLaptop),
|
||||
('phrase', null),
|
||||
],
|
||||
);
|
||||
expect(
|
||||
register.keysChanges.map((change) => change.proof.instanceUrl),
|
||||
everyElement(testInstanceUrl),
|
||||
);
|
||||
await expectLater(
|
||||
endpoints.keys.changePassword(
|
||||
asBen(),
|
||||
proof: proof.copyWith(instanceUrl: 'https://evil.example/'),
|
||||
password: password,
|
||||
),
|
||||
throwsA(
|
||||
isA<PasswordRefusedException>().having(
|
||||
(refusal) => refusal.reason,
|
||||
'reason',
|
||||
PasswordRefusal.otherInstance,
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(register.asked, hasLength(4));
|
||||
});
|
||||
|
||||
test("count the session's own wrong proofs, which the register "
|
||||
'refuses: the tenth in a row ends the session here', () async {
|
||||
register.failing = PasswordRefusedException(
|
||||
|
|
@ -1445,6 +1611,29 @@ void main() {
|
|||
expect(register.deletedWith.single?.totpCode, '123456');
|
||||
});
|
||||
|
||||
test('passes the proof of keys without SRP on to the register, with '
|
||||
'its SimpleLogin sign-in, bound alike', () async {
|
||||
final attempt = const Uuid().v7obj();
|
||||
|
||||
await endpoints.account.delete(
|
||||
asBen(),
|
||||
keysProof: KeysOwnerProof(
|
||||
challenge: const Uuid().v7obj(),
|
||||
signature: ByteData(64),
|
||||
instanceUrl: 'HTTP://LOCALHOST:0',
|
||||
attempt: attempt,
|
||||
),
|
||||
);
|
||||
|
||||
expect(register.deleted, [ben]);
|
||||
expect(register.deletedWith.single, isNull);
|
||||
expect(
|
||||
register.deletedWithKeys.single?.instanceUrl,
|
||||
testInstanceUrl,
|
||||
);
|
||||
expect(register.deletedWithKeys.single?.attempt, attempt);
|
||||
});
|
||||
|
||||
test('of an anonymous person deletes it here alone, once the '
|
||||
'register says it has no such account', () async {
|
||||
await endpoints.account.delete(asAnna());
|
||||
|
|
|
|||
|
|
@ -733,6 +733,180 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
/// A SimpleLogin sign-in of [person]'s through a member, as
|
||||
/// [subject], just now: its attempt, which stands for it ten
|
||||
/// minutes (D20).
|
||||
Future<UuidValue> signedInAgain(
|
||||
UuidValue person,
|
||||
String subject,
|
||||
) async {
|
||||
final start = await remote.beginSimpleLogin(
|
||||
session,
|
||||
marker: 'todo-desktop',
|
||||
caller: person,
|
||||
network: '203.0.113.0/24',
|
||||
);
|
||||
await remote.completeSimpleLogin(
|
||||
session,
|
||||
attempt: start.attempt,
|
||||
code: simpleLogin.authorize(
|
||||
start.authorizationUrl,
|
||||
subject: subject,
|
||||
),
|
||||
caller: person,
|
||||
);
|
||||
return start.attempt;
|
||||
}
|
||||
|
||||
test(
|
||||
"gives a member's account its first keys and handle, once "
|
||||
'SimpleLogin was added to it, its refusals crossing whole',
|
||||
() async {
|
||||
final device = await KeysDevice.make();
|
||||
final attempt = await signedInAgain(ben, 'sl-ben');
|
||||
|
||||
await expectLater(
|
||||
remote.setFirstKeys(
|
||||
session,
|
||||
person: ben,
|
||||
attempt: attempt,
|
||||
setup: device.setup(),
|
||||
),
|
||||
refused(ProfileRefusal.handleInvalid),
|
||||
);
|
||||
await expectLater(
|
||||
remote.setFirstKeys(
|
||||
session,
|
||||
person: ben,
|
||||
attempt: const Uuid().v7obj(),
|
||||
setup: device.setup(),
|
||||
handle: 'ben',
|
||||
),
|
||||
refused(OidcRefusal.unknownAttempt),
|
||||
);
|
||||
await remote.setFirstKeys(
|
||||
session,
|
||||
person: ben,
|
||||
attempt: attempt,
|
||||
setup: device.setup(),
|
||||
handle: 'ben',
|
||||
);
|
||||
|
||||
expect((await remote.signInWaysOf(session, ben)).keys, isTrue);
|
||||
expect((await remote.profileOf(session, ben))!.handle, 'ben');
|
||||
await expectLater(
|
||||
remote.setFirstKeys(
|
||||
session,
|
||||
person: ben,
|
||||
attempt: await signedInAgain(ben, 'sl-ben'),
|
||||
setup: device.setup(),
|
||||
),
|
||||
refused(PasswordRefusal.passwordExists),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test("changes a member's account's keys, each change with the "
|
||||
"password's proof (the phrase's for a lost one) and a sign-in, "
|
||||
'and deletes it alike', () async {
|
||||
final device = await KeysDevice.make();
|
||||
final next = await KeysDevice.make(seed: 7);
|
||||
final start = await remote.beginSimpleLogin(
|
||||
session,
|
||||
marker: 'todo-desktop',
|
||||
network: '203.0.113.0/24',
|
||||
);
|
||||
await remote.completeSimpleLogin(
|
||||
session,
|
||||
attempt: start.attempt,
|
||||
code: simpleLogin.authorize(
|
||||
start.authorizationUrl,
|
||||
subject: 'sl-carla',
|
||||
),
|
||||
);
|
||||
final carla = await remote.signUpWithSimpleLogin(
|
||||
session,
|
||||
attempt: start.attempt,
|
||||
handle: 'carla',
|
||||
setup: device.setup(),
|
||||
);
|
||||
final keepSession = const Uuid().v7obj();
|
||||
|
||||
await remote.changeKeysPassword(
|
||||
session,
|
||||
person: carla,
|
||||
proof: await device.ownerProof(
|
||||
await signedInAgain(carla, 'sl-carla'),
|
||||
challenge: await remote.beginPasswordKeyProof(session, carla),
|
||||
purpose: KeyProofPurpose.newPassword,
|
||||
digest: newKeysPasswordDigest(next.password()),
|
||||
),
|
||||
password: next.password(),
|
||||
keepSession: keepSession,
|
||||
);
|
||||
// Every member is told to end the account's other sessions.
|
||||
await sessionNoticesDelivered();
|
||||
expect(members.sessionsTold.single.$2, carla);
|
||||
expect(members.sessionsTold.single.$4, keepSession);
|
||||
await remote.unlockKeys(
|
||||
session,
|
||||
carla,
|
||||
await next.prove(
|
||||
await remote.beginPasswordKeyProof(session, carla),
|
||||
),
|
||||
);
|
||||
|
||||
await remote.resetKeysPassword(
|
||||
session,
|
||||
person: carla,
|
||||
proof: await device.ownerProof(
|
||||
await signedInAgain(carla, 'sl-carla'),
|
||||
phrase: await remote.beginPhraseKeyProof(session, carla),
|
||||
purpose: KeyProofPurpose.passwordReset,
|
||||
digest: newKeysPasswordDigest(device.password()),
|
||||
),
|
||||
password: device.password(),
|
||||
);
|
||||
await remote.setKeysRecovery(
|
||||
session,
|
||||
person: carla,
|
||||
proof: await device.ownerProof(
|
||||
await signedInAgain(carla, 'sl-carla'),
|
||||
challenge: await remote.beginPasswordKeyProof(session, carla),
|
||||
purpose: KeyProofPurpose.newPhrase,
|
||||
digest: newPhraseDigest(
|
||||
next.recoveryKeys,
|
||||
ByteData.sublistView(next.recoveryProofKey),
|
||||
),
|
||||
),
|
||||
recoveryKeys: next.recoveryKeys,
|
||||
recoveryProofKey: ByteData.sublistView(next.recoveryProofKey),
|
||||
);
|
||||
expect(
|
||||
(await remote.beginPhraseKeyProof(
|
||||
session,
|
||||
carla,
|
||||
)).recoveryKeys.toJson(),
|
||||
next.recoveryKeys.toJson(),
|
||||
);
|
||||
|
||||
final deletion = await signedInAgain(carla, 'sl-carla');
|
||||
await expectLater(
|
||||
remote.deleteAccount(session, carla),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
await remote.deleteAccount(
|
||||
session,
|
||||
carla,
|
||||
keysProof: await device.ownerProof(
|
||||
deletion,
|
||||
challenge: await remote.beginPasswordKeyProof(session, carla),
|
||||
purpose: KeyProofPurpose.deleteAccount,
|
||||
),
|
||||
);
|
||||
expect(await AuthUser.db.findById(session, carla), isNull);
|
||||
});
|
||||
|
||||
test('refuses where the register has no SimpleLogin app', () async {
|
||||
useSimpleLogin(sessionBuilder, null);
|
||||
|
||||
|
|
|
|||
|
|
@ -74,6 +74,15 @@ final class KeysDevice {
|
|||
recoveryProofKey: ByteData.sublistView(recoveryProofKey),
|
||||
);
|
||||
|
||||
/// The password the setup made, as a change sends it: this device's
|
||||
/// password as another's new one.
|
||||
KeysPassword password() => KeysPassword(
|
||||
salt: ByteData.sublistView(salt),
|
||||
costs: newPasswordCosts,
|
||||
passwordProofKey: ByteData.sublistView(passwordProofKey),
|
||||
passwordKeys: passwordKeys,
|
||||
);
|
||||
|
||||
/// The proof of the password for [challenge] and [purpose] (with the
|
||||
/// [digest] of what it sets) on [instanceUrl], by the password's proof
|
||||
/// key or [keys] where given; claiming [claimedUrl] (the same unless
|
||||
|
|
@ -85,19 +94,65 @@ final class KeysDevice {
|
|||
SimpleKeyPair? keys,
|
||||
String instanceUrl = testInstanceUrl,
|
||||
String? claimedUrl,
|
||||
}) => _proof(
|
||||
challenge.challenge,
|
||||
challenge.nonce,
|
||||
purpose: purpose,
|
||||
digest: digest,
|
||||
keys: keys ?? passwordProofKeys,
|
||||
instanceUrl: instanceUrl,
|
||||
claimedUrl: claimedUrl,
|
||||
);
|
||||
|
||||
/// A change's proof (D20): the password's proof for [challenge] (or the
|
||||
/// recovery phrase's for [phrase], by its proof key, where given), with
|
||||
/// the SimpleLogin sign-in under [attempt].
|
||||
Future<KeysOwnerProof> ownerProof(
|
||||
UuidValue attempt, {
|
||||
KeysChallenge? challenge,
|
||||
KeyProofChallenge? phrase,
|
||||
required KeyProofPurpose purpose,
|
||||
List<int> digest = const [],
|
||||
SimpleKeyPair? keys,
|
||||
String instanceUrl = testInstanceUrl,
|
||||
}) async {
|
||||
final proof = await _proof(
|
||||
challenge?.challenge ?? phrase!.challenge,
|
||||
challenge?.nonce ?? phrase!.nonce,
|
||||
purpose: purpose,
|
||||
digest: digest,
|
||||
keys: keys ?? (challenge != null ? passwordProofKeys : recoveryProofKeys),
|
||||
instanceUrl: instanceUrl,
|
||||
);
|
||||
return KeysOwnerProof(
|
||||
challenge: proof.challenge,
|
||||
signature: proof.signature,
|
||||
instanceUrl: proof.instanceUrl,
|
||||
attempt: attempt,
|
||||
);
|
||||
}
|
||||
|
||||
Future<KeysProof> _proof(
|
||||
UuidValue challenge,
|
||||
ByteData nonce, {
|
||||
required KeyProofPurpose purpose,
|
||||
required List<int> digest,
|
||||
required SimpleKeyPair keys,
|
||||
required String instanceUrl,
|
||||
String? claimedUrl,
|
||||
}) async {
|
||||
final signature = await Ed25519().sign(
|
||||
keyProofMessage(
|
||||
challenge.challenge,
|
||||
Uint8List.sublistView(challenge.nonce),
|
||||
challenge,
|
||||
Uint8List.sublistView(nonce),
|
||||
instanceUrl: instanceUrl,
|
||||
purpose: purpose,
|
||||
digest: digest,
|
||||
),
|
||||
keyPair: keys ?? passwordProofKeys,
|
||||
keyPair: keys,
|
||||
);
|
||||
return KeysProof(
|
||||
challenge: challenge.challenge,
|
||||
challenge: challenge,
|
||||
signature: ByteData.sublistView(Uint8List.fromList(signature.bytes)),
|
||||
instanceUrl: claimedUrl ?? instanceUrl,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ import 'package:halkyon_base_server/src/generated/keys/device_keys.dart'
|
|||
as _izazrxr6;
|
||||
import 'package:halkyon_base_server/src/generated/keys/keys_challenge.dart'
|
||||
as _i052zfjx;
|
||||
import 'package:halkyon_base_server/src/generated/keys/keys_owner_proof.dart'
|
||||
as _i561ypzv;
|
||||
import 'package:halkyon_base_server/src/generated/keys/keys_password.dart'
|
||||
as _iri6zrp4;
|
||||
import 'package:halkyon_base_server/src/generated/keys/keys_proof.dart'
|
||||
as _iy592cuk;
|
||||
import 'package:halkyon_base_server/src/generated/keys/keys_setup.dart'
|
||||
|
|
@ -304,6 +308,7 @@ class _AccountEndpoint {
|
|||
_ida.Future<void> delete(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
_i619l6vv.DeletionProof? proof,
|
||||
_i561ypzv.KeysOwnerProof? keysProof,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
|
|
@ -316,7 +321,10 @@ class _AccountEndpoint {
|
|||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'account',
|
||||
methodName: 'delete',
|
||||
parameters: _ist.testObjectToJson({'proof': proof}),
|
||||
parameters: _ist.testObjectToJson({
|
||||
'proof': proof,
|
||||
'keysProof': keysProof,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
|
|
@ -660,6 +668,180 @@ class _KeysEndpoint {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<void> set(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required _is.UuidValue attempt,
|
||||
required _ii64ejy6.KeysSetup setup,
|
||||
String? handle,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'keys',
|
||||
method: 'set',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'keys',
|
||||
methodName: 'set',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'attempt': attempt,
|
||||
'setup': setup,
|
||||
'handle': handle,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<void>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<_iaysae70.KeyProofChallenge> beginPhrase(
|
||||
_ist.TestSessionBuilder sessionBuilder,
|
||||
) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'keys',
|
||||
method: 'beginPhrase',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'keys',
|
||||
methodName: 'beginPhrase',
|
||||
parameters: _ist.testObjectToJson({}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_iaysae70.KeyProofChallenge>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<void> changePassword(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required _i561ypzv.KeysOwnerProof proof,
|
||||
required _iri6zrp4.KeysPassword password,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'keys',
|
||||
method: 'changePassword',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'keys',
|
||||
methodName: 'changePassword',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'proof': proof,
|
||||
'password': password,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<void>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<void> resetPassword(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required _i561ypzv.KeysOwnerProof proof,
|
||||
required _iri6zrp4.KeysPassword password,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'keys',
|
||||
method: 'resetPassword',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'keys',
|
||||
methodName: 'resetPassword',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'proof': proof,
|
||||
'password': password,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<void>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<void> setRecoveryKeys(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required _i561ypzv.KeysOwnerProof proof,
|
||||
required _iji0wmu8.WrappedKeys recoveryKeys,
|
||||
required _idt.ByteData recoveryProofKey,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'keys',
|
||||
method: 'setRecoveryKeys',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'keys',
|
||||
methodName: 'setRecoveryKeys',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'proof': proof,
|
||||
'recoveryKeys': recoveryKeys,
|
||||
'recoveryProofKey': recoveryProofKey,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<void>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _AccountRecoveryEndpoint {
|
||||
|
|
|
|||
|
|
@ -179,6 +179,54 @@ void main() {
|
|||
expect(authenticatorDigest(id), device.authenticatorDigest(id.toBytes()));
|
||||
});
|
||||
|
||||
test('digests a new password for keys without SRP as devices do, '
|
||||
'apart from what ADR 0017 digests', () {
|
||||
final password = KeysPassword(
|
||||
salt: ByteData.sublistView(Uint8List.fromList([1, 2, 3])),
|
||||
costs: PasswordCosts(
|
||||
algorithm: 'argon2id13',
|
||||
opsLimit: 3,
|
||||
memLimit: 64 * 1024 * 1024,
|
||||
),
|
||||
passwordProofKey: ByteData.sublistView(Uint8List.fromList([4, 5])),
|
||||
passwordKeys: WrappedKeys(
|
||||
nonce: ByteData.sublistView(Uint8List.fromList([6])),
|
||||
cipherText: ByteData.sublistView(Uint8List.fromList([7, 8, 9])),
|
||||
),
|
||||
);
|
||||
|
||||
final digest = newKeysPasswordDigest(password);
|
||||
|
||||
expect(
|
||||
digest,
|
||||
device.newKeysPasswordDigest(
|
||||
salt: [1, 2, 3],
|
||||
algorithm: 'argon2id13',
|
||||
opsLimit: 3,
|
||||
memLimit: 64 * 1024 * 1024,
|
||||
proofKey: [4, 5],
|
||||
keysNonce: [6],
|
||||
keysCipherText: [7, 8, 9],
|
||||
),
|
||||
);
|
||||
// The same bytes as a password that signs in: the label tells them
|
||||
// apart.
|
||||
expect(
|
||||
digest,
|
||||
isNot(
|
||||
device.newPasswordDigest(
|
||||
salt: [1, 2, 3],
|
||||
algorithm: 'argon2id13',
|
||||
opsLimit: 3,
|
||||
memLimit: 64 * 1024 * 1024,
|
||||
verifier: [4, 5],
|
||||
keysNonce: [6],
|
||||
keysCipherText: [7, 8, 9],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('writes base URLs as devices do', () {
|
||||
for (final url in [
|
||||
'https://learn.halkyon.eu/',
|
||||
|
|
|
|||
Loading…
Reference in a new issue