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:
parent
d08d8ad545
commit
b72cea1fe0
51 changed files with 105813 additions and 5 deletions
|
|
@ -42,11 +42,18 @@
|
|||
/// - **Sync**: `SyncService` on triggers, the changes still waiting, and
|
||||
/// where it stands (`SyncStatusBloc`, `SyncStatusCard`, `AppMismatchBanner`,
|
||||
/// `SyncTroubleBadge`).
|
||||
/// - **Shared spaces' keys** (Halkyon's ADR 0044, D3, D18): `SharedKeys`
|
||||
/// seals a space's data keys to its members and opens those sealed to
|
||||
/// the person, holding each member to the public keys `KeyPins` pinned
|
||||
/// on first sight, with the safety code to compare them by.
|
||||
///
|
||||
/// An app gives its name, look and own words as an `AppFrame`, and installs
|
||||
/// `HalkyonAppLocalizations.delegate` beside its own.
|
||||
library;
|
||||
|
||||
// A member's public keys, as `SharedKeys` and `KeyPins` take them.
|
||||
export 'package:halkyon_keys/halkyon_keys.dart' show PublicKeys;
|
||||
|
||||
export 'l10n/halkyon_app_localizations.dart';
|
||||
export 'src/app/about.dart';
|
||||
export 'src/app/app_frame.dart' show AppFrame, AppFrameScope, LookSettings;
|
||||
|
|
@ -177,6 +184,10 @@ export 'src/profile/model/public_profile.dart';
|
|||
export 'src/profile/widget/halkyon_profile.dart';
|
||||
export 'src/profile/widget/profile_sheet.dart';
|
||||
export 'src/profile/widget/public_profile_section.dart';
|
||||
export 'src/shared_space/key_pins.dart';
|
||||
export 'src/shared_space/serverpod_shared_keys_gateway.dart';
|
||||
export 'src/shared_space/shared_keys.dart';
|
||||
export 'src/shared_space/shared_keys_gateway.dart';
|
||||
export 'src/simplelogin/bloc/simplelogin_cubit.dart';
|
||||
export 'src/simplelogin/bloc/simplelogin_standing_cubit.dart';
|
||||
export 'src/simplelogin/data/serverpod_simplelogin_gateway.dart';
|
||||
|
|
|
|||
|
|
@ -45,6 +45,31 @@ abstract interface class ContentCipher() {
|
|||
/// Whether the key [keyId] is open here.
|
||||
bool has(String keyId);
|
||||
|
||||
/// The account's own public keys. Throws a [StateError] before [unlock].
|
||||
PublicKeys get ownKeys;
|
||||
|
||||
/// The open key [keyId] sealed to another account, whose public keys are
|
||||
/// [to], and signed by this one (Halkyon's ADR 0044, D3): for a member of
|
||||
/// a space shared with them. Throws a [StateError] for a key not open
|
||||
/// here.
|
||||
Uint8List sealKeyTo(String keyId, PublicKeys to);
|
||||
|
||||
/// Opens [sealed] as the data key [keyId] another account sealed to this
|
||||
/// one, checked against that account's signing key [sealedBy], kept open
|
||||
/// here from then on; false where it doesn't open (not sealed to this
|
||||
/// account, sealed or signed by another, changed). Throws a [StateError]
|
||||
/// before [unlock].
|
||||
bool openSharedKey(
|
||||
String keyId,
|
||||
Uint8List sealed, {
|
||||
required Uint8List sealedBy,
|
||||
});
|
||||
|
||||
/// The safety code of this account and the one whose public keys are
|
||||
/// [other] (halkyon_keys' `safetyCode`, ADR 0044, D18), the same on both
|
||||
/// sides. Throws a [StateError] before [unlock].
|
||||
String safetyCodeWith(PublicKeys other);
|
||||
|
||||
/// [plain] sealed with the key [keyId] for the [context] it is stored in
|
||||
/// (its table, row and column), so it opens nowhere else. Throws a
|
||||
/// [StateError] for a key not open here.
|
||||
|
|
@ -135,6 +160,58 @@ class SodiumContentCipher() implements ContentCipher {
|
|||
@override
|
||||
bool has(String keyId) => _keys.containsKey(keyId);
|
||||
|
||||
@override
|
||||
PublicKeys get ownKeys {
|
||||
final (_, account) = _unlocked();
|
||||
return (
|
||||
encryption: account.encryptionPublicKey,
|
||||
signing: account.signingPublicKey,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Uint8List sealKeyTo(String keyId, PublicKeys to) {
|
||||
final (sodium, account) = _unlocked();
|
||||
final key = _keys[keyId];
|
||||
if (key == null) {
|
||||
throw StateError('The data key $keyId is not open');
|
||||
}
|
||||
return sealDataKey(sodium, key, to: to.encryption, sealer: account);
|
||||
}
|
||||
|
||||
@override
|
||||
bool openSharedKey(
|
||||
String keyId,
|
||||
Uint8List sealed, {
|
||||
required Uint8List sealedBy,
|
||||
}) {
|
||||
final (sodium, account) = _unlocked();
|
||||
try {
|
||||
final key = openDataKey(
|
||||
sodium,
|
||||
sealed,
|
||||
keyId: keyId,
|
||||
keys: account,
|
||||
sealedBy: sealedBy,
|
||||
);
|
||||
_keys.remove(keyId)?.dispose();
|
||||
_keys[keyId] = key;
|
||||
return true;
|
||||
} on KeyRefused catch (refused) {
|
||||
log(
|
||||
'A shared data key not opened: ${refused.reason.name}',
|
||||
name: 'Privacy',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String safetyCodeWith(PublicKeys other) {
|
||||
final (sodium, _) = _unlocked();
|
||||
return safetyCode(sodium, ownKeys, other);
|
||||
}
|
||||
|
||||
@override
|
||||
SealedPayload seal(
|
||||
Uint8List plain, {
|
||||
|
|
|
|||
158
packages/halkyon_app/lib/src/shared_space/key_pins.dart
Normal file
158
packages/halkyon_app/lib/src/shared_space/key_pins.dart
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_keys/halkyon_keys.dart' show PublicKeys;
|
||||
|
||||
/// The public keys a device first saw for a person, and whether their
|
||||
/// safety code was compared (Halkyon's ADR 0044, D18).
|
||||
final class const PinnedKeys({
|
||||
required final PublicKeys keys,
|
||||
final bool verified = false,
|
||||
}) {
|
||||
/// From what [toJson] wrote; null for anything else.
|
||||
static PinnedKeys? fromJson(Object? json) => switch (json) {
|
||||
{
|
||||
'encryption': final String encryption,
|
||||
'signing': final String signing,
|
||||
'verified': final bool verified,
|
||||
} =>
|
||||
_decoded(encryption, signing, verified),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
static PinnedKeys? _decoded(
|
||||
String encryption,
|
||||
String signing,
|
||||
bool verified,
|
||||
) {
|
||||
try {
|
||||
return PinnedKeys(
|
||||
keys: (
|
||||
encryption: base64Decode(encryption),
|
||||
signing: base64Decode(signing),
|
||||
),
|
||||
verified: verified,
|
||||
);
|
||||
} on FormatException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object?> toJson() => {
|
||||
'encryption': base64Encode(keys.encryption),
|
||||
'signing': base64Encode(keys.signing),
|
||||
'verified': verified,
|
||||
};
|
||||
}
|
||||
|
||||
/// Where a device keeps the keys it pinned, by person (an account's ID).
|
||||
abstract interface class KeyPinStore() {
|
||||
Future<Map<String, PinnedKeys>> read();
|
||||
|
||||
Future<void> write(Map<String, PinnedKeys> pins);
|
||||
}
|
||||
|
||||
/// [KeyPinStore] in a JSON file in the person's folder (`key_pins.json`).
|
||||
class FileKeyPinStore(final File _file) implements KeyPinStore {
|
||||
@override
|
||||
Future<Map<String, PinnedKeys>> read() async {
|
||||
try {
|
||||
final json = jsonDecode(await _file.readAsString());
|
||||
return {
|
||||
if (json case final Map<String, Object?> pins)
|
||||
for (final MapEntry(:key, :value) in pins.entries)
|
||||
key: ?PinnedKeys.fromJson(value),
|
||||
};
|
||||
} on PathNotFoundException {
|
||||
return {};
|
||||
} on FormatException {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> write(Map<String, PinnedKeys> pins) async {
|
||||
await _file.parent.create(recursive: true);
|
||||
final temporary = File('${_file.path}.tmp');
|
||||
await temporary.writeAsString(
|
||||
jsonEncode({
|
||||
for (final MapEntry(:key, :value) in pins.entries) key: value.toJson(),
|
||||
}),
|
||||
flush: true,
|
||||
);
|
||||
await temporary.rename(_file.path);
|
||||
}
|
||||
}
|
||||
|
||||
/// How a person's keys compare with those pinned for them.
|
||||
enum PinCheck() {
|
||||
/// Seen for the first time, and pinned now.
|
||||
firstSeen,
|
||||
|
||||
/// The same as pinned.
|
||||
same,
|
||||
|
||||
/// Other than pinned: the instance swapped them, or the person's keys
|
||||
/// were reset. Nothing is sealed to them or taken from them until the
|
||||
/// person trusts the new keys ([KeyPins.trustAnew]).
|
||||
changed,
|
||||
}
|
||||
|
||||
/// The keys a device pinned for each person (ADR 0044, D18), as Signal and
|
||||
/// SSH pin them: the first seen are kept, a change refused until trusted
|
||||
/// anew.
|
||||
class KeyPins(final KeyPinStore _store) {
|
||||
Map<String, PinnedKeys>? _pins;
|
||||
|
||||
Future<Map<String, PinnedKeys>> _all() async => _pins ??= await _store.read();
|
||||
|
||||
/// How [keys] compare with those pinned for [person], pinning them when
|
||||
/// none are.
|
||||
Future<PinCheck> check(String person, PublicKeys keys) async {
|
||||
final pins = await _all();
|
||||
final pinned = pins[person];
|
||||
if (pinned == null) {
|
||||
pins[person] = PinnedKeys(keys: keys);
|
||||
await _store.write(pins);
|
||||
return PinCheck.firstSeen;
|
||||
}
|
||||
return sameKeys(pinned.keys, keys) ? PinCheck.same : PinCheck.changed;
|
||||
}
|
||||
|
||||
/// The keys pinned for [person]; null for none.
|
||||
Future<PinnedKeys?> of(String person) async => (await _all())[person];
|
||||
|
||||
/// Marks [person]'s pinned keys compared, by their safety code.
|
||||
Future<void> markVerified(String person) async {
|
||||
final pins = await _all();
|
||||
if (pins[person] case final pinned?) {
|
||||
pins[person] = PinnedKeys(keys: pinned.keys, verified: true);
|
||||
await _store.write(pins);
|
||||
}
|
||||
}
|
||||
|
||||
/// Pins [keys] for [person] in place of those before, not compared: the
|
||||
/// person said their keys changed for a reason of their own.
|
||||
Future<void> trustAnew(String person, PublicKeys keys) async {
|
||||
final pins = await _all();
|
||||
pins[person] = PinnedKeys(keys: keys);
|
||||
await _store.write(pins);
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether [a] and [b] are the same keys, compared in constant time.
|
||||
bool sameKeys(PublicKeys a, PublicKeys b) =>
|
||||
_bytesEqual(a.encryption, b.encryption) &&
|
||||
_bytesEqual(a.signing, b.signing);
|
||||
|
||||
bool _bytesEqual(Uint8List a, Uint8List b) {
|
||||
if (a.length != b.length) {
|
||||
return false;
|
||||
}
|
||||
var difference = 0;
|
||||
for (var i = 0; i < a.length; i++) {
|
||||
difference |= a[i] ^ b[i];
|
||||
}
|
||||
return difference == 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_app/src/shared_space/shared_keys_gateway.dart';
|
||||
import 'package:halkyon_base_client/halkyon_base_client.dart' as protocol;
|
||||
|
||||
/// [SharedKeysGateway] on halkyon_base's `sharedSpaceKeys` endpoint.
|
||||
class ServerpodSharedKeysGateway(final protocol.Caller _base)
|
||||
implements SharedKeysGateway {
|
||||
@override
|
||||
Future<List<MemberKeys>> memberKeys(String space) async => [
|
||||
for (final member in await _base.sharedSpaceKeys.memberKeys(_id(space)))
|
||||
(
|
||||
person: member.person.uuid,
|
||||
handle: member.handle,
|
||||
keys: (
|
||||
encryption: Uint8List.sublistView(member.encryption),
|
||||
signing: Uint8List.sublistView(member.signing),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
Future<List<KeyNeed>> keysNeeded(String space) async => [
|
||||
for (final need in await _base.sharedSpaceKeys.keysNeeded(_id(space)))
|
||||
(keyId: need.keyId, person: need.person.uuid),
|
||||
];
|
||||
|
||||
@override
|
||||
Future<void> seal(String space, List<SealedKey> keys) =>
|
||||
_base.sharedSpaceKeys.seal(_id(space), [
|
||||
for (final key in keys)
|
||||
protocol.SharedSpaceSealedKey(
|
||||
keyId: key.keyId,
|
||||
person: _id(key.person),
|
||||
sealedKey: ByteData.sublistView(key.sealed),
|
||||
),
|
||||
]);
|
||||
|
||||
@override
|
||||
Future<List<SealedKey>> mine(String space) async => [
|
||||
for (final key in await _base.sharedSpaceKeys.mine(_id(space)))
|
||||
(
|
||||
keyId: key.keyId,
|
||||
person: key.person.uuid,
|
||||
sealed: Uint8List.sublistView(key.sealedKey),
|
||||
),
|
||||
];
|
||||
|
||||
protocol.UuidValue _id(String id) => protocol.UuidValue.withValidation(id);
|
||||
}
|
||||
158
packages/halkyon_app/lib/src/shared_space/shared_keys.dart
Normal file
158
packages/halkyon_app/lib/src/shared_space/shared_keys.dart
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
import 'package:halkyon_app/src/privacy/content_cipher.dart';
|
||||
import 'package:halkyon_app/src/shared_space/key_pins.dart';
|
||||
import 'package:halkyon_app/src/shared_space/shared_keys_gateway.dart';
|
||||
import 'package:halkyon_keys/halkyon_keys.dart' show PublicKeys;
|
||||
|
||||
/// The instance hands other public keys for the person than their own on
|
||||
/// this device: nothing is sealed to keys it chose.
|
||||
final class OwnKeysDiffer() implements Exception {
|
||||
@override
|
||||
String toString() =>
|
||||
"OwnKeysDiffer: the instance's public keys for this account aren't "
|
||||
"the device's";
|
||||
}
|
||||
|
||||
/// What sealing a space's keys to its members did.
|
||||
typedef SealOutcome = ({
|
||||
/// The people a key was sealed to now.
|
||||
Set<String> sealedTo,
|
||||
|
||||
/// Members whose keys changed since this device pinned them, left out
|
||||
/// until trusted anew ([KeyPins.trustAnew]).
|
||||
Set<String> changed,
|
||||
});
|
||||
|
||||
/// What opening the keys sealed to the person did.
|
||||
typedef OpenOutcome = ({
|
||||
/// The keys opened, kept open in the cipher.
|
||||
Set<String> opened,
|
||||
|
||||
/// Keys that didn't open: sealed or signed by keys other than those
|
||||
/// pinned for their sealer, changed, or from a sealer unknown here.
|
||||
Set<String> refused,
|
||||
|
||||
/// Sealers whose keys changed since this device pinned them; their keys
|
||||
/// stay unopened until trusted anew.
|
||||
Set<String> changed,
|
||||
});
|
||||
|
||||
/// A shared space's data keys on the device (Halkyon's ADR 0044, D3, D18):
|
||||
/// each sealed to every member's public keys, which this device pins on
|
||||
/// first sight and holds to from then on, as Signal and SSH do; the
|
||||
/// instance only carries what devices sealed.
|
||||
class SharedKeys(
|
||||
final SharedKeysGateway _gateway,
|
||||
final KeyPins _pins,
|
||||
final ContentCipher _cipher, {
|
||||
required final String me,
|
||||
}) {
|
||||
/// Seals the open key [keyId] to every member of [space] who has keys,
|
||||
/// the person among them: for a space's first key, or the one replacing
|
||||
/// it after someone left. Throws [OwnKeysDiffer] where the instance's
|
||||
/// keys for the person aren't the device's.
|
||||
Future<SealOutcome> share(String space, String keyId) async {
|
||||
final members = await _checkedMembers(space);
|
||||
final sealed = [
|
||||
for (final MapEntry(key: person, value: keys) in members.trusted.entries)
|
||||
(keyId: keyId, person: person, sealed: _cipher.sealKeyTo(keyId, keys)),
|
||||
];
|
||||
await _gateway.seal(space, sealed);
|
||||
return (
|
||||
sealedTo: {for (final key in sealed) key.person},
|
||||
changed: members.changed,
|
||||
);
|
||||
}
|
||||
|
||||
/// Seals the keys of [space] that this device holds to the members who
|
||||
/// lack them: newcomers, and those who have keys only now. Nothing for a
|
||||
/// Viewer, whom the instance asks for none.
|
||||
Future<SealOutcome> sealForNewcomers(String space) async {
|
||||
final needed = await _gateway.keysNeeded(space);
|
||||
if (needed.isEmpty) {
|
||||
return (sealedTo: <String>{}, changed: <String>{});
|
||||
}
|
||||
final members = await _checkedMembers(space);
|
||||
final sealed = [
|
||||
for (final need in needed)
|
||||
if (_cipher.has(need.keyId))
|
||||
if (members.trusted[need.person] case final keys?)
|
||||
(
|
||||
keyId: need.keyId,
|
||||
person: need.person,
|
||||
sealed: _cipher.sealKeyTo(need.keyId, keys),
|
||||
),
|
||||
];
|
||||
if (sealed.isNotEmpty) {
|
||||
await _gateway.seal(space, sealed);
|
||||
}
|
||||
return (
|
||||
sealedTo: {for (final key in sealed) key.person},
|
||||
changed: {
|
||||
for (final need in needed)
|
||||
if (members.changed.contains(need.person)) need.person,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Opens the keys of [space] sealed to the person, each checked against
|
||||
/// its sealer's pinned keys; one a former member sealed is checked
|
||||
/// against the keys pinned for them, if any.
|
||||
Future<OpenOutcome> openMine(String space) async {
|
||||
final mine = await _gateway.mine(space);
|
||||
if (mine.isEmpty) {
|
||||
return (opened: <String>{}, refused: <String>{}, changed: <String>{});
|
||||
}
|
||||
final members = await _checkedMembers(space);
|
||||
final opened = <String>{};
|
||||
final refused = <String>{};
|
||||
final changed = <String>{};
|
||||
for (final key in mine) {
|
||||
if (members.changed.contains(key.person)) {
|
||||
changed.add(key.person);
|
||||
refused.add(key.keyId);
|
||||
continue;
|
||||
}
|
||||
final sealer =
|
||||
members.trusted[key.person] ?? (await _pins.of(key.person))?.keys;
|
||||
final opens =
|
||||
sealer != null &&
|
||||
_cipher.openSharedKey(
|
||||
key.keyId,
|
||||
key.sealed,
|
||||
sealedBy: sealer.signing,
|
||||
);
|
||||
(opens ? opened : refused).add(key.keyId);
|
||||
}
|
||||
return (opened: opened, refused: refused, changed: changed);
|
||||
}
|
||||
|
||||
/// The safety code of the person and [person], over the keys pinned for
|
||||
/// them; null where none are.
|
||||
Future<String?> safetyCodeWith(String person) async {
|
||||
final pinned = await _pins.of(person);
|
||||
return pinned == null ? null : _cipher.safetyCodeWith(pinned.keys);
|
||||
}
|
||||
|
||||
/// [space]'s members with keys, checked against their pins: those whose
|
||||
/// keys are pinned (or now first seen) and those whose keys changed. The
|
||||
/// person's own must be the cipher's.
|
||||
Future<({Map<String, PublicKeys> trusted, Set<String> changed})>
|
||||
_checkedMembers(String space) async {
|
||||
final trusted = <String, PublicKeys>{};
|
||||
final changed = <String>{};
|
||||
for (final member in await _gateway.memberKeys(space)) {
|
||||
if (member.person == me) {
|
||||
if (!sameKeys(member.keys, _cipher.ownKeys)) {
|
||||
throw OwnKeysDiffer();
|
||||
}
|
||||
trusted[me] = member.keys;
|
||||
} else if (await _pins.check(member.person, member.keys) ==
|
||||
PinCheck.changed) {
|
||||
changed.add(member.person);
|
||||
} else {
|
||||
trusted[member.person] = member.keys;
|
||||
}
|
||||
}
|
||||
return (trusted: trusted, changed: changed);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_keys/halkyon_keys.dart' show PublicKeys;
|
||||
|
||||
/// A member of a shared space with their public keys, as the instance hands
|
||||
/// them.
|
||||
typedef MemberKeys = ({String person, String? handle, PublicKeys keys});
|
||||
|
||||
/// A key the device holds that `person` lacks.
|
||||
typedef KeyNeed = ({String keyId, String person});
|
||||
|
||||
/// A key sealed to one person: to `person` on the way to the instance,
|
||||
/// from `person` (its sealer) on the way back.
|
||||
typedef SealedKey = ({String keyId, String person, Uint8List sealed});
|
||||
|
||||
/// The boundary to the keys of spaces shared on the instance (halkyon_base's
|
||||
/// `sharedSpaceKeys`, Halkyon's ADR 0044, D3). Tests replace it with a fake.
|
||||
abstract interface class SharedKeysGateway() {
|
||||
/// The public keys of [space]'s members who have keys.
|
||||
Future<List<MemberKeys>> memberKeys(String space);
|
||||
|
||||
/// The keys of [space] the person holds that members lack.
|
||||
Future<List<KeyNeed>> keysNeeded(String space);
|
||||
|
||||
/// Keeps [keys], each sealed to the member it names.
|
||||
Future<void> seal(String space, List<SealedKey> keys);
|
||||
|
||||
/// The keys of [space] sealed to the person, each naming its sealer.
|
||||
Future<List<SealedKey>> mine(String space);
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import 'dart:typed_data';
|
|||
|
||||
import 'package:halkyon_app/src/password/model/unlocked_account_keys.dart';
|
||||
import 'package:halkyon_app/src/privacy/content_cipher.dart';
|
||||
import 'package:halkyon_keys/halkyon_keys.dart' show PublicKeys;
|
||||
|
||||
/// [ContentCipher] without libsodium, for tests: a key opens only on a
|
||||
/// device unlocked for the account it was made for, and content only under
|
||||
|
|
@ -10,13 +11,19 @@ import 'package:halkyon_app/src/privacy/content_cipher.dart';
|
|||
/// secret (each byte is flipped, so the plain text doesn't show).
|
||||
class FakeContentCipher() implements ContentCipher {
|
||||
String? _account;
|
||||
PublicKeys? _ownKeys;
|
||||
|
||||
/// The keys open here, by their ID, with the account they belong to.
|
||||
final Map<String, String> opened = {};
|
||||
|
||||
@override
|
||||
Future<void> unlock(UnlockedAccountKeys account) async =>
|
||||
_account = base64Encode(account.signingPublicKey);
|
||||
Future<void> unlock(UnlockedAccountKeys account) async {
|
||||
_account = base64Encode(account.signingPublicKey);
|
||||
_ownKeys = (
|
||||
encryption: account.encryptionPublicKey,
|
||||
signing: account.signingPublicKey,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Uint8List newKey(String keyId) {
|
||||
|
|
@ -38,6 +45,43 @@ class FakeContentCipher() implements ContentCipher {
|
|||
@override
|
||||
bool has(String keyId) => opened.containsKey(keyId);
|
||||
|
||||
@override
|
||||
PublicKeys get ownKeys => _ownKeys ?? (throw StateError('Not unlocked'));
|
||||
|
||||
@override
|
||||
Uint8List sealKeyTo(String keyId, PublicKeys to) {
|
||||
final account = _unlocked();
|
||||
if (!has(keyId)) {
|
||||
throw StateError('The data key $keyId is not open');
|
||||
}
|
||||
return Uint8List.fromList(
|
||||
utf8.encode('shared|${base64Encode(to.encryption)}|$keyId|$account'),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool openSharedKey(
|
||||
String keyId,
|
||||
Uint8List sealed, {
|
||||
required Uint8List sealedBy,
|
||||
}) {
|
||||
if (utf8.decode(sealed, allowMalformed: true) !=
|
||||
'shared|${base64Encode(ownKeys.encryption)}|$keyId|'
|
||||
'${base64Encode(sealedBy)}') {
|
||||
return false;
|
||||
}
|
||||
opened[keyId] = _unlocked();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Both accounts' signing keys, in one order: alike on both sides, but
|
||||
/// not the real code's digits.
|
||||
@override
|
||||
String safetyCodeWith(PublicKeys other) => ([
|
||||
base64Encode(ownKeys.signing),
|
||||
base64Encode(other.signing),
|
||||
]..sort()).join(' ');
|
||||
|
||||
@override
|
||||
SealedPayload seal(
|
||||
Uint8List plain, {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,123 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_app/src/shared_space/shared_keys_gateway.dart';
|
||||
import 'package:halkyon_keys/halkyon_keys.dart' show PublicKeys;
|
||||
|
||||
typedef _Sealed = ({String keyId, String to, String by, Uint8List sealed});
|
||||
|
||||
/// The instance's shared spaces' keys in memory, as halkyon_base keeps
|
||||
/// them: members with their public keys (which a test may swap, as a
|
||||
/// hostile instance would), the keys sealed to each, the first staying.
|
||||
/// Each person reaches it through their own [as].
|
||||
class FakeSharedKeysServer() {
|
||||
final _members = <String, Map<String, ({PublicKeys? keys, bool writes})>>{};
|
||||
final _sealed =
|
||||
<
|
||||
String,
|
||||
List<({String keyId, String to, String by, Uint8List sealed})>
|
||||
>{};
|
||||
|
||||
/// Makes [person] a member of [space], writing where [writes], with the
|
||||
/// public keys [keys] the register hands out for them (none where null).
|
||||
void addMember(
|
||||
String space,
|
||||
String person, {
|
||||
PublicKeys? keys,
|
||||
bool writes = true,
|
||||
}) => (_members[space] ??= {})[person] = (keys: keys, writes: writes);
|
||||
|
||||
/// Ends [person]'s membership of [space], with the keys sealed to them.
|
||||
void removeMember(String space, String person) {
|
||||
_members[space]?.remove(person);
|
||||
_sealed[space]?.removeWhere((key) => key.to == person);
|
||||
}
|
||||
|
||||
/// The keys handed out for [person] from now on, in every space.
|
||||
void handOutKeys(String person, PublicKeys keys) {
|
||||
for (final members in _members.values) {
|
||||
if (members[person] case final member?) {
|
||||
members[person] = (keys: keys, writes: member.writes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// How many keys are sealed in [space].
|
||||
int sealedCount(String space) => _sealed[space]?.length ?? 0;
|
||||
|
||||
/// The gateway as [person] reaches it.
|
||||
SharedKeysGateway as(String person) => _View(this, person);
|
||||
|
||||
Map<String, ({PublicKeys? keys, bool writes})> _membersAs(
|
||||
String space,
|
||||
String person,
|
||||
) {
|
||||
final members = _members[space];
|
||||
if (members == null || !members.containsKey(person)) {
|
||||
throw StateError('No such space');
|
||||
}
|
||||
return members;
|
||||
}
|
||||
}
|
||||
|
||||
class _View(final FakeSharedKeysServer _server, final String _person)
|
||||
implements SharedKeysGateway {
|
||||
@override
|
||||
Future<List<MemberKeys>> memberKeys(String space) async => [
|
||||
for (final MapEntry(key: person, value: member)
|
||||
in _server._membersAs(space, _person).entries)
|
||||
if (member.keys case final keys?)
|
||||
(person: person, handle: null, keys: keys),
|
||||
];
|
||||
|
||||
@override
|
||||
Future<List<KeyNeed>> keysNeeded(String space) async {
|
||||
final members = _server._membersAs(space, _person);
|
||||
if (!members[_person]!.writes) {
|
||||
return const [];
|
||||
}
|
||||
final sealed = _server._sealed[space] ?? const <_Sealed>[];
|
||||
final held = {
|
||||
for (final key in sealed)
|
||||
if (key.to == _person) key.keyId,
|
||||
};
|
||||
return [
|
||||
for (final keyId in held)
|
||||
for (final MapEntry(key: person, value: member) in members.entries)
|
||||
if (member.keys != null &&
|
||||
!sealed.any((key) => key.keyId == keyId && key.to == person))
|
||||
(keyId: keyId, person: person),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> seal(String space, List<SealedKey> keys) async {
|
||||
final members = _server._membersAs(space, _person);
|
||||
if (!members[_person]!.writes) {
|
||||
throw StateError('Not allowed');
|
||||
}
|
||||
final sealed = _server._sealed[space] ??= [];
|
||||
for (final key in keys) {
|
||||
if (!members.containsKey(key.person)) {
|
||||
throw StateError('No such member');
|
||||
}
|
||||
if (!sealed.any((k) => k.keyId == key.keyId && k.to == key.person)) {
|
||||
sealed.add((
|
||||
keyId: key.keyId,
|
||||
to: key.person,
|
||||
by: _person,
|
||||
sealed: key.sealed,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<SealedKey>> mine(String space) async {
|
||||
_server._membersAs(space, _person);
|
||||
return [
|
||||
for (final key in _server._sealed[space] ?? const <_Sealed>[])
|
||||
if (key.to == _person)
|
||||
(keyId: key.keyId, person: key.by, sealed: key.sealed),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import 'package:halkyon_app/src/shared_space/key_pins.dart';
|
||||
|
||||
/// [KeyPinStore] in memory; [pins] is what a file would hold.
|
||||
class MemoryKeyPinStore() implements KeyPinStore {
|
||||
Map<String, PinnedKeys> pins = {};
|
||||
|
||||
@override
|
||||
Future<Map<String, PinnedKeys>> read() async => {...pins};
|
||||
|
||||
@override
|
||||
Future<void> write(Map<String, PinnedKeys> pins) async =>
|
||||
this.pins = {...pins};
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ export 'src/testing/fake_oidc_browser.dart';
|
|||
export 'src/testing/fake_password_gateway.dart';
|
||||
export 'src/testing/fake_public_profile_gateway.dart';
|
||||
export 'src/testing/fake_screen_guard.dart';
|
||||
export 'src/testing/fake_shared_keys_gateway.dart';
|
||||
export 'src/testing/fake_simplelogin_gateway.dart';
|
||||
export 'src/testing/fake_speech_input.dart';
|
||||
export 'src/testing/memory_account_key_store.dart';
|
||||
|
|
@ -29,6 +30,7 @@ export 'src/testing/memory_account_wait_store.dart';
|
|||
export 'src/testing/memory_file_cache.dart';
|
||||
export 'src/testing/memory_file_saver.dart';
|
||||
export 'src/testing/memory_instance_store.dart';
|
||||
export 'src/testing/memory_key_pin_store.dart';
|
||||
export 'src/testing/memory_look_store.dart';
|
||||
export 'src/testing/memory_pending_changes_store.dart';
|
||||
export 'src/testing/memory_sign_in_addresses.dart';
|
||||
|
|
|
|||
64
packages/halkyon_app/test/shared_space/key_pins_test.dart
Normal file
64
packages/halkyon_app/test/shared_space/key_pins_test.dart
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
|
||||
/// The public keys a device pins for each person (Halkyon's ADR 0044, D18).
|
||||
void main() {
|
||||
late Directory folder;
|
||||
late File file;
|
||||
|
||||
PublicKeys keys(int mark) => (
|
||||
encryption: Uint8List.fromList(List.filled(32, mark)),
|
||||
signing: Uint8List.fromList(List.filled(32, mark + 1)),
|
||||
);
|
||||
|
||||
setUp(() async {
|
||||
folder = await Directory.systemTemp.createTemp('key_pins');
|
||||
file = File('${folder.path}/person/key_pins.json');
|
||||
});
|
||||
|
||||
tearDown(() => folder.delete(recursive: true));
|
||||
|
||||
test('the first keys seen stay pinned, on the next start too; others '
|
||||
'are a change until trusted anew', () async {
|
||||
final pins = KeyPins(FileKeyPinStore(file));
|
||||
expect(await pins.check('bob', keys(1)), PinCheck.firstSeen);
|
||||
expect(await pins.check('bob', keys(1)), PinCheck.same);
|
||||
expect(await pins.check('bob', keys(3)), PinCheck.changed);
|
||||
|
||||
final later = KeyPins(FileKeyPinStore(file));
|
||||
expect(await later.check('bob', keys(3)), PinCheck.changed);
|
||||
expect(await later.check('bob', keys(1)), PinCheck.same);
|
||||
|
||||
await later.trustAnew('bob', keys(3));
|
||||
expect(
|
||||
await KeyPins(FileKeyPinStore(file)).check('bob', keys(3)),
|
||||
PinCheck.same,
|
||||
);
|
||||
});
|
||||
|
||||
test('compared keys are marked so, until they are trusted anew', () async {
|
||||
final pins = KeyPins(FileKeyPinStore(file));
|
||||
await pins.check('bob', keys(1));
|
||||
expect((await pins.of('bob'))!.verified, isFalse);
|
||||
|
||||
await pins.markVerified('bob');
|
||||
expect((await KeyPins(FileKeyPinStore(file)).of('bob'))!.verified, isTrue);
|
||||
|
||||
await pins.trustAnew('bob', keys(3));
|
||||
expect((await pins.of('bob'))!.verified, isFalse);
|
||||
});
|
||||
|
||||
test('a file not holding pins counts as none', () async {
|
||||
await file.parent.create(recursive: true);
|
||||
await file.writeAsString('{not json');
|
||||
expect(await FileKeyPinStore(file).read(), isEmpty);
|
||||
await file.writeAsString(
|
||||
'{"bob": {"encryption": "%%", "signing": "", '
|
||||
'"verified": false}, "carol": 3}',
|
||||
);
|
||||
expect(await FileKeyPinStore(file).read(), isEmpty);
|
||||
});
|
||||
}
|
||||
191
packages/halkyon_app/test/shared_space/shared_keys_test.dart
Normal file
191
packages/halkyon_app/test/shared_space/shared_keys_test.dart
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_app/testing.dart';
|
||||
|
||||
/// A shared space's data keys sealed to each member on a device (Halkyon's
|
||||
/// ADR 0044, D3), held to the public keys each device pinned on first sight
|
||||
/// (D18), with libsodium as its build hook compiles it.
|
||||
void main() {
|
||||
const space = 'space-1';
|
||||
final crypto = SodiumAccountCrypto();
|
||||
late UnlockedAccountKeys aliceAccount;
|
||||
late UnlockedAccountKeys bobAccount;
|
||||
late UnlockedAccountKeys malloryAccount;
|
||||
|
||||
late FakeSharedKeysServer server;
|
||||
late SodiumContentCipher aliceCipher;
|
||||
late SodiumContentCipher bobCipher;
|
||||
late MemoryKeyPinStore alicePins;
|
||||
late KeyPins aliceKeyPins;
|
||||
late MemoryKeyPinStore bobPins;
|
||||
late SharedKeys alice;
|
||||
late SharedKeys bob;
|
||||
|
||||
PublicKeys keysOf(UnlockedAccountKeys account) => (
|
||||
encryption: account.encryptionPublicKey,
|
||||
signing: account.signingPublicKey,
|
||||
);
|
||||
|
||||
Future<SodiumContentCipher> cipherOf(UnlockedAccountKeys account) async {
|
||||
final cipher = SodiumContentCipher();
|
||||
await cipher.unlock(account);
|
||||
addTearDown(cipher.dispose);
|
||||
return cipher;
|
||||
}
|
||||
|
||||
Matcher sealedTo(Set<String> people, {Set<String> changed = const {}}) =>
|
||||
isA<SealOutcome>()
|
||||
.having((o) => o.sealedTo, 'sealedTo', people)
|
||||
.having((o) => o.changed, 'changed', changed);
|
||||
|
||||
Matcher openedThese(
|
||||
Set<String> keys, {
|
||||
Set<String> refused = const {},
|
||||
Set<String> changed = const {},
|
||||
}) => isA<OpenOutcome>()
|
||||
.having((o) => o.opened, 'opened', keys)
|
||||
.having((o) => o.refused, 'refused', refused)
|
||||
.having((o) => o.changed, 'changed', changed);
|
||||
|
||||
setUpAll(() async {
|
||||
aliceAccount = (await crypto.newAccount('alice password')).keys;
|
||||
bobAccount = (await crypto.newAccount('bob password')).keys;
|
||||
malloryAccount = (await crypto.newAccount('mallory password')).keys;
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
server = FakeSharedKeysServer()
|
||||
..addMember(space, 'alice', keys: keysOf(aliceAccount))
|
||||
..addMember(space, 'bob', keys: keysOf(bobAccount));
|
||||
aliceCipher = await cipherOf(aliceAccount);
|
||||
bobCipher = await cipherOf(bobAccount);
|
||||
alicePins = MemoryKeyPinStore();
|
||||
bobPins = MemoryKeyPinStore();
|
||||
aliceKeyPins = KeyPins(alicePins);
|
||||
alice = SharedKeys(
|
||||
server.as('alice'),
|
||||
aliceKeyPins,
|
||||
aliceCipher,
|
||||
me: 'alice',
|
||||
);
|
||||
bob = SharedKeys(server.as('bob'), KeyPins(bobPins), bobCipher, me: 'bob');
|
||||
});
|
||||
|
||||
test('a key shared with the members opens on their devices, and the '
|
||||
'safety codes on both sides agree', () async {
|
||||
aliceCipher.newKey('k1');
|
||||
|
||||
expect(await alice.share(space, 'k1'), sealedTo({'alice', 'bob'}));
|
||||
expect(await bob.openMine(space), openedThese({'k1'}));
|
||||
|
||||
expect(bobCipher.has('k1'), isTrue);
|
||||
final code = await alice.safetyCodeWith('bob');
|
||||
expect(code, isNotNull);
|
||||
expect(await bob.safetyCodeWith('alice'), code);
|
||||
});
|
||||
|
||||
test('keys swapped after they were pinned are neither sealed to nor '
|
||||
'trusted, and named until trusted anew', () async {
|
||||
aliceCipher.newKey('k1');
|
||||
await alice.share(space, 'k1');
|
||||
await bob.openMine(space);
|
||||
final mallorys = keysOf(malloryAccount);
|
||||
|
||||
server.handOutKeys('bob', mallorys);
|
||||
aliceCipher.newKey('k2');
|
||||
expect(
|
||||
await alice.share(space, 'k2'),
|
||||
sealedTo({'alice'}, changed: {'bob'}),
|
||||
);
|
||||
|
||||
server
|
||||
..handOutKeys('bob', keysOf(bobAccount))
|
||||
..handOutKeys('alice', mallorys);
|
||||
expect(
|
||||
await bob.openMine(space),
|
||||
openedThese({}, refused: {'k1'}, changed: {'alice'}),
|
||||
);
|
||||
|
||||
// Bob's keys changed for a reason Alice trusts.
|
||||
server
|
||||
..handOutKeys('alice', keysOf(aliceAccount))
|
||||
..handOutKeys('bob', mallorys);
|
||||
await aliceKeyPins.trustAnew('bob', mallorys);
|
||||
expect((await alice.share(space, 'k2')).sealedTo, contains('bob'));
|
||||
});
|
||||
|
||||
test('keys swapped before they were first seen open nothing, and the '
|
||||
'safety codes differ', () async {
|
||||
final mallory = await cipherOf(malloryAccount);
|
||||
server.handOutKeys('alice', keysOf(malloryAccount));
|
||||
aliceCipher.newKey('k1');
|
||||
// Alice's own device holds to its own keys, whatever the instance says.
|
||||
await expectLater(alice.share(space, 'k1'), throwsA(isA<OwnKeysDiffer>()));
|
||||
|
||||
// Sealed by the true Alice, checked by Bob against Mallory's keys.
|
||||
server.addMember(space, 'alice', keys: keysOf(aliceAccount));
|
||||
await alice.share(space, 'k1');
|
||||
server.handOutKeys('alice', keysOf(malloryAccount));
|
||||
expect(await bob.openMine(space), openedThese({}, refused: {'k1'}));
|
||||
expect(bobCipher.has('k1'), isFalse);
|
||||
|
||||
expect(
|
||||
await bob.safetyCodeWith('alice'),
|
||||
isNot(await alice.safetyCodeWith('bob')),
|
||||
);
|
||||
expect(
|
||||
await bob.safetyCodeWith('alice'),
|
||||
mallory.safetyCodeWith(keysOf(bobAccount)),
|
||||
);
|
||||
});
|
||||
|
||||
test('a newcomer gets the keys a writer holds, and a Viewer seals '
|
||||
'nothing', () async {
|
||||
final carolAccount = (await crypto.newAccount('carol password')).keys;
|
||||
final carolCipher = await cipherOf(carolAccount);
|
||||
final carol = SharedKeys(
|
||||
server.as('carol'),
|
||||
KeyPins(MemoryKeyPinStore()),
|
||||
carolCipher,
|
||||
me: 'carol',
|
||||
);
|
||||
aliceCipher.newKey('k1');
|
||||
await alice.share(space, 'k1');
|
||||
server.addMember(space, 'carol', keys: keysOf(carolAccount), writes: false);
|
||||
|
||||
expect((await carol.sealForNewcomers(space)).sealedTo, isEmpty);
|
||||
expect((await carol.openMine(space)).opened, isEmpty);
|
||||
|
||||
await bob.openMine(space);
|
||||
expect((await bob.sealForNewcomers(space)).sealedTo, {'carol'});
|
||||
expect((await carol.openMine(space)).opened, {'k1'});
|
||||
expect((await alice.sealForNewcomers(space)).sealedTo, isEmpty);
|
||||
});
|
||||
|
||||
test("a key of a member's who left opens with the keys pinned for "
|
||||
'them', () async {
|
||||
aliceCipher.newKey('k1');
|
||||
await alice.share(space, 'k1');
|
||||
await bob.openMine(space);
|
||||
final other = await cipherOf(bobAccount);
|
||||
final again = SharedKeys(
|
||||
server.as('bob'),
|
||||
KeyPins(bobPins),
|
||||
other,
|
||||
me: 'bob',
|
||||
);
|
||||
|
||||
server.removeMember(space, 'alice');
|
||||
|
||||
expect((await again.openMine(space)).opened, {'k1'});
|
||||
});
|
||||
|
||||
test('a key sealed by someone never pinned, and no longer a member, '
|
||||
"doesn't open", () async {
|
||||
aliceCipher.newKey('k1');
|
||||
await alice.share(space, 'k1');
|
||||
server.removeMember(space, 'alice');
|
||||
|
||||
expect(await bob.openMine(space), openedThese({}, refused: {'k1'}));
|
||||
});
|
||||
}
|
||||
|
|
@ -80,10 +80,16 @@ import 'package:halkyon_base_client/src/protocol/profile/profile_refusal.dart'
|
|||
as _ih722tf9;
|
||||
import 'package:halkyon_base_client/src/protocol/shared_space/shared_space_info.dart'
|
||||
as _i9dv6y4h;
|
||||
import 'package:halkyon_base_client/src/protocol/shared_space/shared_space_key_need.dart'
|
||||
as _ixm5azhv;
|
||||
import 'package:halkyon_base_client/src/protocol/shared_space/shared_space_member_info.dart'
|
||||
as _iyon0cg8;
|
||||
import 'package:halkyon_base_client/src/protocol/shared_space/shared_space_member_keys.dart'
|
||||
as _i6d21238;
|
||||
import 'package:halkyon_base_client/src/protocol/shared_space/shared_space_role.dart'
|
||||
as _i1fyjuki;
|
||||
import 'package:halkyon_base_client/src/protocol/shared_space/shared_space_sealed_key.dart'
|
||||
as _ie2by8eg;
|
||||
import 'package:halkyon_base_client/src/protocol/simplelogin/simplelogin_completed.dart'
|
||||
as _ianfx7qj;
|
||||
import 'package:halkyon_base_client/src/protocol/simplelogin/simplelogin_return.dart'
|
||||
|
|
@ -1442,6 +1448,66 @@ class EndpointSharedSpace extends _isc.EndpointRef {
|
|||
);
|
||||
}
|
||||
|
||||
/// 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]).
|
||||
/// {@category Endpoint}
|
||||
class EndpointSharedSpaceKeys extends _isc.EndpointRef {
|
||||
EndpointSharedSpaceKeys(_isc.EndpointCaller caller) : super(caller);
|
||||
|
||||
@override
|
||||
String get name => 'halkyon_base.sharedSpaceKeys';
|
||||
|
||||
/// The public keys of [space]'s members who have keys, you among them,
|
||||
/// as the register keeps them.
|
||||
_ida.Future<List<_i6d21238.SharedSpaceMemberKeys>> memberKeys(
|
||||
_isc.UuidValue space,
|
||||
) => caller.callServerEndpoint<List<_i6d21238.SharedSpaceMemberKeys>>(
|
||||
'halkyon_base.sharedSpaceKeys',
|
||||
'memberKeys',
|
||||
{'space': space},
|
||||
);
|
||||
|
||||
/// 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]).
|
||||
_ida.Future<List<_ixm5azhv.SharedSpaceKeyNeed>> keysNeeded(
|
||||
_isc.UuidValue space,
|
||||
) => caller.callServerEndpoint<List<_ixm5azhv.SharedSpaceKeyNeed>>(
|
||||
'halkyon_base.sharedSpaceKeys',
|
||||
'keysNeeded',
|
||||
{'space': space},
|
||||
);
|
||||
|
||||
/// 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.
|
||||
_ida.Future<void> seal(
|
||||
_isc.UuidValue space,
|
||||
List<_ie2by8eg.SharedSpaceSealedKey> keys,
|
||||
) => caller.callServerEndpoint<void>(
|
||||
'halkyon_base.sharedSpaceKeys',
|
||||
'seal',
|
||||
{
|
||||
'space': space,
|
||||
'keys': keys,
|
||||
},
|
||||
);
|
||||
|
||||
/// The keys of [space] sealed to you, each naming who sealed it.
|
||||
_ida.Future<List<_ie2by8eg.SharedSpaceSealedKey>> mine(
|
||||
_isc.UuidValue space,
|
||||
) => caller.callServerEndpoint<List<_ie2by8eg.SharedSpaceSealedKey>>(
|
||||
'halkyon_base.sharedSpaceKeys',
|
||||
'mine',
|
||||
{'space': space},
|
||||
);
|
||||
}
|
||||
|
||||
/// Signing in with SimpleLogin (ADR 0020, 3 to 5), signed out or, to add it
|
||||
/// to one's account, signed in. The instance's register keeps the attempts
|
||||
/// and checks SimpleLogin's codes (ADR 0016); a session is this server's.
|
||||
|
|
@ -1597,6 +1663,7 @@ class Caller extends _isc.ModuleEndpointCaller {
|
|||
profile = EndpointProfile(this);
|
||||
profiles = EndpointProfiles(this);
|
||||
sharedSpace = EndpointSharedSpace(this);
|
||||
sharedSpaceKeys = EndpointSharedSpaceKeys(this);
|
||||
simpleLogin = EndpointSimpleLogin(this);
|
||||
}
|
||||
|
||||
|
|
@ -1632,6 +1699,8 @@ class Caller extends _isc.ModuleEndpointCaller {
|
|||
|
||||
late final EndpointSharedSpace sharedSpace;
|
||||
|
||||
late final EndpointSharedSpaceKeys sharedSpaceKeys;
|
||||
|
||||
late final EndpointSimpleLogin simpleLogin;
|
||||
|
||||
@override
|
||||
|
|
@ -1652,6 +1721,7 @@ class Caller extends _isc.ModuleEndpointCaller {
|
|||
'halkyon_base.profile': profile,
|
||||
'halkyon_base.profiles': profiles,
|
||||
'halkyon_base.sharedSpace': sharedSpace,
|
||||
'halkyon_base.sharedSpaceKeys': sharedSpaceKeys,
|
||||
'halkyon_base.simpleLogin': simpleLogin,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,8 +21,14 @@ import 'package:halkyon_base_client/src/protocol/password/authenticator.dart'
|
|||
as _ivfmflch;
|
||||
import 'package:halkyon_base_client/src/protocol/shared_space/shared_space_info.dart'
|
||||
as _i9dv6y4h;
|
||||
import 'package:halkyon_base_client/src/protocol/shared_space/shared_space_key_need.dart'
|
||||
as _ixm5azhv;
|
||||
import 'package:halkyon_base_client/src/protocol/shared_space/shared_space_member_info.dart'
|
||||
as _iyon0cg8;
|
||||
import 'package:halkyon_base_client/src/protocol/shared_space/shared_space_member_keys.dart'
|
||||
as _i6d21238;
|
||||
import 'package:halkyon_base_client/src/protocol/shared_space/shared_space_sealed_key.dart'
|
||||
as _ie2by8eg;
|
||||
import 'package:halkyon_oidc_client/halkyon_oidc_client.dart' as _ibkpwzgl;
|
||||
import 'package:serverpod_auth_core_client/serverpod_auth_core_client.dart'
|
||||
as _iacc;
|
||||
|
|
@ -95,11 +101,14 @@ import 'profile/profile_not_found_exception.dart' as _iim3hfcf;
|
|||
import 'profile/profile_refusal.dart' as _i6j6efd1;
|
||||
import 'profile/profile_refused_exception.dart' as _idvc5e7p;
|
||||
import 'shared_space/shared_space_info.dart' as _i1rvc02f;
|
||||
import 'shared_space/shared_space_key_need.dart' as _imxqlbwp;
|
||||
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_return.dart' as _i8dcjd27;
|
||||
|
|
@ -169,11 +178,14 @@ export 'profile/profile_not_found_exception.dart';
|
|||
export 'profile/profile_refusal.dart';
|
||||
export 'profile/profile_refused_exception.dart';
|
||||
export 'shared_space/shared_space_info.dart';
|
||||
export 'shared_space/shared_space_key_need.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_return.dart';
|
||||
|
|
@ -422,9 +434,15 @@ class Protocol extends _isc.SerializationManager {
|
|||
if (t == _i1rvc02f.SharedSpaceInfo) {
|
||||
return _i1rvc02f.SharedSpaceInfo.fromJson(data) as T;
|
||||
}
|
||||
if (t == _imxqlbwp.SharedSpaceKeyNeed) {
|
||||
return _imxqlbwp.SharedSpaceKeyNeed.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;
|
||||
}
|
||||
|
|
@ -437,6 +455,9 @@ class Protocol extends _isc.SerializationManager {
|
|||
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;
|
||||
}
|
||||
|
|
@ -722,12 +743,22 @@ class Protocol extends _isc.SerializationManager {
|
|||
return (data != null ? _i1rvc02f.SharedSpaceInfo.fromJson(data) : null)
|
||||
as T;
|
||||
}
|
||||
if (t == _isc.getType<_imxqlbwp.SharedSpaceKeyNeed?>()) {
|
||||
return (data != null ? _imxqlbwp.SharedSpaceKeyNeed.fromJson(data) : null)
|
||||
as T;
|
||||
}
|
||||
if (t == _isc.getType<_iz8z2oju.SharedSpaceMemberInfo?>()) {
|
||||
return (data != null
|
||||
? _iz8z2oju.SharedSpaceMemberInfo.fromJson(data)
|
||||
: null)
|
||||
as T;
|
||||
}
|
||||
if (t == _isc.getType<_iqkqjhh4.SharedSpaceMemberKeys?>()) {
|
||||
return (data != null
|
||||
? _iqkqjhh4.SharedSpaceMemberKeys.fromJson(data)
|
||||
: null)
|
||||
as T;
|
||||
}
|
||||
if (t == _isc.getType<_ixm56fil.SharedSpaceNotFoundException?>()) {
|
||||
return (data != null
|
||||
? _ixm56fil.SharedSpaceNotFoundException.fromJson(data)
|
||||
|
|
@ -748,6 +779,12 @@ class Protocol extends _isc.SerializationManager {
|
|||
return (data != null ? _i2fude1g.SharedSpaceRole.fromJson(data) : null)
|
||||
as T;
|
||||
}
|
||||
if (t == _isc.getType<_ifx6y5sl.SharedSpaceSealedKey?>()) {
|
||||
return (data != null
|
||||
? _ifx6y5sl.SharedSpaceSealedKey.fromJson(data)
|
||||
: null)
|
||||
as T;
|
||||
}
|
||||
if (t == _isc.getType<_i51ry5s2.SimpleLoginCompleted?>()) {
|
||||
return (data != null
|
||||
? _i51ry5s2.SimpleLoginCompleted.fromJson(data)
|
||||
|
|
@ -859,6 +896,24 @@ class Protocol extends _isc.SerializationManager {
|
|||
.toList()
|
||||
as T;
|
||||
}
|
||||
if (t == List<_i6d21238.SharedSpaceMemberKeys>) {
|
||||
return (data as List)
|
||||
.map((e) => deserialize<_i6d21238.SharedSpaceMemberKeys>(e))
|
||||
.toList()
|
||||
as T;
|
||||
}
|
||||
if (t == List<_ixm5azhv.SharedSpaceKeyNeed>) {
|
||||
return (data as List)
|
||||
.map((e) => deserialize<_ixm5azhv.SharedSpaceKeyNeed>(e))
|
||||
.toList()
|
||||
as T;
|
||||
}
|
||||
if (t == List<_ie2by8eg.SharedSpaceSealedKey>) {
|
||||
return (data as List)
|
||||
.map((e) => deserialize<_ie2by8eg.SharedSpaceSealedKey>(e))
|
||||
.toList()
|
||||
as T;
|
||||
}
|
||||
try {
|
||||
return _ibkpwzgl.Protocol().deserialize<T>(data, t);
|
||||
} on _isc.DeserializationTypeNotFoundException catch (_) {}
|
||||
|
|
@ -939,11 +994,14 @@ class Protocol extends _isc.SerializationManager {
|
|||
_i6j6efd1.ProfileRefusal => 'ProfileRefusal',
|
||||
_idvc5e7p.ProfileRefusedException => 'ProfileRefusedException',
|
||||
_i1rvc02f.SharedSpaceInfo => 'SharedSpaceInfo',
|
||||
_imxqlbwp.SharedSpaceKeyNeed => 'SharedSpaceKeyNeed',
|
||||
_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',
|
||||
_i8dcjd27.SimpleLoginReturn => 'SimpleLoginReturn',
|
||||
|
|
@ -1096,8 +1154,12 @@ class Protocol extends _isc.SerializationManager {
|
|||
return 'ProfileRefusedException';
|
||||
case _i1rvc02f.SharedSpaceInfo():
|
||||
return 'SharedSpaceInfo';
|
||||
case _imxqlbwp.SharedSpaceKeyNeed():
|
||||
return 'SharedSpaceKeyNeed';
|
||||
case _iz8z2oju.SharedSpaceMemberInfo():
|
||||
return 'SharedSpaceMemberInfo';
|
||||
case _iqkqjhh4.SharedSpaceMemberKeys():
|
||||
return 'SharedSpaceMemberKeys';
|
||||
case _ixm56fil.SharedSpaceNotFoundException():
|
||||
return 'SharedSpaceNotFoundException';
|
||||
case _irg3s7i7.SharedSpaceRefusal():
|
||||
|
|
@ -1106,6 +1168,8 @@ class Protocol extends _isc.SerializationManager {
|
|||
return 'SharedSpaceRefusedException';
|
||||
case _i2fude1g.SharedSpaceRole():
|
||||
return 'SharedSpaceRole';
|
||||
case _ifx6y5sl.SharedSpaceSealedKey():
|
||||
return 'SharedSpaceSealedKey';
|
||||
case _i51ry5s2.SimpleLoginCompleted():
|
||||
return 'SimpleLoginCompleted';
|
||||
case _i5ar1hzl.SimpleLoginOutcome():
|
||||
|
|
@ -1320,9 +1384,15 @@ class Protocol extends _isc.SerializationManager {
|
|||
if (dataClassName == 'SharedSpaceInfo') {
|
||||
return deserialize<_i1rvc02f.SharedSpaceInfo>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'SharedSpaceKeyNeed') {
|
||||
return deserialize<_imxqlbwp.SharedSpaceKeyNeed>(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']);
|
||||
}
|
||||
|
|
@ -1335,6 +1405,9 @@ class Protocol extends _isc.SerializationManager {
|
|||
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']);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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_client/serverpod_client.dart' as _isc;
|
||||
|
||||
/// 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 _isc.SerializableModel, _isc.ProtocolSerialization {
|
||||
SharedSpaceKeyNeed._({
|
||||
required this.keyId,
|
||||
required this.person,
|
||||
});
|
||||
|
||||
factory SharedSpaceKeyNeed({
|
||||
required String keyId,
|
||||
required _isc.UuidValue person,
|
||||
}) = _SharedSpaceKeyNeedImpl;
|
||||
|
||||
factory SharedSpaceKeyNeed.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return SharedSpaceKeyNeed(
|
||||
keyId: jsonSerialization['keyId'] as String,
|
||||
person: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['person']),
|
||||
);
|
||||
}
|
||||
|
||||
String keyId;
|
||||
|
||||
/// The member who lacks it.
|
||||
_isc.UuidValue person;
|
||||
|
||||
/// Returns a shallow copy of this [SharedSpaceKeyNeed]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
SharedSpaceKeyNeed copyWith({
|
||||
String? keyId,
|
||||
_isc.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 _isc.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _SharedSpaceKeyNeedImpl extends SharedSpaceKeyNeed {
|
||||
_SharedSpaceKeyNeedImpl({
|
||||
required String keyId,
|
||||
required _isc.UuidValue person,
|
||||
}) : super._(
|
||||
keyId: keyId,
|
||||
person: person,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [SharedSpaceKeyNeed]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
@override
|
||||
SharedSpaceKeyNeed copyWith({
|
||||
String? keyId,
|
||||
_isc.UuidValue? person,
|
||||
}) {
|
||||
return SharedSpaceKeyNeed(
|
||||
keyId: keyId ?? this.keyId,
|
||||
person: person ?? this.person,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
/* 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_client/serverpod_client.dart' as _isc;
|
||||
|
||||
/// 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 _isc.SerializableModel, _isc.ProtocolSerialization {
|
||||
SharedSpaceMemberKeys._({
|
||||
required this.person,
|
||||
this.handle,
|
||||
required this.encryption,
|
||||
required this.signing,
|
||||
});
|
||||
|
||||
factory SharedSpaceMemberKeys({
|
||||
required _isc.UuidValue person,
|
||||
String? handle,
|
||||
required _idt.ByteData encryption,
|
||||
required _idt.ByteData signing,
|
||||
}) = _SharedSpaceMemberKeysImpl;
|
||||
|
||||
factory SharedSpaceMemberKeys.fromJson(
|
||||
Map<String, dynamic> jsonSerialization,
|
||||
) {
|
||||
return SharedSpaceMemberKeys(
|
||||
person: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['person']),
|
||||
handle: jsonSerialization['handle'] as String?,
|
||||
encryption: _isc.ByteDataJsonExtension.fromJson(
|
||||
jsonSerialization['encryption'],
|
||||
),
|
||||
signing: _isc.ByteDataJsonExtension.fromJson(
|
||||
jsonSerialization['signing'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_isc.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.
|
||||
@_isc.useResult
|
||||
SharedSpaceMemberKeys copyWith({
|
||||
_isc.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 _isc.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _Undefined {}
|
||||
|
||||
class _SharedSpaceMemberKeysImpl extends SharedSpaceMemberKeys {
|
||||
_SharedSpaceMemberKeysImpl({
|
||||
required _isc.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.
|
||||
@_isc.useResult
|
||||
@override
|
||||
SharedSpaceMemberKeys copyWith({
|
||||
_isc.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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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_client/serverpod_client.dart' as _isc;
|
||||
|
||||
/// 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 _isc.SerializableModel, _isc.ProtocolSerialization {
|
||||
SharedSpaceSealedKey._({
|
||||
required this.keyId,
|
||||
required this.person,
|
||||
required this.sealedKey,
|
||||
});
|
||||
|
||||
factory SharedSpaceSealedKey({
|
||||
required String keyId,
|
||||
required _isc.UuidValue person,
|
||||
required _idt.ByteData sealedKey,
|
||||
}) = _SharedSpaceSealedKeyImpl;
|
||||
|
||||
factory SharedSpaceSealedKey.fromJson(
|
||||
Map<String, dynamic> jsonSerialization,
|
||||
) {
|
||||
return SharedSpaceSealedKey(
|
||||
keyId: jsonSerialization['keyId'] as String,
|
||||
person: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['person']),
|
||||
sealedKey: _isc.ByteDataJsonExtension.fromJson(
|
||||
jsonSerialization['sealedKey'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String keyId;
|
||||
|
||||
_isc.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.
|
||||
@_isc.useResult
|
||||
SharedSpaceSealedKey copyWith({
|
||||
String? keyId,
|
||||
_isc.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 _isc.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _SharedSpaceSealedKeyImpl extends SharedSpaceSealedKey {
|
||||
_SharedSpaceSealedKeyImpl({
|
||||
required String keyId,
|
||||
required _isc.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.
|
||||
@_isc.useResult
|
||||
@override
|
||||
SharedSpaceSealedKey copyWith({
|
||||
String? keyId,
|
||||
_isc.UuidValue? person,
|
||||
_idt.ByteData? sealedKey,
|
||||
}) {
|
||||
return SharedSpaceSealedKey(
|
||||
keyId: keyId ?? this.keyId,
|
||||
person: person ?? this.person,
|
||||
sealedKey: sealedKey ?? this.sealedKey.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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']!,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue