fix(auth)!: proofs are bound to the instance the device addresses

Nothing tied a sign-in's proof to the instance it was made for: an
instance a person was lured into adding could pass A, M1 and the code on
to the real register and get a session there, and pass a phrase's proof
on with a password of its own in place of the person's (the second
reading's M6). SRP's identity is now a label, the challenge's ID and
the host the device addressed (passwordIdentity, in halkyon_base_client
and halkyon_base_server alike), and the phrase's proof signs that host,
what it is for (an authenticator's reset or a new password) and the new
password's digest (key proof v2). Every proving call carries the host;
in production it must be the server's apiServer.publicHost (otherwise
otherInstance, which the app shows as an instance it can't trust), in
development any; a member forwards its own. The tests prove that a
relayed sign-in, a proof for another host or purpose, and a swapped
password are refused, nothing changing.

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-20 21:20:12 +02:00
commit dab2c80524
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -236,6 +236,7 @@ Future<Map<String, Object?>?> _answer(
recoveryCode: arguments.optionalString('recoveryCode'),
request: arguments.optionalId('request'),
code: arguments.optionalString('code'),
host: arguments.string('host'),
);
return {'proof': proof.toJson()};
case RegisterOperation.signUpWithCode:
@ -300,6 +301,7 @@ Future<Map<String, Object?>?> _answer(
code: arguments.optionalString('code'),
keyChallenge: arguments.optionalId('keyChallenge'),
signature: arguments.optionalBytes('signature'),
host: arguments.string('host'),
);
return {'pending': pending.toJson()};
case RegisterOperation.beginKeyProof:
@ -317,6 +319,7 @@ Future<Map<String, Object?>?> _answer(
password: arguments.model('password', NewPassword.fromJson),
totpCode: arguments.optionalString('totpCode'),
recoveryCode: arguments.optionalString('recoveryCode'),
host: arguments.string('host'),
);
return _sessionsEnded(ended);
case RegisterOperation.startAccountReset:
@ -354,6 +357,7 @@ Future<Map<String, Object?>?> _answer(
recoveryCode: arguments.optionalString('recoveryCode'),
password: arguments.model('password', NewPassword.fromJson),
keepSession: arguments.optionalId('keepSession'),
host: arguments.string('host'),
);
return const {};
case RegisterOperation.setRecoveryKeys:
@ -367,6 +371,7 @@ Future<Map<String, Object?>?> _answer(
recoveryCode: arguments.optionalString('recoveryCode'),
recoveryKeys: arguments.model('recoveryKeys', WrappedKeys.fromJson),
recoveryProofKey: arguments.bytes('recoveryProofKey'),
host: arguments.string('host'),
);
return const {};
}