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
28 changed files with 912 additions and 448 deletions
|
|
@ -28,7 +28,16 @@ export 'src/password/authenticators.dart'
|
|||
maxAuthenticators,
|
||||
unconfirmedAuthenticatorLifetime;
|
||||
export 'src/password/key_proof.dart'
|
||||
show KeyProofPurpose, keyProofMessage, newPasswordDigest, passwordIdentity;
|
||||
show
|
||||
KeyProofPurpose,
|
||||
PasswordProofPurpose,
|
||||
authenticatorDigest,
|
||||
canonicalInstanceUrl,
|
||||
keyProofMessage,
|
||||
newPasswordDigest,
|
||||
newPhraseDigest,
|
||||
passwordIdentity,
|
||||
publicInstanceUrlOf;
|
||||
export 'src/password/password_config.dart';
|
||||
export 'src/password/passwords.dart'
|
||||
show
|
||||
|
|
|
|||
|
|
@ -21,11 +21,13 @@ class AccountEndpoint extends Endpoint {
|
|||
/// data at once; an anonymous person is this service's alone. An account
|
||||
/// with a password deletes itself only with [proof] (the password's proof
|
||||
/// and a code, `requireOwnerToDelete`), which the register checks, its
|
||||
/// host the one the device addressed here.
|
||||
/// base URL the one the device addressed here.
|
||||
Future<void> delete(Session session, {DeletionProof? proof}) async {
|
||||
final pod = session.serverpod;
|
||||
final person = session.authenticated!.authUserId;
|
||||
final bound = proof?.copyWith(host: deviceHostOn(pod, proof.host));
|
||||
final bound = proof?.copyWith(
|
||||
instanceUrl: deviceInstanceUrlOn(pod, proof.instanceUrl),
|
||||
);
|
||||
// The proof counts for this session (`asOwnerProofTry`), as every
|
||||
// change of how the account is secured does.
|
||||
await asOwnerProofTry(session, bound, () async {
|
||||
|
|
|
|||
|
|
@ -360,8 +360,8 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
type: _is.getType<_idt.ByteData?>(),
|
||||
nullable: true,
|
||||
),
|
||||
'host': _is.ParameterDescription(
|
||||
name: 'host',
|
||||
'instanceUrl': _is.ParameterDescription(
|
||||
name: 'instanceUrl',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
|
|
@ -384,7 +384,7 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
code: params['code'],
|
||||
keyChallenge: params['keyChallenge'],
|
||||
signature: params['signature'],
|
||||
host: params['host'],
|
||||
instanceUrl: params['instanceUrl'],
|
||||
),
|
||||
),
|
||||
'beginKeyProof': _is.MethodConnector(
|
||||
|
|
@ -436,8 +436,8 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
type: _is.getType<String?>(),
|
||||
nullable: true,
|
||||
),
|
||||
'host': _is.ParameterDescription(
|
||||
name: 'host',
|
||||
'instanceUrl': _is.ParameterDescription(
|
||||
name: 'instanceUrl',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
|
|
@ -466,7 +466,7 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
password: params['password'],
|
||||
totpCode: params['totpCode'],
|
||||
recoveryCode: params['recoveryCode'],
|
||||
host: params['host'],
|
||||
instanceUrl: params['instanceUrl'],
|
||||
address: params['address'],
|
||||
language: params['language'],
|
||||
),
|
||||
|
|
@ -872,8 +872,8 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
type: _is.getType<String?>(),
|
||||
nullable: true,
|
||||
),
|
||||
'host': _is.ParameterDescription(
|
||||
name: 'host',
|
||||
'instanceUrl': _is.ParameterDescription(
|
||||
name: 'instanceUrl',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
|
|
@ -904,7 +904,7 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
recoveryCode: params['recoveryCode'],
|
||||
request: params['request'],
|
||||
code: params['code'],
|
||||
host: params['host'],
|
||||
instanceUrl: params['instanceUrl'],
|
||||
address: params['address'],
|
||||
language: params['language'],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import 'package:serverpod/serverpod.dart' as _is;
|
|||
|
||||
/// What deleting an account that has a password takes (ADR 0017; the second
|
||||
/// reading): the password's SRP proof for a challenge `password.beginChange`
|
||||
/// gave, a TOTP or recovery code, and the host the device addressed, as a
|
||||
/// gave, a TOTP or recovery code, and the base URL the device addressed, as a
|
||||
/// change of password does (`OwnerProof`). A session alone deletes only an
|
||||
/// account without one.
|
||||
abstract class DeletionProof
|
||||
|
|
@ -28,7 +28,7 @@ abstract class DeletionProof
|
|||
this.recoveryCode,
|
||||
this.request,
|
||||
this.code,
|
||||
required this.host,
|
||||
required this.instanceUrl,
|
||||
});
|
||||
|
||||
factory DeletionProof({
|
||||
|
|
@ -39,7 +39,7 @@ abstract class DeletionProof
|
|||
String? recoveryCode,
|
||||
_is.UuidValue? request,
|
||||
String? code,
|
||||
required String host,
|
||||
required String instanceUrl,
|
||||
}) = _DeletionProofImpl;
|
||||
|
||||
factory DeletionProof.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
|
|
@ -59,7 +59,7 @@ abstract class DeletionProof
|
|||
? null
|
||||
: _is.UuidValueJsonExtension.fromJson(jsonSerialization['request']),
|
||||
code: jsonSerialization['code'] as String?,
|
||||
host: jsonSerialization['host'] as String,
|
||||
instanceUrl: jsonSerialization['instanceUrl'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -81,9 +81,10 @@ abstract class DeletionProof
|
|||
|
||||
String? code;
|
||||
|
||||
/// The host the device addressed the instance by: the server it reached
|
||||
/// checks it is its own before the register binds the proof to it.
|
||||
String host;
|
||||
/// The instance's base URL the device addressed (scheme, host, port,
|
||||
/// path): the server it reached checks it is its own before the
|
||||
/// register binds the proof to it.
|
||||
String instanceUrl;
|
||||
|
||||
/// Returns a shallow copy of this [DeletionProof]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
|
|
@ -96,7 +97,7 @@ abstract class DeletionProof
|
|||
String? recoveryCode,
|
||||
_is.UuidValue? request,
|
||||
String? code,
|
||||
String? host,
|
||||
String? instanceUrl,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
|
|
@ -109,7 +110,7 @@ abstract class DeletionProof
|
|||
if (recoveryCode != null) 'recoveryCode': recoveryCode,
|
||||
if (request != null) 'request': request?.toJson(),
|
||||
if (code != null) 'code': code,
|
||||
'host': host,
|
||||
'instanceUrl': instanceUrl,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -124,7 +125,7 @@ abstract class DeletionProof
|
|||
if (recoveryCode != null) 'recoveryCode': recoveryCode,
|
||||
if (request != null) 'request': request?.toJson(),
|
||||
if (code != null) 'code': code,
|
||||
'host': host,
|
||||
'instanceUrl': instanceUrl,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -145,7 +146,7 @@ class _DeletionProofImpl extends DeletionProof {
|
|||
String? recoveryCode,
|
||||
_is.UuidValue? request,
|
||||
String? code,
|
||||
required String host,
|
||||
required String instanceUrl,
|
||||
}) : super._(
|
||||
challenge: challenge,
|
||||
clientValue: clientValue,
|
||||
|
|
@ -154,7 +155,7 @@ class _DeletionProofImpl extends DeletionProof {
|
|||
recoveryCode: recoveryCode,
|
||||
request: request,
|
||||
code: code,
|
||||
host: host,
|
||||
instanceUrl: instanceUrl,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [DeletionProof]
|
||||
|
|
@ -169,7 +170,7 @@ class _DeletionProofImpl extends DeletionProof {
|
|||
Object? recoveryCode = _Undefined,
|
||||
Object? request = _Undefined,
|
||||
Object? code = _Undefined,
|
||||
String? host,
|
||||
String? instanceUrl,
|
||||
}) {
|
||||
return DeletionProof(
|
||||
challenge: challenge ?? this.challenge,
|
||||
|
|
@ -179,7 +180,7 @@ class _DeletionProofImpl extends DeletionProof {
|
|||
recoveryCode: recoveryCode is String? ? recoveryCode : this.recoveryCode,
|
||||
request: request is _is.UuidValue? ? request : this.request,
|
||||
code: code is String? ? code : this.code,
|
||||
host: host ?? this.host,
|
||||
instanceUrl: instanceUrl ?? this.instanceUrl,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ import 'package:serverpod/serverpod.dart' as _is;
|
|||
|
||||
/// What a signed-in owner proves to change how their account is secured
|
||||
/// (ADR 0017; the third reading, as Proton has it): the password's SRP
|
||||
/// proof for a challenge `password.beginChange` gave, bound to the host the
|
||||
/// device addressed, and a TOTP code from one of the account's confirmed
|
||||
/// proof for a challenge `password.beginChange` gave, bound to the base URL
|
||||
/// the device addressed, and a TOTP code from one of the account's confirmed
|
||||
/// authenticators or one of its recovery codes. Adding an authenticator once
|
||||
/// one is confirmed, removing a confirmed one, new recovery codes, a new
|
||||
/// recovery phrase and a new password take it: a session alone never gets a
|
||||
|
|
@ -27,7 +27,7 @@ abstract class OwnerProof
|
|||
required this.challenge,
|
||||
required this.clientValue,
|
||||
required this.clientProof,
|
||||
required this.host,
|
||||
required this.instanceUrl,
|
||||
this.totpCode,
|
||||
this.recoveryCode,
|
||||
});
|
||||
|
|
@ -36,7 +36,7 @@ abstract class OwnerProof
|
|||
required _is.UuidValue challenge,
|
||||
required _idt.ByteData clientValue,
|
||||
required _idt.ByteData clientProof,
|
||||
required String host,
|
||||
required String instanceUrl,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
}) = _OwnerProofImpl;
|
||||
|
|
@ -52,7 +52,7 @@ abstract class OwnerProof
|
|||
clientProof: _is.ByteDataJsonExtension.fromJson(
|
||||
jsonSerialization['clientProof'],
|
||||
),
|
||||
host: jsonSerialization['host'] as String,
|
||||
instanceUrl: jsonSerialization['instanceUrl'] as String,
|
||||
totpCode: jsonSerialization['totpCode'] as String?,
|
||||
recoveryCode: jsonSerialization['recoveryCode'] as String?,
|
||||
);
|
||||
|
|
@ -64,9 +64,10 @@ abstract class OwnerProof
|
|||
|
||||
_idt.ByteData clientProof;
|
||||
|
||||
/// The host the device addressed the instance by: the server it reached
|
||||
/// checks it is its own before the register binds the proof to it.
|
||||
String host;
|
||||
/// The instance's base URL the device addressed (scheme, host, port,
|
||||
/// path): the server it reached checks it is its own before the
|
||||
/// register binds the proof to it.
|
||||
String instanceUrl;
|
||||
|
||||
String? totpCode;
|
||||
|
||||
|
|
@ -79,7 +80,7 @@ abstract class OwnerProof
|
|||
_is.UuidValue? challenge,
|
||||
_idt.ByteData? clientValue,
|
||||
_idt.ByteData? clientProof,
|
||||
String? host,
|
||||
String? instanceUrl,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
});
|
||||
|
|
@ -90,7 +91,7 @@ abstract class OwnerProof
|
|||
'challenge': challenge.toJson(),
|
||||
'clientValue': clientValue.toJson(),
|
||||
'clientProof': clientProof.toJson(),
|
||||
'host': host,
|
||||
'instanceUrl': instanceUrl,
|
||||
if (totpCode != null) 'totpCode': totpCode,
|
||||
if (recoveryCode != null) 'recoveryCode': recoveryCode,
|
||||
};
|
||||
|
|
@ -103,7 +104,7 @@ abstract class OwnerProof
|
|||
'challenge': challenge.toJson(),
|
||||
'clientValue': clientValue.toJson(),
|
||||
'clientProof': clientProof.toJson(),
|
||||
'host': host,
|
||||
'instanceUrl': instanceUrl,
|
||||
if (totpCode != null) 'totpCode': totpCode,
|
||||
if (recoveryCode != null) 'recoveryCode': recoveryCode,
|
||||
};
|
||||
|
|
@ -122,14 +123,14 @@ class _OwnerProofImpl extends OwnerProof {
|
|||
required _is.UuidValue challenge,
|
||||
required _idt.ByteData clientValue,
|
||||
required _idt.ByteData clientProof,
|
||||
required String host,
|
||||
required String instanceUrl,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
}) : super._(
|
||||
challenge: challenge,
|
||||
clientValue: clientValue,
|
||||
clientProof: clientProof,
|
||||
host: host,
|
||||
instanceUrl: instanceUrl,
|
||||
totpCode: totpCode,
|
||||
recoveryCode: recoveryCode,
|
||||
);
|
||||
|
|
@ -142,7 +143,7 @@ class _OwnerProofImpl extends OwnerProof {
|
|||
_is.UuidValue? challenge,
|
||||
_idt.ByteData? clientValue,
|
||||
_idt.ByteData? clientProof,
|
||||
String? host,
|
||||
String? instanceUrl,
|
||||
Object? totpCode = _Undefined,
|
||||
Object? recoveryCode = _Undefined,
|
||||
}) {
|
||||
|
|
@ -150,7 +151,7 @@ class _OwnerProofImpl extends OwnerProof {
|
|||
challenge: challenge ?? this.challenge,
|
||||
clientValue: clientValue ?? this.clientValue.clone(),
|
||||
clientProof: clientProof ?? this.clientProof.clone(),
|
||||
host: host ?? this.host,
|
||||
instanceUrl: instanceUrl ?? this.instanceUrl,
|
||||
totpCode: totpCode is String? ? totpCode : this.totpCode,
|
||||
recoveryCode: recoveryCode is String? ? recoveryCode : this.recoveryCode,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ Future<ResetPending> resetAuthenticator(
|
|||
String? code,
|
||||
UuidValue? keyChallenge,
|
||||
ByteData? signature,
|
||||
required String host,
|
||||
required String instanceUrl,
|
||||
}) async {
|
||||
final pod = session.serverpod;
|
||||
final keys = requirePasswordKeys(pod);
|
||||
|
|
@ -62,7 +62,7 @@ Future<ResetPending> resetAuthenticator(
|
|||
challenge: challenge,
|
||||
clientValue: clientValue,
|
||||
clientProof: clientProof,
|
||||
host: host,
|
||||
instanceUrl: instanceUrl,
|
||||
);
|
||||
final to = normalizedAddress(address);
|
||||
if (to == null || addressHash(pod, to) != proved.addressHash) {
|
||||
|
|
@ -85,7 +85,7 @@ Future<ResetPending> resetAuthenticator(
|
|||
keys,
|
||||
challenge: keyChallenge,
|
||||
signature: signature,
|
||||
host: host,
|
||||
instanceUrl: instanceUrl,
|
||||
purpose: KeyProofPurpose.authenticatorReset,
|
||||
);
|
||||
if (owner.authUserId != account) {
|
||||
|
|
@ -127,7 +127,7 @@ Future<AccountSessionsEnded> resetPasswordWithPhrase(
|
|||
required NewPassword password,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
required String host,
|
||||
required String instanceUrl,
|
||||
required String address,
|
||||
required String language,
|
||||
}) async {
|
||||
|
|
@ -140,7 +140,7 @@ Future<AccountSessionsEnded> resetPasswordWithPhrase(
|
|||
keys,
|
||||
challenge: challenge,
|
||||
signature: signature,
|
||||
host: host,
|
||||
instanceUrl: instanceUrl,
|
||||
purpose: KeyProofPurpose.passwordReset,
|
||||
digest: newPasswordDigest(password),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class AccountRecoveryEndpoint extends Endpoint {
|
|||
String? code,
|
||||
UuidValue? keyChallenge,
|
||||
ByteData? signature,
|
||||
required String host,
|
||||
required String instanceUrl,
|
||||
}) => AccountRegister.of(session.serverpod).resetAuthenticator(
|
||||
session,
|
||||
challenge: challenge,
|
||||
|
|
@ -43,7 +43,7 @@ class AccountRecoveryEndpoint extends Endpoint {
|
|||
code: code,
|
||||
keyChallenge: keyChallenge,
|
||||
signature: signature,
|
||||
host: deviceHostOn(session.serverpod, host),
|
||||
instanceUrl: deviceInstanceUrlOn(session.serverpod, instanceUrl),
|
||||
);
|
||||
|
||||
/// The first step of proving the recovery phrase for [address]: the
|
||||
|
|
@ -59,8 +59,8 @@ class AccountRecoveryEndpoint extends Endpoint {
|
|||
/// Sets a new [password] with the recovery phrase's [signature] for
|
||||
/// [challenge] and a [totpCode] or [recoveryCode]; the keys stay. Every
|
||||
/// session of the account ends, in every service of the instance, and
|
||||
/// this one signs in. [host] is the host the device addressed this
|
||||
/// instance by, which the phrase's proof signs; [address] and [language],
|
||||
/// this one signs in. [instanceUrl] is the base URL the device addressed
|
||||
/// this instance by, which the phrase's proof signs; [address] and [language],
|
||||
/// as the device typed them, serve only to tell the account's owner
|
||||
/// should codes come to wait. Refusals as `resetPasswordWithPhrase`'s.
|
||||
Future<AuthSuccess> resetPassword(
|
||||
|
|
@ -70,7 +70,7 @@ class AccountRecoveryEndpoint extends Endpoint {
|
|||
required NewPassword password,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
required String host,
|
||||
required String instanceUrl,
|
||||
required String address,
|
||||
required String language,
|
||||
}) async {
|
||||
|
|
@ -82,7 +82,7 @@ class AccountRecoveryEndpoint extends Endpoint {
|
|||
password: password,
|
||||
totpCode: totpCode,
|
||||
recoveryCode: recoveryCode,
|
||||
host: deviceHostOn(session.serverpod, host),
|
||||
instanceUrl: deviceInstanceUrlOn(session.serverpod, instanceUrl),
|
||||
address: address,
|
||||
language: language,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -97,8 +97,9 @@ class AuthenticatorEndpoint extends Endpoint {
|
|||
);
|
||||
}
|
||||
|
||||
/// [proof] bound to the host the device addressed, checked to be this
|
||||
/// server's own before anything counts (`deviceHostOn`).
|
||||
OwnerProof? _bound(Session session, OwnerProof? proof) =>
|
||||
proof?.copyWith(host: deviceHostOn(session.serverpod, proof.host));
|
||||
/// [proof] bound to the base URL the device addressed, checked to be
|
||||
/// this server's own before anything counts (`deviceInstanceUrlOn`).
|
||||
OwnerProof? _bound(Session session, OwnerProof? proof) => proof?.copyWith(
|
||||
instanceUrl: deviceInstanceUrlOn(session.serverpod, proof.instanceUrl),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
### What deleting an account that has a password takes (ADR 0017; the second
|
||||
### reading): the password's SRP proof for a challenge `password.beginChange`
|
||||
### gave, a TOTP or recovery code, and the host the device addressed, as a
|
||||
### gave, a TOTP or recovery code, and the base URL the device addressed, as a
|
||||
### change of password does (`OwnerProof`). A session alone deletes only an
|
||||
### account without one.
|
||||
class: DeletionProof
|
||||
|
|
@ -16,6 +16,7 @@ fields:
|
|||
### eighth finding).
|
||||
request: UuidValue?
|
||||
code: String?
|
||||
### The host the device addressed the instance by: the server it reached
|
||||
### checks it is its own before the register binds the proof to it.
|
||||
host: String
|
||||
### The instance's base URL the device addressed (scheme, host, port,
|
||||
### path): the server it reached checks it is its own before the
|
||||
### register binds the proof to it.
|
||||
instanceUrl: String
|
||||
|
|
|
|||
|
|
@ -6,24 +6,97 @@ import 'package:cryptography/cryptography.dart';
|
|||
import 'package:halkyon_base_server/src/generated/protocol.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
|
||||
// What a device proves and signs, bound to the instance it addresses (ADR
|
||||
// 0017; the second reading's M6): SRP's identity and the recovery phrase's
|
||||
// proof carry the host the device addressed, so a hostile instance a person
|
||||
// was lured into adding can't relay them to the real one, which takes them
|
||||
// only for its own host; and a phrase's proof for a new password signs that
|
||||
// password, so nobody between can swap in another. The phrase's proof key
|
||||
// is the phrase's own (`recoveryProofKey`), never the account's signing key.
|
||||
// What a device proves and signs, bound to the instance it addresses and
|
||||
// to what the proof is for (ADR 0017; the second reading's M6, the full
|
||||
// reading's device medium 1): SRP's identity and the recovery phrase's
|
||||
// proof carry the instance's base URL the device addressed, so a hostile
|
||||
// instance a person was lured into adding can't relay them to the real
|
||||
// one, which takes them only for its own; the owner's proof carries what it
|
||||
// is for and the digest of what it sets, so nobody between can swap in a
|
||||
// password, a phrase or an authenticator of their own, nor take a proof
|
||||
// made for one change to make another; and a phrase's proof for a new
|
||||
// password signs that password. The phrase's proof key is the phrase's own
|
||||
// (`recoveryProofKey`), never the account's signing key.
|
||||
// halkyon_base_client's `key_proof.dart` makes the same bytes.
|
||||
|
||||
/// SRP's identity I for the sign-in [challenge] on [host] (in lower case):
|
||||
/// x comes from Argon2id over the password and salt alone, so I only binds
|
||||
/// the proof, to this sign-in and this instance.
|
||||
Uint8List passwordIdentity(UuidValue challenge, String host) => _framed([
|
||||
utf8.encode('halkyon srp identity v1'),
|
||||
/// What a proof of the password is made for: the words SRP's identity
|
||||
/// carries. Worked out from the call that brings the proof, never sent.
|
||||
enum PasswordProofPurpose {
|
||||
/// Signing in.
|
||||
signIn('sign in'),
|
||||
|
||||
/// Asking for the reset of a lost authenticator.
|
||||
authenticatorReset('authenticator reset'),
|
||||
|
||||
/// A new password while signed in.
|
||||
newPassword('new password'),
|
||||
|
||||
/// A new recovery phrase while signed in.
|
||||
newPhrase('new phrase'),
|
||||
|
||||
/// New recovery codes in place of the old.
|
||||
newRecoveryCodes('new recovery codes'),
|
||||
|
||||
/// Confirming the account's first authenticator.
|
||||
firstAuthenticator('first authenticator'),
|
||||
|
||||
/// Confirming another authenticator.
|
||||
addAuthenticator('add authenticator'),
|
||||
|
||||
/// Removing a confirmed authenticator.
|
||||
removeAuthenticator('remove authenticator'),
|
||||
|
||||
/// Calling off the account's resets waiting.
|
||||
callOffResets('call off resets'),
|
||||
|
||||
/// Deleting the account.
|
||||
deleteAccount('delete account');
|
||||
|
||||
PasswordProofPurpose(this.label);
|
||||
|
||||
/// The words SRP's identity carries.
|
||||
final String label;
|
||||
}
|
||||
|
||||
/// SRP's identity I for [challenge] on [instanceUrl] (the instance's base
|
||||
/// URL the device addressed, [canonicalInstanceUrl]), for [purpose], with
|
||||
/// the [digest] of what the proof sets: x comes from Argon2id over the
|
||||
/// password and salt alone, so I only binds the proof, to this challenge,
|
||||
/// this instance, this purpose and this change.
|
||||
Uint8List passwordIdentity(
|
||||
UuidValue challenge,
|
||||
String instanceUrl, {
|
||||
PasswordProofPurpose purpose = PasswordProofPurpose.signIn,
|
||||
List<int> digest = const [],
|
||||
}) => _framed([
|
||||
utf8.encode('halkyon srp identity v2'),
|
||||
challenge.toBytes(),
|
||||
utf8.encode(host.toLowerCase()),
|
||||
utf8.encode(canonicalInstanceUrl(instanceUrl)),
|
||||
utf8.encode(purpose.label),
|
||||
digest,
|
||||
]);
|
||||
|
||||
/// [url], an instance's base URL, as proofs bind it: the scheme and host in
|
||||
/// lower case, the port only where it isn't the scheme's own, the path
|
||||
/// ending in `/`, nothing after it. Refused ([FormatException]) for what is
|
||||
/// no HTTP or HTTPS address of a host, or names someone in it.
|
||||
String canonicalInstanceUrl(String url) {
|
||||
final uri = Uri.tryParse(url.trim());
|
||||
final scheme = uri?.scheme.toLowerCase();
|
||||
if (uri == null ||
|
||||
(scheme != 'https' && scheme != 'http') ||
|
||||
uri.host.isEmpty ||
|
||||
uri.userInfo.isNotEmpty) {
|
||||
throw FormatException("Not an instance's base URL", url);
|
||||
}
|
||||
final host = uri.host.toLowerCase();
|
||||
final shownHost = host.contains(':') ? '[$host]' : host;
|
||||
final ownPort = scheme == 'https' ? 443 : 80;
|
||||
final port = uri.hasPort && uri.port != ownPort ? ':${uri.port}' : '';
|
||||
final path = uri.path.endsWith('/') ? uri.path : '${uri.path}/';
|
||||
return '$scheme://$shownHost$port$path';
|
||||
}
|
||||
|
||||
/// What proving the recovery phrase is for.
|
||||
enum KeyProofPurpose {
|
||||
/// Asking for the reset of a lost authenticator.
|
||||
|
|
@ -39,44 +112,57 @@ enum KeyProofPurpose {
|
|||
}
|
||||
|
||||
/// What the device signs with the phrase's proof key: a label naming the
|
||||
/// format, the challenge's ID (its 16 bytes), its nonce, the [host] the
|
||||
/// device addressed, the [purpose] and the [digest] of what it sets
|
||||
/// ([newPasswordDigest]), each framed by its length; so a signature made
|
||||
/// for one challenge, instance, purpose or password never counts for
|
||||
/// another.
|
||||
/// format, the challenge's ID (its 16 bytes), its nonce, the [instanceUrl]
|
||||
/// the device addressed ([canonicalInstanceUrl]), the [purpose] and the
|
||||
/// [digest] of what it sets ([newPasswordDigest]), each framed by its
|
||||
/// length; so a signature made for one challenge, instance, purpose or
|
||||
/// password never counts for another.
|
||||
Uint8List keyProofMessage(
|
||||
UuidValue challenge,
|
||||
List<int> nonce, {
|
||||
required String host,
|
||||
required String instanceUrl,
|
||||
required KeyProofPurpose purpose,
|
||||
List<int> digest = const [],
|
||||
}) => _framed([
|
||||
utf8.encode('halkyon key proof v2'),
|
||||
utf8.encode('halkyon key proof v3'),
|
||||
challenge.toBytes(),
|
||||
nonce,
|
||||
utf8.encode(host.toLowerCase()),
|
||||
utf8.encode(canonicalInstanceUrl(instanceUrl)),
|
||||
utf8.encode(purpose.label),
|
||||
digest,
|
||||
]);
|
||||
|
||||
/// The SHA-256 of [password] as the device sent it, framed: what a
|
||||
/// phrase's proof for it signs.
|
||||
Uint8List newPasswordDigest(NewPassword password) => Uint8List.fromList(
|
||||
hashes.sha256
|
||||
.convert(
|
||||
_framed([
|
||||
utf8.encode('halkyon new password v1'),
|
||||
Uint8List.sublistView(password.salt),
|
||||
utf8.encode(password.costs.algorithm),
|
||||
utf8.encode('${password.costs.opsLimit}'),
|
||||
utf8.encode('${password.costs.memLimit}'),
|
||||
Uint8List.sublistView(password.verifier),
|
||||
Uint8List.sublistView(password.passwordKeys.nonce),
|
||||
Uint8List.sublistView(password.passwordKeys.cipherText),
|
||||
]),
|
||||
)
|
||||
.bytes,
|
||||
);
|
||||
/// The SHA-256 of [password] as the device sent it, framed: what a proof
|
||||
/// for it binds (the owner's, or a phrase's).
|
||||
Uint8List newPasswordDigest(NewPassword password) => _sha256([
|
||||
utf8.encode('halkyon new password v1'),
|
||||
Uint8List.sublistView(password.salt),
|
||||
utf8.encode(password.costs.algorithm),
|
||||
utf8.encode('${password.costs.opsLimit}'),
|
||||
utf8.encode('${password.costs.memLimit}'),
|
||||
Uint8List.sublistView(password.verifier),
|
||||
Uint8List.sublistView(password.passwordKeys.nonce),
|
||||
Uint8List.sublistView(password.passwordKeys.cipherText),
|
||||
]);
|
||||
|
||||
/// The SHA-256 of a new recovery phrase as the device sent it (the keys
|
||||
/// wrapped under it, [recoveryKeys], and its proof key's public half,
|
||||
/// [proofKey]), framed: what the owner's proof for it binds.
|
||||
Uint8List newPhraseDigest(WrappedKeys recoveryKeys, ByteData proofKey) =>
|
||||
_sha256([
|
||||
utf8.encode('halkyon new phrase v1'),
|
||||
Uint8List.sublistView(recoveryKeys.nonce),
|
||||
Uint8List.sublistView(recoveryKeys.cipherText),
|
||||
Uint8List.sublistView(proofKey),
|
||||
]);
|
||||
|
||||
/// The SHA-256 of authenticator [id] (its 16 bytes), framed: what the
|
||||
/// owner's proof for confirming or removing it binds.
|
||||
Uint8List authenticatorDigest(UuidValue id) =>
|
||||
_sha256([utf8.encode('halkyon authenticator v1'), id.toBytes()]);
|
||||
|
||||
Uint8List _sha256(List<List<int>> parts) =>
|
||||
Uint8List.fromList(hashes.sha256.convert(_framed(parts)).bytes);
|
||||
|
||||
/// [parts], each after its length in four bytes, big-endian.
|
||||
Uint8List _framed(List<List<int>> parts) {
|
||||
|
|
@ -92,32 +178,48 @@ Uint8List _framed(List<List<int>> parts) {
|
|||
return bytes.takeBytes();
|
||||
}
|
||||
|
||||
/// The host a device addressed this server by ([claimed]), which its proofs
|
||||
/// are bound to: where the server runs for real, only its own public host
|
||||
/// ([publicHost], `apiServer.publicHost`), refused otherwise
|
||||
/// ([PasswordRefusal.otherInstance]: the instance is configured under
|
||||
/// another name than the device reaches it by); in development whatever
|
||||
/// the device names, as an emulator reaches the developer's machine as
|
||||
/// 10.0.2.2 and a phone on the network by its address. A relaying
|
||||
/// instance gains nothing either way: the device's proof names the host it
|
||||
/// addressed, and the real one checks it for its own.
|
||||
String deviceHost({
|
||||
/// The base URL a device addressed this server by ([claimed]), which its
|
||||
/// proofs are bound to ([canonicalInstanceUrl]): where the server runs for
|
||||
/// real, only its own public one ([publicUrl], `apiServer`'s public scheme,
|
||||
/// host and port; Serverpod serves at the root, so its path is `/`),
|
||||
/// refused otherwise ([PasswordRefusal.otherInstance]: the instance is
|
||||
/// configured under another address than the device reaches it by, or
|
||||
/// the device names another instance); in development whatever the device
|
||||
/// names, as an emulator reaches the developer's machine as 10.0.2.2 and a
|
||||
/// phone on the network by its address. A relaying instance gains nothing
|
||||
/// either way: the device's proof names the instance it addressed, and the
|
||||
/// real one checks it for its own.
|
||||
String deviceInstanceUrl({
|
||||
required String runMode,
|
||||
required String publicHost,
|
||||
required String publicUrl,
|
||||
required String claimed,
|
||||
}) {
|
||||
final host = claimed.trim().toLowerCase();
|
||||
if (runMode != ServerpodRunMode.development &&
|
||||
host != publicHost.trim().toLowerCase()) {
|
||||
final String url;
|
||||
try {
|
||||
url = canonicalInstanceUrl(claimed);
|
||||
} on FormatException {
|
||||
throw PasswordRefusedException(reason: PasswordRefusal.otherInstance);
|
||||
}
|
||||
return host;
|
||||
if (runMode != ServerpodRunMode.development &&
|
||||
url != canonicalInstanceUrl(publicUrl)) {
|
||||
throw PasswordRefusedException(reason: PasswordRefusal.otherInstance);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
/// [deviceHost] on [pod].
|
||||
String deviceHostOn(Serverpod pod, String claimed) => deviceHost(
|
||||
/// [pod]'s own base URL, as devices address it: `apiServer`'s public
|
||||
/// scheme, host and port, at the root.
|
||||
String publicInstanceUrlOf(Serverpod pod) {
|
||||
final api = pod.config.apiServer;
|
||||
return canonicalInstanceUrl(
|
||||
'${api.publicScheme}://${api.publicHost}:${api.publicPort}/',
|
||||
);
|
||||
}
|
||||
|
||||
/// [deviceInstanceUrl] on [pod].
|
||||
String deviceInstanceUrlOn(Serverpod pod, String claimed) => deviceInstanceUrl(
|
||||
runMode: pod.runMode,
|
||||
publicHost: pod.config.apiServer.publicHost,
|
||||
publicUrl: publicInstanceUrlOf(pod),
|
||||
claimed: claimed,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
### What a signed-in owner proves to change how their account is secured
|
||||
### (ADR 0017; the third reading, as Proton has it): the password's SRP
|
||||
### proof for a challenge `password.beginChange` gave, bound to the host the
|
||||
### device addressed, and a TOTP code from one of the account's confirmed
|
||||
### proof for a challenge `password.beginChange` gave, bound to the base URL
|
||||
### the device addressed, and a TOTP code from one of the account's confirmed
|
||||
### authenticators or one of its recovery codes. Adding an authenticator once
|
||||
### one is confirmed, removing a confirmed one, new recovery codes, a new
|
||||
### recovery phrase and a new password take it: a session alone never gets a
|
||||
|
|
@ -11,8 +11,9 @@ fields:
|
|||
challenge: UuidValue
|
||||
clientValue: ByteData
|
||||
clientProof: ByteData
|
||||
### The host the device addressed the instance by: the server it reached
|
||||
### checks it is its own before the register binds the proof to it.
|
||||
host: String
|
||||
### The instance's base URL the device addressed (scheme, host, port,
|
||||
### path): the server it reached checks it is its own before the
|
||||
### register binds the proof to it.
|
||||
instanceUrl: String
|
||||
totpCode: String?
|
||||
recoveryCode: String?
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ Future<void> setRecoveryKeys(
|
|||
}
|
||||
|
||||
/// Proves [person] may delete their account: one with a password only
|
||||
/// with [proof] (the password's proof bound to the host the device
|
||||
/// with [proof] (the password's proof bound to the base URL the device
|
||||
/// addressed and a code, as a change of password takes: a session alone,
|
||||
/// or a phone with its authenticator, would otherwise delete the account and
|
||||
/// everything encrypted under its keys); one without, by its session alone,
|
||||
|
|
@ -157,7 +157,7 @@ Future<void> requireOwnerToDelete(
|
|||
challenge: proof.challenge,
|
||||
clientValue: proof.clientValue,
|
||||
clientProof: proof.clientProof,
|
||||
host: proof.host,
|
||||
instanceUrl: proof.instanceUrl,
|
||||
);
|
||||
final totp = proof.totpCode?.trim() ?? '';
|
||||
final recovery = proof.recoveryCode?.trim() ?? '';
|
||||
|
|
@ -213,7 +213,7 @@ Future<AccountPassword> proveOwnersPassword(
|
|||
challenge: proof.challenge,
|
||||
clientValue: proof.clientValue,
|
||||
clientProof: proof.clientProof,
|
||||
host: proof.host,
|
||||
instanceUrl: proof.instanceUrl,
|
||||
);
|
||||
return proved.password;
|
||||
}
|
||||
|
|
@ -244,7 +244,7 @@ Future<AccountPassword> proveOwner(
|
|||
challenge: proof.challenge,
|
||||
clientValue: proof.clientValue,
|
||||
clientProof: proof.clientProof,
|
||||
host: proof.host,
|
||||
instanceUrl: proof.instanceUrl,
|
||||
);
|
||||
await ownersCode(
|
||||
session,
|
||||
|
|
@ -266,7 +266,7 @@ Future<PasswordProved> _provePassword(
|
|||
required UuidValue challenge,
|
||||
required ByteData clientValue,
|
||||
required ByteData clientProof,
|
||||
required String host,
|
||||
required String instanceUrl,
|
||||
}) async {
|
||||
final proved = await provePassword(
|
||||
session,
|
||||
|
|
@ -274,7 +274,7 @@ Future<PasswordProved> _provePassword(
|
|||
challenge: challenge,
|
||||
clientValue: clientValue,
|
||||
clientProof: clientProof,
|
||||
host: host,
|
||||
instanceUrl: instanceUrl,
|
||||
purpose: SignInChallengePurpose.change,
|
||||
);
|
||||
if (proved.password.authUserId != person) {
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ class PasswordEndpoint extends Endpoint {
|
|||
}) {
|
||||
final authenticated = session.authenticated!;
|
||||
final bound = proof.copyWith(
|
||||
host: deviceHostOn(session.serverpod, proof.host),
|
||||
instanceUrl: deviceInstanceUrlOn(session.serverpod, proof.instanceUrl),
|
||||
);
|
||||
return asOwnerProofTry(
|
||||
session,
|
||||
|
|
@ -123,7 +123,7 @@ class PasswordEndpoint extends Endpoint {
|
|||
required OwnerProof proof,
|
||||
}) {
|
||||
final bound = proof.copyWith(
|
||||
host: deviceHostOn(session.serverpod, proof.host),
|
||||
instanceUrl: deviceInstanceUrlOn(session.serverpod, proof.instanceUrl),
|
||||
);
|
||||
return asOwnerProofTry(
|
||||
session,
|
||||
|
|
@ -147,7 +147,7 @@ class PasswordEndpoint extends Endpoint {
|
|||
required ByteData recoveryProofKey,
|
||||
}) {
|
||||
final bound = proof.copyWith(
|
||||
host: deviceHostOn(session.serverpod, proof.host),
|
||||
instanceUrl: deviceInstanceUrlOn(session.serverpod, proof.instanceUrl),
|
||||
);
|
||||
return asOwnerProofTry(
|
||||
session,
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ class PasswordSignInEndpoint extends Endpoint {
|
|||
/// (or, for a setup never finished, a [code] mailed for [request] to the
|
||||
/// address the sign-in began with). Signed in, SRP's M2 (which the device checks before trusting anything
|
||||
/// else), the account's keys as the register keeps them, and the session.
|
||||
/// [host] is the host the device addressed this instance by, which its
|
||||
/// proof is bound to; [address] and [language], the address the sign-in
|
||||
/// [instanceUrl] is the base URL the device addressed this instance by,
|
||||
/// which its proof is bound to; [address] and [language], the address the sign-in
|
||||
/// began with as the device typed it, serve only to tell its owner should
|
||||
/// codes come to wait (the instance keeps no address). Refusals as
|
||||
/// `completePasswordSignIn`'s.
|
||||
|
|
@ -46,7 +46,7 @@ class PasswordSignInEndpoint extends Endpoint {
|
|||
String? recoveryCode,
|
||||
UuidValue? request,
|
||||
String? code,
|
||||
required String host,
|
||||
required String instanceUrl,
|
||||
required String address,
|
||||
required String language,
|
||||
}) async {
|
||||
|
|
@ -60,7 +60,7 @@ class PasswordSignInEndpoint extends Endpoint {
|
|||
recoveryCode: recoveryCode,
|
||||
request: request,
|
||||
code: code,
|
||||
host: deviceHostOn(session.serverpod, host),
|
||||
instanceUrl: deviceInstanceUrlOn(session.serverpod, instanceUrl),
|
||||
address: address,
|
||||
language: language,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -766,7 +766,7 @@ Future<PasswordProof> completePasswordSignIn(
|
|||
String? recoveryCode,
|
||||
UuidValue? request,
|
||||
String? code,
|
||||
required String host,
|
||||
required String instanceUrl,
|
||||
required String address,
|
||||
required String language,
|
||||
}) async {
|
||||
|
|
@ -777,7 +777,7 @@ Future<PasswordProof> completePasswordSignIn(
|
|||
challenge: challenge,
|
||||
clientValue: clientValue,
|
||||
clientProof: clientProof,
|
||||
host: host,
|
||||
instanceUrl: instanceUrl,
|
||||
);
|
||||
// Before any code is checked.
|
||||
final notice = requireNoticeTo(
|
||||
|
|
@ -889,8 +889,8 @@ typedef PasswordProved = ({
|
|||
/// Takes [challenge] (once) and checks the device's proof against it, as
|
||||
/// [completePasswordSignIn] and the reset of a lost authenticator do; SRP's
|
||||
/// identity is [passwordIdentity] of the challenge's ID (so nobody learns an
|
||||
/// account's ID from its address) and of the [host] the device addressed
|
||||
/// (`deviceHost`, or the host a member names), so a proof a hostile
|
||||
/// account's ID from its address) and of the [instanceUrl] the device
|
||||
/// addressed (`deviceInstanceUrl`, or the one a member names), so a proof a hostile
|
||||
/// instance relays here never counts. Refusals as
|
||||
/// [completePasswordSignIn]'s, but for the second factor.
|
||||
Future<PasswordProved> provePassword(
|
||||
|
|
@ -899,7 +899,7 @@ Future<PasswordProved> provePassword(
|
|||
required UuidValue challenge,
|
||||
required ByteData clientValue,
|
||||
required ByteData clientProof,
|
||||
required String host,
|
||||
required String instanceUrl,
|
||||
SignInChallengePurpose purpose = SignInChallengePurpose.password,
|
||||
}) async {
|
||||
final kept = await takeChallenge(session, challenge, purpose);
|
||||
|
|
@ -913,7 +913,7 @@ Future<PasswordProved> provePassword(
|
|||
final clientBytes = Uint8List.sublistView(clientValue);
|
||||
final verification = srpVerify(
|
||||
passwordGroup,
|
||||
identity: passwordIdentity(challenge, host),
|
||||
identity: passwordIdentity(challenge, instanceUrl),
|
||||
salt: password == null ? decoy.salt : Uint8List.sublistView(password.salt),
|
||||
verifier: password == null
|
||||
? decoy.verifier
|
||||
|
|
@ -1367,7 +1367,7 @@ Future<KeyProofChallenge> beginKeyProof(
|
|||
}
|
||||
|
||||
/// Takes the key proof [challenge] (once) and checks [signature], the
|
||||
/// signature over [keyProofMessage] (for the [host] the device addressed,
|
||||
/// signature over [keyProofMessage] (for the [instanceUrl] the device addressed,
|
||||
/// the [purpose] and the [digest] of what it sets) by the recovery
|
||||
/// phrase's own proof key
|
||||
/// (`recoveryProofKey`: derived from the phrase alone, so the account's
|
||||
|
|
@ -1381,7 +1381,7 @@ Future<AccountPassword> proveRecoveryPhrase(
|
|||
PasswordKeys keys, {
|
||||
required UuidValue challenge,
|
||||
required ByteData signature,
|
||||
required String host,
|
||||
required String instanceUrl,
|
||||
required KeyProofPurpose purpose,
|
||||
List<int> digest = const [],
|
||||
}) async {
|
||||
|
|
@ -1401,7 +1401,7 @@ Future<AccountPassword> proveRecoveryPhrase(
|
|||
keyProofMessage(
|
||||
challenge,
|
||||
_unhex(kept.secret),
|
||||
host: host,
|
||||
instanceUrl: instanceUrl,
|
||||
purpose: purpose,
|
||||
digest: digest,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ abstract interface class AccountRegister {
|
|||
String? recoveryCode,
|
||||
UuidValue? request,
|
||||
String? code,
|
||||
required String host,
|
||||
required String instanceUrl,
|
||||
required String address,
|
||||
required String language,
|
||||
});
|
||||
|
|
@ -262,7 +262,7 @@ abstract interface class AccountRegister {
|
|||
String? code,
|
||||
UuidValue? keyChallenge,
|
||||
ByteData? signature,
|
||||
required String host,
|
||||
required String instanceUrl,
|
||||
});
|
||||
|
||||
/// The first step of proving the recovery phrase for [address], the
|
||||
|
|
@ -283,7 +283,7 @@ abstract interface class AccountRegister {
|
|||
required NewPassword password,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
required String host,
|
||||
required String instanceUrl,
|
||||
required String address,
|
||||
required String language,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -260,7 +260,7 @@ Future<Map<String, Object?>?> _answer(
|
|||
recoveryCode: arguments.optionalString('recoveryCode'),
|
||||
request: arguments.optionalId('request'),
|
||||
code: arguments.optionalString('code'),
|
||||
host: arguments.string('host'),
|
||||
instanceUrl: arguments.string('instanceUrl'),
|
||||
address: arguments.string('address'),
|
||||
language: arguments.string('language'),
|
||||
);
|
||||
|
|
@ -331,7 +331,7 @@ Future<Map<String, Object?>?> _answer(
|
|||
code: arguments.optionalString('code'),
|
||||
keyChallenge: arguments.optionalId('keyChallenge'),
|
||||
signature: arguments.optionalBytes('signature'),
|
||||
host: arguments.string('host'),
|
||||
instanceUrl: arguments.string('instanceUrl'),
|
||||
);
|
||||
return {'pending': pending.toJson()};
|
||||
case RegisterOperation.beginKeyProof:
|
||||
|
|
@ -349,7 +349,7 @@ Future<Map<String, Object?>?> _answer(
|
|||
password: arguments.model('password', NewPassword.fromJson),
|
||||
totpCode: arguments.optionalString('totpCode'),
|
||||
recoveryCode: arguments.optionalString('recoveryCode'),
|
||||
host: arguments.string('host'),
|
||||
instanceUrl: arguments.string('instanceUrl'),
|
||||
address: arguments.string('address'),
|
||||
language: arguments.string('language'),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -462,7 +462,7 @@ final class LocalRegister implements AccountRegister {
|
|||
String? recoveryCode,
|
||||
UuidValue? request,
|
||||
String? code,
|
||||
required String host,
|
||||
required String instanceUrl,
|
||||
required String address,
|
||||
required String language,
|
||||
}) => passwords.completePasswordSignIn(
|
||||
|
|
@ -474,7 +474,7 @@ final class LocalRegister implements AccountRegister {
|
|||
recoveryCode: recoveryCode,
|
||||
request: request,
|
||||
code: code,
|
||||
host: host,
|
||||
instanceUrl: instanceUrl,
|
||||
address: address,
|
||||
language: language,
|
||||
);
|
||||
|
|
@ -558,7 +558,7 @@ final class LocalRegister implements AccountRegister {
|
|||
String? code,
|
||||
UuidValue? keyChallenge,
|
||||
ByteData? signature,
|
||||
required String host,
|
||||
required String instanceUrl,
|
||||
}) => recovery.resetAuthenticator(
|
||||
session,
|
||||
challenge: challenge,
|
||||
|
|
@ -570,7 +570,7 @@ final class LocalRegister implements AccountRegister {
|
|||
code: code,
|
||||
keyChallenge: keyChallenge,
|
||||
signature: signature,
|
||||
host: host,
|
||||
instanceUrl: instanceUrl,
|
||||
);
|
||||
|
||||
@override
|
||||
|
|
@ -588,7 +588,7 @@ final class LocalRegister implements AccountRegister {
|
|||
required NewPassword password,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
required String host,
|
||||
required String instanceUrl,
|
||||
required String address,
|
||||
required String language,
|
||||
}) => recovery.resetPasswordWithPhrase(
|
||||
|
|
@ -598,7 +598,7 @@ final class LocalRegister implements AccountRegister {
|
|||
password: password,
|
||||
totpCode: totpCode,
|
||||
recoveryCode: recoveryCode,
|
||||
host: host,
|
||||
instanceUrl: instanceUrl,
|
||||
address: address,
|
||||
language: language,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -345,13 +345,13 @@ final class RemoteRegister implements AccountRegister {
|
|||
String? recoveryCode,
|
||||
UuidValue? request,
|
||||
String? code,
|
||||
required String host,
|
||||
required String instanceUrl,
|
||||
required String address,
|
||||
required String language,
|
||||
}) async => _model<PasswordProof>(
|
||||
RegisterOperation.completePasswordSignIn,
|
||||
await _ask(RegisterOperation.completePasswordSignIn, {
|
||||
'host': host,
|
||||
'instanceUrl': instanceUrl,
|
||||
'address': address,
|
||||
'language': language,
|
||||
'challenge': challenge.uuid,
|
||||
|
|
@ -472,11 +472,11 @@ final class RemoteRegister implements AccountRegister {
|
|||
String? code,
|
||||
UuidValue? keyChallenge,
|
||||
ByteData? signature,
|
||||
required String host,
|
||||
required String instanceUrl,
|
||||
}) async => _model<ResetPending>(
|
||||
RegisterOperation.resetAuthenticator,
|
||||
await _ask(RegisterOperation.resetAuthenticator, {
|
||||
'host': host,
|
||||
'instanceUrl': instanceUrl,
|
||||
'challenge': challenge.uuid,
|
||||
'clientValue': _bytes(clientValue),
|
||||
'clientProof': _bytes(clientProof),
|
||||
|
|
@ -514,13 +514,13 @@ final class RemoteRegister implements AccountRegister {
|
|||
required NewPassword password,
|
||||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
required String host,
|
||||
required String instanceUrl,
|
||||
required String address,
|
||||
required String language,
|
||||
}) async => _sessionsEnded(
|
||||
RegisterOperation.resetPasswordWithPhrase,
|
||||
await _ask(RegisterOperation.resetPasswordWithPhrase, {
|
||||
'host': host,
|
||||
'instanceUrl': instanceUrl,
|
||||
'address': address,
|
||||
'language': language,
|
||||
'challenge': challenge.uuid,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
),
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue