feat(base): SimpleLogin accounts' keys without SRP, signing up in one call (ADR 0020, step 3)

An account that signs in with SimpleLogin keeps its keys under a password
that signs nobody in (D19): `account_password` keeps the password's own
proof key (Ed25519, `passwordProofKey`) in place of SRP's verifier, which
becomes nullable. The migration adds the column and drops the NOT NULL,
nothing else.

- `simpleLogin.signUp(attempt, handle, setup)`: someone new whose identity
  waits under the attempt gets, in one transaction, an account
  (identified), the link, a profile with the handle and the keys; then a
  session (D18). Refused, the identity still waiting, for a setup out of
  range and a handle refused as saving a profile refuses it.
- `simpleLogin.checkHandle(attempt, handle)`: whether someone new could
  take a handle, at most 30 an hour per attempt and 60 per network.
- `simpleLogin.complete` says, for an account, whether it has its keys and
  its handle, and keeps the sign-in ten minutes under its attempt, the
  account as its caller: D20's confirmation for the changes to come.
- `keys.begin`, `keys.unlock`: the keys for a signed-in device once the
  password is proven with its proof key, for a nonce, bound to the base
  URL the device addressed; wrong proofs counted per session as ADR
  0017's.
- An authenticator is enrolled only for a password that signs in.
- Members ask the register for all of it; `RemoteRegister` and the
  instance API carry the four new operations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-23 21:49:44 +02:00
commit 090d901e92
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -89,6 +89,40 @@ void main() {
);
});
test("has the devices' purposes, each with a label of its own, and "
'signs the bytes they sign for each', () {
final challenge = UuidValue.fromString(
'01994a3c-7b2e-7c00-8a1b-0123456789ab',
);
final nonce = List.generate(32, (i) => i * 3 % 256);
final labels = KeyProofPurpose.values.map((purpose) => purpose.label);
expect(
labels,
device.KeyProofPurpose.values.map((purpose) => purpose.label),
);
expect(labels.toSet(), hasLength(KeyProofPurpose.values.length));
for (final (index, purpose) in KeyProofPurpose.values.indexed) {
expect(
keyProofMessage(
challenge,
nonce,
instanceUrl: 'https://learn.halkyon.eu/',
purpose: purpose,
digest: [index],
),
device.keyProofMessage(
challenge.toBytes(),
nonce,
instanceUrl: 'https://learn.halkyon.eu/',
purpose: device.KeyProofPurpose.values[index],
digest: [index],
),
reason: purpose.label,
);
}
});
test("binds SRP's identity as devices do, for every purpose, with the "
'digest of what it sets', () {
final challenge = UuidValue.fromString(