feat(base,app,keys): keys of shared spaces, sealed to each member

Step 3 of ADR 0044 (D3, D18, D19). A shared space's data keys are
sealed on a device to each member's public keys; the instance keeps
what devices sealed and never opens it.

- halkyon_keys: the safety code of two accounts' public keys
  (`safetyCode`), 60 digits, the same on both sides.
- halkyon_base: the register hands out accounts' public keys
  (`public-keys-of`, local and remote); the table `shared_space_key`
  and the endpoint `sharedSpaceKeys` (memberKeys, keysNeeded, seal,
  mine). Only the Owner and Editors seal, the first key sealed to a
  member stays, and a member's keys go with their membership. Every
  host's migration creates the table.
- halkyon_app: `ContentCipher` seals a key to another account and
  opens one sealed by another; `SharedKeys` shares a space's key with
  its members, seals keys to newcomers and opens the person's own,
  holding each member to the public keys `KeyPins` pinned on first
  sight (`key_pins.json` in the person's folder), a change refused
  until trusted anew.

Learn takes them into its `Privacy` in step 4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-11 11:28:13 +01:00
commit b72cea1fe0
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
31 changed files with 105813 additions and 5 deletions

View file

@ -30,6 +30,7 @@ Endpoints, under the nickname `base` in a host's client (`client.modules.base.em
| `profile` | `mine`, `save`, `checkHandle` (with every answer about a new handle, at most 30 an hour per person and 60 per network), `checkLinks` (at most 10 an hour), `setAvatar`, `removeAvatar`, `remove` (what it shows goes, the handle stays, private again: Halkyon Learn's ADR 0020, D27): the caller's own public profile | `identified` |
| `profiles` | `byHandle`: anyone's public profile once they went public, whatever the handle's case | signed in |
| `sharedSpace` | spaces people share (Halkyon's ADR 0044, step 2), on the host's sync engine through its `SpaceGrants` (refused, `unavailable`, without one): `create(kind)`, the caller its Owner; `mine`; `members(space)`, by membership ID and public profile, never by account, those invited and waiting last; `invite(space, handle, role)`, the Owner's (or any member's where the space lets members invite), a member at once where the person has an account on this service, else invited until they first sign in there (D13, D17), counted as an answer about a handle; `setRole`, `remove`, `handOn`, `delete`, the Owner's; `leave`, the Owner naming who takes the space on, its only member deleting it instead. A space the caller isn't in can't be told from one that doesn't exist (`SharedSpaceNotFoundException`); Owner and Editor write in the engine, a Viewer reads | `identified` |
| `sharedSpaceKeys` | a shared space's data keys, each sealed on a device to one member (Halkyon's ADR 0044, D3, step 3), which the instance keeps and never opens: `memberKeys(space)`, the public keys of its members who have keys, by account, as the register hands them out, for a device to seal to and pin (D18); `keysNeeded(space)`, the keys the caller holds that members lack, for the Owner and Editors, none for a Viewer; `seal(space, keys)`, refused for a Viewer (`notAllowed`) and for anyone outside the space, the first key sealed to a member staying; `mine(space)`, those sealed to the caller, each naming its sealer. A member's keys go with their membership. A space the caller isn't in can't be told from one that doesn't exist | `identified` |
- Email keeps only HMAC-SHA256 of the address, keyed with `emailSignInPepper` (`email_identity`), and its
codes as Argon2id hashes (`email_code_request`). Codes are eight Crockford characters,

View file

@ -37,6 +37,8 @@ import 'package:halkyon_base_server/src/generated/password/wrapped_keys.dart'
as _iji0wmu8;
import 'package:halkyon_base_server/src/generated/shared_space/shared_space_role.dart'
as _i4jk9by9;
import 'package:halkyon_base_server/src/generated/shared_space/shared_space_sealed_key.dart'
as _ik1elru1;
import 'package:halkyon_base_server/src/generated/simplelogin/simplelogin_return.dart'
as _igmh0yku;
import 'package:halkyon_oidc_server/halkyon_oidc_server.dart' as _ipoqfiqr;
@ -60,6 +62,7 @@ import '../password/password_endpoint.dart' as _ig7fyg1b;
import '../password/password_sign_in_endpoint.dart' as _ik13m562;
import '../profile/profile_endpoint.dart' as _i6ky944g;
import '../shared_space/shared_space_endpoint.dart' as _i858tn7r;
import '../shared_space/shared_space_keys_endpoint.dart' as _izzpx5a5;
import '../simplelogin/simplelogin_endpoint.dart' as _ia8c9alv;
class Endpoints extends _is.EndpointDispatch {
@ -162,6 +165,12 @@ class Endpoints extends _is.EndpointDispatch {
'sharedSpace',
'halkyon_base',
),
'sharedSpaceKeys': _izzpx5a5.SharedSpaceKeysEndpoint()
..initialize(
server,
'sharedSpaceKeys',
'halkyon_base',
),
'simpleLogin': _ia8c9alv.SimpleLoginEndpoint()
..initialize(
server,
@ -2134,6 +2143,102 @@ class Endpoints extends _is.EndpointDispatch {
),
},
);
connectors['sharedSpaceKeys'] = _is.EndpointConnector(
name: 'sharedSpaceKeys',
endpoint: endpoints['sharedSpaceKeys']!,
methodConnectors: {
'memberKeys': _is.MethodConnector(
name: 'memberKeys',
params: {
'space': _is.ParameterDescription(
name: 'space',
type: _is.getType<_is.UuidValue>(),
nullable: false,
),
},
call:
(
_is.Session session,
Map<String, dynamic> params,
) async =>
(endpoints['sharedSpaceKeys']
as _izzpx5a5.SharedSpaceKeysEndpoint)
.memberKeys(
session,
params['space'],
),
),
'keysNeeded': _is.MethodConnector(
name: 'keysNeeded',
params: {
'space': _is.ParameterDescription(
name: 'space',
type: _is.getType<_is.UuidValue>(),
nullable: false,
),
},
call:
(
_is.Session session,
Map<String, dynamic> params,
) async =>
(endpoints['sharedSpaceKeys']
as _izzpx5a5.SharedSpaceKeysEndpoint)
.keysNeeded(
session,
params['space'],
),
),
'seal': _is.MethodConnector(
name: 'seal',
params: {
'space': _is.ParameterDescription(
name: 'space',
type: _is.getType<_is.UuidValue>(),
nullable: false,
),
'keys': _is.ParameterDescription(
name: 'keys',
type: _is.getType<List<_ik1elru1.SharedSpaceSealedKey>>(),
nullable: false,
),
},
call:
(
_is.Session session,
Map<String, dynamic> params,
) async =>
(endpoints['sharedSpaceKeys']
as _izzpx5a5.SharedSpaceKeysEndpoint)
.seal(
session,
params['space'],
params['keys'],
),
),
'mine': _is.MethodConnector(
name: 'mine',
params: {
'space': _is.ParameterDescription(
name: 'space',
type: _is.getType<_is.UuidValue>(),
nullable: false,
),
},
call:
(
_is.Session session,
Map<String, dynamic> params,
) async =>
(endpoints['sharedSpaceKeys']
as _izzpx5a5.SharedSpaceKeysEndpoint)
.mine(
session,
params['space'],
),
),
},
);
connectors['simpleLogin'] = _is.EndpointConnector(
name: 'simpleLogin',
endpoint: endpoints['simpleLogin']!,

View file

@ -0,0 +1,94 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:typed_data' as _idt;
import 'package:serverpod/serverpod.dart' as _is;
/// An account's public keys as the register keeps them (ADR 0017): X25519's,
/// which data keys are sealed to, and Ed25519's, which its signatures check
/// against. What others seal shared keys to and check them by (Halkyon's ADR
/// 0044, D3 and D18).
abstract class AccountPublicKeys
implements _is.SerializableModel, _is.ProtocolSerialization {
AccountPublicKeys._({
required this.encryption,
required this.signing,
});
factory AccountPublicKeys({
required _idt.ByteData encryption,
required _idt.ByteData signing,
}) = _AccountPublicKeysImpl;
factory AccountPublicKeys.fromJson(Map<String, dynamic> jsonSerialization) {
return AccountPublicKeys(
encryption: _is.ByteDataJsonExtension.fromJson(
jsonSerialization['encryption'],
),
signing: _is.ByteDataJsonExtension.fromJson(jsonSerialization['signing']),
);
}
_idt.ByteData encryption;
_idt.ByteData signing;
/// Returns a shallow copy of this [AccountPublicKeys]
/// with some or all fields replaced by the given arguments.
@_is.useResult
AccountPublicKeys copyWith({
_idt.ByteData? encryption,
_idt.ByteData? signing,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'halkyon_base.AccountPublicKeys',
'encryption': encryption.toJson(),
'signing': signing.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {};
}
@override
String toString() {
return _is.SerializationManager.encode(this);
}
}
class _AccountPublicKeysImpl extends AccountPublicKeys {
_AccountPublicKeysImpl({
required _idt.ByteData encryption,
required _idt.ByteData signing,
}) : super._(
encryption: encryption,
signing: signing,
);
/// Returns a shallow copy of this [AccountPublicKeys]
/// with some or all fields replaced by the given arguments.
@_is.useResult
@override
AccountPublicKeys copyWith({
_idt.ByteData? encryption,
_idt.ByteData? signing,
}) {
return AccountPublicKeys(
encryption: encryption ?? this.encryption.clone(),
signing: signing ?? this.signing.clone(),
);
}
}

View file

@ -21,8 +21,14 @@ import 'package:halkyon_base_server/src/generated/password/authenticator.dart'
as _ip34xal6;
import 'package:halkyon_base_server/src/generated/shared_space/shared_space_info.dart'
as _i582118y;
import 'package:halkyon_base_server/src/generated/shared_space/shared_space_key_need.dart'
as _ije0apa9;
import 'package:halkyon_base_server/src/generated/shared_space/shared_space_member_info.dart'
as _izxzwa6x;
import 'package:halkyon_base_server/src/generated/shared_space/shared_space_member_keys.dart'
as _ia0j1un6;
import 'package:halkyon_base_server/src/generated/shared_space/shared_space_sealed_key.dart'
as _ik1elru1;
import 'package:halkyon_oidc_server/halkyon_oidc_server.dart' as _ipoqfiqr;
import 'package:serverpod/protocol.dart' as _isp;
import 'package:serverpod/serverpod.dart' as _is;
@ -71,6 +77,7 @@ import 'files/file_limit_exceeded_exception.dart' as _iaz1i85g;
import 'files/file_usage.dart' as _i45yjkiy;
import 'files/stored_file_not_found_exception.dart' as _i8o9pcsj;
import 'instance/instance_basics.dart' as _i8jixqx6;
import 'keys/account_public_keys.dart' as _iiomvou1;
import 'keys/device_keys.dart' as _iz50cwqw;
import 'keys/keys_challenge.dart' as _itebbsfc;
import 'keys/keys_owner_proof.dart' as _iis161ma;
@ -116,12 +123,16 @@ import 'register/sessions_ended_notice.dart' as _io5c3tod;
import 'shared_space/shared_space.dart' as _iampeqql;
import 'shared_space/shared_space_info.dart' as _i1rvc02f;
import 'shared_space/shared_space_invitation.dart' as _i7h4we7t;
import 'shared_space/shared_space_key.dart' as _i9bjihm2;
import 'shared_space/shared_space_key_need.dart' as _imxqlbwp;
import 'shared_space/shared_space_member.dart' as _iw7ixstr;
import 'shared_space/shared_space_member_info.dart' as _iz8z2oju;
import 'shared_space/shared_space_member_keys.dart' as _iqkqjhh4;
import 'shared_space/shared_space_not_found_exception.dart' as _ixm56fil;
import 'shared_space/shared_space_refusal.dart' as _irg3s7i7;
import 'shared_space/shared_space_refused_exception.dart' as _iooym5cy;
import 'shared_space/shared_space_role.dart' as _i2fude1g;
import 'shared_space/shared_space_sealed_key.dart' as _ifx6y5sl;
import 'simplelogin/simplelogin_completed.dart' as _i51ry5s2;
import 'simplelogin/simplelogin_outcome.dart' as _i5ar1hzl;
import 'simplelogin/simplelogin_proof.dart' as _iauqtzog;
@ -167,6 +178,7 @@ export 'files/file_limit_exceeded_exception.dart';
export 'files/file_usage.dart';
export 'files/stored_file_not_found_exception.dart';
export 'instance/instance_basics.dart';
export 'keys/account_public_keys.dart';
export 'keys/device_keys.dart';
export 'keys/keys_challenge.dart';
export 'keys/keys_owner_proof.dart';
@ -212,12 +224,16 @@ export 'register/sessions_ended_notice.dart';
export 'shared_space/shared_space.dart';
export 'shared_space/shared_space_info.dart';
export 'shared_space/shared_space_invitation.dart';
export 'shared_space/shared_space_key.dart';
export 'shared_space/shared_space_key_need.dart';
export 'shared_space/shared_space_member.dart';
export 'shared_space/shared_space_member_info.dart';
export 'shared_space/shared_space_member_keys.dart';
export 'shared_space/shared_space_not_found_exception.dart';
export 'shared_space/shared_space_refusal.dart';
export 'shared_space/shared_space_refused_exception.dart';
export 'shared_space/shared_space_role.dart';
export 'shared_space/shared_space_sealed_key.dart';
export 'simplelogin/simplelogin_completed.dart';
export 'simplelogin/simplelogin_outcome.dart';
export 'simplelogin/simplelogin_proof.dart';
@ -1402,6 +1418,106 @@ class Protocol extends _is.DatabaseSerializationManager {
],
managed: true,
),
_isp.TableDefinition(
name: 'shared_space_key',
dartName: 'SharedSpaceKey',
schema: 'public',
module: 'halkyon_base',
columns: [
_isp.ColumnDefinition(
name: 'id',
columnType: _isp.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue?',
columnDefault: 'random_v7',
),
_isp.ColumnDefinition(
name: 'sharedSpaceId',
columnType: _isp.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue',
),
_isp.ColumnDefinition(
name: 'keyId',
columnType: _isp.ColumnType.text,
isNullable: false,
dartType: 'String',
),
_isp.ColumnDefinition(
name: 'recipientId',
columnType: _isp.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue',
),
_isp.ColumnDefinition(
name: 'sealedKey',
columnType: _isp.ColumnType.bytea,
isNullable: false,
dartType: 'dart:typed_data:ByteData',
),
_isp.ColumnDefinition(
name: 'sealedBy',
columnType: _isp.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue',
),
_isp.ColumnDefinition(
name: 'createdAt',
columnType: _isp.ColumnType.timestampWithoutTimeZone,
isNullable: false,
dartType: 'DateTime',
),
],
foreignKeys: [
_isp.ForeignKeyDefinition(
constraintName: 'shared_space_key_fk_0',
columns: ['sharedSpaceId'],
referenceTable: 'shared_space',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isp.ForeignKeyAction.noAction,
onDelete: _isp.ForeignKeyAction.cascade,
matchType: null,
),
],
indexes: [
_isp.IndexDefinition(
indexName: 'shared_space_key_unique_idx',
tableSpace: null,
elements: [
_isp.IndexElementDefinition(
type: _isp.IndexElementDefinitionType.column,
definition: 'sharedSpaceId',
),
_isp.IndexElementDefinition(
type: _isp.IndexElementDefinitionType.column,
definition: 'keyId',
),
_isp.IndexElementDefinition(
type: _isp.IndexElementDefinitionType.column,
definition: 'recipientId',
),
],
type: 'btree',
isUnique: true,
isPrimary: false,
),
_isp.IndexDefinition(
indexName: 'shared_space_key_recipient_idx',
tableSpace: null,
elements: [
_isp.IndexElementDefinition(
type: _isp.IndexElementDefinitionType.column,
definition: 'recipientId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
_isp.TableDefinition(
name: 'shared_space_member',
dartName: 'SharedSpaceMember',
@ -1905,6 +2021,9 @@ class Protocol extends _is.DatabaseSerializationManager {
if (t == _i8jixqx6.InstanceBasics) {
return _i8jixqx6.InstanceBasics.fromJson(data) as T;
}
if (t == _iiomvou1.AccountPublicKeys) {
return _iiomvou1.AccountPublicKeys.fromJson(data) as T;
}
if (t == _iz50cwqw.DeviceKeys) {
return _iz50cwqw.DeviceKeys.fromJson(data) as T;
}
@ -2040,12 +2159,21 @@ class Protocol extends _is.DatabaseSerializationManager {
if (t == _i7h4we7t.SharedSpaceInvitation) {
return _i7h4we7t.SharedSpaceInvitation.fromJson(data) as T;
}
if (t == _i9bjihm2.SharedSpaceKey) {
return _i9bjihm2.SharedSpaceKey.fromJson(data) as T;
}
if (t == _imxqlbwp.SharedSpaceKeyNeed) {
return _imxqlbwp.SharedSpaceKeyNeed.fromJson(data) as T;
}
if (t == _iw7ixstr.SharedSpaceMember) {
return _iw7ixstr.SharedSpaceMember.fromJson(data) as T;
}
if (t == _iz8z2oju.SharedSpaceMemberInfo) {
return _iz8z2oju.SharedSpaceMemberInfo.fromJson(data) as T;
}
if (t == _iqkqjhh4.SharedSpaceMemberKeys) {
return _iqkqjhh4.SharedSpaceMemberKeys.fromJson(data) as T;
}
if (t == _ixm56fil.SharedSpaceNotFoundException) {
return _ixm56fil.SharedSpaceNotFoundException.fromJson(data) as T;
}
@ -2058,6 +2186,9 @@ class Protocol extends _is.DatabaseSerializationManager {
if (t == _i2fude1g.SharedSpaceRole) {
return _i2fude1g.SharedSpaceRole.fromJson(data) as T;
}
if (t == _ifx6y5sl.SharedSpaceSealedKey) {
return _ifx6y5sl.SharedSpaceSealedKey.fromJson(data) as T;
}
if (t == _i51ry5s2.SimpleLoginCompleted) {
return _i51ry5s2.SimpleLoginCompleted.fromJson(data) as T;
}
@ -2242,6 +2373,10 @@ class Protocol extends _is.DatabaseSerializationManager {
return (data != null ? _i8jixqx6.InstanceBasics.fromJson(data) : null)
as T;
}
if (t == _is.getType<_iiomvou1.AccountPublicKeys?>()) {
return (data != null ? _iiomvou1.AccountPublicKeys.fromJson(data) : null)
as T;
}
if (t == _is.getType<_iz50cwqw.DeviceKeys?>()) {
return (data != null ? _iz50cwqw.DeviceKeys.fromJson(data) : null) as T;
}
@ -2432,6 +2567,14 @@ class Protocol extends _is.DatabaseSerializationManager {
: null)
as T;
}
if (t == _is.getType<_i9bjihm2.SharedSpaceKey?>()) {
return (data != null ? _i9bjihm2.SharedSpaceKey.fromJson(data) : null)
as T;
}
if (t == _is.getType<_imxqlbwp.SharedSpaceKeyNeed?>()) {
return (data != null ? _imxqlbwp.SharedSpaceKeyNeed.fromJson(data) : null)
as T;
}
if (t == _is.getType<_iw7ixstr.SharedSpaceMember?>()) {
return (data != null ? _iw7ixstr.SharedSpaceMember.fromJson(data) : null)
as T;
@ -2442,6 +2585,12 @@ class Protocol extends _is.DatabaseSerializationManager {
: null)
as T;
}
if (t == _is.getType<_iqkqjhh4.SharedSpaceMemberKeys?>()) {
return (data != null
? _iqkqjhh4.SharedSpaceMemberKeys.fromJson(data)
: null)
as T;
}
if (t == _is.getType<_ixm56fil.SharedSpaceNotFoundException?>()) {
return (data != null
? _ixm56fil.SharedSpaceNotFoundException.fromJson(data)
@ -2462,6 +2611,12 @@ class Protocol extends _is.DatabaseSerializationManager {
return (data != null ? _i2fude1g.SharedSpaceRole.fromJson(data) : null)
as T;
}
if (t == _is.getType<_ifx6y5sl.SharedSpaceSealedKey?>()) {
return (data != null
? _ifx6y5sl.SharedSpaceSealedKey.fromJson(data)
: null)
as T;
}
if (t == _is.getType<_i51ry5s2.SimpleLoginCompleted?>()) {
return (data != null
? _i51ry5s2.SimpleLoginCompleted.fromJson(data)
@ -2577,6 +2732,24 @@ class Protocol extends _is.DatabaseSerializationManager {
.toList()
as T;
}
if (t == List<_ia0j1un6.SharedSpaceMemberKeys>) {
return (data as List)
.map((e) => deserialize<_ia0j1un6.SharedSpaceMemberKeys>(e))
.toList()
as T;
}
if (t == List<_ije0apa9.SharedSpaceKeyNeed>) {
return (data as List)
.map((e) => deserialize<_ije0apa9.SharedSpaceKeyNeed>(e))
.toList()
as T;
}
if (t == List<_ik1elru1.SharedSpaceSealedKey>) {
return (data as List)
.map((e) => deserialize<_ik1elru1.SharedSpaceSealedKey>(e))
.toList()
as T;
}
try {
return _ipoqfiqr.Protocol().deserialize<T>(data, t);
} on _is.DeserializationTypeNotFoundException catch (_) {}
@ -2635,6 +2808,7 @@ class Protocol extends _is.DatabaseSerializationManager {
_i45yjkiy.FileUsage => 'FileUsage',
_i8o9pcsj.StoredFileNotFoundException => 'StoredFileNotFoundException',
_i8jixqx6.InstanceBasics => 'InstanceBasics',
_iiomvou1.AccountPublicKeys => 'AccountPublicKeys',
_iz50cwqw.DeviceKeys => 'DeviceKeys',
_itebbsfc.KeysChallenge => 'KeysChallenge',
_iis161ma.KeysOwnerProof => 'KeysOwnerProof',
@ -2680,12 +2854,16 @@ class Protocol extends _is.DatabaseSerializationManager {
_iampeqql.SharedSpace => 'SharedSpace',
_i1rvc02f.SharedSpaceInfo => 'SharedSpaceInfo',
_i7h4we7t.SharedSpaceInvitation => 'SharedSpaceInvitation',
_i9bjihm2.SharedSpaceKey => 'SharedSpaceKey',
_imxqlbwp.SharedSpaceKeyNeed => 'SharedSpaceKeyNeed',
_iw7ixstr.SharedSpaceMember => 'SharedSpaceMember',
_iz8z2oju.SharedSpaceMemberInfo => 'SharedSpaceMemberInfo',
_iqkqjhh4.SharedSpaceMemberKeys => 'SharedSpaceMemberKeys',
_ixm56fil.SharedSpaceNotFoundException => 'SharedSpaceNotFoundException',
_irg3s7i7.SharedSpaceRefusal => 'SharedSpaceRefusal',
_iooym5cy.SharedSpaceRefusedException => 'SharedSpaceRefusedException',
_i2fude1g.SharedSpaceRole => 'SharedSpaceRole',
_ifx6y5sl.SharedSpaceSealedKey => 'SharedSpaceSealedKey',
_i51ry5s2.SimpleLoginCompleted => 'SimpleLoginCompleted',
_i5ar1hzl.SimpleLoginOutcome => 'SimpleLoginOutcome',
_iauqtzog.SimpleLoginProof => 'SimpleLoginProof',
@ -2789,6 +2967,8 @@ class Protocol extends _is.DatabaseSerializationManager {
return 'StoredFileNotFoundException';
case _i8jixqx6.InstanceBasics():
return 'InstanceBasics';
case _iiomvou1.AccountPublicKeys():
return 'AccountPublicKeys';
case _iz50cwqw.DeviceKeys():
return 'DeviceKeys';
case _itebbsfc.KeysChallenge():
@ -2879,10 +3059,16 @@ class Protocol extends _is.DatabaseSerializationManager {
return 'SharedSpaceInfo';
case _i7h4we7t.SharedSpaceInvitation():
return 'SharedSpaceInvitation';
case _i9bjihm2.SharedSpaceKey():
return 'SharedSpaceKey';
case _imxqlbwp.SharedSpaceKeyNeed():
return 'SharedSpaceKeyNeed';
case _iw7ixstr.SharedSpaceMember():
return 'SharedSpaceMember';
case _iz8z2oju.SharedSpaceMemberInfo():
return 'SharedSpaceMemberInfo';
case _iqkqjhh4.SharedSpaceMemberKeys():
return 'SharedSpaceMemberKeys';
case _ixm56fil.SharedSpaceNotFoundException():
return 'SharedSpaceNotFoundException';
case _irg3s7i7.SharedSpaceRefusal():
@ -2891,6 +3077,8 @@ class Protocol extends _is.DatabaseSerializationManager {
return 'SharedSpaceRefusedException';
case _i2fude1g.SharedSpaceRole():
return 'SharedSpaceRole';
case _ifx6y5sl.SharedSpaceSealedKey():
return 'SharedSpaceSealedKey';
case _i51ry5s2.SimpleLoginCompleted():
return 'SimpleLoginCompleted';
case _i5ar1hzl.SimpleLoginOutcome():
@ -3036,6 +3224,9 @@ class Protocol extends _is.DatabaseSerializationManager {
if (dataClassName == 'InstanceBasics') {
return deserialize<_i8jixqx6.InstanceBasics>(data['data']);
}
if (dataClassName == 'AccountPublicKeys') {
return deserialize<_iiomvou1.AccountPublicKeys>(data['data']);
}
if (dataClassName == 'DeviceKeys') {
return deserialize<_iz50cwqw.DeviceKeys>(data['data']);
}
@ -3171,12 +3362,21 @@ class Protocol extends _is.DatabaseSerializationManager {
if (dataClassName == 'SharedSpaceInvitation') {
return deserialize<_i7h4we7t.SharedSpaceInvitation>(data['data']);
}
if (dataClassName == 'SharedSpaceKey') {
return deserialize<_i9bjihm2.SharedSpaceKey>(data['data']);
}
if (dataClassName == 'SharedSpaceKeyNeed') {
return deserialize<_imxqlbwp.SharedSpaceKeyNeed>(data['data']);
}
if (dataClassName == 'SharedSpaceMember') {
return deserialize<_iw7ixstr.SharedSpaceMember>(data['data']);
}
if (dataClassName == 'SharedSpaceMemberInfo') {
return deserialize<_iz8z2oju.SharedSpaceMemberInfo>(data['data']);
}
if (dataClassName == 'SharedSpaceMemberKeys') {
return deserialize<_iqkqjhh4.SharedSpaceMemberKeys>(data['data']);
}
if (dataClassName == 'SharedSpaceNotFoundException') {
return deserialize<_ixm56fil.SharedSpaceNotFoundException>(data['data']);
}
@ -3189,6 +3389,9 @@ class Protocol extends _is.DatabaseSerializationManager {
if (dataClassName == 'SharedSpaceRole') {
return deserialize<_i2fude1g.SharedSpaceRole>(data['data']);
}
if (dataClassName == 'SharedSpaceSealedKey') {
return deserialize<_ifx6y5sl.SharedSpaceSealedKey>(data['data']);
}
if (dataClassName == 'SimpleLoginCompleted') {
return deserialize<_i51ry5s2.SimpleLoginCompleted>(data['data']);
}
@ -3334,6 +3537,8 @@ class Protocol extends _is.DatabaseSerializationManager {
return _iampeqql.SharedSpace.t;
case _i7h4we7t.SharedSpaceInvitation:
return _i7h4we7t.SharedSpaceInvitation.t;
case _i9bjihm2.SharedSpaceKey:
return _i9bjihm2.SharedSpaceKey.t;
case _iw7ixstr.SharedSpaceMember:
return _iw7ixstr.SharedSpaceMember.t;
}

View file

@ -94,6 +94,11 @@ sharedSpace:
- handOn:
- leave:
- delete:
sharedSpaceKeys:
- memberKeys:
- keysNeeded:
- seal:
- mine:
simpleLogin:
- begin:
- complete:

View file

@ -0,0 +1,722 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:typed_data' as _idt;
import 'package:serverpod/serverpod.dart' as _is;
/// A data key of a shared space sealed to one member (ADR 0044, D3): by a
/// device of a member who writes and holds it, to the member's X25519
/// public key, signed by the sealer (halkyon_keys' `sealDataKey`). The
/// instance keeps it as it is and never opens it. The first one sealed for
/// a member and key stays; the member's go with their membership.
abstract class SharedSpaceKey
implements _is.TableRow<_is.UuidValue?>, _is.ProtocolSerialization {
SharedSpaceKey._({
this.id,
required this.sharedSpaceId,
required this.keyId,
required this.recipientId,
required this.sealedKey,
required this.sealedBy,
required this.createdAt,
});
factory SharedSpaceKey({
_is.UuidValue? id,
required _is.UuidValue sharedSpaceId,
required String keyId,
required _is.UuidValue recipientId,
required _idt.ByteData sealedKey,
required _is.UuidValue sealedBy,
required DateTime createdAt,
}) = _SharedSpaceKeyImpl;
factory SharedSpaceKey.fromJson(Map<String, dynamic> jsonSerialization) {
return SharedSpaceKey(
id: jsonSerialization['id'] == null
? null
: _is.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
sharedSpaceId: _is.UuidValueJsonExtension.fromJson(
jsonSerialization['sharedSpaceId'],
),
keyId: jsonSerialization['keyId'] as String,
recipientId: _is.UuidValueJsonExtension.fromJson(
jsonSerialization['recipientId'],
),
sealedKey: _is.ByteDataJsonExtension.fromJson(
jsonSerialization['sealedKey'],
),
sealedBy: _is.UuidValueJsonExtension.fromJson(
jsonSerialization['sealedBy'],
),
createdAt: _is.DateTimeJsonExtension.fromJson(
jsonSerialization['createdAt'],
),
);
}
static final t = SharedSpaceKeyTable();
static const db = SharedSpaceKeyRepository._();
@override
_is.UuidValue? id;
_is.UuidValue sharedSpaceId;
/// The key's ID, which the rows it seals name in their `keyId`.
String keyId;
/// Whom it is sealed to: the register's account ID.
_is.UuidValue recipientId;
/// The sealer's signature, then the sealed box.
_idt.ByteData sealedKey;
/// Who sealed it: the register's account ID, whose signing key checks it.
_is.UuidValue sealedBy;
DateTime createdAt;
@override
_is.Table<_is.UuidValue?> get table => t;
/// Returns a shallow copy of this [SharedSpaceKey]
/// with some or all fields replaced by the given arguments.
@_is.useResult
SharedSpaceKey copyWith({
_is.UuidValue? id,
_is.UuidValue? sharedSpaceId,
String? keyId,
_is.UuidValue? recipientId,
_idt.ByteData? sealedKey,
_is.UuidValue? sealedBy,
DateTime? createdAt,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'halkyon_base.SharedSpaceKey',
if (id != null) 'id': id?.toJson(),
'sharedSpaceId': sharedSpaceId.toJson(),
'keyId': keyId,
'recipientId': recipientId.toJson(),
'sealedKey': sealedKey.toJson(),
'sealedBy': sealedBy.toJson(),
'createdAt': createdAt.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {};
}
static SharedSpaceKeyInclude include() {
return SharedSpaceKeyInclude._();
}
static SharedSpaceKeyIncludeList includeList({
_is.WhereExpressionBuilder<SharedSpaceKeyTable>? where,
int? limit,
int? offset,
_is.OrderByBuilder<SharedSpaceKeyTable>? orderBy,
_is.OrderByListBuilder<SharedSpaceKeyTable>? orderByList,
SharedSpaceKeyInclude? include,
}) {
return SharedSpaceKeyIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(SharedSpaceKey.t),
orderByList: orderByList?.call(SharedSpaceKey.t),
include: include,
);
}
@override
String toString() {
return _is.SerializationManager.encode(this);
}
}
class _Undefined {}
class _SharedSpaceKeyImpl extends SharedSpaceKey {
_SharedSpaceKeyImpl({
_is.UuidValue? id,
required _is.UuidValue sharedSpaceId,
required String keyId,
required _is.UuidValue recipientId,
required _idt.ByteData sealedKey,
required _is.UuidValue sealedBy,
required DateTime createdAt,
}) : super._(
id: id,
sharedSpaceId: sharedSpaceId,
keyId: keyId,
recipientId: recipientId,
sealedKey: sealedKey,
sealedBy: sealedBy,
createdAt: createdAt,
);
/// Returns a shallow copy of this [SharedSpaceKey]
/// with some or all fields replaced by the given arguments.
@_is.useResult
@override
SharedSpaceKey copyWith({
Object? id = _Undefined,
_is.UuidValue? sharedSpaceId,
String? keyId,
_is.UuidValue? recipientId,
_idt.ByteData? sealedKey,
_is.UuidValue? sealedBy,
DateTime? createdAt,
}) {
return SharedSpaceKey(
id: id is _is.UuidValue? ? id : this.id,
sharedSpaceId: sharedSpaceId ?? this.sharedSpaceId,
keyId: keyId ?? this.keyId,
recipientId: recipientId ?? this.recipientId,
sealedKey: sealedKey ?? this.sealedKey.clone(),
sealedBy: sealedBy ?? this.sealedBy,
createdAt: createdAt ?? this.createdAt,
);
}
}
class SharedSpaceKeyUpdateTable extends _is.UpdateTable<SharedSpaceKeyTable> {
SharedSpaceKeyUpdateTable(super.table);
_is.ColumnValue<_is.UuidValue, _is.UuidValue> sharedSpaceId(
_is.UuidValue value,
) => _is.ColumnValue(
table.sharedSpaceId,
value,
);
_is.ColumnValue<String, String> keyId(String value) => _is.ColumnValue(
table.keyId,
value,
);
_is.ColumnValue<_is.UuidValue, _is.UuidValue> recipientId(
_is.UuidValue value,
) => _is.ColumnValue(
table.recipientId,
value,
);
_is.ColumnValue<_idt.ByteData, _idt.ByteData> sealedKey(
_idt.ByteData value,
) => _is.ColumnValue(
table.sealedKey,
value,
);
_is.ColumnValue<_is.UuidValue, _is.UuidValue> sealedBy(_is.UuidValue value) =>
_is.ColumnValue(
table.sealedBy,
value,
);
_is.ColumnValue<DateTime, DateTime> createdAt(DateTime value) =>
_is.ColumnValue(
table.createdAt,
value,
);
}
class SharedSpaceKeyTable extends _is.Table<_is.UuidValue?> {
SharedSpaceKeyTable({super.tableRelation})
: super(tableName: 'shared_space_key') {
updateTable = SharedSpaceKeyUpdateTable(this);
sharedSpaceId = _is.ColumnUuid(
'sharedSpaceId',
this,
);
keyId = _is.ColumnString(
'keyId',
this,
);
recipientId = _is.ColumnUuid(
'recipientId',
this,
);
sealedKey = _is.ColumnByteData(
'sealedKey',
this,
);
sealedBy = _is.ColumnUuid(
'sealedBy',
this,
);
createdAt = _is.ColumnDateTime(
'createdAt',
this,
);
}
late final SharedSpaceKeyUpdateTable updateTable;
late final _is.ColumnUuid sharedSpaceId;
/// The key's ID, which the rows it seals name in their `keyId`.
late final _is.ColumnString keyId;
/// Whom it is sealed to: the register's account ID.
late final _is.ColumnUuid recipientId;
/// The sealer's signature, then the sealed box.
late final _is.ColumnByteData sealedKey;
/// Who sealed it: the register's account ID, whose signing key checks it.
late final _is.ColumnUuid sealedBy;
late final _is.ColumnDateTime createdAt;
@override
List<_is.Column> get columns => [
id,
sharedSpaceId,
keyId,
recipientId,
sealedKey,
sealedBy,
createdAt,
];
}
class SharedSpaceKeyInclude extends _is.IncludeObject {
SharedSpaceKeyInclude._();
@override
Map<String, _is.Include?> get includes => {};
@override
_is.Table<_is.UuidValue?> get table => SharedSpaceKey.t;
}
class SharedSpaceKeyIncludeList extends _is.IncludeList {
SharedSpaceKeyIncludeList._({
_is.WhereExpressionBuilder<SharedSpaceKeyTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(SharedSpaceKey.t);
}
@override
Map<String, _is.Include?> get includes => include?.includes ?? {};
@override
_is.Table<_is.UuidValue?> get table => SharedSpaceKey.t;
}
class SharedSpaceKeyRepository {
const SharedSpaceKeyRepository._();
/// Returns a list of [SharedSpaceKey]s matching the given query parameters.
///
/// Use [where] to specify which items to include in the return value.
/// If none is specified, all items will be returned.
///
/// To specify the order of the items use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// The maximum number of items can be set by [limit]. If no limit is set,
/// all items matching the query will be returned.
///
/// [offset] defines how many items to skip, after which [limit] (or all)
/// items are read from the database.
///
/// ```dart
/// var persons = await Persons.db.find(
/// session,
/// where: (t) => t.lastName.equals('Jones'),
/// orderBy: (t) => t.firstName,
/// limit: 100,
/// );
/// ```
Future<List<SharedSpaceKey>> find(
_is.DatabaseSession session, {
_is.WhereExpressionBuilder<SharedSpaceKeyTable>? where,
int? limit,
int? offset,
_is.OrderByBuilder<SharedSpaceKeyTable>? orderBy,
_is.OrderByListBuilder<SharedSpaceKeyTable>? orderByList,
_is.Transaction? transaction,
_is.LockMode? lockMode,
_is.LockBehavior? lockBehavior,
}) async {
return session.db.find<SharedSpaceKey>(
where: where?.call(SharedSpaceKey.t),
orderBy: orderBy?.call(SharedSpaceKey.t),
orderByList: orderByList?.call(SharedSpaceKey.t),
limit: limit,
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [SharedSpaceKey] matching the given query parameters.
///
/// Use [where] to specify which items to include in the return value.
/// If none is specified, all items will be returned.
///
/// To specify the order use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// [offset] defines how many items to skip, after which the next one will be picked.
///
/// ```dart
/// var youngestPerson = await Persons.db.findFirstRow(
/// session,
/// where: (t) => t.lastName.equals('Jones'),
/// orderBy: (t) => t.age,
/// );
/// ```
Future<SharedSpaceKey?> findFirstRow(
_is.DatabaseSession session, {
_is.WhereExpressionBuilder<SharedSpaceKeyTable>? where,
int? offset,
_is.OrderByBuilder<SharedSpaceKeyTable>? orderBy,
_is.OrderByListBuilder<SharedSpaceKeyTable>? orderByList,
_is.Transaction? transaction,
_is.LockMode? lockMode,
_is.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<SharedSpaceKey>(
where: where?.call(SharedSpaceKey.t),
orderBy: orderBy?.call(SharedSpaceKey.t),
orderByList: orderByList?.call(SharedSpaceKey.t),
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [SharedSpaceKey] by its [id] or null if no such row exists.
Future<SharedSpaceKey?> findById(
_is.DatabaseSession session,
_is.UuidValue id, {
_is.Transaction? transaction,
_is.LockMode? lockMode,
_is.LockBehavior? lockBehavior,
}) async {
return session.db.findById<SharedSpaceKey>(
id,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [SharedSpaceKey]s in the list and returns the inserted rows.
///
/// The returned [SharedSpaceKey]s will have their `id` fields set.
///
/// This is an atomic operation, meaning that if one of the rows fails to
/// insert, none of the rows will be inserted.
///
/// If [ignoreConflicts] is set to `true`, rows that conflict with existing
/// rows are silently skipped, and only the successfully inserted rows are
/// returned.
///
/// If [noReturn] is set to `true`, the inserted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<SharedSpaceKey>> insert(
_is.DatabaseSession session,
List<SharedSpaceKey> rows, {
_is.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<SharedSpaceKey>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [SharedSpaceKey] and returns the inserted row.
///
/// The returned [SharedSpaceKey] will have its `id` field set.
Future<SharedSpaceKey> insertRow(
_is.DatabaseSession session,
SharedSpaceKey row, {
_is.Transaction? transaction,
}) async {
return session.db.insertRow<SharedSpaceKey>(
row,
transaction: transaction,
);
}
/// Upserts all [SharedSpaceKey]s in the list and returns the resulting rows.
///
/// If a row conflicts on the given [conflictColumns], the existing row is
/// updated with the new values. Otherwise, a new row is inserted.
///
/// If [updateColumns] is provided, only those columns will be updated on
/// conflict. If null, all non-conflict, non-id columns are updated.
///
/// If [updateWhere] is provided, the update only applies to rows matching the
/// given expression. Conflicting rows that don't match are skipped and not
/// returned, so the resulting list may be shorter than [rows].
///
/// The returned [SharedSpaceKey]s will have their `id` fields set.
///
/// This is an atomic operation, meaning that if one of the rows fails,
/// none of the rows will be affected.
///
/// If [noReturn] is set to `true`, the resulting rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<SharedSpaceKey>> upsert(
_is.DatabaseSession session,
List<SharedSpaceKey> rows, {
required _is.ColumnSelections<SharedSpaceKeyTable> conflictColumns,
_is.ColumnSelections<SharedSpaceKeyTable>? updateColumns,
_is.WhereExpressionBuilder<SharedSpaceKeyTable>? updateWhere,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<SharedSpaceKey>(
rows,
conflictColumns: conflictColumns(SharedSpaceKey.t),
updateColumns: updateColumns?.call(SharedSpaceKey.t),
updateWhere: updateWhere?.call(SharedSpaceKey.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [SharedSpaceKey] and returns the resulting row.
///
/// If the row conflicts on the given [conflictColumns], the existing row is
/// updated. Otherwise, a new row is inserted.
///
/// If [updateColumns] is provided, only those columns will be updated on
/// conflict. If null, all non-conflict, non-id columns are updated.
///
/// If [updateWhere] is provided, the update only applies when the existing
/// row matches the expression. Returns `null` if no row was affected — for
/// example when [updateWhere] does not match the conflicting row.
///
/// The returned [SharedSpaceKey] will have its `id` field set.
Future<SharedSpaceKey?> upsertRow(
_is.DatabaseSession session,
SharedSpaceKey row, {
required _is.ColumnSelections<SharedSpaceKeyTable> conflictColumns,
_is.ColumnSelections<SharedSpaceKeyTable>? updateColumns,
_is.WhereExpressionBuilder<SharedSpaceKeyTable>? updateWhere,
_is.Transaction? transaction,
}) async {
return session.db.upsertRow<SharedSpaceKey>(
row,
conflictColumns: conflictColumns(SharedSpaceKey.t),
updateColumns: updateColumns?.call(SharedSpaceKey.t),
updateWhere: updateWhere?.call(SharedSpaceKey.t),
transaction: transaction,
);
}
/// Updates all [SharedSpaceKey]s in the list and returns the updated rows. If
/// [columns] is provided, only those columns will be updated. Defaults to
/// all columns.
/// This is an atomic operation, meaning that if one of the rows fails to
/// update, none of the rows will be updated.
///
/// If [noReturn] is set to `true`, the updated rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<SharedSpaceKey>> update(
_is.DatabaseSession session,
List<SharedSpaceKey> rows, {
_is.ColumnSelections<SharedSpaceKeyTable>? columns,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<SharedSpaceKey>(
rows,
columns: columns?.call(SharedSpaceKey.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [SharedSpaceKey]. The row needs to have its id set.
/// Optionally, a list of [columns] can be provided to only update those
/// columns. Defaults to all columns.
Future<SharedSpaceKey> updateRow(
_is.DatabaseSession session,
SharedSpaceKey row, {
_is.ColumnSelections<SharedSpaceKeyTable>? columns,
_is.Transaction? transaction,
}) async {
return session.db.updateRow<SharedSpaceKey>(
row,
columns: columns?.call(SharedSpaceKey.t),
transaction: transaction,
);
}
/// Updates a single [SharedSpaceKey] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<SharedSpaceKey?> updateById(
_is.DatabaseSession session,
_is.UuidValue id, {
required _is.ColumnValueListBuilder<SharedSpaceKeyUpdateTable> columnValues,
_is.Transaction? transaction,
}) async {
return session.db.updateById<SharedSpaceKey>(
id,
columnValues: columnValues(SharedSpaceKey.t.updateTable),
transaction: transaction,
);
}
/// Updates all [SharedSpaceKey]s matching the [where] expression with the specified [columnValues].
/// Returns the list of updated rows.
///
/// If [noReturn] is set to `true`, the updated rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<SharedSpaceKey>> updateWhere(
_is.DatabaseSession session, {
required _is.ColumnValueListBuilder<SharedSpaceKeyUpdateTable> columnValues,
required _is.WhereExpressionBuilder<SharedSpaceKeyTable> where,
int? limit,
int? offset,
_is.OrderByBuilder<SharedSpaceKeyTable>? orderBy,
_is.OrderByListBuilder<SharedSpaceKeyTable>? orderByList,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<SharedSpaceKey>(
columnValues: columnValues(SharedSpaceKey.t.updateTable),
where: where(SharedSpaceKey.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(SharedSpaceKey.t),
orderByList: orderByList?.call(SharedSpaceKey.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [SharedSpaceKey]s in the list and returns the deleted rows.
///
/// To specify the order of the returned rows use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// This is an atomic operation, meaning that if one of the rows fail to
/// be deleted, none of the rows will be deleted.
///
/// If [noReturn] is set to `true`, the deleted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<SharedSpaceKey>> delete(
_is.DatabaseSession session,
List<SharedSpaceKey> rows, {
_is.OrderByBuilder<SharedSpaceKeyTable>? orderBy,
_is.OrderByListBuilder<SharedSpaceKeyTable>? orderByList,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<SharedSpaceKey>(
rows,
orderBy: orderBy?.call(SharedSpaceKey.t),
orderByList: orderByList?.call(SharedSpaceKey.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [SharedSpaceKey].
Future<SharedSpaceKey> deleteRow(
_is.DatabaseSession session,
SharedSpaceKey row, {
_is.Transaction? transaction,
}) async {
return session.db.deleteRow<SharedSpaceKey>(
row,
transaction: transaction,
);
}
/// Deletes all rows matching the [where] expression.
///
/// To specify the order of the returned rows use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// If [noReturn] is set to `true`, the deleted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<SharedSpaceKey>> deleteWhere(
_is.DatabaseSession session, {
required _is.WhereExpressionBuilder<SharedSpaceKeyTable> where,
_is.OrderByBuilder<SharedSpaceKeyTable>? orderBy,
_is.OrderByListBuilder<SharedSpaceKeyTable>? orderByList,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<SharedSpaceKey>(
where: where(SharedSpaceKey.t),
orderBy: orderBy?.call(SharedSpaceKey.t),
orderByList: orderByList?.call(SharedSpaceKey.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Counts the number of rows matching the [where] expression. If omitted,
/// will return the count of all rows in the table.
Future<int> count(
_is.DatabaseSession session, {
_is.WhereExpressionBuilder<SharedSpaceKeyTable>? where,
int? limit,
_is.Transaction? transaction,
}) async {
return session.db.count<SharedSpaceKey>(
where: where?.call(SharedSpaceKey.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [SharedSpaceKey] rows matching the [where] expression.
Future<void> lockRows(
_is.DatabaseSession session, {
required _is.WhereExpressionBuilder<SharedSpaceKeyTable> where,
required _is.LockMode lockMode,
required _is.Transaction transaction,
_is.LockBehavior lockBehavior = _is.LockBehavior.wait,
}) async {
return session.db.lockRows<SharedSpaceKey>(
where: where(SharedSpaceKey.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}

View file

@ -0,0 +1,94 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:serverpod/serverpod.dart' as _is;
/// A key the caller holds that a member of the space lacks: for the
/// caller's device to seal to them (ADR 0044, D3).
abstract class SharedSpaceKeyNeed
implements _is.SerializableModel, _is.ProtocolSerialization {
SharedSpaceKeyNeed._({
required this.keyId,
required this.person,
});
factory SharedSpaceKeyNeed({
required String keyId,
required _is.UuidValue person,
}) = _SharedSpaceKeyNeedImpl;
factory SharedSpaceKeyNeed.fromJson(Map<String, dynamic> jsonSerialization) {
return SharedSpaceKeyNeed(
keyId: jsonSerialization['keyId'] as String,
person: _is.UuidValueJsonExtension.fromJson(jsonSerialization['person']),
);
}
String keyId;
/// The member who lacks it.
_is.UuidValue person;
/// Returns a shallow copy of this [SharedSpaceKeyNeed]
/// with some or all fields replaced by the given arguments.
@_is.useResult
SharedSpaceKeyNeed copyWith({
String? keyId,
_is.UuidValue? person,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'halkyon_base.SharedSpaceKeyNeed',
'keyId': keyId,
'person': person.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'halkyon_base.SharedSpaceKeyNeed',
'keyId': keyId,
'person': person.toJson(),
};
}
@override
String toString() {
return _is.SerializationManager.encode(this);
}
}
class _SharedSpaceKeyNeedImpl extends SharedSpaceKeyNeed {
_SharedSpaceKeyNeedImpl({
required String keyId,
required _is.UuidValue person,
}) : super._(
keyId: keyId,
person: person,
);
/// Returns a shallow copy of this [SharedSpaceKeyNeed]
/// with some or all fields replaced by the given arguments.
@_is.useResult
@override
SharedSpaceKeyNeed copyWith({
String? keyId,
_is.UuidValue? person,
}) {
return SharedSpaceKeyNeed(
keyId: keyId ?? this.keyId,
person: person ?? this.person,
);
}
}

View file

@ -0,0 +1,130 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:typed_data' as _idt;
import 'package:serverpod/serverpod.dart' as _is;
/// A member's public keys, as the register keeps them, which a device pins
/// the first time it sees them and seals keys to (ADR 0044, D3 and D18).
/// Named by the member's account ID, as the space's synced rows name their
/// authors: a device pins keys per person, across every space it shares
/// with them.
abstract class SharedSpaceMemberKeys
implements _is.SerializableModel, _is.ProtocolSerialization {
SharedSpaceMemberKeys._({
required this.person,
this.handle,
required this.encryption,
required this.signing,
});
factory SharedSpaceMemberKeys({
required _is.UuidValue person,
String? handle,
required _idt.ByteData encryption,
required _idt.ByteData signing,
}) = _SharedSpaceMemberKeysImpl;
factory SharedSpaceMemberKeys.fromJson(
Map<String, dynamic> jsonSerialization,
) {
return SharedSpaceMemberKeys(
person: _is.UuidValueJsonExtension.fromJson(jsonSerialization['person']),
handle: jsonSerialization['handle'] as String?,
encryption: _is.ByteDataJsonExtension.fromJson(
jsonSerialization['encryption'],
),
signing: _is.ByteDataJsonExtension.fromJson(jsonSerialization['signing']),
);
}
_is.UuidValue person;
/// Their handle; null without a public profile.
String? handle;
/// X25519's, which keys are sealed to.
_idt.ByteData encryption;
/// Ed25519's, which their signatures check against.
_idt.ByteData signing;
/// Returns a shallow copy of this [SharedSpaceMemberKeys]
/// with some or all fields replaced by the given arguments.
@_is.useResult
SharedSpaceMemberKeys copyWith({
_is.UuidValue? person,
String? handle,
_idt.ByteData? encryption,
_idt.ByteData? signing,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'halkyon_base.SharedSpaceMemberKeys',
'person': person.toJson(),
if (handle != null) 'handle': handle,
'encryption': encryption.toJson(),
'signing': signing.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'halkyon_base.SharedSpaceMemberKeys',
'person': person.toJson(),
if (handle != null) 'handle': handle,
'encryption': encryption.toJson(),
'signing': signing.toJson(),
};
}
@override
String toString() {
return _is.SerializationManager.encode(this);
}
}
class _Undefined {}
class _SharedSpaceMemberKeysImpl extends SharedSpaceMemberKeys {
_SharedSpaceMemberKeysImpl({
required _is.UuidValue person,
String? handle,
required _idt.ByteData encryption,
required _idt.ByteData signing,
}) : super._(
person: person,
handle: handle,
encryption: encryption,
signing: signing,
);
/// Returns a shallow copy of this [SharedSpaceMemberKeys]
/// with some or all fields replaced by the given arguments.
@_is.useResult
@override
SharedSpaceMemberKeys copyWith({
_is.UuidValue? person,
Object? handle = _Undefined,
_idt.ByteData? encryption,
_idt.ByteData? signing,
}) {
return SharedSpaceMemberKeys(
person: person ?? this.person,
handle: handle is String? ? handle : this.handle,
encryption: encryption ?? this.encryption.clone(),
signing: signing ?? this.signing.clone(),
);
}
}

View file

@ -0,0 +1,112 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:typed_data' as _idt;
import 'package:serverpod/serverpod.dart' as _is;
/// A key a device sealed to a member, as it sends it (ADR 0044, D3), or as
/// the member gets it back: [person] is whom it is sealed to on the way
/// in, and who sealed it on the way out.
abstract class SharedSpaceSealedKey
implements _is.SerializableModel, _is.ProtocolSerialization {
SharedSpaceSealedKey._({
required this.keyId,
required this.person,
required this.sealedKey,
});
factory SharedSpaceSealedKey({
required String keyId,
required _is.UuidValue person,
required _idt.ByteData sealedKey,
}) = _SharedSpaceSealedKeyImpl;
factory SharedSpaceSealedKey.fromJson(
Map<String, dynamic> jsonSerialization,
) {
return SharedSpaceSealedKey(
keyId: jsonSerialization['keyId'] as String,
person: _is.UuidValueJsonExtension.fromJson(jsonSerialization['person']),
sealedKey: _is.ByteDataJsonExtension.fromJson(
jsonSerialization['sealedKey'],
),
);
}
String keyId;
_is.UuidValue person;
/// The sealer's signature, then the sealed box.
_idt.ByteData sealedKey;
/// Returns a shallow copy of this [SharedSpaceSealedKey]
/// with some or all fields replaced by the given arguments.
@_is.useResult
SharedSpaceSealedKey copyWith({
String? keyId,
_is.UuidValue? person,
_idt.ByteData? sealedKey,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'halkyon_base.SharedSpaceSealedKey',
'keyId': keyId,
'person': person.toJson(),
'sealedKey': sealedKey.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'halkyon_base.SharedSpaceSealedKey',
'keyId': keyId,
'person': person.toJson(),
'sealedKey': sealedKey.toJson(),
};
}
@override
String toString() {
return _is.SerializationManager.encode(this);
}
}
class _SharedSpaceSealedKeyImpl extends SharedSpaceSealedKey {
_SharedSpaceSealedKeyImpl({
required String keyId,
required _is.UuidValue person,
required _idt.ByteData sealedKey,
}) : super._(
keyId: keyId,
person: person,
sealedKey: sealedKey,
);
/// Returns a shallow copy of this [SharedSpaceSealedKey]
/// with some or all fields replaced by the given arguments.
@_is.useResult
@override
SharedSpaceSealedKey copyWith({
String? keyId,
_is.UuidValue? person,
_idt.ByteData? sealedKey,
}) {
return SharedSpaceSealedKey(
keyId: keyId ?? this.keyId,
person: person ?? this.person,
sealedKey: sealedKey ?? this.sealedKey.clone(),
);
}
}

View file

@ -0,0 +1,9 @@
### An account's public keys as the register keeps them (ADR 0017): X25519's,
### which data keys are sealed to, and Ed25519's, which its signatures check
### against. What others seal shared keys to and check them by (Halkyon's ADR
### 0044, D3 and D18).
class: AccountPublicKeys
serverOnly: true
fields:
encryption: ByteData
signing: ByteData

View file

@ -53,6 +53,14 @@ abstract interface class AccountRegister {
Set<UuidValue> people,
);
/// The public keys of those of [people] who have keys (ADR 0017), by
/// person: what a device seals a shared key to and checks its sealer by
/// (Halkyon's ADR 0044, D3), pinning them (D18).
Future<Map<UuidValue, AccountPublicKeys>> publicKeysOf(
Session session,
Set<UuidValue> people,
);
/// The public profile whose handle is [handle], whatever its case; null for
/// none.
Future<Profile?> profileByHandle(Session session, String handle);

View file

@ -109,6 +109,22 @@ Future<Map<String, Object?>?> _answer(
key.uuid: value.toJson(),
},
};
case RegisterOperation.publicKeysOf:
final keys = await register.publicKeysOf(
session,
arguments.ids('people'),
);
return {
'keys': {
for (final MapEntry(:key, :value) in keys.entries)
key.uuid: {
'encryption': base64Encode(
Uint8List.sublistView(value.encryption),
),
'signing': base64Encode(Uint8List.sublistView(value.signing)),
},
},
};
case RegisterOperation.profileByHandle:
final profile = await register.profileByHandle(
session,

View file

@ -65,6 +65,28 @@ final class LocalRegister implements AccountRegister {
};
}
@override
Future<Map<UuidValue, AccountPublicKeys>> publicKeysOf(
Session session,
Set<UuidValue> people,
) async {
if (people.isEmpty) {
return const {};
}
// SELECT … FROM "account_password" WHERE "authUserId" = ANY($1)
final rows = await AccountPassword.db.find(
session,
where: (t) => t.authUserId.inSet(people),
);
return {
for (final row in rows)
row.authUserId: AccountPublicKeys(
encryption: row.encryptionPublicKey,
signing: row.signingPublicKey,
),
};
}
@override
Future<Profile?> profileByHandle(Session session, String handle) async {
if (!isHandle(handle)) {

View file

@ -16,6 +16,7 @@ const registerPath = '/halkyon-base/register';
abstract final class RegisterOperation {
static const profileOf = 'profile-of';
static const profilesOf = 'profiles-of';
static const publicKeysOf = 'public-keys-of';
static const profileByHandle = 'profile-by-handle';
static const personWithHandle = 'person-with-handle';
static const saveProfile = 'save-profile';

View file

@ -140,6 +140,32 @@ final class RemoteRegister implements AccountRegister {
};
}
@override
Future<Map<UuidValue, AccountPublicKeys>> publicKeysOf(
Session session,
Set<UuidValue> people,
) async {
if (people.isEmpty) {
return const {};
}
final answer = await _ask(RegisterOperation.publicKeysOf, {
'people': [for (final person in people) person.uuid],
});
ByteData bytes(Object? value) => switch (value) {
final String text => ByteData.sublistView(base64Decode(text)),
_ => throw RegisterUnavailableException('publicKeysOf: no keys'),
};
return {
if (answer['keys'] case final Map<String, Object?> keys)
for (final MapEntry(:key, :value) in keys.entries)
if (value case final Map<String, Object?> pair)
UuidValue.withValidation(key): AccountPublicKeys(
encryption: bytes(pair['encryption']),
signing: bytes(pair['signing']),
),
};
}
@override
Future<Profile?> profileByHandle(Session session, String handle) async =>
_profile(

View file

@ -0,0 +1,26 @@
### A data key of a shared space sealed to one member (ADR 0044, D3): by a
### device of a member who writes and holds it, to the member's X25519
### public key, signed by the sealer (halkyon_keys' `sealDataKey`). The
### instance keeps it as it is and never opens it. The first one sealed for
### a member and key stays; the member's go with their membership.
class: SharedSpaceKey
serverOnly: true
table: shared_space_key
fields:
id: UuidValue?, defaultPersist=random_v7
sharedSpaceId: UuidValue, relation(parent=shared_space, onDelete=Cascade)
### The key's ID, which the rows it seals name in their `keyId`.
keyId: String
### Whom it is sealed to: the register's account ID.
recipientId: UuidValue
### The sealer's signature, then the sealed box.
sealedKey: ByteData
### Who sealed it: the register's account ID, whose signing key checks it.
sealedBy: UuidValue
createdAt: DateTime
indexes:
shared_space_key_unique_idx:
fields: sharedSpaceId, keyId, recipientId
unique: true
shared_space_key_recipient_idx:
fields: recipientId

View file

@ -0,0 +1,7 @@
### A key the caller holds that a member of the space lacks: for the
### caller's device to seal to them (ADR 0044, D3).
class: SharedSpaceKeyNeed
fields:
keyId: String
### The member who lacks it.
person: UuidValue

View file

@ -0,0 +1,186 @@
import 'package:clock/clock.dart';
import 'package:halkyon_base_server/src/auth/scopes.dart';
import 'package:halkyon_base_server/src/generated/protocol.dart';
import 'package:halkyon_base_server/src/profile/profiles.dart';
import 'package:halkyon_base_server/src/register/account_register.dart';
import 'package:serverpod/serverpod.dart';
import 'package:serverpod_auth_idp_server/core.dart';
/// The data keys of a space you share, each sealed on a device to one
/// member (ADR 0044, D3): the members' public keys to seal to and pin
/// (D18), the keys a member still lacks, sealing them, and your own. The
/// instance keeps what devices sealed and never opens it. Only for people
/// with an identity; a space you aren't a member of can't be told from one
/// that doesn't exist ([SharedSpaceNotFoundException]).
class SharedSpaceKeysEndpoint extends Endpoint {
@override
Set<Scope> get requiredScopes => {identified};
/// The public keys of [space]'s members who have keys, you among them,
/// as the register keeps them.
Future<List<SharedSpaceMemberKeys>> memberKeys(
Session session,
UuidValue space,
) async {
final members = await _membersAsCaller(session, space);
final people = {for (final member in members) member.authUserId};
final (keys, profiles) = await (
AccountRegister.of(session.serverpod).publicKeysOf(session, people),
profilesOf(session, people),
).wait;
return [
for (final member in members)
if (keys[member.authUserId] case final keys?)
SharedSpaceMemberKeys(
person: member.authUserId,
handle: profiles[member.authUserId]?.handle,
encryption: keys.encryption,
signing: keys.signing,
),
];
}
/// The keys of [space] you hold that members who have keys lack, for
/// your device to seal to them; none while you may not seal ([seal]).
Future<List<SharedSpaceKeyNeed>> keysNeeded(
Session session,
UuidValue space,
) async {
final person = _caller(session);
final members = await _membersAsCaller(session, space);
if (!_writes(members, person)) {
return const [];
}
// SELECT … FROM "shared_space_key" WHERE "sharedSpaceId" = $1
final sealed = await SharedSpaceKey.db.find(
session,
where: (t) => t.sharedSpaceId.equals(space),
);
final held = {
for (final key in sealed)
if (key.recipientId == person) key.keyId,
};
final keys = await AccountRegister.of(session.serverpod).publicKeysOf(
session,
{for (final member in members) member.authUserId},
);
return [
for (final keyId in held.toList()..sort())
for (final member in members)
if (keys.containsKey(member.authUserId) &&
!sealed.any(
(key) =>
key.keyId == keyId && key.recipientId == member.authUserId,
))
SharedSpaceKeyNeed(keyId: keyId, person: member.authUserId),
];
}
/// Keeps [keys], each sealed on your device to the member it names, as
/// sealed by you; one a member has already stays as it was, as does the
/// first of two the call names alike. Refused for a
/// Viewer: only those who write, the Owner and Editors, seal keys, so a
/// reader can't hand a newcomer a key of their own making.
Future<void> seal(
Session session,
UuidValue space,
List<SharedSpaceSealedKey> keys,
) async {
final person = _caller(session);
final members = await _membersAsCaller(session, space);
if (!_writes(members, person)) {
throw SharedSpaceRefusedException(reason: SharedSpaceRefusal.notAllowed);
}
final memberIds = {for (final member in members) member.authUserId};
if (keys.any((key) => !memberIds.contains(key.person))) {
throw SharedSpaceNotFoundException(what: 'member');
}
final now = clock.now().toUtc();
await session.db.transaction((transaction) async {
// The space's row held, so two devices sealing at once take turns.
// SELECT … FROM "shared_space" WHERE "id" = $1 FOR UPDATE
await SharedSpace.db.findById(
session,
space,
transaction: transaction,
lockMode: LockMode.forUpdate,
);
// SELECT … FROM "shared_space_key" WHERE "sharedSpaceId" = $1
final there = {
for (final key in await SharedSpaceKey.db.find(
session,
where: (t) => t.sharedSpaceId.equals(space),
transaction: transaction,
))
(key.keyId, key.recipientId),
};
final added = <(String, UuidValue)>{};
// INSERT INTO "shared_space_key" (…) VALUES (…), …
await SharedSpaceKey.db.insert(session, [
for (final key in keys)
if (!there.contains((key.keyId, key.person)) &&
added.add((key.keyId, key.person)))
SharedSpaceKey(
sharedSpaceId: space,
keyId: key.keyId,
recipientId: key.person,
sealedKey: key.sealedKey,
sealedBy: person,
createdAt: now,
),
], transaction: transaction);
});
}
/// The keys of [space] sealed to you, each naming who sealed it.
Future<List<SharedSpaceSealedKey>> mine(
Session session,
UuidValue space,
) async {
final person = _caller(session);
await _membersAsCaller(session, space);
// SELECT … FROM "shared_space_key"
// WHERE "sharedSpaceId" = $1 AND "recipientId" = $2
return [
for (final key in await SharedSpaceKey.db.find(
session,
where: (t) =>
t.sharedSpaceId.equals(space) & t.recipientId.equals(person),
orderBy: (t) => t.createdAt,
))
SharedSpaceSealedKey(
keyId: key.keyId,
person: key.sealedBy,
sealedKey: key.sealedKey,
),
];
}
/// The members of [space] where the caller is one of them;
/// [SharedSpaceNotFoundException] otherwise.
Future<List<SharedSpaceMember>> _membersAsCaller(
Session session,
UuidValue space,
) async {
final person = _caller(session);
// SELECT … FROM "shared_space_member" WHERE "sharedSpaceId" = $1
// ORDER BY "joinedAt"
final members = await SharedSpaceMember.db.find(
session,
where: (t) => t.sharedSpaceId.equals(space),
orderBy: (t) => t.joinedAt,
);
if (!members.any((member) => member.authUserId == person)) {
throw SharedSpaceNotFoundException(what: 'space');
}
return members;
}
/// Whether [person] writes in the space of [members]: its Owner or an
/// Editor.
bool _writes(List<SharedSpaceMember> members, UuidValue person) =>
members.firstWhere((member) => member.authUserId == person).role !=
SharedSpaceRole.viewer;
UuidValue _caller(Session session) => session.authenticated!.authUserId;
}

View file

@ -0,0 +1,14 @@
### A member's public keys, as the register keeps them, which a device pins
### the first time it sees them and seals keys to (ADR 0044, D3 and D18).
### Named by the member's account ID, as the space's synced rows name their
### authors: a device pins keys per person, across every space it shares
### with them.
class: SharedSpaceMemberKeys
fields:
person: UuidValue
### Their handle; null without a public profile.
handle: String?
### X25519's, which keys are sealed to.
encryption: ByteData
### Ed25519's, which their signatures check against.
signing: ByteData

View file

@ -0,0 +1,9 @@
### A key a device sealed to a member, as it sends it (ADR 0044, D3), or as
### the member gets it back: [person] is whom it is sealed to on the way
### in, and who sealed it on the way out.
class: SharedSpaceSealedKey
fields:
keyId: String
person: UuidValue
### The sealer's signature, then the sealed box.
sealedKey: ByteData

Some files were not shown because too many files have changed in this diff Show more