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:
Tizian 「ティツィアーン」 2026-10-23 21:49:44 +02:00
commit 090d901e92
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -20,6 +20,14 @@ import 'package:halkyon_base_server/src/generated/auth/sign_in_ways.dart'
as _ik4ah9vo;
import 'package:halkyon_base_server/src/generated/instance/instance_basics.dart'
as _ihusd8tw;
import 'package:halkyon_base_server/src/generated/keys/device_keys.dart'
as _izazrxr6;
import 'package:halkyon_base_server/src/generated/keys/keys_challenge.dart'
as _i052zfjx;
import 'package:halkyon_base_server/src/generated/keys/keys_proof.dart'
as _iy592cuk;
import 'package:halkyon_base_server/src/generated/keys/keys_setup.dart'
as _ii64ejy6;
import 'package:halkyon_base_server/src/generated/password/authenticator.dart'
as _ip34xal6;
import 'package:halkyon_base_server/src/generated/password/authenticator_enrolment.dart'
@ -200,6 +208,8 @@ class TestEndpoints {
late final _InstanceEndpoint instance;
late final _KeysEndpoint keys;
late final _AccountRecoveryEndpoint accountRecovery;
late final _AuthenticatorEndpoint authenticator;
@ -246,6 +256,10 @@ class _InternalTestEndpoints extends TestEndpoints
endpoints,
serializationManager,
);
keys = _KeysEndpoint(
endpoints,
serializationManager,
);
accountRecovery = _AccountRecoveryEndpoint(
endpoints,
serializationManager,
@ -576,6 +590,78 @@ class _InstanceEndpoint {
}
}
class _KeysEndpoint {
_KeysEndpoint(
this._endpointDispatch,
this._serializationManager,
);
final _is.EndpointDispatch _endpointDispatch;
final _is.SerializationManager _serializationManager;
_ida.Future<_i052zfjx.KeysChallenge> begin(
_ist.TestSessionBuilder sessionBuilder,
) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'keys',
method: 'begin',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'keys',
methodName: 'begin',
parameters: _ist.testObjectToJson({}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<_i052zfjx.KeysChallenge>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<_izazrxr6.DeviceKeys> unlock(
_ist.TestSessionBuilder sessionBuilder,
_iy592cuk.KeysProof proof,
) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'keys',
method: 'unlock',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'keys',
methodName: 'unlock',
parameters: _ist.testObjectToJson({'proof': proof}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<_izazrxr6.DeviceKeys>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
}
class _AccountRecoveryEndpoint {
_AccountRecoveryEndpoint(
this._endpointDispatch,
@ -1661,4 +1747,76 @@ class _SimpleLoginEndpoint {
}
});
}
_ida.Future<_idszjcpz.ProfileRefusal?> checkHandle(
_ist.TestSessionBuilder sessionBuilder, {
required _is.UuidValue attempt,
required String handle,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'simpleLogin',
method: 'checkHandle',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'simpleLogin',
methodName: 'checkHandle',
parameters: _ist.testObjectToJson({
'attempt': attempt,
'handle': handle,
}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<_idszjcpz.ProfileRefusal?>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<_iacs.AuthSuccess> signUp(
_ist.TestSessionBuilder sessionBuilder, {
required _is.UuidValue attempt,
required String handle,
required _ii64ejy6.KeysSetup setup,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'simpleLogin',
method: 'signUp',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'simpleLogin',
methodName: 'signUp',
parameters: _ist.testObjectToJson({
'attempt': attempt,
'handle': handle,
'setup': setup,
}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<_iacs.AuthSuccess>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
}