fix(app)!: every owner's proof binds the change it approves
The device's answer takes what it is made for and the digest of what it sets, as the register now requires: the new password (made before the old one's proof, which binds it), the new phrase's wrapping and proof key, the authenticator confirmed (the first or another) or removed, new recovery codes, calling resets off, deleting, and asking for an authenticator's reset apart from signing in. proof_digests.dart maps the protocol's objects onto the shared framing; the key proof's password digest uses it too. The fake gateway refuses a proof made for anything but the call's own change, as the register does, so every flow's tests hold the repository to it; the fake's authenticators have UUIDs, as the register's do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
92d2df4695
commit
0a7679396b
1 changed files with 286 additions and 40 deletions
|
|
@ -0,0 +1,87 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_app/src/password/data/proof_digests.dart';
|
||||
import 'package:halkyon_base_client/halkyon_base_client.dart' as protocol;
|
||||
import 'package:halkyon_base_client/srp.dart' as srp;
|
||||
|
||||
ByteData _bytes(List<int> bytes) =>
|
||||
ByteData.sublistView(Uint8List.fromList(bytes));
|
||||
|
||||
protocol.NewPassword _password({
|
||||
List<int> salt = const [1, 2],
|
||||
int opsLimit = 3,
|
||||
List<int> verifier = const [3, 4],
|
||||
List<int> nonce = const [5],
|
||||
List<int> cipherText = const [6, 7],
|
||||
}) => protocol.NewPassword(
|
||||
salt: _bytes(salt),
|
||||
costs: protocol.PasswordCosts(
|
||||
algorithm: 'argon2id13',
|
||||
opsLimit: opsLimit,
|
||||
memLimit: 64 * 1024 * 1024,
|
||||
),
|
||||
verifier: _bytes(verifier),
|
||||
passwordKeys: protocol.WrappedKeys(
|
||||
nonce: _bytes(nonce),
|
||||
cipherText: _bytes(cipherText),
|
||||
),
|
||||
);
|
||||
|
||||
void main() {
|
||||
test("a new password's digest is the shared framing's over every part "
|
||||
'the device sends, so none can be swapped', () {
|
||||
final digest = newPasswordDigestOf(_password());
|
||||
|
||||
expect(
|
||||
digest,
|
||||
srp.newPasswordDigest(
|
||||
salt: [1, 2],
|
||||
algorithm: 'argon2id13',
|
||||
opsLimit: 3,
|
||||
memLimit: 64 * 1024 * 1024,
|
||||
verifier: [3, 4],
|
||||
keysNonce: [5],
|
||||
keysCipherText: [6, 7],
|
||||
),
|
||||
);
|
||||
for (final other in [
|
||||
_password(salt: [1, 3]),
|
||||
_password(opsLimit: 4),
|
||||
_password(verifier: [3, 5]),
|
||||
_password(nonce: [6]),
|
||||
_password(cipherText: [6, 8]),
|
||||
]) {
|
||||
expect(newPasswordDigestOf(other), isNot(digest));
|
||||
}
|
||||
});
|
||||
|
||||
test("a new phrase's digest takes its wrapping and its proof key", () {
|
||||
final keys = protocol.WrappedKeys(
|
||||
nonce: _bytes([1]),
|
||||
cipherText: _bytes([2, 3]),
|
||||
);
|
||||
|
||||
expect(
|
||||
newPhraseDigestOf(keys, _bytes([4, 5])),
|
||||
srp.newPhraseDigest(
|
||||
keysNonce: [1],
|
||||
keysCipherText: [2, 3],
|
||||
proofKey: [4, 5],
|
||||
),
|
||||
);
|
||||
expect(
|
||||
newPhraseDigestOf(keys, _bytes([4, 6])),
|
||||
isNot(newPhraseDigestOf(keys, _bytes([4, 5]))),
|
||||
);
|
||||
});
|
||||
|
||||
test("an authenticator's digest is its ID's, as the register reads it", () {
|
||||
const id = '01994a3c-7b2e-7c00-8a1b-0123456789ab';
|
||||
|
||||
expect(
|
||||
authenticatorDigestOf(id),
|
||||
srp.authenticatorDigest(protocol.UuidValue.fromString(id).toBytes()),
|
||||
);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue