halkyon/packages/halkyon_base_client/test/key_proof_test.dart

257 lines
6.8 KiB
Dart
Raw Permalink Normal View History

feat(app): sign in with a password and TOTP on the device ADR 0017, step 3: halkyon_app's side, which Learn and Todo take. - The account's cryptography on the device (`SodiumAccountCrypto`): halkyon_keys and SRP's client half, each step on an isolate of its own. sodium hands out results as views of native memory its finalizers free once the isolate that made them ends, so everything crossing back is copied first: a view read freed memory and crashed the process. - `PasswordGateway` on the module's endpoints (steps without a session through a client holding none; an authenticator set up with a session granted but not yet the device's), and `PasswordRepository` running the flows: signing in (M2 checked, the keys opened before anything is trusted), signing up, a password for someone signed in, an account never finished, the authenticators, recovery codes, changing the password, a new recovery phrase, and getting back in. - The unlocked keys in the system's keyring per instance and person (`SecureAccountKeyStore`), gone once another person signs in or nobody, and with the instance's removal. - The screens: the start screen's Sign in and Create an account (and when GitHub sign-in ends), the sign-in sheet (codes, recovery codes, an account never finished, a lost authenticator), the account's setup (the recovery phrase shown once, three random words typed back before anything reaches the instance; the authenticator's QR code, drawn with pub.dev's qr; the recovery codes), getting back in, and the account section (`AccountSecuritySection`). - `FileSaver` moves from Learn into halkyon_app, for the phrase and the codes in Todo too; `AppDependencies` names it and `passwords`. - halkyon_keys rebuilds account keys from the secret keys a device kept; the key proof's message is in halkyon_base_client, checked against the register's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-20 15:42:45 +02:00
import 'dart:convert';
import 'package:crypto/crypto.dart';
feat(app): sign in with a password and TOTP on the device ADR 0017, step 3: halkyon_app's side, which Learn and Todo take. - The account's cryptography on the device (`SodiumAccountCrypto`): halkyon_keys and SRP's client half, each step on an isolate of its own. sodium hands out results as views of native memory its finalizers free once the isolate that made them ends, so everything crossing back is copied first: a view read freed memory and crashed the process. - `PasswordGateway` on the module's endpoints (steps without a session through a client holding none; an authenticator set up with a session granted but not yet the device's), and `PasswordRepository` running the flows: signing in (M2 checked, the keys opened before anything is trusted), signing up, a password for someone signed in, an account never finished, the authenticators, recovery codes, changing the password, a new recovery phrase, and getting back in. - The unlocked keys in the system's keyring per instance and person (`SecureAccountKeyStore`), gone once another person signs in or nobody, and with the instance's removal. - The screens: the start screen's Sign in and Create an account (and when GitHub sign-in ends), the sign-in sheet (codes, recovery codes, an account never finished, a lost authenticator), the account's setup (the recovery phrase shown once, three random words typed back before anything reaches the instance; the authenticator's QR code, drawn with pub.dev's qr; the recovery codes), getting back in, and the account section (`AccountSecuritySection`). - `FileSaver` moves from Learn into halkyon_app, for the phrase and the codes in Todo too; `AppDependencies` names it and `passwords`. - halkyon_keys rebuilds account keys from the secret keys a device kept; the key proof's message is in halkyon_base_client, checked against the register's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-20 15:42:45 +02:00
import 'package:halkyon_base_client/srp.dart';
import 'package:test/test.dart';
/// [parts], each after its length in four bytes, big-endian.
List<int> _framed(List<List<int>> parts) => [
for (final part in parts) ...[
(part.length >> 24) & 0xff,
(part.length >> 16) & 0xff,
(part.length >> 8) & 0xff,
part.length & 0xff,
...part,
],
];
feat(app): sign in with a password and TOTP on the device ADR 0017, step 3: halkyon_app's side, which Learn and Todo take. - The account's cryptography on the device (`SodiumAccountCrypto`): halkyon_keys and SRP's client half, each step on an isolate of its own. sodium hands out results as views of native memory its finalizers free once the isolate that made them ends, so everything crossing back is copied first: a view read freed memory and crashed the process. - `PasswordGateway` on the module's endpoints (steps without a session through a client holding none; an authenticator set up with a session granted but not yet the device's), and `PasswordRepository` running the flows: signing in (M2 checked, the keys opened before anything is trusted), signing up, a password for someone signed in, an account never finished, the authenticators, recovery codes, changing the password, a new recovery phrase, and getting back in. - The unlocked keys in the system's keyring per instance and person (`SecureAccountKeyStore`), gone once another person signs in or nobody, and with the instance's removal. - The screens: the start screen's Sign in and Create an account (and when GitHub sign-in ends), the sign-in sheet (codes, recovery codes, an account never finished, a lost authenticator), the account's setup (the recovery phrase shown once, three random words typed back before anything reaches the instance; the authenticator's QR code, drawn with pub.dev's qr; the recovery codes), getting back in, and the account section (`AccountSecuritySection`). - `FileSaver` moves from Learn into halkyon_app, for the phrase and the codes in Todo too; `AppDependencies` names it and `passwords`. - halkyon_keys rebuilds account keys from the secret keys a device kept; the key proof's message is in halkyon_base_client, checked against the register's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-20 15:42:45 +02:00
void main() {
final challenge = List.generate(16, (i) => i);
final nonce = List.generate(32, (i) => 200 + i % 50);
test("SRP's identity is the label, the challenge, the instance's base URL "
'the device addresses, what the proof is for and the digest of what it '
'sets, each framed by its length', () {
final digest = List.generate(32, (i) => i * 5);
expect(
passwordIdentity(
challenge,
'https://learn.halkyon.eu/',
purpose: PasswordProofPurpose.newPassword,
digest: digest,
),
_framed([
utf8.encode('halkyon srp identity v2'),
challenge,
utf8.encode('https://learn.halkyon.eu/'),
utf8.encode('new password'),
digest,
]),
);
expect(
passwordIdentity(challenge, 'https://learn.halkyon.eu/'),
_framed([
utf8.encode('halkyon srp identity v2'),
challenge,
utf8.encode('https://learn.halkyon.eu/'),
utf8.encode('sign in'),
<int>[],
]),
);
});
test('every purpose has a label of its own', () {
final labels = {
for (final purpose in PasswordProofPurpose.values) purpose.label,
};
expect(labels, hasLength(PasswordProofPurpose.values.length));
});
test('an identity differs for another instance, purpose or digest', () {
final identity = passwordIdentity(
challenge,
'https://learn.halkyon.eu/',
purpose: PasswordProofPurpose.newPassword,
digest: [1],
);
for (final other in [
passwordIdentity(
challenge,
'https://learn.halkyon.eu:8443/',
purpose: PasswordProofPurpose.newPassword,
digest: [1],
),
passwordIdentity(
challenge,
'https://learn.halkyon.eu/other/',
purpose: PasswordProofPurpose.newPassword,
digest: [1],
),
passwordIdentity(
challenge,
'https://learn.halkyon.eu/',
purpose: PasswordProofPurpose.newPhrase,
digest: [1],
),
passwordIdentity(
challenge,
'https://learn.halkyon.eu/',
purpose: PasswordProofPurpose.newPassword,
digest: [2],
),
]) {
expect(other, isNot(identity));
}
});
group("an instance's base URL", () {
for (final (typed, canonical) in [
('https://learn.halkyon.eu/', 'https://learn.halkyon.eu/'),
('https://Learn.Halkyon.EU', 'https://learn.halkyon.eu/'),
('HTTPS://learn.halkyon.eu:443/', 'https://learn.halkyon.eu/'),
('http://localhost:80/', 'http://localhost/'),
('http://localhost:8080', 'http://localhost:8080/'),
('http://localhost:0/', 'http://localhost:0/'),
('https://example.org:8443/learn', 'https://example.org:8443/learn/'),
('https://example.org/learn/?q=1#top', 'https://example.org/learn/'),
('http://[::1]:8080/', 'http://[::1]:8080/'),
(' https://learn.halkyon.eu/ ', 'https://learn.halkyon.eu/'),
]) {
test('$typed is $canonical', () {
expect(canonicalInstanceUrl(typed), canonical);
});
}
for (final typed in [
'learn.halkyon.eu',
'ftp://learn.halkyon.eu/',
'https:///',
'https://someone@learn.halkyon.eu/',
]) {
test('$typed is none', () {
expect(() => canonicalInstanceUrl(typed), throwsFormatException);
});
}
});
test('the key proof message is the label, the challenge, the nonce, the '
"instance's base URL, what it proves the phrase for and what that "
'sets, framed, as the register checks it', () {
final digest = List.generate(32, (i) => i * 3);
expect(
keyProofMessage(
challenge,
nonce,
instanceUrl: 'https://learn.halkyon.eu/',
purpose: KeyProofPurpose.passwordReset,
digest: digest,
),
_framed([
utf8.encode('halkyon key proof v3'),
challenge,
nonce,
utf8.encode('https://learn.halkyon.eu/'),
utf8.encode('password reset'),
digest,
]),
);
expect(
keyProofMessage(
challenge,
nonce,
instanceUrl: 'https://learn.halkyon.eu/',
purpose: KeyProofPurpose.authenticatorReset,
),
_framed([
utf8.encode('halkyon key proof v3'),
challenge,
nonce,
utf8.encode('https://learn.halkyon.eu/'),
utf8.encode('authenticator reset'),
<int>[],
]),
);
});
feat(app): sign in with a password and TOTP on the device ADR 0017, step 3: halkyon_app's side, which Learn and Todo take. - The account's cryptography on the device (`SodiumAccountCrypto`): halkyon_keys and SRP's client half, each step on an isolate of its own. sodium hands out results as views of native memory its finalizers free once the isolate that made them ends, so everything crossing back is copied first: a view read freed memory and crashed the process. - `PasswordGateway` on the module's endpoints (steps without a session through a client holding none; an authenticator set up with a session granted but not yet the device's), and `PasswordRepository` running the flows: signing in (M2 checked, the keys opened before anything is trusted), signing up, a password for someone signed in, an account never finished, the authenticators, recovery codes, changing the password, a new recovery phrase, and getting back in. - The unlocked keys in the system's keyring per instance and person (`SecureAccountKeyStore`), gone once another person signs in or nobody, and with the instance's removal. - The screens: the start screen's Sign in and Create an account (and when GitHub sign-in ends), the sign-in sheet (codes, recovery codes, an account never finished, a lost authenticator), the account's setup (the recovery phrase shown once, three random words typed back before anything reaches the instance; the authenticator's QR code, drawn with pub.dev's qr; the recovery codes), getting back in, and the account section (`AccountSecuritySection`). - `FileSaver` moves from Learn into halkyon_app, for the phrase and the codes in Todo too; `AppDependencies` names it and `passwords`. - halkyon_keys rebuilds account keys from the secret keys a device kept; the key proof's message is in halkyon_base_client, checked against the register's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-20 15:42:45 +02:00
test("a new password's digest is SHA-256 over its parts, framed: the "
'salt, the costs, the verifier and the wrapping', () {
final digest = newPasswordDigest(
salt: [1, 2],
algorithm: 'argon2id13',
opsLimit: 3,
memLimit: 67108864,
verifier: [3, 4, 5],
keysNonce: [6],
keysCipherText: [7, 8],
);
feat(app): sign in with a password and TOTP on the device ADR 0017, step 3: halkyon_app's side, which Learn and Todo take. - The account's cryptography on the device (`SodiumAccountCrypto`): halkyon_keys and SRP's client half, each step on an isolate of its own. sodium hands out results as views of native memory its finalizers free once the isolate that made them ends, so everything crossing back is copied first: a view read freed memory and crashed the process. - `PasswordGateway` on the module's endpoints (steps without a session through a client holding none; an authenticator set up with a session granted but not yet the device's), and `PasswordRepository` running the flows: signing in (M2 checked, the keys opened before anything is trusted), signing up, a password for someone signed in, an account never finished, the authenticators, recovery codes, changing the password, a new recovery phrase, and getting back in. - The unlocked keys in the system's keyring per instance and person (`SecureAccountKeyStore`), gone once another person signs in or nobody, and with the instance's removal. - The screens: the start screen's Sign in and Create an account (and when GitHub sign-in ends), the sign-in sheet (codes, recovery codes, an account never finished, a lost authenticator), the account's setup (the recovery phrase shown once, three random words typed back before anything reaches the instance; the authenticator's QR code, drawn with pub.dev's qr; the recovery codes), getting back in, and the account section (`AccountSecuritySection`). - `FileSaver` moves from Learn into halkyon_app, for the phrase and the codes in Todo too; `AppDependencies` names it and `passwords`. - halkyon_keys rebuilds account keys from the secret keys a device kept; the key proof's message is in halkyon_base_client, checked against the register's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-20 15:42:45 +02:00
expect(
digest,
sha256
.convert(
_framed([
utf8.encode('halkyon new password v1'),
[1, 2],
utf8.encode('argon2id13'),
utf8.encode('3'),
utf8.encode('67108864'),
[3, 4, 5],
[6],
[7, 8],
]),
)
.bytes,
);
expect(
newPasswordDigest(
salt: [1, 2],
algorithm: 'argon2id13',
opsLimit: 3,
memLimit: 67108864,
verifier: [3, 4, 5],
keysNonce: [6],
keysCipherText: [7, 9],
),
isNot(digest),
);
feat(app): sign in with a password and TOTP on the device ADR 0017, step 3: halkyon_app's side, which Learn and Todo take. - The account's cryptography on the device (`SodiumAccountCrypto`): halkyon_keys and SRP's client half, each step on an isolate of its own. sodium hands out results as views of native memory its finalizers free once the isolate that made them ends, so everything crossing back is copied first: a view read freed memory and crashed the process. - `PasswordGateway` on the module's endpoints (steps without a session through a client holding none; an authenticator set up with a session granted but not yet the device's), and `PasswordRepository` running the flows: signing in (M2 checked, the keys opened before anything is trusted), signing up, a password for someone signed in, an account never finished, the authenticators, recovery codes, changing the password, a new recovery phrase, and getting back in. - The unlocked keys in the system's keyring per instance and person (`SecureAccountKeyStore`), gone once another person signs in or nobody, and with the instance's removal. - The screens: the start screen's Sign in and Create an account (and when GitHub sign-in ends), the sign-in sheet (codes, recovery codes, an account never finished, a lost authenticator), the account's setup (the recovery phrase shown once, three random words typed back before anything reaches the instance; the authenticator's QR code, drawn with pub.dev's qr; the recovery codes), getting back in, and the account section (`AccountSecuritySection`). - `FileSaver` moves from Learn into halkyon_app, for the phrase and the codes in Todo too; `AppDependencies` names it and `passwords`. - halkyon_keys rebuilds account keys from the secret keys a device kept; the key proof's message is in halkyon_base_client, checked against the register's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-20 15:42:45 +02:00
});
test("a new phrase's digest is SHA-256 over the keys' wrapping under it "
'and its proof key, framed', () {
final digest = newPhraseDigest(
keysNonce: [1, 2],
keysCipherText: [3, 4, 5],
proofKey: [6, 7],
);
expect(
digest,
sha256
.convert(
_framed([
utf8.encode('halkyon new phrase v1'),
[1, 2],
[3, 4, 5],
[6, 7],
]),
)
.bytes,
);
expect(
newPhraseDigest(
keysNonce: [1, 2],
keysCipherText: [3, 4, 5],
proofKey: [6, 8],
),
isNot(digest),
);
});
test("an authenticator's digest is SHA-256 over its ID, framed", () {
final id = List.generate(16, (i) => 100 + i);
expect(
authenticatorDigest(id),
sha256
.convert(_framed([utf8.encode('halkyon authenticator v1'), id]))
.bytes,
);
expect(
authenticatorDigest([...id.take(15), 0]),
isNot(authenticatorDigest(id)),
);
});
feat(app): sign in with a password and TOTP on the device ADR 0017, step 3: halkyon_app's side, which Learn and Todo take. - The account's cryptography on the device (`SodiumAccountCrypto`): halkyon_keys and SRP's client half, each step on an isolate of its own. sodium hands out results as views of native memory its finalizers free once the isolate that made them ends, so everything crossing back is copied first: a view read freed memory and crashed the process. - `PasswordGateway` on the module's endpoints (steps without a session through a client holding none; an authenticator set up with a session granted but not yet the device's), and `PasswordRepository` running the flows: signing in (M2 checked, the keys opened before anything is trusted), signing up, a password for someone signed in, an account never finished, the authenticators, recovery codes, changing the password, a new recovery phrase, and getting back in. - The unlocked keys in the system's keyring per instance and person (`SecureAccountKeyStore`), gone once another person signs in or nobody, and with the instance's removal. - The screens: the start screen's Sign in and Create an account (and when GitHub sign-in ends), the sign-in sheet (codes, recovery codes, an account never finished, a lost authenticator), the account's setup (the recovery phrase shown once, three random words typed back before anything reaches the instance; the authenticator's QR code, drawn with pub.dev's qr; the recovery codes), getting back in, and the account section (`AccountSecuritySection`). - `FileSaver` moves from Learn into halkyon_app, for the phrase and the codes in Todo too; `AppDependencies` names it and `passwords`. - halkyon_keys rebuilds account keys from the secret keys a device kept; the key proof's message is in halkyon_base_client, checked against the register's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-20 15:42:45 +02:00
}