feat(crypto): SRP-6a, the register's half and the device's
ADR 0017's step 1. Generic over group and hash (SrpGroup); production takes RFC 5054's 3072-bit group with SHA-256. k = H(N | PAD(g)), u = H(PAD(A) | PAD(B)), K = H(PAD(S)), M1 = H(H(N) xor H(g) | H(I) | s | PAD(A) | PAD(B) | K), M2 = H(PAD(A) | M1 | K). A and B must lie in 1..N-1 and u must not be 0; a and b are 256 bits from Random.secure(); proofs are compared in constant time. x comes from the caller (halkyon_keys makes it); RFC 5054's own x is there for the vectors only. - The register's half in halkyon_base_server (base.dart: srpChallenge, srpVerify), the device's written by hand in halkyon_base_client (srp.dart: SrpClient, srpVerifier). Serverpod's generator cleans and writes only a client's lib/src/protocol/ (serverpod_cli 4.0.4's _getDirectoriesRequiringCleaning; a generate leaves srp/ untouched). - Tests: RFC 5054 Appendix B on both halves; the client against the register, succeeding and every refusal. - The module's tests depend on its client, and pub resolves every member's dev dependencies, so Learn's server image takes the module's client into its workspace too. - The format check covers the hand-written half. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
ae0d88818b
commit
bc499713b8
1 changed files with 1369 additions and 27 deletions
|
|
@ -101,6 +101,32 @@ the same ID. Sessions stay each service's own: every app signs in itself.
|
|||
- **The register being down:** members can't sign anyone in, change profiles or delete
|
||||
accounts; their open sessions go on working.
|
||||
|
||||
## SRP-6a (ADR 0017)
|
||||
|
||||
The password never reaches the register: the device proves it knows it with SRP-6a
|
||||
(RFC 5054), and the register keeps only the verifier v = g^x mod N, which `halkyon_keys`'
|
||||
x makes on the device (Argon2id, so a stolen verifier costs an Argon2id per guess). The
|
||||
two halves share the arithmetic, generic over the group and the hash (`SrpGroup`):
|
||||
|
||||
- the register's in `package:halkyon_base_server/base.dart`: `srpChallenge(group,
|
||||
verifier)` gives `SrpChallenge` (B for the client; b kept for this sign-in only), and
|
||||
`srpVerify(group, identity:, salt:, verifier:, challenge:, clientValue:, clientProof:)`
|
||||
gives `SrpAccepted` (M2 and K) or `SrpRefused` (`invalidClientValue`, `wrongProof`);
|
||||
- the device's in `package:halkyon_base_client/srp.dart`, written by hand beside the
|
||||
generated client (the generator writes only `lib/src/protocol/`): `SrpClient(group)`
|
||||
with its `publicValue` A, `respond(identity:, salt:, privateValue:, serverValue:)`
|
||||
giving `SrpClientProof` (M1, K, `verifiesServer(M2)`) or `SrpClientRefused`, and
|
||||
`srpVerifier(group, x)` for sign-up.
|
||||
|
||||
Production uses `SrpGroup.rfc5054Group3072` (RFC 5054's 3072-bit group, g = 5) with
|
||||
SHA-256. k = H(N | PAD(g)), u = H(PAD(A) | PAD(B)), K = H(PAD(S)),
|
||||
M1 = H(H(N) xor H(g) | H(I) | s | PAD(A) | PAD(B) | K), M2 = H(PAD(A) | M1 | K), numbers as
|
||||
unsigned big-endian bytes, PAD to N's length. A and B must lie in 1..N−1 (so neither is
|
||||
≡ 0 mod N) and u must not be 0; a and b are 256 bits from `Random.secure()`; proofs are
|
||||
compared in constant time. RFC 5054 Appendix B's vectors check both halves
|
||||
(`SrpGroup.rfc5054TestVectorGroup`, SHA-1), and `test/unit/srp/srp_interop_test.dart` runs
|
||||
the client against the register, every refusal included.
|
||||
|
||||
## Taking it into a host
|
||||
|
||||
1. Depend on both packages by path, within the repository's workspace (an app's
|
||||
|
|
|
|||
Loading…
Reference in a new issue