fix(auth): a capped wait, a long instance secret, writes of their own columns
Three of the second reading's lows: - A member waits at most 5 seconds (maxClockSkew) for the register's moment before making a session after a reset; a clock further behind is refused at once (unavailable, logged), rather than holding the request for as long as the clocks differ. - instanceSecret, which lets a service set passwords and delete accounts for anyone, stops the server when set shorter than 32 characters, as the password keys do. - A change of password, a reset keeping the keys and a new phrase each write their own columns alone (writeNewPassword, columns: in setRecoveryKeys), so one made from the row as it was before another never undoes it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
3ded5b932f
commit
5b9cb85b0f
2 changed files with 186 additions and 38 deletions
|
|
@ -3,6 +3,8 @@ 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 writeNewPassword;
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
|
@ -412,6 +414,39 @@ void main() {
|
|||
expect(await proves(freshProof), isTrue);
|
||||
});
|
||||
|
||||
test('and a change of password made from the row as it was before '
|
||||
'each write their own columns alone: neither undoes the '
|
||||
'other', () async {
|
||||
final before = (await AccountPassword.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(anna),
|
||||
))!;
|
||||
await newPhrase();
|
||||
final newPassword = device.newPassword();
|
||||
|
||||
await writeNewPassword(session, before, newPassword);
|
||||
|
||||
final after = (await AccountPassword.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(anna),
|
||||
))!;
|
||||
expect(
|
||||
WrappedKeys(
|
||||
nonce: after.recoveryKeysNonce,
|
||||
cipherText: after.recoveryKeysCipherText,
|
||||
).toJson(),
|
||||
fresh.toJson(),
|
||||
);
|
||||
expect(
|
||||
Uint8List.sublistView(after.recoveryProofKey!),
|
||||
Uint8List.sublistView(freshProofKey),
|
||||
);
|
||||
expect(
|
||||
Uint8List.sublistView(after.verifier),
|
||||
Uint8List.sublistView(newPassword.verifier),
|
||||
);
|
||||
});
|
||||
|
||||
test("is refused alike without the password's proof, for a wrong "
|
||||
"one, another person's, and a missing or wrong code, keeping "
|
||||
'the wrapping', () async {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'dart:typed_data';
|
|||
import 'package:halkyon_base_server/base.dart';
|
||||
import 'package:halkyon_base_server/src/register/deletion_notices.dart';
|
||||
import 'package:halkyon_base_server/src/register/instance_api.dart';
|
||||
import 'package:halkyon_base_server/src/register/people.dart' show maxClockSkew;
|
||||
import 'package:halkyon_base_server/src/register/register_protocol.dart';
|
||||
import 'package:halkyon_base_server/src/register/remote_register.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
|
|
@ -1068,6 +1069,61 @@ void main() {
|
|||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test("waits a few seconds at most for the register's moment: a "
|
||||
'clock further behind is refused at once, and signs nobody in, '
|
||||
'rather than holding the request', () async {
|
||||
register
|
||||
..proof = PasswordProof(
|
||||
account: ben,
|
||||
serverProof: ByteData(32),
|
||||
encryptionPublicKey: ByteData(32),
|
||||
signingPublicKey: ByteData(32),
|
||||
passwordKeys: WrappedKeys(
|
||||
nonce: ByteData(24),
|
||||
cipherText: ByteData(112),
|
||||
),
|
||||
needsAuthenticator: false,
|
||||
)
|
||||
..endedBefore = DateTime.now().toUtc().add(
|
||||
const Duration(minutes: 1),
|
||||
);
|
||||
final watch = Stopwatch()..start();
|
||||
|
||||
await expectLater(
|
||||
endpoints.accountRecovery.resetPassword(
|
||||
notSignedIn(),
|
||||
challenge: const Uuid().v7obj(),
|
||||
signature: ByteData(64),
|
||||
password: NewPassword(
|
||||
salt: ByteData(16),
|
||||
costs: newPasswordCosts,
|
||||
verifier: ByteData(384),
|
||||
passwordKeys: WrappedKeys(
|
||||
nonce: ByteData(24),
|
||||
cipherText: ByteData(112),
|
||||
),
|
||||
),
|
||||
totpCode: '123456',
|
||||
host: testHost,
|
||||
),
|
||||
throwsA(
|
||||
isA<PasswordRefusedException>().having(
|
||||
(error) => error.reason,
|
||||
'reason',
|
||||
PasswordRefusal.unavailable,
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(watch.elapsed, lessThan(maxClockSkew));
|
||||
expect(
|
||||
await ServerSideSession.db.count(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(ben),
|
||||
),
|
||||
0,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('pruning', () {
|
||||
|
|
@ -1135,6 +1191,21 @@ void main() {
|
|||
expect(() => checkInstanceSecret(pod), returnsNormally);
|
||||
});
|
||||
|
||||
test('does not start with a secret shorter than 32 characters, alone '
|
||||
'or not: it gates every account of the instance', () {
|
||||
for (final accounts in [
|
||||
const HoldsRegister(),
|
||||
HoldsRegister(members: [_learn]),
|
||||
MemberOfRegister(_learn),
|
||||
]) {
|
||||
useBase(sessionBuilder, accounts: accounts);
|
||||
useInstanceSecret(pod, 'x' * 31);
|
||||
expect(() => checkInstanceSecret(pod), throwsStateError);
|
||||
useInstanceSecret(pod, 'x' * 32);
|
||||
expect(() => checkInstanceSecret(pod), returnsNormally);
|
||||
}
|
||||
});
|
||||
|
||||
test('asks the register at its address with the secret, and asks '
|
||||
'nothing without one', () {
|
||||
AccountRegister.use(pod, null);
|
||||
|
|
|
|||
Loading…
Reference in a new issue