feat(base): SimpleLogin accounts' keys without SRP, signing up in one call (ADR 0020, step 3)
An account that signs in with SimpleLogin keeps its keys under a password that signs nobody in (D19): `account_password` keeps the password's own proof key (Ed25519, `passwordProofKey`) in place of SRP's verifier, which becomes nullable. The migration adds the column and drops the NOT NULL, nothing else. - `simpleLogin.signUp(attempt, handle, setup)`: someone new whose identity waits under the attempt gets, in one transaction, an account (identified), the link, a profile with the handle and the keys; then a session (D18). Refused, the identity still waiting, for a setup out of range and a handle refused as saving a profile refuses it. - `simpleLogin.checkHandle(attempt, handle)`: whether someone new could take a handle, at most 30 an hour per attempt and 60 per network. - `simpleLogin.complete` says, for an account, whether it has its keys and its handle, and keeps the sign-in ten minutes under its attempt, the account as its caller: D20's confirmation for the changes to come. - `keys.begin`, `keys.unlock`: the keys for a signed-in device once the password is proven with its proof key, for a nonce, bound to the base URL the device addressed; wrong proofs counted per session as ADR 0017's. - An authenticator is enrolled only for a password that signs in. - Members ask the register for all of it; `RemoteRegister` and the instance API carry the four new operations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
c6831b0b7b
commit
090d901e92
1 changed files with 46254 additions and 78 deletions
|
|
@ -0,0 +1,119 @@
|
|||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:cryptography/cryptography.dart';
|
||||
import 'package:halkyon_base_server/base.dart';
|
||||
import 'package:halkyon_base_server/src/password/passwords.dart' show keysRowOf;
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
|
||||
import 'password_device.dart';
|
||||
|
||||
/// A device with the keys of an account that signs in with SimpleLogin
|
||||
/// (Halkyon Learn's ADR 0020, D19), as halkyon_keys makes them, without
|
||||
/// libsodium: the password's and the recovery phrase's proof keys real
|
||||
/// Ed25519 pairs, so it proves either; what only a device opens (the
|
||||
/// wrapped keys) random bytes of the right length.
|
||||
final class KeysDevice {
|
||||
KeysDevice._(
|
||||
this._random,
|
||||
this.passwordProofKeys,
|
||||
this.passwordProofKey,
|
||||
this.recoveryProofKeys,
|
||||
this.recoveryProofKey,
|
||||
) : salt = _bytes(_random, 16),
|
||||
encryptionPublicKey = _bytes(_random, 32),
|
||||
signingPublicKey = _bytes(_random, 32);
|
||||
|
||||
/// A new device's keys, from [seed].
|
||||
static Future<KeysDevice> make({int seed = 23}) async {
|
||||
final random = Random(seed);
|
||||
final password = await Ed25519().newKeyPairFromSeed(_bytes(random, 32));
|
||||
final phrase = await Ed25519().newKeyPairFromSeed(_bytes(random, 32));
|
||||
return KeysDevice._(
|
||||
random,
|
||||
password,
|
||||
Uint8List.fromList((await password.extractPublicKey()).bytes),
|
||||
phrase,
|
||||
Uint8List.fromList((await phrase.extractPublicKey()).bytes),
|
||||
);
|
||||
}
|
||||
|
||||
final Random _random;
|
||||
|
||||
/// The password's own proof key, from the password alone.
|
||||
SimpleKeyPair passwordProofKeys;
|
||||
Uint8List passwordProofKey;
|
||||
|
||||
/// The recovery phrase's own proof key, from the phrase alone.
|
||||
SimpleKeyPair recoveryProofKeys;
|
||||
Uint8List recoveryProofKey;
|
||||
|
||||
/// The password's salt, as the device made it.
|
||||
Uint8List salt;
|
||||
final Uint8List encryptionPublicKey;
|
||||
final Uint8List signingPublicKey;
|
||||
|
||||
WrappedKeys _wrapped() => WrappedKeys(
|
||||
nonce: ByteData.sublistView(_bytes(_random, 24)),
|
||||
cipherText: ByteData.sublistView(_bytes(_random, wrappedKeysLength)),
|
||||
);
|
||||
|
||||
/// The keys as the setup wrapped them.
|
||||
late final WrappedKeys passwordKeys = _wrapped();
|
||||
late final WrappedKeys recoveryKeys = _wrapped();
|
||||
|
||||
/// The keys the device sends for its account.
|
||||
KeysSetup setup() => KeysSetup(
|
||||
salt: ByteData.sublistView(salt),
|
||||
costs: newPasswordCosts,
|
||||
passwordProofKey: ByteData.sublistView(passwordProofKey),
|
||||
encryptionPublicKey: ByteData.sublistView(encryptionPublicKey),
|
||||
signingPublicKey: ByteData.sublistView(signingPublicKey),
|
||||
passwordKeys: passwordKeys,
|
||||
recoveryKeys: recoveryKeys,
|
||||
recoveryProofKey: ByteData.sublistView(recoveryProofKey),
|
||||
);
|
||||
|
||||
/// The proof of the password for [challenge] and [purpose] (with the
|
||||
/// [digest] of what it sets) on [instanceUrl], by the password's proof
|
||||
/// key or [keys] where given; claiming [claimedUrl] (the same unless
|
||||
/// given) as the base URL it addressed.
|
||||
Future<KeysProof> prove(
|
||||
KeysChallenge challenge, {
|
||||
KeyProofPurpose purpose = KeyProofPurpose.unlockKeys,
|
||||
List<int> digest = const [],
|
||||
SimpleKeyPair? keys,
|
||||
String instanceUrl = testInstanceUrl,
|
||||
String? claimedUrl,
|
||||
}) async {
|
||||
final signature = await Ed25519().sign(
|
||||
keyProofMessage(
|
||||
challenge.challenge,
|
||||
Uint8List.sublistView(challenge.nonce),
|
||||
instanceUrl: instanceUrl,
|
||||
purpose: purpose,
|
||||
digest: digest,
|
||||
),
|
||||
keyPair: keys ?? passwordProofKeys,
|
||||
);
|
||||
return KeysProof(
|
||||
challenge: challenge.challenge,
|
||||
signature: ByteData.sublistView(Uint8List.fromList(signature.bytes)),
|
||||
instanceUrl: claimedUrl ?? instanceUrl,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// [person]'s keys as [device] made them, written straight into the
|
||||
/// register's table: an account that signs in with SimpleLogin.
|
||||
Future<void> giveKeys(
|
||||
Session session,
|
||||
UuidValue person,
|
||||
KeysDevice device,
|
||||
) => AccountPassword.db.insertRow(
|
||||
session,
|
||||
keysRowOf(person, device.setup(), now: DateTime.now().toUtc()),
|
||||
);
|
||||
|
||||
Uint8List _bytes(Random random, int length) =>
|
||||
Uint8List.fromList([for (var i = 0; i < length; i++) random.nextInt(256)]);
|
||||
Loading…
Reference in a new issue