fix(auth)!: the recovery phrase is proved by a key of its own
A key proof was a signature by the account's signing key, which every signed-in device keeps in its keyring: whoever could read one, with a code from its authenticator, set a new password without the password or the phrase (the second reading's H1). The phrase now proves itself with an Ed25519 key derived from its bits alone (halkyon_keys' recoveryProofKeysOf, KDF context hlkrprf1, a sibling of the recovery key), whose public half every setup, new phrase and account reset sends and the register keeps (account_password.recoveryProofKey); the device derives the secret half only while the phrase is typed. An account without one (none exist outside development) proves no phrase until it sets a new one; decoys check against a decoy proof key alike. Migration (module, Learn's and Todo's servers, Learn's device schema): ALTER TABLE "account_password" ADD COLUMN "recoveryProofKey" bytea; nullable, so rows made before keep working and no data is touched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
38bb090575
commit
aea0c447fc
3 changed files with 40952 additions and 93 deletions
|
|
@ -459,7 +459,8 @@ class EndpointPassword extends _isc.EndpointRef {
|
|||
);
|
||||
|
||||
/// A new recovery phrase: [recoveryKeys], the caller's keys wrapped under
|
||||
/// it on the device, replace the old wrapping, with the password's proof
|
||||
/// it on the device, replace the old wrapping, and [recoveryProofKey], the
|
||||
/// public half of its proof key, the old one, with the password's proof
|
||||
/// for [challenge] ([beginChange]) and a [totpCode] or [recoveryCode], as
|
||||
/// [change] takes. Refusals as `setRecoveryKeys`'.
|
||||
_ida.Future<void> setRecoveryKeys({
|
||||
|
|
@ -469,6 +470,7 @@ class EndpointPassword extends _isc.EndpointRef {
|
|||
String? totpCode,
|
||||
String? recoveryCode,
|
||||
required _i3so7e0p.WrappedKeys recoveryKeys,
|
||||
required _idt.ByteData recoveryProofKey,
|
||||
}) => caller.callServerEndpoint<void>(
|
||||
'halkyon_base.password',
|
||||
'setRecoveryKeys',
|
||||
|
|
@ -479,6 +481,7 @@ class EndpointPassword extends _isc.EndpointRef {
|
|||
'totpCode': totpCode,
|
||||
'recoveryCode': recoveryCode,
|
||||
'recoveryKeys': recoveryKeys,
|
||||
'recoveryProofKey': recoveryProofKey,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ import '../password/wrapped_keys.dart' as _i2rxumte;
|
|||
/// What a device needs to prove the recovery phrase (ADR 0017): the
|
||||
/// account's private keys wrapped under the phrase's key and its public
|
||||
/// keys, which the device opens with the phrase, and a nonce it signs with
|
||||
/// the signing key so opened, within two minutes, once. Alike for an address
|
||||
/// nobody has.
|
||||
/// the phrase's own proof key, within two minutes, once. Alike for an
|
||||
/// address nobody has.
|
||||
abstract class KeyProofChallenge
|
||||
implements _isc.SerializableModel, _isc.ProtocolSerialization {
|
||||
KeyProofChallenge._({
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ abstract class PasswordSetup
|
|||
required this.signingPublicKey,
|
||||
required this.passwordKeys,
|
||||
required this.recoveryKeys,
|
||||
required this.recoveryProofKey,
|
||||
});
|
||||
|
||||
factory PasswordSetup({
|
||||
|
|
@ -41,6 +42,7 @@ abstract class PasswordSetup
|
|||
required _idt.ByteData signingPublicKey,
|
||||
required _i2rxumte.WrappedKeys passwordKeys,
|
||||
required _i2rxumte.WrappedKeys recoveryKeys,
|
||||
required _idt.ByteData recoveryProofKey,
|
||||
}) = _PasswordSetupImpl;
|
||||
|
||||
factory PasswordSetup.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
|
|
@ -64,6 +66,9 @@ abstract class PasswordSetup
|
|||
recoveryKeys: _iwgbpyo5.Protocol().deserialize<_i2rxumte.WrappedKeys>(
|
||||
jsonSerialization['recoveryKeys'],
|
||||
),
|
||||
recoveryProofKey: _isc.ByteDataJsonExtension.fromJson(
|
||||
jsonSerialization['recoveryProofKey'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -82,6 +87,10 @@ abstract class PasswordSetup
|
|||
|
||||
_i2rxumte.WrappedKeys recoveryKeys;
|
||||
|
||||
/// The public half of the recovery phrase's proof key (Ed25519, from the
|
||||
/// phrase alone).
|
||||
_idt.ByteData recoveryProofKey;
|
||||
|
||||
/// Returns a shallow copy of this [PasswordSetup]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
|
|
@ -93,6 +102,7 @@ abstract class PasswordSetup
|
|||
_idt.ByteData? signingPublicKey,
|
||||
_i2rxumte.WrappedKeys? passwordKeys,
|
||||
_i2rxumte.WrappedKeys? recoveryKeys,
|
||||
_idt.ByteData? recoveryProofKey,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
|
|
@ -105,6 +115,7 @@ abstract class PasswordSetup
|
|||
'signingPublicKey': signingPublicKey.toJson(),
|
||||
'passwordKeys': passwordKeys.toJson(),
|
||||
'recoveryKeys': recoveryKeys.toJson(),
|
||||
'recoveryProofKey': recoveryProofKey.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -119,6 +130,7 @@ abstract class PasswordSetup
|
|||
'signingPublicKey': signingPublicKey.toJson(),
|
||||
'passwordKeys': passwordKeys.toJsonForProtocol(),
|
||||
'recoveryKeys': recoveryKeys.toJsonForProtocol(),
|
||||
'recoveryProofKey': recoveryProofKey.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -137,6 +149,7 @@ class _PasswordSetupImpl extends PasswordSetup {
|
|||
required _idt.ByteData signingPublicKey,
|
||||
required _i2rxumte.WrappedKeys passwordKeys,
|
||||
required _i2rxumte.WrappedKeys recoveryKeys,
|
||||
required _idt.ByteData recoveryProofKey,
|
||||
}) : super._(
|
||||
salt: salt,
|
||||
costs: costs,
|
||||
|
|
@ -145,6 +158,7 @@ class _PasswordSetupImpl extends PasswordSetup {
|
|||
signingPublicKey: signingPublicKey,
|
||||
passwordKeys: passwordKeys,
|
||||
recoveryKeys: recoveryKeys,
|
||||
recoveryProofKey: recoveryProofKey,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [PasswordSetup]
|
||||
|
|
@ -159,6 +173,7 @@ class _PasswordSetupImpl extends PasswordSetup {
|
|||
_idt.ByteData? signingPublicKey,
|
||||
_i2rxumte.WrappedKeys? passwordKeys,
|
||||
_i2rxumte.WrappedKeys? recoveryKeys,
|
||||
_idt.ByteData? recoveryProofKey,
|
||||
}) {
|
||||
return PasswordSetup(
|
||||
salt: salt ?? this.salt.clone(),
|
||||
|
|
@ -169,6 +184,7 @@ class _PasswordSetupImpl extends PasswordSetup {
|
|||
signingPublicKey: signingPublicKey ?? this.signingPublicKey.clone(),
|
||||
passwordKeys: passwordKeys ?? this.passwordKeys.copyWith(),
|
||||
recoveryKeys: recoveryKeys ?? this.recoveryKeys.copyWith(),
|
||||
recoveryProofKey: recoveryProofKey ?? this.recoveryProofKey.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue