fix(auth)!: proofs are bound to the instance's whole base URL
SRP's identity and the recovery phrase's proof carried the host alone, so instances on one host under another port, path or scheme could relay each other's proofs (the full reading's device low 4). Both now carry the base URL the device addresses, written alike on both sides (canonicalInstanceUrl: scheme and host in lower case, the port only where it isn't the scheme's own, the path ending in /); the register takes its own public scheme, host and port (Serverpod serves at the root), a member the URL it names, development anything. The identity also gains a purpose and the digest of what a proof sets, both empty so far (signing in), with the framing for a new phrase's and an authenticator's digest beside the new password's: the owner's proofs bind them next. Cross-tests hold both sides to the same bytes. OwnerProof's and DeletionProof's host, and every endpoint's, become instanceUrl. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
8ec2743bfa
commit
1ad5d84110
9 changed files with 912 additions and 448 deletions
|
|
@ -117,7 +117,7 @@ void main() {
|
|||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
totpCode: totpCode ?? codeOf(enrolment, at: now),
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
);
|
||||
|
|
@ -340,7 +340,7 @@ void main() {
|
|||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
totpCode: '000000',
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
),
|
||||
|
|
@ -392,7 +392,7 @@ void main() {
|
|||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
totpCode: codeOf(enrolment, at: now),
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
);
|
||||
|
|
@ -705,7 +705,7 @@ void main() {
|
|||
signature: signature,
|
||||
password: newPassword,
|
||||
totpCode: codeOf(enrolment, at: now),
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
),
|
||||
|
|
@ -822,13 +822,13 @@ void main() {
|
|||
/// Deletes Anna's account from her phone, with the password's proof
|
||||
/// (hers by default, by [privateValue] otherwise, none [withoutProof])
|
||||
/// and [totpCode] (her authenticator's now by default), bound to
|
||||
/// [host].
|
||||
/// [instanceUrl].
|
||||
Future<void> delete({
|
||||
BigInt? privateValue,
|
||||
String? totpCode,
|
||||
bool withoutCode = false,
|
||||
bool withoutProof = false,
|
||||
String host = testHost,
|
||||
String instanceUrl = testInstanceUrl,
|
||||
}) => atNow(() async {
|
||||
if (withoutProof) {
|
||||
return await endpoints.account.delete(onPhone());
|
||||
|
|
@ -837,7 +837,7 @@ void main() {
|
|||
final answer = device.answer(
|
||||
challenge,
|
||||
privateValue: privateValue,
|
||||
host: host,
|
||||
instanceUrl: instanceUrl,
|
||||
);
|
||||
await endpoints.account.delete(
|
||||
onPhone(),
|
||||
|
|
@ -848,7 +848,7 @@ void main() {
|
|||
totpCode: withoutCode
|
||||
? null
|
||||
: totpCode ?? codeOf(enrolment, at: now),
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
|
@ -874,7 +874,7 @@ void main() {
|
|||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
await expectLater(
|
||||
delete(host: 'learn.evil.example'),
|
||||
delete(instanceUrl: 'https://learn.evil.example/'),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
expect(await AuthUser.db.findById(session, anna), isNotNull);
|
||||
|
|
@ -928,7 +928,7 @@ void main() {
|
|||
totpCode: totpCode,
|
||||
request: mailed?.request,
|
||||
code: mailed?.code,
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
|
@ -977,7 +977,7 @@ void main() {
|
|||
language: 'en',
|
||||
request: code.request,
|
||||
code: code.code,
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
);
|
||||
});
|
||||
// Before it takes effect, the second factor works: no code
|
||||
|
|
@ -1005,7 +1005,7 @@ void main() {
|
|||
clientProof: answer.clientProof,
|
||||
request: mailed.request,
|
||||
code: mailed.code,
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
|
@ -1083,7 +1083,7 @@ void main() {
|
|||
signature: signature,
|
||||
password: newPassword,
|
||||
totpCode: codeOf(enrolment, at: now),
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
),
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ void main() {
|
|||
clientProof: answer.clientProof,
|
||||
totpCode: totpCode,
|
||||
recoveryCode: recoveryCode,
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: address,
|
||||
language: 'en',
|
||||
);
|
||||
|
|
@ -623,7 +623,7 @@ void main() {
|
|||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
totpCode: codeOf(complete.enrolment, at: now),
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
),
|
||||
|
|
@ -764,7 +764,7 @@ void main() {
|
|||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
totpCode: codeOf(complete.enrolment, at: now),
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
),
|
||||
|
|
@ -793,7 +793,7 @@ void main() {
|
|||
clientValue: lateAnswer.clientValue,
|
||||
clientProof: lateAnswer.clientProof,
|
||||
totpCode: codeOf(complete.enrolment, at: now),
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
),
|
||||
|
|
@ -807,7 +807,7 @@ void main() {
|
|||
challenge: const Uuid().v7obj(),
|
||||
clientValue: lateAnswer.clientValue,
|
||||
clientProof: lateAnswer.clientProof,
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
),
|
||||
|
|
@ -835,7 +835,7 @@ void main() {
|
|||
),
|
||||
clientProof: ByteData(32),
|
||||
totpCode: codeOf(complete.enrolment, at: now),
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
),
|
||||
|
|
@ -845,14 +845,43 @@ void main() {
|
|||
}
|
||||
});
|
||||
|
||||
test('refuses a proof made for another host, as an instance the '
|
||||
'device was lured into adding would relay it, alike; and tells a '
|
||||
'device naming another host that the instance has another '
|
||||
'name', () async {
|
||||
test('refuses a proof made for another instance (another host, '
|
||||
'port, path or scheme), as an instance the device was lured into '
|
||||
'adding would relay it, alike; and tells a device naming another '
|
||||
'that the instance has another address', () async {
|
||||
final complete = await annaComplete();
|
||||
|
||||
// The device proved its password to learn.evil.example, which
|
||||
// passes A and M1 on as they are.
|
||||
// The device proved its password to another instance, which
|
||||
// passes A and M1 on as they are: on another host, or beside this
|
||||
// one on the same host.
|
||||
for (final other in [
|
||||
'https://learn.evil.example/',
|
||||
'http://localhost:8081/',
|
||||
'http://localhost:0/evil/',
|
||||
'https://localhost:0/',
|
||||
]) {
|
||||
await expectLater(
|
||||
atNow(() async {
|
||||
final challenge = await endpoints.passwordSignIn.begin(
|
||||
notSignedIn(),
|
||||
address: 'anna@example.org',
|
||||
);
|
||||
final answer = device.answer(challenge, instanceUrl: other);
|
||||
return await endpoints.passwordSignIn.complete(
|
||||
notSignedIn(),
|
||||
challenge: challenge.challenge,
|
||||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
totpCode: codeOf(complete.enrolment, at: now),
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
);
|
||||
}),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
reason: other,
|
||||
);
|
||||
}
|
||||
await expectLater(
|
||||
atNow(() async {
|
||||
final challenge = await endpoints.passwordSignIn.begin(
|
||||
|
|
@ -861,7 +890,7 @@ void main() {
|
|||
);
|
||||
final answer = device.answer(
|
||||
challenge,
|
||||
host: 'learn.evil.example',
|
||||
instanceUrl: 'https://learn.evil.example/',
|
||||
);
|
||||
return await endpoints.passwordSignIn.complete(
|
||||
notSignedIn(),
|
||||
|
|
@ -869,30 +898,7 @@ void main() {
|
|||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
totpCode: codeOf(complete.enrolment, at: now),
|
||||
host: testHost,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
);
|
||||
}),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
);
|
||||
await expectLater(
|
||||
atNow(() async {
|
||||
final challenge = await endpoints.passwordSignIn.begin(
|
||||
notSignedIn(),
|
||||
address: 'anna@example.org',
|
||||
);
|
||||
final answer = device.answer(
|
||||
challenge,
|
||||
host: 'learn.evil.example',
|
||||
);
|
||||
return await endpoints.passwordSignIn.complete(
|
||||
notSignedIn(),
|
||||
challenge: challenge.challenge,
|
||||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
totpCode: codeOf(complete.enrolment, at: now),
|
||||
host: 'learn.evil.example',
|
||||
instanceUrl: 'https://learn.evil.example/',
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
);
|
||||
|
|
@ -975,7 +981,7 @@ void main() {
|
|||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
totpCode: '000000',
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: address,
|
||||
language: 'en',
|
||||
),
|
||||
|
|
@ -1026,7 +1032,7 @@ void main() {
|
|||
clientProof: answer.clientProof,
|
||||
totpCode: recoveryCode == null ? totpCode : null,
|
||||
recoveryCode: recoveryCode,
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'Anna@example.org',
|
||||
language: 'de',
|
||||
);
|
||||
|
|
@ -1122,7 +1128,7 @@ void main() {
|
|||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
totpCode: '${100000 + i}',
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
);
|
||||
|
|
@ -1165,7 +1171,7 @@ void main() {
|
|||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
totpCode: codeOf(complete.enrolment, at: now),
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: address,
|
||||
language: 'en',
|
||||
);
|
||||
|
|
@ -1208,7 +1214,7 @@ void main() {
|
|||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
totpCode: '000000',
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
);
|
||||
|
|
@ -1474,7 +1480,7 @@ void main() {
|
|||
language: 'en',
|
||||
request: code.request,
|
||||
code: code.code,
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
);
|
||||
});
|
||||
now = now.add(resetDelay);
|
||||
|
|
@ -2156,7 +2162,7 @@ void main() {
|
|||
language: 'de',
|
||||
request: code.request,
|
||||
code: code.code,
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -2331,7 +2337,7 @@ void main() {
|
|||
language: 'en',
|
||||
keyChallenge: keyChallenge.challenge,
|
||||
signature: await device.sign(keyChallenge),
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -2392,7 +2398,7 @@ void main() {
|
|||
keyChallenge,
|
||||
keys: await otherSigningKeys(),
|
||||
),
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
);
|
||||
}),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
|
|
@ -2411,7 +2417,7 @@ void main() {
|
|||
clientProof: answer.clientProof,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
);
|
||||
}),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
|
|
@ -2449,7 +2455,7 @@ void main() {
|
|||
language: 'en',
|
||||
request: code.request,
|
||||
code: code.code,
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
),
|
||||
),
|
||||
refusedAsEmail(EmailSignInRefusal.mailFailed),
|
||||
|
|
@ -2485,7 +2491,7 @@ void main() {
|
|||
),
|
||||
password: newPassword,
|
||||
totpCode: totpCode,
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: address,
|
||||
language: 'en',
|
||||
);
|
||||
|
|
@ -2629,7 +2635,7 @@ void main() {
|
|||
challenge: challenge.challenge,
|
||||
signature: signature,
|
||||
password: newPassword,
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
),
|
||||
|
|
@ -2644,7 +2650,7 @@ void main() {
|
|||
signature: signature,
|
||||
password: newPassword,
|
||||
totpCode: codeOf(complete.enrolment, at: now),
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
),
|
||||
|
|
@ -2666,7 +2672,7 @@ void main() {
|
|||
signature: signature,
|
||||
password: newPassword,
|
||||
totpCode: codeOf(complete.enrolment, at: now),
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
),
|
||||
|
|
@ -2719,7 +2725,7 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test('takes no proof made for another host, for an authenticator '
|
||||
test('takes no proof made for another instance, for an authenticator '
|
||||
'reset, or for another password than the one sent: whoever '
|
||||
'stands between swaps in nothing', () async {
|
||||
final complete = await annaComplete();
|
||||
|
|
@ -2738,7 +2744,7 @@ void main() {
|
|||
signature: await sign(challenge, newPassword),
|
||||
password: newPassword,
|
||||
totpCode: codeOf(complete.enrolment, at: now),
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
);
|
||||
|
|
@ -2749,7 +2755,7 @@ void main() {
|
|||
(challenge, password) => device.sign(
|
||||
challenge,
|
||||
password: password,
|
||||
host: 'learn.evil.example',
|
||||
instanceUrl: 'https://learn.evil.example/',
|
||||
),
|
||||
),
|
||||
refused(PasswordRefusal.wrongCredentials),
|
||||
|
|
@ -2803,7 +2809,7 @@ void main() {
|
|||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
totpCode: '000000',
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
),
|
||||
|
|
@ -2843,7 +2849,7 @@ void main() {
|
|||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
totpCode: '000000',
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
),
|
||||
|
|
@ -2904,7 +2910,7 @@ void main() {
|
|||
signature: signature,
|
||||
password: newPassword,
|
||||
totpCode: codeOf(complete.enrolment, at: now),
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
),
|
||||
|
|
@ -3104,7 +3110,7 @@ void main() {
|
|||
language: 'en',
|
||||
request: code.request,
|
||||
code: code.code,
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
);
|
||||
});
|
||||
// An hour on, whoever reads her mail asks for the account's reset.
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ void main() {
|
|||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
totpCode: totp,
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
);
|
||||
|
|
@ -162,7 +162,7 @@ void main() {
|
|||
language: 'en',
|
||||
request: code.request,
|
||||
code: code.code,
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -226,7 +226,7 @@ void main() {
|
|||
password: newPassword,
|
||||
totpCode: totp,
|
||||
recoveryCode: recovery,
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ void main() {
|
|||
totpCode: totpCode,
|
||||
request: mailed?.request,
|
||||
code: wrongMailedCode ?? mailed?.code,
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: address,
|
||||
language: 'en',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -60,8 +60,8 @@ final class _Register implements AccountRegister {
|
|||
/// Thrown by every call while set: the register refusing, or down.
|
||||
Exception? failing;
|
||||
|
||||
/// The hosts sign-ins were bound to, as members named them.
|
||||
final hosts = <String>[];
|
||||
/// The base URLs sign-ins were bound to, as members named them.
|
||||
final instanceUrls = <String>[];
|
||||
|
||||
/// The accounts warned that a session was ended for its wrong proofs, and
|
||||
/// those whose warning was dismissed.
|
||||
|
|
@ -294,11 +294,11 @@ final class _Register implements AccountRegister {
|
|||
String? recoveryCode,
|
||||
UuidValue? request,
|
||||
String? code,
|
||||
required String host,
|
||||
required String instanceUrl,
|
||||
String? address,
|
||||
String? language,
|
||||
}) {
|
||||
hosts.add(host);
|
||||
instanceUrls.add(instanceUrl);
|
||||
return _answer('completePasswordSignIn', _proven);
|
||||
}
|
||||
|
||||
|
|
@ -374,7 +374,7 @@ final class _Register implements AccountRegister {
|
|||
String? code,
|
||||
UuidValue? keyChallenge,
|
||||
ByteData? signature,
|
||||
required String host,
|
||||
required String instanceUrl,
|
||||
}) => _answer(
|
||||
'resetAuthenticator',
|
||||
() => ResetPending(takesEffectAt: DateTime.utc(2026, 10, 23)),
|
||||
|
|
@ -407,7 +407,7 @@ final class _Register implements AccountRegister {
|
|||
required NewPassword password,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
required String host,
|
||||
required String instanceUrl,
|
||||
String? address,
|
||||
String? language,
|
||||
}) => _answer(
|
||||
|
|
@ -711,7 +711,7 @@ void main() {
|
|||
clientValue: ByteData(384),
|
||||
clientProof: ByteData(32),
|
||||
totpCode: '123456',
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
);
|
||||
|
|
@ -721,8 +721,8 @@ void main() {
|
|||
'completePasswordSignIn',
|
||||
]);
|
||||
expect(register.networks, hasLength(1));
|
||||
// Bound to the host the device addressed: this member's.
|
||||
expect(register.hosts, [testHost]);
|
||||
// Bound to the base URL the device addressed: this member's.
|
||||
expect(register.instanceUrls, [testInstanceUrl]);
|
||||
expect(signedIn.authUserId, carla);
|
||||
expect(await scopesOf(carla), {identified.name});
|
||||
expect(
|
||||
|
|
@ -760,7 +760,7 @@ void main() {
|
|||
challenge: challenge.challenge,
|
||||
clientValue: ByteData(384),
|
||||
clientProof: ByteData(32),
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
);
|
||||
|
|
@ -789,7 +789,7 @@ void main() {
|
|||
challenge: const Uuid().v7obj(),
|
||||
clientValue: ByteData(384),
|
||||
clientProof: ByteData(32),
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
totpCode: '123456',
|
||||
),
|
||||
),
|
||||
|
|
@ -837,7 +837,7 @@ void main() {
|
|||
challenge: const Uuid().v7obj(),
|
||||
clientValue: ByteData(384),
|
||||
clientProof: ByteData(32),
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
),
|
||||
|
|
@ -1005,7 +1005,7 @@ void main() {
|
|||
});
|
||||
|
||||
test("passes the password's proof on to the register, bound to "
|
||||
'the host the device addressed here', () async {
|
||||
'the base URL the device addressed here', () async {
|
||||
await endpoints.account.delete(
|
||||
asBen(),
|
||||
proof: DeletionProof(
|
||||
|
|
@ -1013,12 +1013,12 @@ void main() {
|
|||
clientValue: ByteData(384),
|
||||
clientProof: ByteData(32),
|
||||
totpCode: '123456',
|
||||
host: 'LOCALHOST',
|
||||
instanceUrl: 'HTTP://LOCALHOST:0',
|
||||
),
|
||||
);
|
||||
|
||||
expect(register.deleted, [ben]);
|
||||
expect(register.deletedWith.single?.host, testHost);
|
||||
expect(register.deletedWith.single?.instanceUrl, testInstanceUrl);
|
||||
expect(register.deletedWith.single?.totpCode, '123456');
|
||||
});
|
||||
|
||||
|
|
@ -1217,7 +1217,7 @@ void main() {
|
|||
challenge: challenge.challenge,
|
||||
clientValue: ByteData(384),
|
||||
clientProof: ByteData(32),
|
||||
host: 'LOCALHOST',
|
||||
instanceUrl: 'HTTP://LOCALHOST:0',
|
||||
totpCode: '123456',
|
||||
),
|
||||
password: NewPassword(
|
||||
|
|
@ -1233,8 +1233,8 @@ void main() {
|
|||
|
||||
expect(register.asked, ['beginPasswordChange', 'changePassword']);
|
||||
expect(register.changed, {ben: bensLaptop});
|
||||
// Bound to the host the device addressed here.
|
||||
expect(register.changedWith.single.host, testHost);
|
||||
// Bound to the base URL the device addressed here.
|
||||
expect(register.changedWith.single.instanceUrl, testInstanceUrl);
|
||||
expect(register.changedWith.single.totpCode, '123456');
|
||||
});
|
||||
|
||||
|
|
@ -1250,7 +1250,7 @@ void main() {
|
|||
challenge: const Uuid().v7obj(),
|
||||
clientValue: ByteData(384),
|
||||
clientProof: ByteData(32),
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
totpCode: '000000',
|
||||
),
|
||||
);
|
||||
|
|
@ -1300,7 +1300,7 @@ void main() {
|
|||
challenge: const Uuid().v7obj(),
|
||||
clientValue: ByteData(384),
|
||||
clientProof: ByteData(32),
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
totpCode: '000000',
|
||||
),
|
||||
),
|
||||
|
|
@ -1332,7 +1332,7 @@ void main() {
|
|||
clientValue: ByteData(384),
|
||||
clientProof: ByteData(32),
|
||||
totpCode: '123456',
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
),
|
||||
);
|
||||
expect(register.asked, contains('callOffResets'));
|
||||
|
|
@ -1346,7 +1346,7 @@ void main() {
|
|||
challenge: const Uuid().v7obj(),
|
||||
clientValue: ByteData(384),
|
||||
clientProof: ByteData(32),
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
),
|
||||
),
|
||||
throwsA(isA<ServerpodUnauthenticatedException>()),
|
||||
|
|
@ -1401,7 +1401,7 @@ void main() {
|
|||
),
|
||||
),
|
||||
totpCode: '123456',
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
);
|
||||
|
|
@ -1453,7 +1453,7 @@ void main() {
|
|||
),
|
||||
),
|
||||
totpCode: '123456',
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
),
|
||||
|
|
@ -1513,7 +1513,7 @@ void main() {
|
|||
),
|
||||
),
|
||||
totpCode: '123456',
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
),
|
||||
|
|
|
|||
|
|
@ -605,7 +605,7 @@ void main() {
|
|||
clientValue: answer.clientValue,
|
||||
clientProof: answer.clientProof,
|
||||
totpCode: codeOf(enrolment, at: now),
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
);
|
||||
|
|
@ -648,7 +648,7 @@ void main() {
|
|||
clientValue: againAnswer.clientValue,
|
||||
clientProof: againAnswer.clientProof,
|
||||
totpCode: codeOf(enrolment, at: now),
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
),
|
||||
|
|
@ -686,7 +686,7 @@ void main() {
|
|||
),
|
||||
password: newPassword,
|
||||
totpCode: codeOf(enrolment, at: now),
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'anna@example.org',
|
||||
language: 'en',
|
||||
),
|
||||
|
|
@ -718,7 +718,7 @@ void main() {
|
|||
language: 'en',
|
||||
request: resetRequest,
|
||||
code: mailbox.codeFor('anna@example.org'),
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
);
|
||||
expect(pending.takesEffectAt, now.add(resetDelay));
|
||||
|
||||
|
|
@ -987,7 +987,7 @@ void main() {
|
|||
clientProof: answer.clientProof,
|
||||
request: await code(EmailCodePurpose.finishSetup),
|
||||
code: mailbox.codeFor('carla@example.org'),
|
||||
host: testHost,
|
||||
instanceUrl: testInstanceUrl,
|
||||
address: 'carla@example.org',
|
||||
language: 'en',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import 'package:halkyon_base_client/srp.dart' as srp;
|
|||
import 'package:halkyon_base_server/base.dart';
|
||||
import 'package:halkyon_base_server/src/srp/srp_math.dart';
|
||||
|
||||
/// The host devices address the tests' server by: its `apiServer.publicHost`
|
||||
/// (config/test.yaml), which SRP's identity and the phrase's proofs are
|
||||
/// bound to.
|
||||
const testHost = 'localhost';
|
||||
/// The base URL devices address the tests' server by: its `apiServer`'s
|
||||
/// public scheme, host and port (config/test.yaml), which SRP's identity
|
||||
/// and the phrase's proofs are bound to.
|
||||
const testInstanceUrl = 'http://localhost:0/';
|
||||
|
||||
/// The instance keys the tests' register holds (ADR 0017).
|
||||
const testPasswordKeys = (
|
||||
|
|
@ -124,9 +124,10 @@ final class PasswordDevice {
|
|||
);
|
||||
}
|
||||
|
||||
/// The device's answer to [challenge] (A and M1) on [host], with its
|
||||
/// password or the [privateValue] of another; and whether a register's M2
|
||||
/// is the one that answer expects.
|
||||
/// The device's answer to [challenge] (A and M1) on [instanceUrl], for
|
||||
/// [purpose] with the [digest] of what it sets, with its password or the
|
||||
/// [privateValue] of another; and whether a register's M2 is the one that
|
||||
/// answer expects.
|
||||
({
|
||||
ByteData clientValue,
|
||||
ByteData clientProof,
|
||||
|
|
@ -135,11 +136,18 @@ final class PasswordDevice {
|
|||
answer(
|
||||
PasswordChallenge challenge, {
|
||||
BigInt? privateValue,
|
||||
String host = testHost,
|
||||
String instanceUrl = testInstanceUrl,
|
||||
srp.PasswordProofPurpose purpose = srp.PasswordProofPurpose.signIn,
|
||||
List<int> digest = const [],
|
||||
}) {
|
||||
final client = srp.SrpClient(_clientGroup, random: _random);
|
||||
final step = client.respond(
|
||||
identity: srp.passwordIdentity(challenge.challenge.toBytes(), host),
|
||||
identity: srp.passwordIdentity(
|
||||
challenge.challenge.toBytes(),
|
||||
instanceUrl,
|
||||
purpose: purpose,
|
||||
digest: digest,
|
||||
),
|
||||
salt: Uint8List.sublistView(challenge.salt),
|
||||
privateValue: privateValue ?? this.privateValue,
|
||||
serverValue: numberOf(Uint8List.sublistView(challenge.serverValue)),
|
||||
|
|
@ -163,33 +171,38 @@ final class PasswordDevice {
|
|||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
BigInt? privateValue,
|
||||
String host = testHost,
|
||||
String instanceUrl = testInstanceUrl,
|
||||
}) {
|
||||
final reply = answer(challenge, privateValue: privateValue, host: host);
|
||||
final reply = answer(
|
||||
challenge,
|
||||
privateValue: privateValue,
|
||||
instanceUrl: instanceUrl,
|
||||
);
|
||||
return OwnerProof(
|
||||
challenge: challenge.challenge,
|
||||
clientValue: reply.clientValue,
|
||||
clientProof: reply.clientProof,
|
||||
host: host,
|
||||
instanceUrl: instanceUrl,
|
||||
totpCode: totpCode,
|
||||
recoveryCode: recoveryCode,
|
||||
);
|
||||
}
|
||||
|
||||
/// The signature proving the recovery phrase for [challenge] on [host]:
|
||||
/// The signature proving the recovery phrase for [challenge] on
|
||||
/// [instanceUrl]:
|
||||
/// for the new [password] it sets, or for an authenticator's reset
|
||||
/// without one; by the phrase's proof key, or [keys]' where given.
|
||||
Future<ByteData> sign(
|
||||
KeyProofChallenge challenge, {
|
||||
SimpleKeyPair? keys,
|
||||
NewPassword? password,
|
||||
String host = testHost,
|
||||
String instanceUrl = testInstanceUrl,
|
||||
}) async {
|
||||
final signature = await Ed25519().sign(
|
||||
keyProofMessage(
|
||||
challenge.challenge,
|
||||
Uint8List.sublistView(challenge.nonce),
|
||||
host: host,
|
||||
instanceUrl: instanceUrl,
|
||||
purpose: password == null
|
||||
? KeyProofPurpose.authenticatorReset
|
||||
: KeyProofPurpose.passwordReset,
|
||||
|
|
|
|||
|
|
@ -586,7 +586,7 @@ class _AccountRecoveryEndpoint {
|
|||
String? code,
|
||||
_is.UuidValue? keyChallenge,
|
||||
_idt.ByteData? signature,
|
||||
required String host,
|
||||
required String instanceUrl,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
|
|
@ -609,7 +609,7 @@ class _AccountRecoveryEndpoint {
|
|||
'code': code,
|
||||
'keyChallenge': keyChallenge,
|
||||
'signature': signature,
|
||||
'host': host,
|
||||
'instanceUrl': instanceUrl,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
|
|
@ -664,7 +664,7 @@ class _AccountRecoveryEndpoint {
|
|||
required _ijye8mcv.NewPassword password,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
required String host,
|
||||
required String instanceUrl,
|
||||
required String address,
|
||||
required String language,
|
||||
}) async {
|
||||
|
|
@ -685,7 +685,7 @@ class _AccountRecoveryEndpoint {
|
|||
'password': password,
|
||||
'totpCode': totpCode,
|
||||
'recoveryCode': recoveryCode,
|
||||
'host': host,
|
||||
'instanceUrl': instanceUrl,
|
||||
'address': address,
|
||||
'language': language,
|
||||
}),
|
||||
|
|
@ -1223,7 +1223,7 @@ class _PasswordSignInEndpoint {
|
|||
String? recoveryCode,
|
||||
_is.UuidValue? request,
|
||||
String? code,
|
||||
required String host,
|
||||
required String instanceUrl,
|
||||
required String address,
|
||||
required String language,
|
||||
}) async {
|
||||
|
|
@ -1246,7 +1246,7 @@ class _PasswordSignInEndpoint {
|
|||
'recoveryCode': recoveryCode,
|
||||
'request': request,
|
||||
'code': code,
|
||||
'host': host,
|
||||
'instanceUrl': instanceUrl,
|
||||
'address': address,
|
||||
'language': language,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ Uint8List _hex(String hex) => Uint8List.fromList([
|
|||
Uint8List _message(UuidValue challenge, List<int> nonce) => keyProofMessage(
|
||||
challenge,
|
||||
nonce,
|
||||
host: 'learn.halkyon.eu',
|
||||
instanceUrl: 'https://learn.halkyon.eu/',
|
||||
purpose: KeyProofPurpose.authenticatorReset,
|
||||
);
|
||||
|
||||
|
|
@ -61,14 +61,14 @@ void main() {
|
|||
keyProofMessage(
|
||||
challenge,
|
||||
nonce,
|
||||
host: 'Learn.Halkyon.EU',
|
||||
instanceUrl: 'HTTPS://Learn.Halkyon.EU:443',
|
||||
purpose: KeyProofPurpose.passwordReset,
|
||||
digest: digest,
|
||||
),
|
||||
device.keyProofMessage(
|
||||
challenge.toBytes(),
|
||||
nonce,
|
||||
host: 'learn.halkyon.eu',
|
||||
instanceUrl: 'https://learn.halkyon.eu/',
|
||||
purpose: device.KeyProofPurpose.passwordReset,
|
||||
digest: digest,
|
||||
),
|
||||
|
|
@ -77,59 +77,146 @@ void main() {
|
|||
keyProofMessage(
|
||||
challenge,
|
||||
nonce,
|
||||
host: 'learn.halkyon.eu',
|
||||
instanceUrl: 'https://learn.halkyon.eu/',
|
||||
purpose: KeyProofPurpose.authenticatorReset,
|
||||
),
|
||||
device.keyProofMessage(
|
||||
challenge.toBytes(),
|
||||
nonce,
|
||||
host: 'learn.halkyon.eu',
|
||||
instanceUrl: 'https://learn.halkyon.eu/',
|
||||
purpose: device.KeyProofPurpose.authenticatorReset,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("binds SRP's identity as devices do, for every purpose, with the "
|
||||
'digest of what it sets', () {
|
||||
final challenge = UuidValue.fromString(
|
||||
'01994a3c-7b2e-7c00-8a1b-0123456789ab',
|
||||
);
|
||||
final digest = List.generate(32, (i) => 255 - i);
|
||||
|
||||
expect(
|
||||
passwordIdentity(challenge, 'learn.halkyon.eu'),
|
||||
device.passwordIdentity(challenge.toBytes(), 'LEARN.halkyon.eu'),
|
||||
PasswordProofPurpose.values.map((purpose) => purpose.label),
|
||||
device.PasswordProofPurpose.values.map((purpose) => purpose.label),
|
||||
);
|
||||
for (final purpose in PasswordProofPurpose.values) {
|
||||
expect(
|
||||
passwordIdentity(
|
||||
challenge,
|
||||
'https://learn.halkyon.eu',
|
||||
purpose: purpose,
|
||||
digest: digest,
|
||||
),
|
||||
device.passwordIdentity(
|
||||
challenge.toBytes(),
|
||||
'https://LEARN.halkyon.eu/',
|
||||
purpose: device.PasswordProofPurpose.values.byName(purpose.name),
|
||||
digest: digest,
|
||||
),
|
||||
reason: purpose.name,
|
||||
);
|
||||
}
|
||||
expect(
|
||||
passwordIdentity(challenge, 'https://learn.halkyon.eu/'),
|
||||
device.passwordIdentity(
|
||||
challenge.toBytes(),
|
||||
'https://learn.halkyon.eu/',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("takes the host a device names only where it is the instance's "
|
||||
'own, but in development', () {
|
||||
test("digests a new phrase and an authenticator's ID as devices do", () {
|
||||
final keys = WrappedKeys(
|
||||
nonce: ByteData.sublistView(Uint8List.fromList([1, 2])),
|
||||
cipherText: ByteData.sublistView(Uint8List.fromList([3, 4, 5])),
|
||||
);
|
||||
final proofKey = ByteData.sublistView(Uint8List.fromList([6, 7]));
|
||||
final id = UuidValue.fromString('01994a3c-7b2e-7c00-8a1b-0123456789ab');
|
||||
|
||||
expect(
|
||||
deviceHost(
|
||||
runMode: ServerpodRunMode.production,
|
||||
publicHost: 'learn.halkyon.eu',
|
||||
claimed: ' Learn.Halkyon.eu ',
|
||||
newPhraseDigest(keys, proofKey),
|
||||
device.newPhraseDigest(
|
||||
keysNonce: [1, 2],
|
||||
keysCipherText: [3, 4, 5],
|
||||
proofKey: [6, 7],
|
||||
),
|
||||
'learn.halkyon.eu',
|
||||
);
|
||||
expect(authenticatorDigest(id), device.authenticatorDigest(id.toBytes()));
|
||||
});
|
||||
|
||||
test('writes base URLs as devices do', () {
|
||||
for (final url in [
|
||||
'https://learn.halkyon.eu/',
|
||||
'HTTPS://Learn.Halkyon.EU:443',
|
||||
'http://localhost:0',
|
||||
'http://localhost:80/',
|
||||
'https://example.org:8443/learn?x#y',
|
||||
'http://[::1]:8080/',
|
||||
]) {
|
||||
expect(
|
||||
canonicalInstanceUrl(url),
|
||||
device.canonicalInstanceUrl(url),
|
||||
reason: url,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("takes the base URL a device names only where it is the instance's "
|
||||
'own, written any way, but in development', () {
|
||||
expect(
|
||||
deviceInstanceUrl(
|
||||
runMode: ServerpodRunMode.production,
|
||||
publicUrl: 'https://learn.halkyon.eu:443/',
|
||||
claimed: ' HTTPS://Learn.Halkyon.eu ',
|
||||
),
|
||||
'https://learn.halkyon.eu/',
|
||||
);
|
||||
for (final runMode in [
|
||||
ServerpodRunMode.production,
|
||||
ServerpodRunMode.staging,
|
||||
ServerpodRunMode.test,
|
||||
]) {
|
||||
expect(
|
||||
() => deviceHost(
|
||||
runMode: runMode,
|
||||
publicHost: 'learn.halkyon.eu',
|
||||
claimed: 'learn.evil.example',
|
||||
),
|
||||
throwsA(
|
||||
isA<PasswordRefusedException>().having(
|
||||
(refused) => refused.reason,
|
||||
'reason',
|
||||
PasswordRefusal.otherInstance,
|
||||
for (final claimed in [
|
||||
'https://learn.evil.example/',
|
||||
'https://learn.halkyon.eu:8443/',
|
||||
'https://learn.halkyon.eu/evil/',
|
||||
'http://learn.halkyon.eu/',
|
||||
'learn.halkyon.eu',
|
||||
'not a url at all',
|
||||
]) {
|
||||
expect(
|
||||
() => deviceInstanceUrl(
|
||||
runMode: runMode,
|
||||
publicUrl: 'https://learn.halkyon.eu:443/',
|
||||
claimed: claimed,
|
||||
),
|
||||
),
|
||||
);
|
||||
throwsA(
|
||||
isA<PasswordRefusedException>().having(
|
||||
(refused) => refused.reason,
|
||||
'reason',
|
||||
PasswordRefusal.otherInstance,
|
||||
),
|
||||
),
|
||||
reason: '$runMode $claimed',
|
||||
);
|
||||
}
|
||||
}
|
||||
expect(
|
||||
deviceHost(
|
||||
deviceInstanceUrl(
|
||||
runMode: ServerpodRunMode.development,
|
||||
publicHost: 'localhost',
|
||||
publicUrl: 'http://localhost:8080/',
|
||||
claimed: 'http://10.0.2.2:8080',
|
||||
),
|
||||
'http://10.0.2.2:8080/',
|
||||
);
|
||||
expect(
|
||||
() => deviceInstanceUrl(
|
||||
runMode: ServerpodRunMode.development,
|
||||
publicUrl: 'http://localhost:8080/',
|
||||
claimed: '10.0.2.2',
|
||||
),
|
||||
'10.0.2.2',
|
||||
throwsA(isA<PasswordRefusedException>()),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue