diff --git a/lib/components/contact_invitation_display.dart b/lib/components/contact_invitation_display.dart new file mode 100644 index 0000000..eef2d1b --- /dev/null +++ b/lib/components/contact_invitation_display.dart @@ -0,0 +1,25 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class ContactInvitationDisplay extends ConsumerWidget { + const ContactInvitationDisplay({super.key}); + //final LocalAccount account; + + @override + Widget build(BuildContext context, WidgetRef ref) { + //final logins = ref.watch(loginsProvider); + + return ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 300), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [const Expanded(child: Text('Contact Invitation'))])); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + //properties.add(DiagnosticsProperty('account', account)); + } +} diff --git a/lib/entities/identity.dart b/lib/entities/identity.dart index 21a5375..2e1dcbc 100644 --- a/lib/entities/identity.dart +++ b/lib/entities/identity.dart @@ -1,18 +1,21 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:veilid/veilid.dart'; + +import '../veilid_support/veilid_support.dart'; +import 'proto.dart' as proto; part 'identity.freezed.dart'; part 'identity.g.dart'; +const String veilidChatAccountKey = 'com.veilid.veilidchat'; + // AccountOwnerInfo is the key and owner info for the account dht key that is // stored in the identity key @freezed class AccountRecordInfo with _$AccountRecordInfo { const factory AccountRecordInfo({ // Top level account keys and secrets - required TypedKey key, - required KeyPair owner, + required OwnedDHTRecordPointer accountRecord, }) = _AccountRecordInfo; factory AccountRecordInfo.fromJson(dynamic json) => @@ -77,4 +80,89 @@ extension IdentityMasterExtension on IdentityMaster { KeyPair masterWriter(SecretKey secret) => KeyPair(key: masterPublicKey, secret: secret); + + Future readAccountFromIdentity( + {required SharedSecret identitySecret}) async { + // Read the identity key to get the account keys + final pool = await DHTRecordPool.instance(); + + final identityRecordCrypto = await DHTRecordCryptoPrivate.fromSecret( + identityRecordKey.kind, identitySecret); + + late final AccountRecordInfo accountRecordInfo; + await (await pool.openRead(identityRecordKey, + parent: masterRecordKey, crypto: identityRecordCrypto)) + .scope((identityRec) async { + final identity = await identityRec.getJson(Identity.fromJson); + if (identity == null) { + // Identity could not be read or decrypted from DHT + throw StateError('identity could not be read'); + } + final accountRecords = IMapOfSets.from(identity.accountRecords); + final vcAccounts = accountRecords.get(veilidChatAccountKey); + if (vcAccounts.length != 1) { + // No veilidchat account, or multiple accounts + // somehow associated with identity + throw StateError('no single veilidchat account'); + } + + accountRecordInfo = vcAccounts.first; + }); + + return accountRecordInfo; + } + + /// Creates a new Account associated with master identity and store it in the + /// identity key. + Future newAccount({ + required SharedSecret identitySecret, + required String name, + required String title, + }) async { + final pool = await DHTRecordPool.instance(); + + /////// Add account with profile to DHT + + // Open identity key for writing + await (await pool.openWrite( + identityRecordKey, identityWriter(identitySecret), + parent: masterRecordKey)) + .scope((identityRec) async { + // Create new account to insert into identity + await (await pool.create(parent: identityRec.key)) + .deleteScope((accountRec) async { + // Make empty contact request list + final contactRequests = await (await DHTShortArray.create()) + .scope((r) => r.record.ownedDHTRecordPointer); + + // Make account object + final account = proto.Account() + ..profile = (proto.Profile() + ..name = name + ..title = title) + ..contactRequests = contactRequests.toProto(); + + // Write account key + await accountRec.eventualWriteProtobuf(account); + + // Update identity key to include account + final newAccountRecordInfo = AccountRecordInfo( + accountRecord: OwnedDHTRecordPointer( + recordKey: accountRec.key, owner: accountRec.ownerKeyPair!)); + await identityRec.eventualUpdateJson(Identity.fromJson, + (oldIdentity) async { + final oldAccountRecords = IMapOfSets.from(oldIdentity.accountRecords); + // Only allow one account per identity for veilidchat + if (oldAccountRecords.get(veilidChatAccountKey).isNotEmpty) { + throw StateError( + 'Only one account per identity allowed for VeilidChat'); + } + final accountRecords = oldAccountRecords + .add(veilidChatAccountKey, newAccountRecordInfo) + .asIMap(); + return oldIdentity.copyWith(accountRecords: accountRecords); + }); + }); + }); + } } diff --git a/lib/entities/identity.freezed.dart b/lib/entities/identity.freezed.dart index 0e15e96..79ea210 100644 --- a/lib/entities/identity.freezed.dart +++ b/lib/entities/identity.freezed.dart @@ -21,8 +21,7 @@ AccountRecordInfo _$AccountRecordInfoFromJson(Map json) { /// @nodoc mixin _$AccountRecordInfo { // Top level account keys and secrets - Typed get key => throw _privateConstructorUsedError; - KeyPair get owner => throw _privateConstructorUsedError; + OwnedDHTRecordPointer get accountRecord => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -36,7 +35,9 @@ abstract class $AccountRecordInfoCopyWith<$Res> { AccountRecordInfo value, $Res Function(AccountRecordInfo) then) = _$AccountRecordInfoCopyWithImpl<$Res, AccountRecordInfo>; @useResult - $Res call({Typed key, KeyPair owner}); + $Res call({OwnedDHTRecordPointer accountRecord}); + + $OwnedDHTRecordPointerCopyWith<$Res> get accountRecord; } /// @nodoc @@ -52,20 +53,23 @@ class _$AccountRecordInfoCopyWithImpl<$Res, $Val extends AccountRecordInfo> @pragma('vm:prefer-inline') @override $Res call({ - Object? key = null, - Object? owner = null, + Object? accountRecord = null, }) { return _then(_value.copyWith( - key: null == key - ? _value.key - : key // ignore: cast_nullable_to_non_nullable - as Typed, - owner: null == owner - ? _value.owner - : owner // ignore: cast_nullable_to_non_nullable - as KeyPair, + accountRecord: null == accountRecord + ? _value.accountRecord + : accountRecord // ignore: cast_nullable_to_non_nullable + as OwnedDHTRecordPointer, ) as $Val); } + + @override + @pragma('vm:prefer-inline') + $OwnedDHTRecordPointerCopyWith<$Res> get accountRecord { + return $OwnedDHTRecordPointerCopyWith<$Res>(_value.accountRecord, (value) { + return _then(_value.copyWith(accountRecord: value) as $Val); + }); + } } /// @nodoc @@ -76,7 +80,10 @@ abstract class _$$_AccountRecordInfoCopyWith<$Res> __$$_AccountRecordInfoCopyWithImpl<$Res>; @override @useResult - $Res call({Typed key, KeyPair owner}); + $Res call({OwnedDHTRecordPointer accountRecord}); + + @override + $OwnedDHTRecordPointerCopyWith<$Res> get accountRecord; } /// @nodoc @@ -90,18 +97,13 @@ class __$$_AccountRecordInfoCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? key = null, - Object? owner = null, + Object? accountRecord = null, }) { return _then(_$_AccountRecordInfo( - key: null == key - ? _value.key - : key // ignore: cast_nullable_to_non_nullable - as Typed, - owner: null == owner - ? _value.owner - : owner // ignore: cast_nullable_to_non_nullable - as KeyPair, + accountRecord: null == accountRecord + ? _value.accountRecord + : accountRecord // ignore: cast_nullable_to_non_nullable + as OwnedDHTRecordPointer, )); } } @@ -109,20 +111,18 @@ class __$$_AccountRecordInfoCopyWithImpl<$Res> /// @nodoc @JsonSerializable() class _$_AccountRecordInfo implements _AccountRecordInfo { - const _$_AccountRecordInfo({required this.key, required this.owner}); + const _$_AccountRecordInfo({required this.accountRecord}); factory _$_AccountRecordInfo.fromJson(Map json) => _$$_AccountRecordInfoFromJson(json); // Top level account keys and secrets @override - final Typed key; - @override - final KeyPair owner; + final OwnedDHTRecordPointer accountRecord; @override String toString() { - return 'AccountRecordInfo(key: $key, owner: $owner)'; + return 'AccountRecordInfo(accountRecord: $accountRecord)'; } @override @@ -130,13 +130,13 @@ class _$_AccountRecordInfo implements _AccountRecordInfo { return identical(this, other) || (other.runtimeType == runtimeType && other is _$_AccountRecordInfo && - (identical(other.key, key) || other.key == key) && - (identical(other.owner, owner) || other.owner == owner)); + (identical(other.accountRecord, accountRecord) || + other.accountRecord == accountRecord)); } @JsonKey(ignore: true) @override - int get hashCode => Object.hash(runtimeType, key, owner); + int get hashCode => Object.hash(runtimeType, accountRecord); @JsonKey(ignore: true) @override @@ -155,16 +155,14 @@ class _$_AccountRecordInfo implements _AccountRecordInfo { abstract class _AccountRecordInfo implements AccountRecordInfo { const factory _AccountRecordInfo( - {required final Typed key, - required final KeyPair owner}) = _$_AccountRecordInfo; + {required final OwnedDHTRecordPointer accountRecord}) = + _$_AccountRecordInfo; factory _AccountRecordInfo.fromJson(Map json) = _$_AccountRecordInfo.fromJson; @override // Top level account keys and secrets - Typed get key; - @override - KeyPair get owner; + OwnedDHTRecordPointer get accountRecord; @override @JsonKey(ignore: true) _$$_AccountRecordInfoCopyWith<_$_AccountRecordInfo> get copyWith => diff --git a/lib/entities/identity.g.dart b/lib/entities/identity.g.dart index 7910de1..5374d27 100644 --- a/lib/entities/identity.g.dart +++ b/lib/entities/identity.g.dart @@ -8,15 +8,13 @@ part of 'identity.dart'; _$_AccountRecordInfo _$$_AccountRecordInfoFromJson(Map json) => _$_AccountRecordInfo( - key: Typed.fromJson(json['key']), - owner: KeyPair.fromJson(json['owner']), + accountRecord: OwnedDHTRecordPointer.fromJson(json['account_record']), ); Map _$$_AccountRecordInfoToJson( _$_AccountRecordInfo instance) => { - 'key': instance.key.toJson(), - 'owner': instance.owner.toJson(), + 'account_record': instance.accountRecord.toJson(), }; _$_Identity _$$_IdentityFromJson(Map json) => _$_Identity( diff --git a/lib/entities/local_account.dart b/lib/entities/local_account.dart index 8ecb443..5c52157 100644 --- a/lib/entities/local_account.dart +++ b/lib/entities/local_account.dart @@ -3,7 +3,7 @@ import 'dart:typed_data'; import 'package:change_case/change_case.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:veilid/veilid.dart'; +import '../veilid_support/veilid_support.dart'; import 'identity.dart'; part 'local_account.freezed.dart'; @@ -37,10 +37,9 @@ class LocalAccount with _$LocalAccount { const factory LocalAccount({ // The master key record for the account, containing the identityPublicKey required IdentityMaster identityMaster, - // The encrypted identity secret that goes with the identityPublicKey - @Uint8ListJsonConverter() required Uint8List identitySecretKeyBytes, - // The salt for the identity secret key encryption - @Uint8ListJsonConverter() required Uint8List identitySecretSaltBytes, + // The encrypted identity secret that goes with + // the identityPublicKey with appended salt + @Uint8ListJsonConverter() required Uint8List identitySecretBytes, // The kind of encryption input used on the account required EncryptionKeyType encryptionKeyType, // If account is not hidden, password can be retrieved via diff --git a/lib/entities/local_account.freezed.dart b/lib/entities/local_account.freezed.dart index b978d00..0112d7a 100644 --- a/lib/entities/local_account.freezed.dart +++ b/lib/entities/local_account.freezed.dart @@ -22,12 +22,10 @@ LocalAccount _$LocalAccountFromJson(Map json) { mixin _$LocalAccount { // The master key record for the account, containing the identityPublicKey IdentityMaster get identityMaster => - throw _privateConstructorUsedError; // The encrypted identity secret that goes with the identityPublicKey + throw _privateConstructorUsedError; // The encrypted identity secret that goes with +// the identityPublicKey with appended salt @Uint8ListJsonConverter() - Uint8List get identitySecretKeyBytes => - throw _privateConstructorUsedError; // The salt for the identity secret key encryption - @Uint8ListJsonConverter() - Uint8List get identitySecretSaltBytes => + Uint8List get identitySecretBytes => throw _privateConstructorUsedError; // The kind of encryption input used on the account EncryptionKeyType get encryptionKeyType => throw _privateConstructorUsedError; // If account is not hidden, password can be retrieved via @@ -52,8 +50,7 @@ abstract class $LocalAccountCopyWith<$Res> { @useResult $Res call( {IdentityMaster identityMaster, - @Uint8ListJsonConverter() Uint8List identitySecretKeyBytes, - @Uint8ListJsonConverter() Uint8List identitySecretSaltBytes, + @Uint8ListJsonConverter() Uint8List identitySecretBytes, EncryptionKeyType encryptionKeyType, bool biometricsEnabled, bool hiddenAccount, @@ -76,8 +73,7 @@ class _$LocalAccountCopyWithImpl<$Res, $Val extends LocalAccount> @override $Res call({ Object? identityMaster = null, - Object? identitySecretKeyBytes = null, - Object? identitySecretSaltBytes = null, + Object? identitySecretBytes = null, Object? encryptionKeyType = null, Object? biometricsEnabled = null, Object? hiddenAccount = null, @@ -88,13 +84,9 @@ class _$LocalAccountCopyWithImpl<$Res, $Val extends LocalAccount> ? _value.identityMaster : identityMaster // ignore: cast_nullable_to_non_nullable as IdentityMaster, - identitySecretKeyBytes: null == identitySecretKeyBytes - ? _value.identitySecretKeyBytes - : identitySecretKeyBytes // ignore: cast_nullable_to_non_nullable - as Uint8List, - identitySecretSaltBytes: null == identitySecretSaltBytes - ? _value.identitySecretSaltBytes - : identitySecretSaltBytes // ignore: cast_nullable_to_non_nullable + identitySecretBytes: null == identitySecretBytes + ? _value.identitySecretBytes + : identitySecretBytes // ignore: cast_nullable_to_non_nullable as Uint8List, encryptionKeyType: null == encryptionKeyType ? _value.encryptionKeyType @@ -134,8 +126,7 @@ abstract class _$$_LocalAccountCopyWith<$Res> @useResult $Res call( {IdentityMaster identityMaster, - @Uint8ListJsonConverter() Uint8List identitySecretKeyBytes, - @Uint8ListJsonConverter() Uint8List identitySecretSaltBytes, + @Uint8ListJsonConverter() Uint8List identitySecretBytes, EncryptionKeyType encryptionKeyType, bool biometricsEnabled, bool hiddenAccount, @@ -157,8 +148,7 @@ class __$$_LocalAccountCopyWithImpl<$Res> @override $Res call({ Object? identityMaster = null, - Object? identitySecretKeyBytes = null, - Object? identitySecretSaltBytes = null, + Object? identitySecretBytes = null, Object? encryptionKeyType = null, Object? biometricsEnabled = null, Object? hiddenAccount = null, @@ -169,13 +159,9 @@ class __$$_LocalAccountCopyWithImpl<$Res> ? _value.identityMaster : identityMaster // ignore: cast_nullable_to_non_nullable as IdentityMaster, - identitySecretKeyBytes: null == identitySecretKeyBytes - ? _value.identitySecretKeyBytes - : identitySecretKeyBytes // ignore: cast_nullable_to_non_nullable - as Uint8List, - identitySecretSaltBytes: null == identitySecretSaltBytes - ? _value.identitySecretSaltBytes - : identitySecretSaltBytes // ignore: cast_nullable_to_non_nullable + identitySecretBytes: null == identitySecretBytes + ? _value.identitySecretBytes + : identitySecretBytes // ignore: cast_nullable_to_non_nullable as Uint8List, encryptionKeyType: null == encryptionKeyType ? _value.encryptionKeyType @@ -202,8 +188,7 @@ class __$$_LocalAccountCopyWithImpl<$Res> class _$_LocalAccount implements _LocalAccount { const _$_LocalAccount( {required this.identityMaster, - @Uint8ListJsonConverter() required this.identitySecretKeyBytes, - @Uint8ListJsonConverter() required this.identitySecretSaltBytes, + @Uint8ListJsonConverter() required this.identitySecretBytes, required this.encryptionKeyType, required this.biometricsEnabled, required this.hiddenAccount, @@ -215,14 +200,11 @@ class _$_LocalAccount implements _LocalAccount { // The master key record for the account, containing the identityPublicKey @override final IdentityMaster identityMaster; -// The encrypted identity secret that goes with the identityPublicKey +// The encrypted identity secret that goes with +// the identityPublicKey with appended salt @override @Uint8ListJsonConverter() - final Uint8List identitySecretKeyBytes; -// The salt for the identity secret key encryption - @override - @Uint8ListJsonConverter() - final Uint8List identitySecretSaltBytes; + final Uint8List identitySecretBytes; // The kind of encryption input used on the account @override final EncryptionKeyType encryptionKeyType; @@ -239,7 +221,7 @@ class _$_LocalAccount implements _LocalAccount { @override String toString() { - return 'LocalAccount(identityMaster: $identityMaster, identitySecretKeyBytes: $identitySecretKeyBytes, identitySecretSaltBytes: $identitySecretSaltBytes, encryptionKeyType: $encryptionKeyType, biometricsEnabled: $biometricsEnabled, hiddenAccount: $hiddenAccount, name: $name)'; + return 'LocalAccount(identityMaster: $identityMaster, identitySecretBytes: $identitySecretBytes, encryptionKeyType: $encryptionKeyType, biometricsEnabled: $biometricsEnabled, hiddenAccount: $hiddenAccount, name: $name)'; } @override @@ -250,9 +232,7 @@ class _$_LocalAccount implements _LocalAccount { (identical(other.identityMaster, identityMaster) || other.identityMaster == identityMaster) && const DeepCollectionEquality() - .equals(other.identitySecretKeyBytes, identitySecretKeyBytes) && - const DeepCollectionEquality().equals( - other.identitySecretSaltBytes, identitySecretSaltBytes) && + .equals(other.identitySecretBytes, identitySecretBytes) && (identical(other.encryptionKeyType, encryptionKeyType) || other.encryptionKeyType == encryptionKeyType) && (identical(other.biometricsEnabled, biometricsEnabled) || @@ -267,8 +247,7 @@ class _$_LocalAccount implements _LocalAccount { int get hashCode => Object.hash( runtimeType, identityMaster, - const DeepCollectionEquality().hash(identitySecretKeyBytes), - const DeepCollectionEquality().hash(identitySecretSaltBytes), + const DeepCollectionEquality().hash(identitySecretBytes), encryptionKeyType, biometricsEnabled, hiddenAccount, @@ -291,9 +270,7 @@ class _$_LocalAccount implements _LocalAccount { abstract class _LocalAccount implements LocalAccount { const factory _LocalAccount( {required final IdentityMaster identityMaster, - @Uint8ListJsonConverter() required final Uint8List identitySecretKeyBytes, - @Uint8ListJsonConverter() - required final Uint8List identitySecretSaltBytes, + @Uint8ListJsonConverter() required final Uint8List identitySecretBytes, required final EncryptionKeyType encryptionKeyType, required final bool biometricsEnabled, required final bool hiddenAccount, @@ -304,12 +281,10 @@ abstract class _LocalAccount implements LocalAccount { @override // The master key record for the account, containing the identityPublicKey IdentityMaster get identityMaster; - @override // The encrypted identity secret that goes with the identityPublicKey + @override // The encrypted identity secret that goes with +// the identityPublicKey with appended salt @Uint8ListJsonConverter() - Uint8List get identitySecretKeyBytes; - @override // The salt for the identity secret key encryption - @Uint8ListJsonConverter() - Uint8List get identitySecretSaltBytes; + Uint8List get identitySecretBytes; @override // The kind of encryption input used on the account EncryptionKeyType get encryptionKeyType; @override // If account is not hidden, password can be retrieved via diff --git a/lib/entities/local_account.g.dart b/lib/entities/local_account.g.dart index 51780f7..372a59c 100644 --- a/lib/entities/local_account.g.dart +++ b/lib/entities/local_account.g.dart @@ -9,10 +9,8 @@ part of 'local_account.dart'; _$_LocalAccount _$$_LocalAccountFromJson(Map json) => _$_LocalAccount( identityMaster: IdentityMaster.fromJson(json['identity_master']), - identitySecretKeyBytes: const Uint8ListJsonConverter() - .fromJson(json['identity_secret_key_bytes'] as String), - identitySecretSaltBytes: const Uint8ListJsonConverter() - .fromJson(json['identity_secret_salt_bytes'] as String), + identitySecretBytes: const Uint8ListJsonConverter() + .fromJson(json['identity_secret_bytes'] as String), encryptionKeyType: EncryptionKeyType.fromJson(json['encryption_key_type']), biometricsEnabled: json['biometrics_enabled'] as bool, @@ -23,10 +21,8 @@ _$_LocalAccount _$$_LocalAccountFromJson(Map json) => Map _$$_LocalAccountToJson(_$_LocalAccount instance) => { 'identity_master': instance.identityMaster.toJson(), - 'identity_secret_key_bytes': const Uint8ListJsonConverter() - .toJson(instance.identitySecretKeyBytes), - 'identity_secret_salt_bytes': const Uint8ListJsonConverter() - .toJson(instance.identitySecretSaltBytes), + 'identity_secret_bytes': + const Uint8ListJsonConverter().toJson(instance.identitySecretBytes), 'encryption_key_type': instance.encryptionKeyType.toJson(), 'biometrics_enabled': instance.biometricsEnabled, 'hidden_account': instance.hiddenAccount, diff --git a/lib/entities/proto.dart b/lib/entities/proto.dart index bae4032..eeecfce 100644 --- a/lib/entities/proto.dart +++ b/lib/entities/proto.dart @@ -1,6 +1,6 @@ import 'dart:typed_data'; -import 'package:veilid/veilid.dart'; +import '../veilid_support/veilid_support.dart'; import 'proto/veilidchat.pb.dart' as proto; @@ -124,3 +124,34 @@ extension TypedKeyProto on TypedKey { static TypedKey fromProto(proto.TypedKey p) => TypedKey(kind: p.kind, value: CryptoKeyProto.fromProto(p.value)); } + +/// KeyPair protobuf marshaling +/// +extension KeyPairProto on KeyPair { + proto.KeyPair toProto() { + final out = proto.KeyPair() + ..key = key.toProto() + ..secret = secret.toProto(); + return out; + } + + static KeyPair fromProto(proto.KeyPair p) => KeyPair( + key: CryptoKeyProto.fromProto(p.key), + secret: CryptoKeyProto.fromProto(p.secret)); +} + +/// OwnedDHTRecordPointer protobuf marshaling +/// +extension OwnedDHTRecordPointerProto on OwnedDHTRecordPointer { + proto.OwnedDHTRecordPointer toProto() { + final out = proto.OwnedDHTRecordPointer() + ..recordKey = recordKey.toProto() + ..owner = owner.toProto(); + return out; + } + + static OwnedDHTRecordPointer fromProto(proto.OwnedDHTRecordPointer p) => + OwnedDHTRecordPointer( + recordKey: TypedKeyProto.fromProto(p.recordKey), + owner: KeyPairProto.fromProto(p.owner)); +} diff --git a/lib/entities/proto/veilidchat.pb.dart b/lib/entities/proto/veilidchat.pb.dart index aad81a0..6c6b9fe 100644 --- a/lib/entities/proto/veilidchat.pb.dart +++ b/lib/entities/proto/veilidchat.pb.dart @@ -468,6 +468,62 @@ class TypedKey extends $pb.GeneratedMessage { CryptoKey ensureValue() => $_ensure(1); } +class KeyPair extends $pb.GeneratedMessage { + factory KeyPair() => create(); + KeyPair._() : super(); + factory KeyPair.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory KeyPair.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'KeyPair', createEmptyInstance: create) + ..aOM(1, _omitFieldNames ? '' : 'key', subBuilder: CryptoKey.create) + ..aOM(2, _omitFieldNames ? '' : 'secret', subBuilder: CryptoKey.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + KeyPair clone() => KeyPair()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + KeyPair copyWith(void Function(KeyPair) updates) => super.copyWith((message) => updates(message as KeyPair)) as KeyPair; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static KeyPair create() => KeyPair._(); + KeyPair createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static KeyPair getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static KeyPair? _defaultInstance; + + @$pb.TagNumber(1) + CryptoKey get key => $_getN(0); + @$pb.TagNumber(1) + set key(CryptoKey v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasKey() => $_has(0); + @$pb.TagNumber(1) + void clearKey() => clearField(1); + @$pb.TagNumber(1) + CryptoKey ensureKey() => $_ensure(0); + + @$pb.TagNumber(2) + CryptoKey get secret => $_getN(1); + @$pb.TagNumber(2) + set secret(CryptoKey v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasSecret() => $_has(1); + @$pb.TagNumber(2) + void clearSecret() => clearField(2); + @$pb.TagNumber(2) + CryptoKey ensureSecret() => $_ensure(1); +} + class DHTData extends $pb.GeneratedMessage { factory DHTData() => create(); DHTData._() : super(); @@ -1125,8 +1181,7 @@ class OwnedDHTRecordPointer extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'OwnedDHTRecordPointer', createEmptyInstance: create) ..aOM(1, _omitFieldNames ? '' : 'recordKey', subBuilder: TypedKey.create) - ..aOM(2, _omitFieldNames ? '' : 'ownerKey', subBuilder: CryptoKey.create) - ..aOM(3, _omitFieldNames ? '' : 'ownerSecret', subBuilder: CryptoKey.create) + ..aOM(2, _omitFieldNames ? '' : 'owner', subBuilder: KeyPair.create) ..hasRequiredFields = false ; @@ -1163,26 +1218,15 @@ class OwnedDHTRecordPointer extends $pb.GeneratedMessage { TypedKey ensureRecordKey() => $_ensure(0); @$pb.TagNumber(2) - CryptoKey get ownerKey => $_getN(1); + KeyPair get owner => $_getN(1); @$pb.TagNumber(2) - set ownerKey(CryptoKey v) { setField(2, v); } + set owner(KeyPair v) { setField(2, v); } @$pb.TagNumber(2) - $core.bool hasOwnerKey() => $_has(1); + $core.bool hasOwner() => $_has(1); @$pb.TagNumber(2) - void clearOwnerKey() => clearField(2); + void clearOwner() => clearField(2); @$pb.TagNumber(2) - CryptoKey ensureOwnerKey() => $_ensure(1); - - @$pb.TagNumber(3) - CryptoKey get ownerSecret => $_getN(2); - @$pb.TagNumber(3) - set ownerSecret(CryptoKey v) { setField(3, v); } - @$pb.TagNumber(3) - $core.bool hasOwnerSecret() => $_has(2); - @$pb.TagNumber(3) - void clearOwnerSecret() => clearField(3); - @$pb.TagNumber(3) - CryptoKey ensureOwnerSecret() => $_ensure(2); + KeyPair ensureOwner() => $_ensure(1); } class Account extends $pb.GeneratedMessage { diff --git a/lib/entities/proto/veilidchat.pbjson.dart b/lib/entities/proto/veilidchat.pbjson.dart index 4d81fa8..512010a 100644 --- a/lib/entities/proto/veilidchat.pbjson.dart +++ b/lib/entities/proto/veilidchat.pbjson.dart @@ -146,6 +146,20 @@ final $typed_data.Uint8List typedKeyDescriptor = $convert.base64Decode( 'CghUeXBlZEtleRISCgRraW5kGAEgASgHUgRraW5kEiAKBXZhbHVlGAIgASgLMgouQ3J5cHRvS2' 'V5UgV2YWx1ZQ=='); +@$core.Deprecated('Use keyPairDescriptor instead') +const KeyPair$json = { + '1': 'KeyPair', + '2': [ + {'1': 'key', '3': 1, '4': 1, '5': 11, '6': '.CryptoKey', '10': 'key'}, + {'1': 'secret', '3': 2, '4': 1, '5': 11, '6': '.CryptoKey', '10': 'secret'}, + ], +}; + +/// Descriptor for `KeyPair`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List keyPairDescriptor = $convert.base64Decode( + 'CgdLZXlQYWlyEhwKA2tleRgBIAEoCzIKLkNyeXB0b0tleVIDa2V5EiIKBnNlY3JldBgCIAEoCz' + 'IKLkNyeXB0b0tleVIGc2VjcmV0'); + @$core.Deprecated('Use dHTDataDescriptor instead') const DHTData$json = { '1': 'DHTData', @@ -313,16 +327,14 @@ const OwnedDHTRecordPointer$json = { '1': 'OwnedDHTRecordPointer', '2': [ {'1': 'record_key', '3': 1, '4': 1, '5': 11, '6': '.TypedKey', '10': 'recordKey'}, - {'1': 'owner_key', '3': 2, '4': 1, '5': 11, '6': '.CryptoKey', '10': 'ownerKey'}, - {'1': 'owner_secret', '3': 3, '4': 1, '5': 11, '6': '.CryptoKey', '10': 'ownerSecret'}, + {'1': 'owner', '3': 2, '4': 1, '5': 11, '6': '.KeyPair', '10': 'owner'}, ], }; /// Descriptor for `OwnedDHTRecordPointer`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List ownedDHTRecordPointerDescriptor = $convert.base64Decode( 'ChVPd25lZERIVFJlY29yZFBvaW50ZXISKAoKcmVjb3JkX2tleRgBIAEoCzIJLlR5cGVkS2V5Ug' - 'lyZWNvcmRLZXkSJwoJb3duZXJfa2V5GAIgASgLMgouQ3J5cHRvS2V5Ughvd25lcktleRItCgxv' - 'd25lcl9zZWNyZXQYAyABKAsyCi5DcnlwdG9LZXlSC293bmVyU2VjcmV0'); + 'lyZWNvcmRLZXkSHgoFb3duZXIYAiABKAsyCC5LZXlQYWlyUgVvd25lcg=='); @$core.Deprecated('Use accountDescriptor instead') const Account$json = { diff --git a/lib/entities/user_login.dart b/lib/entities/user_login.dart index e307fe9..071011c 100644 --- a/lib/entities/user_login.dart +++ b/lib/entities/user_login.dart @@ -1,6 +1,8 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:veilid/veilid.dart'; + +import '../veilid_support/veilid_support.dart'; +import 'identity.dart'; part 'user_login.freezed.dart'; part 'user_login.g.dart'; @@ -15,6 +17,9 @@ class UserLogin with _$UserLogin { required TypedKey accountMasterRecordKey, // The identity secret as unlocked from the local accounts table required TypedSecret identitySecret, + // The account record key, owner key and secret pulled from the identity + required AccountRecordInfo accountRecordInfo, + // The time this login was most recently used required Timestamp lastActive, }) = _UserLogin; diff --git a/lib/entities/user_login.freezed.dart b/lib/entities/user_login.freezed.dart index f13f3f6..a76f54d 100644 --- a/lib/entities/user_login.freezed.dart +++ b/lib/entities/user_login.freezed.dart @@ -24,6 +24,8 @@ mixin _$UserLogin { Typed get accountMasterRecordKey => throw _privateConstructorUsedError; // The identity secret as unlocked from the local accounts table Typed get identitySecret => + throw _privateConstructorUsedError; // The account record key, owner key and secret pulled from the identity + AccountRecordInfo get accountRecordInfo => throw _privateConstructorUsedError; // The time this login was most recently used Timestamp get lastActive => throw _privateConstructorUsedError; @@ -41,7 +43,10 @@ abstract class $UserLoginCopyWith<$Res> { $Res call( {Typed accountMasterRecordKey, Typed identitySecret, + AccountRecordInfo accountRecordInfo, Timestamp lastActive}); + + $AccountRecordInfoCopyWith<$Res> get accountRecordInfo; } /// @nodoc @@ -59,6 +64,7 @@ class _$UserLoginCopyWithImpl<$Res, $Val extends UserLogin> $Res call({ Object? accountMasterRecordKey = null, Object? identitySecret = null, + Object? accountRecordInfo = null, Object? lastActive = null, }) { return _then(_value.copyWith( @@ -70,12 +76,24 @@ class _$UserLoginCopyWithImpl<$Res, $Val extends UserLogin> ? _value.identitySecret : identitySecret // ignore: cast_nullable_to_non_nullable as Typed, + accountRecordInfo: null == accountRecordInfo + ? _value.accountRecordInfo + : accountRecordInfo // ignore: cast_nullable_to_non_nullable + as AccountRecordInfo, lastActive: null == lastActive ? _value.lastActive : lastActive // ignore: cast_nullable_to_non_nullable as Timestamp, ) as $Val); } + + @override + @pragma('vm:prefer-inline') + $AccountRecordInfoCopyWith<$Res> get accountRecordInfo { + return $AccountRecordInfoCopyWith<$Res>(_value.accountRecordInfo, (value) { + return _then(_value.copyWith(accountRecordInfo: value) as $Val); + }); + } } /// @nodoc @@ -88,7 +106,11 @@ abstract class _$$_UserLoginCopyWith<$Res> implements $UserLoginCopyWith<$Res> { $Res call( {Typed accountMasterRecordKey, Typed identitySecret, + AccountRecordInfo accountRecordInfo, Timestamp lastActive}); + + @override + $AccountRecordInfoCopyWith<$Res> get accountRecordInfo; } /// @nodoc @@ -104,6 +126,7 @@ class __$$_UserLoginCopyWithImpl<$Res> $Res call({ Object? accountMasterRecordKey = null, Object? identitySecret = null, + Object? accountRecordInfo = null, Object? lastActive = null, }) { return _then(_$_UserLogin( @@ -115,6 +138,10 @@ class __$$_UserLoginCopyWithImpl<$Res> ? _value.identitySecret : identitySecret // ignore: cast_nullable_to_non_nullable as Typed, + accountRecordInfo: null == accountRecordInfo + ? _value.accountRecordInfo + : accountRecordInfo // ignore: cast_nullable_to_non_nullable + as AccountRecordInfo, lastActive: null == lastActive ? _value.lastActive : lastActive // ignore: cast_nullable_to_non_nullable @@ -129,6 +156,7 @@ class _$_UserLogin implements _UserLogin { const _$_UserLogin( {required this.accountMasterRecordKey, required this.identitySecret, + required this.accountRecordInfo, required this.lastActive}); factory _$_UserLogin.fromJson(Map json) => @@ -140,13 +168,16 @@ class _$_UserLogin implements _UserLogin { // The identity secret as unlocked from the local accounts table @override final Typed identitySecret; +// The account record key, owner key and secret pulled from the identity + @override + final AccountRecordInfo accountRecordInfo; // The time this login was most recently used @override final Timestamp lastActive; @override String toString() { - return 'UserLogin(accountMasterRecordKey: $accountMasterRecordKey, identitySecret: $identitySecret, lastActive: $lastActive)'; + return 'UserLogin(accountMasterRecordKey: $accountMasterRecordKey, identitySecret: $identitySecret, accountRecordInfo: $accountRecordInfo, lastActive: $lastActive)'; } @override @@ -158,14 +189,16 @@ class _$_UserLogin implements _UserLogin { other.accountMasterRecordKey == accountMasterRecordKey) && (identical(other.identitySecret, identitySecret) || other.identitySecret == identitySecret) && + (identical(other.accountRecordInfo, accountRecordInfo) || + other.accountRecordInfo == accountRecordInfo) && (identical(other.lastActive, lastActive) || other.lastActive == lastActive)); } @JsonKey(ignore: true) @override - int get hashCode => Object.hash( - runtimeType, accountMasterRecordKey, identitySecret, lastActive); + int get hashCode => Object.hash(runtimeType, accountMasterRecordKey, + identitySecret, accountRecordInfo, lastActive); @JsonKey(ignore: true) @override @@ -185,6 +218,7 @@ abstract class _UserLogin implements UserLogin { const factory _UserLogin( {required final Typed accountMasterRecordKey, required final Typed identitySecret, + required final AccountRecordInfo accountRecordInfo, required final Timestamp lastActive}) = _$_UserLogin; factory _UserLogin.fromJson(Map json) = @@ -194,6 +228,8 @@ abstract class _UserLogin implements UserLogin { Typed get accountMasterRecordKey; @override // The identity secret as unlocked from the local accounts table Typed get identitySecret; + @override // The account record key, owner key and secret pulled from the identity + AccountRecordInfo get accountRecordInfo; @override // The time this login was most recently used Timestamp get lastActive; @override diff --git a/lib/entities/user_login.g.dart b/lib/entities/user_login.g.dart index 17ed500..9cc7f6f 100644 --- a/lib/entities/user_login.g.dart +++ b/lib/entities/user_login.g.dart @@ -11,6 +11,8 @@ _$_UserLogin _$$_UserLoginFromJson(Map json) => _$_UserLogin( json['account_master_record_key']), identitySecret: Typed.fromJson(json['identity_secret']), + accountRecordInfo: + AccountRecordInfo.fromJson(json['account_record_info']), lastActive: Timestamp.fromJson(json['last_active']), ); @@ -18,6 +20,7 @@ Map _$$_UserLoginToJson(_$_UserLogin instance) => { 'account_master_record_key': instance.accountMasterRecordKey.toJson(), 'identity_secret': instance.identitySecret.toJson(), + 'account_record_info': instance.accountRecordInfo.toJson(), 'last_active': instance.lastActive.toJson(), }; diff --git a/lib/entities/veilidchat.proto b/lib/entities/veilidchat.proto index ee5af13..89bc8fa 100644 --- a/lib/entities/veilidchat.proto +++ b/lib/entities/veilidchat.proto @@ -42,7 +42,7 @@ message Nonce { fixed32 u5 = 6; } -// 36-byte typed crpyto key +// 36-byte typed crypto key message TypedKey { // CryptoKind FourCC in bigendian format fixed32 kind = 1; @@ -50,6 +50,15 @@ message TypedKey { CryptoKey value = 2; } +// Key pair +message KeyPair { + // Public key + CryptoKey key = 1; + // Private key + CryptoKey secret = 2; +} + + // DHTData - represents chunked blob data in the DHT // Header in subkey 0 follows this structure // @@ -226,14 +235,12 @@ message Profile { optional TypedKey avatar = 5; } -// A pointer to an owned DHT record +// A pointer to an child DHT record message OwnedDHTRecordPointer { // DHT Record key TypedKey record_key = 1; // DHT record owner key - CryptoKey owner_key = 2; - // DHT record owner secret - CryptoKey owner_secret = 3; + KeyPair owner = 2; } // A record of an individual account diff --git a/lib/pages/main_pager/account_page.dart b/lib/pages/main_pager/account_page.dart index 41663d9..6fd057c 100644 --- a/lib/pages/main_pager/account_page.dart +++ b/lib/pages/main_pager/account_page.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_translate/flutter_translate.dart'; -import 'package:veilid/veilid.dart'; import '../../components/contact_list_widget.dart'; import '../../components/profile.dart'; @@ -13,6 +12,7 @@ import '../../providers/account.dart'; import '../../providers/local_accounts.dart'; import '../../providers/logins.dart'; import '../../tools/tools.dart'; +import '../../veilid_support/veilid_support.dart'; class AccountPage extends ConsumerStatefulWidget { const AccountPage({super.key}); @@ -40,7 +40,11 @@ class AccountPageState extends ConsumerState { // ignore: prefer_expression_function_bodies Widget buildAccountList(BuildContext context) { - return Center(child: Text("account list")); + return Column(children: [ + Center(child: Text("Small Profile")), + Center(child: Text("Contact invitations")), + Center(child: Text("Contacts")) + ]); } Widget buildUnlockAccount( @@ -97,7 +101,9 @@ class AccountPageState extends ConsumerState { // Delete account await ref .read(localAccountsProvider.notifier) - .deleteAccount(activeUserLogin); + .deleteLocalAccount(activeUserLogin); + // Switch to no active user login + await ref.read(loginsProvider.notifier).switchToAccount(null); }); return waitingPage(context); case AccountInfoStatus.accountInvalid: @@ -109,7 +115,9 @@ class AccountPageState extends ConsumerState { // Delete account await ref .read(localAccountsProvider.notifier) - .deleteAccount(activeUserLogin); + .deleteLocalAccount(activeUserLogin); + // Switch to no active user login + await ref.read(loginsProvider.notifier).switchToAccount(null); }); return waitingPage(context); case AccountInfoStatus.accountLocked: diff --git a/lib/pages/main_pager/main_pager.dart b/lib/pages/main_pager/main_pager.dart index 31b3a36..686e9a5 100644 --- a/lib/pages/main_pager/main_pager.dart +++ b/lib/pages/main_pager/main_pager.dart @@ -6,6 +6,7 @@ import 'package:flutter_translate/flutter_translate.dart'; import 'package:stylish_bottom_bar/model/bar_items.dart'; import 'package:stylish_bottom_bar/stylish_bottom_bar.dart'; +import '../../components/contact_invitation_display.dart'; import 'account_page.dart'; import 'chats_page.dart'; @@ -101,6 +102,25 @@ class MainPagerState extends ConsumerState return bottomBarItems; } + Future _onNewContactInvitation(BuildContext context) async { + Scaffold.of(context).showBottomSheet((context) => SizedBox( + height: 200, child: Center(child: ContactInvitationDisplay()))); + } + + Future _onNewChat(BuildContext context) async { + // + } + + Future _onFloatingActionButtonPressed(BuildContext context) async { + if (_currentPage == 0) { + // New contact invitation + return _onNewContactInvitation(context); + } else if (_currentPage == 1) { + // New chat + return _onNewChat(context); + } + } + @override // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { @@ -153,18 +173,15 @@ class MainPagerState extends ConsumerState ), floatingActionButton: FloatingActionButton( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(14))), - //foregroundColor: theme.colorScheme.secondary, - backgroundColor: theme.colorScheme.secondaryContainer, - child: Icon( - _fabIconList[_currentPage], - color: theme.colorScheme.onSecondaryContainer, - ), - onPressed: () { - // xxx - }, - ), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(14))), + //foregroundColor: theme.colorScheme.secondary, + backgroundColor: theme.colorScheme.secondaryContainer, + child: Icon( + _fabIconList[_currentPage], + color: theme.colorScheme.onSecondaryContainer, + ), + onPressed: () async => _onFloatingActionButtonPressed(context)), floatingActionButtonLocation: FloatingActionButtonLocation.endDocked, ); } diff --git a/lib/pages/new_account.dart b/lib/pages/new_account.dart index 0c81dc5..7da5ede 100644 --- a/lib/pages/new_account.dart +++ b/lib/pages/new_account.dart @@ -5,10 +5,8 @@ import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; -import 'package:quickalert/quickalert.dart'; import '../components/default_app_bar.dart'; -import '../entities/proto.dart' as proto; import '../providers/local_accounts.dart'; import '../providers/logins.dart'; import '../providers/window_control.dart'; @@ -40,21 +38,24 @@ class NewAccountPageState extends ConsumerState { }); } + /// Creates a new master identity, an account associated with the master + /// identity, stores the account in the identity key and then logs into + /// that account with no password set at this time Future createAccount() async { - final imws = await newIdentityMaster(); - try { - final localAccounts = ref.read(localAccountsProvider.notifier); - final logins = ref.read(loginsProvider.notifier); + final localAccounts = ref.read(localAccountsProvider.notifier); + final logins = ref.read(loginsProvider.notifier); - final profile = proto.Profile() - ..name = _formKey.currentState!.fields[formFieldName]!.value as String - ..title = - _formKey.currentState!.fields[formFieldTitle]!.value as String; - final account = proto.Account()..profile = profile; - final localAccount = await localAccounts.newAccount( + final name = _formKey.currentState!.fields[formFieldName]!.value as String; + final title = + _formKey.currentState!.fields[formFieldTitle]!.value as String; + + final imws = await IdentityMasterWithSecrets.create(); + try { + final localAccount = await localAccounts.newLocalAccount( identityMaster: imws.identityMaster, identitySecret: imws.identitySecret, - account: account); + name: name, + title: title); // Log in the new account by default with no pin final ok = await logins diff --git a/lib/providers/account.dart b/lib/providers/account.dart index 56df9ec..5dfd8ad 100644 --- a/lib/providers/account.dart +++ b/lib/providers/account.dart @@ -1,8 +1,5 @@ -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:veilid/veilid.dart'; -import '../entities/entities.dart'; import '../entities/proto.dart' as proto; import '../veilid_support/veilid_support.dart'; @@ -30,11 +27,12 @@ class AccountInfo { proto.Account? account; } +/// Get an account from the identity key and if it is logged in and we +/// have its secret available, return the account record contents @riverpod Future fetchAccount(FetchAccountRef ref, {required TypedKey accountMasterRecordKey}) async { // Get which local account we want to fetch the profile for - final veilid = await eventualVeilid.future; final localAccount = await ref.watch( fetchLocalAccountProvider(accountMasterRecordKey: accountMasterRecordKey) .future); @@ -56,55 +54,17 @@ Future fetchAccount(FetchAccountRef ref, return AccountInfo(status: AccountInfoStatus.accountLocked, active: active); } - // Read the identity key to get the account keys - final dhtctx = (await veilid.routingContext()) - .withPrivacy() - .withSequencing(Sequencing.ensureOrdered); - final identityRecordCrypto = await DHTRecordCryptoPrivate.fromSecret( - localAccount.identityMaster.identityRecordKey.kind, - login.identitySecret.value); - - late final TypedKey accountRecordKey; - late final KeyPair accountRecordOwner; - - await (await DHTRecord.openRead( - dhtctx, localAccount.identityMaster.identityRecordKey, - crypto: identityRecordCrypto)) - .scope((identityRec) async { - final identity = await identityRec.getJson(Identity.fromJson); - if (identity == null) { - // Identity could not be read or decrypted from DHT - return AccountInfo( - status: AccountInfoStatus.accountInvalid, active: active); - } - final accountRecords = IMapOfSets.from(identity.accountRecords); - final vcAccounts = accountRecords.get(veilidChatAccountKey); - if (vcAccounts.length != 1) { - // No veilidchat account, or multiple accounts - // somehow associated with identity - return AccountInfo( - status: AccountInfoStatus.accountInvalid, active: active); - } - final accountRecordInfo = vcAccounts.first; - accountRecordKey = accountRecordInfo.key; - accountRecordOwner = accountRecordInfo.owner; - }); - // Pull the account DHT key, decode it and return it - final accountRecordCrypto = await DHTRecordCryptoPrivate.fromSecret( - accountRecordKey.kind, accountRecordOwner.secret); - late final proto.Account account; - await (await DHTRecord.openRead(dhtctx, accountRecordKey, - crypto: accountRecordCrypto)) - .scope((accountRec) async { - final protoAccount = await accountRec.getProtobuf(proto.Account.fromBuffer); - if (protoAccount == null) { - // Account could not be read or decrypted from DHT - return AccountInfo( - status: AccountInfoStatus.accountInvalid, active: active); - } - account = protoAccount; - }); + final pool = await DHTRecordPool.instance(); + final account = await (await pool.openOwned( + login.accountRecordInfo.accountRecord, + parent: localAccount.identityMaster.identityRecordKey)) + .scope((accountRec) => accountRec.getProtobuf(proto.Account.fromBuffer)); + if (account == null) { + // Account could not be read or decrypted from DHT + return AccountInfo( + status: AccountInfoStatus.accountInvalid, active: active); + } // Got account, decrypted and decoded return AccountInfo( diff --git a/lib/providers/account.g.dart b/lib/providers/account.g.dart index dec57bb..1c9df41 100644 --- a/lib/providers/account.g.dart +++ b/lib/providers/account.g.dart @@ -6,7 +6,7 @@ part of 'account.dart'; // RiverpodGenerator // ************************************************************************** -String _$fetchAccountHash() => r'4d94703d07a21509650e19f60ea67ac96a39742e'; +String _$fetchAccountHash() => r'88dadc0d005cef8b3df1d03088c8a5da728c333c'; /// Copied from Dart SDK class _SystemHash { @@ -31,16 +31,28 @@ class _SystemHash { typedef FetchAccountRef = AutoDisposeFutureProviderRef; -/// See also [fetchAccount]. +/// Get an account from the identity key and if it is logged in and we +/// have its secret available, return the account record contents +/// +/// Copied from [fetchAccount]. @ProviderFor(fetchAccount) const fetchAccountProvider = FetchAccountFamily(); -/// See also [fetchAccount]. +/// Get an account from the identity key and if it is logged in and we +/// have its secret available, return the account record contents +/// +/// Copied from [fetchAccount]. class FetchAccountFamily extends Family> { - /// See also [fetchAccount]. + /// Get an account from the identity key and if it is logged in and we + /// have its secret available, return the account record contents + /// + /// Copied from [fetchAccount]. const FetchAccountFamily(); - /// See also [fetchAccount]. + /// Get an account from the identity key and if it is logged in and we + /// have its secret available, return the account record contents + /// + /// Copied from [fetchAccount]. FetchAccountProvider call({ required Typed accountMasterRecordKey, }) { @@ -73,9 +85,15 @@ class FetchAccountFamily extends Family> { String? get name => r'fetchAccountProvider'; } -/// See also [fetchAccount]. +/// Get an account from the identity key and if it is logged in and we +/// have its secret available, return the account record contents +/// +/// Copied from [fetchAccount]. class FetchAccountProvider extends AutoDisposeFutureProvider { - /// See also [fetchAccount]. + /// Get an account from the identity key and if it is logged in and we + /// have its secret available, return the account record contents + /// + /// Copied from [fetchAccount]. FetchAccountProvider({ required this.accountMasterRecordKey, }) : super.internal( diff --git a/lib/providers/contact_request_records.dart b/lib/providers/contact_request_records.dart deleted file mode 100644 index ee90e99..0000000 --- a/lib/providers/contact_request_records.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:veilid/veilid.dart'; - -import '../entities/entities.dart'; -import '../entities/proto.dart' as proto; -import '../tools/tools.dart'; -import '../veilid_support/dht_short_array.dart'; -import '../veilid_support/veilid_support.dart'; -import 'logins.dart'; - -part 'contact_request_records.g.dart'; - -// Contact invitation records stored in Account -class ContactRequestRecords { - DHTShortArray _backingArray; - - Future newContactRequest( - proto.EncryptionKind encryptionKind, - String encryptionKey, - ) async { - // - } -} - -class ContactRequestRecordsParams { - ContactRequestRecordsParams({required this.contactRequestsDHTListKey}); - TypedKey contactRequestsDHTListKey; -} - -@riverpod -Future fetchContactRequestRecords( - FetchContactRequestRecordsRef ref, - {required ContactRequestRecordsParams params}) async { - // final localAccounts = await ref.watch(localAccountsProvider.future); - // try { - // return localAccounts.firstWhere( - // (e) => e.identityMaster.masterRecordKey == accountMasterRecordKey); - // } on Exception catch (e) { - // if (e is StateError) { - // return null; - // } - // rethrow; - // } -} diff --git a/lib/providers/contact_request_records.g.dart b/lib/providers/contact_request_records.g.dart deleted file mode 100644 index af5b142..0000000 --- a/lib/providers/contact_request_records.g.dart +++ /dev/null @@ -1,117 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'contact_request_records.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$fetchContactRequestRecordsHash() => - r'603c6d81b22d1cb4fd26cf32b98d3206ff6bc38c'; - -/// Copied from Dart SDK -class _SystemHash { - _SystemHash._(); - - static int combine(int hash, int value) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + value); - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); - return hash ^ (hash >> 6); - } - - static int finish(int hash) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); - // ignore: parameter_assignments - hash = hash ^ (hash >> 11); - return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); - } -} - -typedef FetchContactRequestRecordsRef - = AutoDisposeFutureProviderRef; - -/// See also [fetchContactRequestRecords]. -@ProviderFor(fetchContactRequestRecords) -const fetchContactRequestRecordsProvider = FetchContactRequestRecordsFamily(); - -/// See also [fetchContactRequestRecords]. -class FetchContactRequestRecordsFamily - extends Family> { - /// See also [fetchContactRequestRecords]. - const FetchContactRequestRecordsFamily(); - - /// See also [fetchContactRequestRecords]. - FetchContactRequestRecordsProvider call({ - required ContactRequestRecordsParams params, - }) { - return FetchContactRequestRecordsProvider( - params: params, - ); - } - - @override - FetchContactRequestRecordsProvider getProviderOverride( - covariant FetchContactRequestRecordsProvider provider, - ) { - return call( - params: provider.params, - ); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'fetchContactRequestRecordsProvider'; -} - -/// See also [fetchContactRequestRecords]. -class FetchContactRequestRecordsProvider - extends AutoDisposeFutureProvider { - /// See also [fetchContactRequestRecords]. - FetchContactRequestRecordsProvider({ - required this.params, - }) : super.internal( - (ref) => fetchContactRequestRecords( - ref, - params: params, - ), - from: fetchContactRequestRecordsProvider, - name: r'fetchContactRequestRecordsProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$fetchContactRequestRecordsHash, - dependencies: FetchContactRequestRecordsFamily._dependencies, - allTransitiveDependencies: - FetchContactRequestRecordsFamily._allTransitiveDependencies, - ); - - final ContactRequestRecordsParams params; - - @override - bool operator ==(Object other) { - return other is FetchContactRequestRecordsProvider && - other.params == params; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, params.hashCode); - - return _SystemHash.finish(hash); - } -} -// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions diff --git a/lib/providers/local_accounts.dart b/lib/providers/local_accounts.dart index 450986a..c2be457 100644 --- a/lib/providers/local_accounts.dart +++ b/lib/providers/local_accounts.dart @@ -4,18 +4,16 @@ import 'dart:typed_data'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:veilid/veilid.dart'; import '../entities/entities.dart'; import '../entities/proto.dart' as proto; import '../tools/tools.dart'; import '../veilid_support/veilid_support.dart'; +import 'account.dart'; import 'logins.dart'; part 'local_accounts.g.dart'; -const String veilidChatAccountKey = 'com.veilid.veilidchat'; - // Local account manager @riverpod class LocalAccounts extends _$LocalAccounts @@ -53,84 +51,71 @@ class LocalAccounts extends _$LocalAccounts state = AsyncValue.data(updated); } - /// Creates a new account associated with master identity - Future newAccount( - {required IdentityMaster identityMaster, - required SecretKey identitySecret, - required proto.Account account, + /// Make encrypted identitySecret + Future _encryptIdentitySecret( + {required SecretKey identitySecret, + required CryptoKind cryptoKind, EncryptionKeyType encryptionKeyType = EncryptionKeyType.none, String encryptionKey = ''}) async { final veilid = await eventualVeilid.future; - final localAccounts = state.requireValue; - // Encrypt identitySecret with key late final Uint8List identitySecretBytes; - late final Uint8List identitySecretSaltBytes; - switch (encryptionKeyType) { case EncryptionKeyType.none: identitySecretBytes = identitySecret.decode(); - identitySecretSaltBytes = Uint8List(0); case EncryptionKeyType.pin: case EncryptionKeyType.password: - final cs = - await veilid.getCryptoSystem(identityMaster.identityRecordKey.kind); + final cs = await veilid.getCryptoSystem(cryptoKind); final ekbytes = Uint8List.fromList(utf8.encode(encryptionKey)); final nonce = await cs.randomNonce(); - identitySecretSaltBytes = nonce.decode(); + final identitySecretSaltBytes = nonce.decode(); final sharedSecret = await cs.deriveSharedSecret(ekbytes, identitySecretSaltBytes); - identitySecretBytes = - await cs.cryptNoAuth(identitySecret.decode(), nonce, sharedSecret); + identitySecretBytes = (await cs.cryptNoAuth( + identitySecret.decode(), nonce, sharedSecret)) + ..addAll(identitySecretSaltBytes); } + return identitySecretBytes; + } + + /// Creates a new Account associated with master identity + /// Adds a logged-out LocalAccount to track its existence on this device + Future newLocalAccount( + {required IdentityMaster identityMaster, + required SecretKey identitySecret, + required String name, + required String title, + EncryptionKeyType encryptionKeyType = EncryptionKeyType.none, + String encryptionKey = ''}) async { + final localAccounts = state.requireValue; + + /////// Add account with profile to DHT + await identityMaster.newAccount( + identitySecret: identitySecret, + name: name, + title: title, + ); + + // Encrypt identitySecret with key + final identitySecretBytes = await _encryptIdentitySecret( + identitySecret: identitySecret, + cryptoKind: identityMaster.identityRecordKey.kind, + encryptionKey: encryptionKey, + encryptionKeyType: encryptionKeyType); // Create local account object + // Does not contain the account key or its secret + // as that is not to be persisted, and only pulled from the identity key + // and optionally decrypted with the unlock password final localAccount = LocalAccount( identityMaster: identityMaster, - identitySecretKeyBytes: identitySecretBytes, - identitySecretSaltBytes: identitySecretSaltBytes, + identitySecretBytes: identitySecretBytes, encryptionKeyType: encryptionKeyType, biometricsEnabled: false, hiddenAccount: false, - name: account.profile.name, + name: name, ); - /////// Add account with profile to DHT - - // Create private routing context - final dhtctx = (await veilid.routingContext()) - .withPrivacy() - .withSequencing(Sequencing.ensureOrdered); - - // Open identity key for writing - await (await DHTRecord.openWrite(dhtctx, identityMaster.identityRecordKey, - identityMaster.identityWriter(identitySecret))) - .scope((identityRec) async { - // Create new account to insert into identity - await (await DHTRecord.create(dhtctx)).deleteScope((accountRec) async { - // Write account key - await accountRec.eventualWriteProtobuf(account); - - // Update identity key to include account - final newAccountRecordInfo = AccountRecordInfo( - key: accountRec.key, owner: accountRec.ownerKeyPair!); - - await identityRec.eventualUpdateJson(Identity.fromJson, - (oldIdentity) async { - final oldAccountRecords = IMapOfSets.from(oldIdentity.accountRecords); - // Only allow one account per identity for veilidchat - if (oldAccountRecords.get(veilidChatAccountKey).isNotEmpty) { - throw StateError( - 'Only one account per identity allowed for VeilidChat'); - } - final accountRecords = oldAccountRecords - .add(veilidChatAccountKey, newAccountRecordInfo) - .asIMap(); - return oldIdentity.copyWith(accountRecords: accountRecords); - }); - }); - }); - // Add local account object to internal store final newLocalAccounts = localAccounts.add(localAccount); await store(newLocalAccounts); @@ -141,7 +126,7 @@ class LocalAccounts extends _$LocalAccounts } /// Remove an account and wipe the messages for this account from this device - Future deleteAccount(TypedKey accountMasterRecordKey) async { + Future deleteLocalAccount(TypedKey accountMasterRecordKey) async { final logins = ref.read(loginsProvider.notifier); await logins.logout(accountMasterRecordKey); @@ -159,6 +144,8 @@ class LocalAccounts extends _$LocalAccounts /// Import an account from another VeilidChat instance /// Recover an account with the master identity secret + + /// Delete an account from all devices } @riverpod diff --git a/lib/providers/local_accounts.g.dart b/lib/providers/local_accounts.g.dart index 058aa5b..fb18af3 100644 --- a/lib/providers/local_accounts.g.dart +++ b/lib/providers/local_accounts.g.dart @@ -112,7 +112,7 @@ class FetchLocalAccountProvider } } -String _$localAccountsHash() => r'd6ced0ad7108c1111603235cf394faa5f6bcdae1'; +String _$localAccountsHash() => r'a9a1e1765188556858ec982c9e99f780756ade1e'; /// See also [LocalAccounts]. @ProviderFor(LocalAccounts) diff --git a/lib/providers/logins.dart b/lib/providers/logins.dart index 4542467..15d7fd5 100644 --- a/lib/providers/logins.dart +++ b/lib/providers/logins.dart @@ -3,7 +3,6 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:veilid/veilid.dart'; import '../entities/entities.dart'; import '../veilid_support/veilid_support.dart'; @@ -46,8 +45,44 @@ class Logins extends _$Logins with AsyncTableDBBacked { state = AsyncValue.data(updated); } - Future loginWithNone(TypedKey accountMasterRecordKey) async { + Future _loginCommon( + IdentityMaster identityMaster, SecretKey identitySecret) async { final veilid = await eventualVeilid.future; + final cs = + await veilid.getCryptoSystem(identityMaster.identityRecordKey.kind); + final keyOk = await cs.validateKeyPair( + identityMaster.identityPublicKey, identitySecret); + if (!keyOk) { + throw Exception('Identity is corrupted'); + } + + // Read the identity key to get the account keys + final accountRecordInfo = await identityMaster.readAccountFromIdentity( + identitySecret: identitySecret); + + // Add to user logins and select it + final current = state.requireValue; + final now = veilid.now(); + final updated = current.copyWith( + userLogins: current.userLogins.replaceFirstWhere( + (ul) => ul.accountMasterRecordKey == identityMaster.masterRecordKey, + (ul) => ul != null + ? ul.copyWith(lastActive: now) + : UserLogin( + accountMasterRecordKey: identityMaster.masterRecordKey, + identitySecret: + TypedSecret(kind: cs.kind(), value: identitySecret), + accountRecordInfo: accountRecordInfo, + lastActive: now), + addIfNotFound: true), + activeUserLogin: identityMaster.masterRecordKey); + await store(updated); + state = AsyncValue.data(updated); + + return true; + } + + Future loginWithNone(TypedKey accountMasterRecordKey) async { final localAccounts = ref.read(localAccountsProvider).requireValue; // Get account, throws if not found @@ -62,36 +97,10 @@ class Logins extends _$Logins with AsyncTableDBBacked { } final identitySecret = - SecretKey.fromBytes(localAccount.identitySecretKeyBytes); + SecretKey.fromBytes(localAccount.identitySecretBytes); - // Validate this secret with the identity public key - final cs = await veilid - .getCryptoSystem(localAccount.identityMaster.identityRecordKey.kind); - final keyOk = await cs.validateKeyPair( - localAccount.identityMaster.identityPublicKey, identitySecret); - if (!keyOk) { - throw Exception('Identity is corrupted'); - } - - // Add to user logins and select it - final current = state.requireValue; - final now = veilid.now(); - final updated = current.copyWith( - userLogins: current.userLogins.replaceFirstWhere( - (ul) => ul.accountMasterRecordKey == accountMasterRecordKey, - (ul) => ul != null - ? ul.copyWith(lastActive: now) - : UserLogin( - accountMasterRecordKey: accountMasterRecordKey, - identitySecret: - TypedSecret(kind: cs.kind(), value: identitySecret), - lastActive: now), - addIfNotFound: true), - activeUserLogin: accountMasterRecordKey); - await store(updated); - state = AsyncValue.data(updated); - - return true; + // Validate this secret with the identity public key and log in + return _loginCommon(localAccount.identityMaster, identitySecret); } Future loginWithPasswordOrPin( @@ -112,39 +121,21 @@ class Logins extends _$Logins with AsyncTableDBBacked { } final cs = await veilid .getCryptoSystem(localAccount.identityMaster.identityRecordKey.kind); - final ekbytes = Uint8List.fromList(utf8.encode(encryptionKey)); - final eksalt = localAccount.identitySecretSaltBytes; - final nonce = Nonce.fromBytes(eksalt); - final sharedSecret = await cs.deriveSharedSecret(ekbytes, eksalt); - final identitySecret = SecretKey.fromBytes(await cs.cryptNoAuth( - localAccount.identitySecretKeyBytes, nonce, sharedSecret)); + final encryptionKeyBytes = Uint8List.fromList(utf8.encode(encryptionKey)); - // Validate this secret with the identity public key - final keyOk = await cs.validateKeyPair( - localAccount.identityMaster.identityPublicKey, identitySecret); - if (!keyOk) { - return false; - } + final identitySecretKeyBytes = + localAccount.identitySecretBytes.sublist(0, SecretKey.decodedLength()); + final identitySecretSaltBytes = + localAccount.identitySecretBytes.sublist(SecretKey.decodedLength()); - // Add to user logins and select it - final current = state.requireValue; - final now = veilid.now(); - final updated = current.copyWith( - userLogins: current.userLogins.replaceFirstWhere( - (ul) => ul.accountMasterRecordKey == accountMasterRecordKey, - (ul) => ul != null - ? ul.copyWith(lastActive: now) - : UserLogin( - accountMasterRecordKey: accountMasterRecordKey, - identitySecret: - TypedSecret(kind: cs.kind(), value: identitySecret), - lastActive: now), - addIfNotFound: true), - activeUserLogin: accountMasterRecordKey); - await store(updated); - state = AsyncValue.data(updated); + final nonce = Nonce.fromBytes(identitySecretSaltBytes); + final sharedSecret = await cs.deriveSharedSecret( + encryptionKeyBytes, identitySecretSaltBytes); + final identitySecret = SecretKey.fromBytes( + await cs.cryptNoAuth(identitySecretKeyBytes, nonce, sharedSecret)); - return true; + // Validate this secret with the identity public key and log in + return _loginCommon(localAccount.identityMaster, identitySecret); } Future logout(TypedKey? accountMasterRecordKey) async { diff --git a/lib/providers/logins.g.dart b/lib/providers/logins.g.dart index b4f1ed8..7bbb28e 100644 --- a/lib/providers/logins.g.dart +++ b/lib/providers/logins.g.dart @@ -111,7 +111,7 @@ class FetchLoginProvider extends AutoDisposeFutureProvider { } } -String _$loginsHash() => r'ed9dbe91a248f662ccb0fac6edf5b1892cf2ef92'; +String _$loginsHash() => r'5720eaacf858b2e1d69ebf9d2a981173a30f8592'; /// See also [Logins]. @ProviderFor(Logins) diff --git a/lib/providers/providers.dart b/lib/providers/providers.dart deleted file mode 100644 index 250e5f3..0000000 --- a/lib/providers/providers.dart +++ /dev/null @@ -1,5 +0,0 @@ -export 'account.dart'; -export 'connection_state.dart'; -export 'local_accounts.dart'; -export 'logins.dart'; -export 'window_control.dart'; diff --git a/lib/providers/veilid_instance.dart b/lib/providers/veilid_instance.dart new file mode 100644 index 0000000..a1bcb3f --- /dev/null +++ b/lib/providers/veilid_instance.dart @@ -0,0 +1,9 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import '../veilid_support/veilid_support.dart'; + +part 'veilid_instance.g.dart'; + +// Expose the Veilid instance as a FutureProvider +@riverpod +FutureOr veilidInstance(VeilidInstanceRef ref) async => + await eventualVeilid.future; diff --git a/lib/veilid_support/veilid_init.g.dart b/lib/providers/veilid_instance.g.dart similarity index 96% rename from lib/veilid_support/veilid_init.g.dart rename to lib/providers/veilid_instance.g.dart index bc2ade3..0f64356 100644 --- a/lib/veilid_support/veilid_init.g.dart +++ b/lib/providers/veilid_instance.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'veilid_init.dart'; +part of 'veilid_instance.dart'; // ************************************************************************** // RiverpodGenerator diff --git a/lib/veilid_support/dht_record.dart b/lib/veilid_support/dht_support/dht_record.dart similarity index 62% rename from lib/veilid_support/dht_record.dart rename to lib/veilid_support/dht_support/dht_record.dart index c493635..5b605e9 100644 --- a/lib/veilid_support/dht_record.dart +++ b/lib/veilid_support/dht_support/dht_record.dart @@ -4,96 +4,70 @@ import 'dart:typed_data'; import 'package:protobuf/protobuf.dart'; import 'package:veilid/veilid.dart'; -import '../tools/tools.dart'; -import 'veilid_support.dart'; +import '../../tools/tools.dart'; +import '../veilid_support.dart'; class DHTRecord { DHTRecord( - {required VeilidRoutingContext dhtctx, + {required VeilidRoutingContext routingContext, required DHTRecordDescriptor recordDescriptor, int defaultSubkey = 0, KeyPair? writer, - this.crypto = const DHTRecordCryptoPublic()}) - : _dhtctx = dhtctx, + DHTRecordCrypto crypto = const DHTRecordCryptoPublic()}) + : _crypto = crypto, + _routingContext = routingContext, _recordDescriptor = recordDescriptor, _defaultSubkey = defaultSubkey, _writer = writer, + _open = false, + _valid = true, _subkeySeqCache = {}; - final VeilidRoutingContext _dhtctx; + final VeilidRoutingContext _routingContext; final DHTRecordDescriptor _recordDescriptor; final int _defaultSubkey; final KeyPair? _writer; final Map _subkeySeqCache; - DHTRecordCrypto crypto; - - static Future create(VeilidRoutingContext dhtctx, - {DHTSchema schema = const DHTSchema.dflt(oCnt: 1), - int defaultSubkey = 0, - DHTRecordCrypto? crypto}) async { - final recordDescriptor = await dhtctx.createDHTRecord(schema); - - final rec = DHTRecord( - dhtctx: dhtctx, - recordDescriptor: recordDescriptor, - defaultSubkey: defaultSubkey, - writer: recordDescriptor.ownerKeyPair(), - crypto: crypto ?? - await DHTRecordCryptoPrivate.fromTypedKeyPair( - recordDescriptor.ownerTypedKeyPair()!)); - - return rec; - } - - static Future openRead( - VeilidRoutingContext dhtctx, TypedKey recordKey, - {int defaultSubkey = 0, DHTRecordCrypto? crypto}) async { - final recordDescriptor = await dhtctx.openDHTRecord(recordKey, null); - final rec = DHTRecord( - dhtctx: dhtctx, - recordDescriptor: recordDescriptor, - defaultSubkey: defaultSubkey, - crypto: crypto ?? const DHTRecordCryptoPublic()); - - return rec; - } - - static Future openWrite( - VeilidRoutingContext dhtctx, - TypedKey recordKey, - KeyPair writer, { - int defaultSubkey = 0, - DHTRecordCrypto? crypto, - }) async { - final recordDescriptor = await dhtctx.openDHTRecord(recordKey, writer); - final rec = DHTRecord( - dhtctx: dhtctx, - recordDescriptor: recordDescriptor, - defaultSubkey: defaultSubkey, - writer: writer, - crypto: crypto ?? - await DHTRecordCryptoPrivate.fromTypedKeyPair( - TypedKeyPair.fromKeyPair(recordKey.kind, writer))); - return rec; - } + final DHTRecordCrypto _crypto; + bool _open; + bool _valid; int subkeyOrDefault(int subkey) => (subkey == -1) ? _defaultSubkey : subkey; - VeilidRoutingContext get routingContext => _dhtctx; + VeilidRoutingContext get routingContext => _routingContext; TypedKey get key => _recordDescriptor.key; PublicKey get owner => _recordDescriptor.owner; KeyPair? get ownerKeyPair => _recordDescriptor.ownerKeyPair(); DHTSchema get schema => _recordDescriptor.schema; KeyPair? get writer => _writer; + OwnedDHTRecordPointer get ownedDHTRecordPointer => + OwnedDHTRecordPointer(recordKey: key, owner: ownerKeyPair!); Future close() async { - await _dhtctx.closeDHTRecord(_recordDescriptor.key); + if (!_valid) { + throw StateError('already deleted'); + } + if (!_open) { + return; + } + final pool = await DHTRecordPool.instance(); + await _routingContext.closeDHTRecord(_recordDescriptor.key); + pool.recordClosed(this); + _open = false; } Future delete() async { - await _dhtctx.deleteDHTRecord(_recordDescriptor.key); + if (!_valid) { + throw StateError('already deleted'); + } + if (_open) { + await close(); + } + final pool = await DHTRecordPool.instance(); + await pool.deleteDeep(key); + _valid = false; } - Future scope(Future Function(DHTRecord) scopeFunction) async { + Future scope(FutureOr Function(DHTRecord) scopeFunction) async { try { return await scopeFunction(this); } finally { @@ -101,7 +75,8 @@ class DHTRecord { } } - Future deleteScope(Future Function(DHTRecord) scopeFunction) async { + Future deleteScope( + FutureOr Function(DHTRecord) scopeFunction) async { try { final out = await scopeFunction(this); await close(); @@ -117,8 +92,8 @@ class DHTRecord { bool forceRefresh = false, bool onlyUpdates = false}) async { subkey = subkeyOrDefault(subkey); - final valueData = - await _dhtctx.getDHTValue(_recordDescriptor.key, subkey, forceRefresh); + final valueData = await _routingContext.getDHTValue( + _recordDescriptor.key, subkey, forceRefresh); if (valueData == null) { return null; } @@ -126,7 +101,7 @@ class DHTRecord { if (lastSeq != null && valueData.seq <= lastSeq) { return null; } - final out = crypto.decrypt(valueData.data, subkey); + final out = _crypto.decrypt(valueData.data, subkey); _subkeySeqCache[subkey] = valueData.seq; return out; } @@ -159,11 +134,11 @@ class DHTRecord { Future tryWriteBytes(Uint8List newValue, {int subkey = -1}) async { subkey = subkeyOrDefault(subkey); - newValue = await crypto.encrypt(newValue, subkey); + newValue = await _crypto.encrypt(newValue, subkey); // Set the new data if possible - final valueData = - await _dhtctx.setDHTValue(_recordDescriptor.key, subkey, newValue); + final valueData = await _routingContext.setDHTValue( + _recordDescriptor.key, subkey, newValue); if (valueData == null) { return null; } @@ -172,13 +147,13 @@ class DHTRecord { Future eventualWriteBytes(Uint8List newValue, {int subkey = -1}) async { subkey = subkeyOrDefault(subkey); - newValue = await crypto.encrypt(newValue, subkey); + newValue = await _crypto.encrypt(newValue, subkey); ValueData? valueData; do { // Set the new data - valueData = - await _dhtctx.setDHTValue(_recordDescriptor.key, subkey, newValue); + valueData = await _routingContext.setDHTValue( + _recordDescriptor.key, subkey, newValue); // Repeat if newer data on the network was found } while (valueData != null); @@ -191,7 +166,7 @@ class DHTRecord { // Get existing identity key, do not allow force refresh here // because if we need a refresh the setDHTValue will fail anyway var valueData = - await _dhtctx.getDHTValue(_recordDescriptor.key, subkey, false); + await _routingContext.getDHTValue(_recordDescriptor.key, subkey, false); // Ensure it exists already if (valueData == null) { throw const FormatException('value does not exist'); @@ -201,13 +176,13 @@ class DHTRecord { _subkeySeqCache[subkey] = valueData!.seq; // Update the data - final oldData = await crypto.decrypt(valueData.data, subkey); + final oldData = await _crypto.decrypt(valueData.data, subkey); final updatedData = await update(oldData); - final newData = await crypto.encrypt(updatedData, subkey); + final newData = await _crypto.encrypt(updatedData, subkey); // Set it back - valueData = - await _dhtctx.setDHTValue(_recordDescriptor.key, subkey, newData); + valueData = await _routingContext.setDHTValue( + _recordDescriptor.key, subkey, newData); // Repeat if newer data on the network was found } while (valueData != null); diff --git a/lib/veilid_support/dht_record_crypto.dart b/lib/veilid_support/dht_support/dht_record_crypto.dart similarity index 98% rename from lib/veilid_support/dht_record_crypto.dart rename to lib/veilid_support/dht_support/dht_record_crypto.dart index ce9c358..c1e37c2 100644 --- a/lib/veilid_support/dht_record_crypto.dart +++ b/lib/veilid_support/dht_support/dht_record_crypto.dart @@ -3,7 +3,7 @@ import 'dart:typed_data'; import 'package:veilid/veilid.dart'; -import 'veilid_init.dart'; +import '../veilid_init.dart'; abstract class DHTRecordCrypto { FutureOr encrypt(Uint8List data, int subkey); diff --git a/lib/veilid_support/dht_support/dht_record_pool.dart b/lib/veilid_support/dht_support/dht_record_pool.dart new file mode 100644 index 0000000..e636bbe --- /dev/null +++ b/lib/veilid_support/dht_support/dht_record_pool.dart @@ -0,0 +1,272 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../veilid_support.dart'; + +part 'dht_record_pool.freezed.dart'; +part 'dht_record_pool.g.dart'; + +/// Record pool that managed DHTRecords and allows for tagged deletion +@freezed +class DHTRecordPoolAllocations with _$DHTRecordPoolAllocations { + const factory DHTRecordPoolAllocations({ + required IMap> childrenByParent, + required IMap parentByChild, + }) = _DHTRecordPoolAllocations; + + factory DHTRecordPoolAllocations.fromJson(dynamic json) => + _$DHTRecordPoolAllocationsFromJson(json as Map); +} + +/// Pointer to an owned record, with key, owner key and owner secret +/// Ensure that these are only serialized encrypted +@freezed +class OwnedDHTRecordPointer with _$OwnedDHTRecordPointer { + const factory OwnedDHTRecordPointer({ + required TypedKey recordKey, + required KeyPair owner, + }) = _OwnedDHTRecordPointer; + + factory OwnedDHTRecordPointer.fromJson(dynamic json) => + _$OwnedDHTRecordPointerFromJson(json as Map); +} + +class DHTRecordPool with AsyncTableDBBacked { + DHTRecordPool._(Veilid veilid, VeilidRoutingContext routingContext) + : _state = DHTRecordPoolAllocations( + childrenByParent: IMap(), parentByChild: IMap()), + _opened = {}, + _routingContext = routingContext, + _veilid = veilid; + + // Persistent DHT record list + DHTRecordPoolAllocations _state; + // Which DHT records are currently open + final Map _opened; + // Default routing context to use for new keys + final VeilidRoutingContext _routingContext; + // Convenience accessor + final Veilid _veilid; + + static DHTRecordPool? _singleton; + + ////////////////////////////////////////////////////////////// + /// AsyncTableDBBacked + @override + String tableName() => 'dht_record_pool'; + @override + String tableKeyName() => 'pool_allocations'; + @override + DHTRecordPoolAllocations valueFromJson(Object? obj) => obj != null + ? DHTRecordPoolAllocations.fromJson(obj) + : DHTRecordPoolAllocations( + childrenByParent: IMap(), parentByChild: IMap()); + @override + Object? valueToJson(DHTRecordPoolAllocations val) => val.toJson(); + + ////////////////////////////////////////////////////////////// + + static Future instance() async { + if (_singleton == null) { + final veilid = await eventualVeilid.future; + final routingContext = (await veilid.routingContext()) + .withPrivacy() + .withSequencing(Sequencing.preferOrdered); + + final globalPool = DHTRecordPool._(veilid, routingContext); + globalPool._state = await globalPool.load(); + _singleton = globalPool; + } + return _singleton!; + } + + Veilid get veilid => _veilid; + + void _recordOpened(DHTRecord record) { + assert(!_opened.containsKey(record.key), 'record already opened'); + _opened[record.key] = record; + } + + void recordClosed(DHTRecord record) { + assert(_opened.containsKey(record.key), 'record already closed'); + _opened.remove(record.key); + } + + Future deleteDeep(TypedKey parent) async { + // Collect all dependencies + final allDeps = []; + final currentDeps = [parent]; + while (currentDeps.isNotEmpty) { + final nextDep = currentDeps.removeLast(); + + // Remove this child from its parent + _removeDependency(nextDep); + + // Ensure all records are closed before delete + assert(!_opened.containsKey(nextDep), 'should not delete opened record'); + + allDeps.add(nextDep); + final childDeps = _state.childrenByParent[nextDep]?.toList() ?? []; + currentDeps.addAll(childDeps); + } + + // Delete all records + final allFutures = >[]; + for (final dep in allDeps) { + allFutures.add(_routingContext.deleteDHTRecord(dep)); + } + await Future.wait(allFutures); + } + + void _addDependency(TypedKey parent, TypedKey child) { + final childrenOfParent = + _state.childrenByParent[parent] ?? ISet(); + if (childrenOfParent.contains(child)) { + throw StateError('Dependency added twice: $parent -> $child'); + } + if (_state.parentByChild.containsKey(child)) { + throw StateError('Child has two parents: $child <- $parent'); + } + if (_state.childrenByParent.containsKey(child)) { + // dependencies should be opened after their parents + throw StateError('Child is not a leaf: $child'); + } + + _state = _state.copyWith( + childrenByParent: + _state.childrenByParent.add(parent, childrenOfParent.add(child)), + parentByChild: _state.parentByChild.add(child, parent)); + } + + void _removeDependency(TypedKey child) { + final parent = _state.parentByChild[child]; + if (parent == null) { + return; + } + final children = _state.childrenByParent[parent]!.remove(child); + if (children.isEmpty) { + _state = _state.copyWith( + childrenByParent: _state.childrenByParent.remove(parent), + parentByChild: _state.parentByChild.remove(child)); + } else { + _state = _state.copyWith( + childrenByParent: _state.childrenByParent.add(parent, children), + parentByChild: _state.parentByChild.remove(child)); + } + } + + /////////////////////////////////////////////////////////////////////// + + /// Create a root DHTRecord that has no dependent records + Future create( + {VeilidRoutingContext? routingContext, + TypedKey? parent, + DHTSchema schema = const DHTSchema.dflt(oCnt: 1), + int defaultSubkey = 0, + DHTRecordCrypto? crypto}) async { + final dhtctx = routingContext ?? _routingContext; + final recordDescriptor = await dhtctx.createDHTRecord(schema); + + final rec = DHTRecord( + routingContext: dhtctx, + recordDescriptor: recordDescriptor, + defaultSubkey: defaultSubkey, + writer: recordDescriptor.ownerKeyPair(), + crypto: crypto ?? + await DHTRecordCryptoPrivate.fromTypedKeyPair( + recordDescriptor.ownerTypedKeyPair()!)); + + if (parent != null) { + _addDependency(parent, rec.key); + } + _recordOpened(rec); + + return rec; + } + + /// Open a DHTRecord readonly + Future openRead(TypedKey recordKey, + {VeilidRoutingContext? routingContext, + TypedKey? parent, + int defaultSubkey = 0, + DHTRecordCrypto? crypto}) async { + // If we are opening a key that already exists + // make sure we are using the same parent if one was specified + final existingParent = _state.parentByChild[recordKey]; + assert(existingParent == parent, 'wrong parent for opened key'); + + // Open from the veilid api + final dhtctx = routingContext ?? _routingContext; + final recordDescriptor = await dhtctx.openDHTRecord(recordKey, null); + final rec = DHTRecord( + routingContext: dhtctx, + recordDescriptor: recordDescriptor, + defaultSubkey: defaultSubkey, + crypto: crypto ?? const DHTRecordCryptoPublic()); + + // Register the dependency if specified + if (parent != null) { + _addDependency(parent, rec.key); + } + _recordOpened(rec); + + return rec; + } + + /// Open a DHTRecord writable + Future openWrite( + TypedKey recordKey, + KeyPair writer, { + VeilidRoutingContext? routingContext, + TypedKey? parent, + int defaultSubkey = 0, + DHTRecordCrypto? crypto, + }) async { + // If we are opening a key that already exists + // make sure we are using the same parent if one was specified + final existingParent = _state.parentByChild[recordKey]; + assert(existingParent == parent, 'wrong parent for opened key'); + + // Open from the veilid api + final dhtctx = routingContext ?? _routingContext; + final recordDescriptor = await dhtctx.openDHTRecord(recordKey, writer); + final rec = DHTRecord( + routingContext: dhtctx, + recordDescriptor: recordDescriptor, + defaultSubkey: defaultSubkey, + writer: writer, + crypto: crypto ?? + await DHTRecordCryptoPrivate.fromTypedKeyPair( + TypedKeyPair.fromKeyPair(recordKey.kind, writer))); + + // Register the dependency if specified + if (parent != null) { + _addDependency(parent, rec.key); + } + _recordOpened(rec); + + return rec; + } + + /// Open a DHTRecord owned + /// This is the same as writable but uses an OwnedDHTRecordPointer + /// for convenience and uses symmetric encryption on the key + /// This is primarily used for backing up private content on to the DHT + /// to synchronizing it between devices. Because it is 'owned', the correct + /// parent must be specified. + Future openOwned( + OwnedDHTRecordPointer ownedDHTRecordPointer, { + required TypedKey parent, + VeilidRoutingContext? routingContext, + int defaultSubkey = 0, + DHTRecordCrypto? crypto, + }) => + openWrite( + ownedDHTRecordPointer.recordKey, + ownedDHTRecordPointer.owner, + routingContext: routingContext, + parent: parent, + defaultSubkey: defaultSubkey, + crypto: crypto, + ); +} diff --git a/lib/veilid_support/dht_support/dht_record_pool.freezed.dart b/lib/veilid_support/dht_support/dht_record_pool.freezed.dart new file mode 100644 index 0000000..c517e5b --- /dev/null +++ b/lib/veilid_support/dht_support/dht_record_pool.freezed.dart @@ -0,0 +1,357 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'dht_record_pool.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +DHTRecordPoolAllocations _$DHTRecordPoolAllocationsFromJson( + Map json) { + return _DHTRecordPoolAllocations.fromJson(json); +} + +/// @nodoc +mixin _$DHTRecordPoolAllocations { + IMap, ISet>> + get childrenByParent => throw _privateConstructorUsedError; + IMap, Typed> + get parentByChild => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $DHTRecordPoolAllocationsCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $DHTRecordPoolAllocationsCopyWith<$Res> { + factory $DHTRecordPoolAllocationsCopyWith(DHTRecordPoolAllocations value, + $Res Function(DHTRecordPoolAllocations) then) = + _$DHTRecordPoolAllocationsCopyWithImpl<$Res, DHTRecordPoolAllocations>; + @useResult + $Res call( + {IMap, ISet>> + childrenByParent, + IMap, Typed> + parentByChild}); +} + +/// @nodoc +class _$DHTRecordPoolAllocationsCopyWithImpl<$Res, + $Val extends DHTRecordPoolAllocations> + implements $DHTRecordPoolAllocationsCopyWith<$Res> { + _$DHTRecordPoolAllocationsCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? childrenByParent = null, + Object? parentByChild = null, + }) { + return _then(_value.copyWith( + childrenByParent: null == childrenByParent + ? _value.childrenByParent + : childrenByParent // ignore: cast_nullable_to_non_nullable + as IMap, + ISet>>, + parentByChild: null == parentByChild + ? _value.parentByChild + : parentByChild // ignore: cast_nullable_to_non_nullable + as IMap, Typed>, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$_DHTRecordPoolAllocationsCopyWith<$Res> + implements $DHTRecordPoolAllocationsCopyWith<$Res> { + factory _$$_DHTRecordPoolAllocationsCopyWith( + _$_DHTRecordPoolAllocations value, + $Res Function(_$_DHTRecordPoolAllocations) then) = + __$$_DHTRecordPoolAllocationsCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {IMap, ISet>> + childrenByParent, + IMap, Typed> + parentByChild}); +} + +/// @nodoc +class __$$_DHTRecordPoolAllocationsCopyWithImpl<$Res> + extends _$DHTRecordPoolAllocationsCopyWithImpl<$Res, + _$_DHTRecordPoolAllocations> + implements _$$_DHTRecordPoolAllocationsCopyWith<$Res> { + __$$_DHTRecordPoolAllocationsCopyWithImpl(_$_DHTRecordPoolAllocations _value, + $Res Function(_$_DHTRecordPoolAllocations) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? childrenByParent = null, + Object? parentByChild = null, + }) { + return _then(_$_DHTRecordPoolAllocations( + childrenByParent: null == childrenByParent + ? _value.childrenByParent + : childrenByParent // ignore: cast_nullable_to_non_nullable + as IMap, + ISet>>, + parentByChild: null == parentByChild + ? _value.parentByChild + : parentByChild // ignore: cast_nullable_to_non_nullable + as IMap, Typed>, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_DHTRecordPoolAllocations implements _DHTRecordPoolAllocations { + const _$_DHTRecordPoolAllocations( + {required this.childrenByParent, required this.parentByChild}); + + factory _$_DHTRecordPoolAllocations.fromJson(Map json) => + _$$_DHTRecordPoolAllocationsFromJson(json); + + @override + final IMap, ISet>> + childrenByParent; + @override + final IMap, Typed> + parentByChild; + + @override + String toString() { + return 'DHTRecordPoolAllocations(childrenByParent: $childrenByParent, parentByChild: $parentByChild)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_DHTRecordPoolAllocations && + (identical(other.childrenByParent, childrenByParent) || + other.childrenByParent == childrenByParent) && + (identical(other.parentByChild, parentByChild) || + other.parentByChild == parentByChild)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, childrenByParent, parentByChild); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_DHTRecordPoolAllocationsCopyWith<_$_DHTRecordPoolAllocations> + get copyWith => __$$_DHTRecordPoolAllocationsCopyWithImpl< + _$_DHTRecordPoolAllocations>(this, _$identity); + + @override + Map toJson() { + return _$$_DHTRecordPoolAllocationsToJson( + this, + ); + } +} + +abstract class _DHTRecordPoolAllocations implements DHTRecordPoolAllocations { + const factory _DHTRecordPoolAllocations( + {required final IMap, + ISet>> + childrenByParent, + required final IMap, + Typed> + parentByChild}) = _$_DHTRecordPoolAllocations; + + factory _DHTRecordPoolAllocations.fromJson(Map json) = + _$_DHTRecordPoolAllocations.fromJson; + + @override + IMap, ISet>> + get childrenByParent; + @override + IMap, Typed> + get parentByChild; + @override + @JsonKey(ignore: true) + _$$_DHTRecordPoolAllocationsCopyWith<_$_DHTRecordPoolAllocations> + get copyWith => throw _privateConstructorUsedError; +} + +OwnedDHTRecordPointer _$OwnedDHTRecordPointerFromJson( + Map json) { + return _OwnedDHTRecordPointer.fromJson(json); +} + +/// @nodoc +mixin _$OwnedDHTRecordPointer { + Typed get recordKey => + throw _privateConstructorUsedError; + KeyPair get owner => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $OwnedDHTRecordPointerCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $OwnedDHTRecordPointerCopyWith<$Res> { + factory $OwnedDHTRecordPointerCopyWith(OwnedDHTRecordPointer value, + $Res Function(OwnedDHTRecordPointer) then) = + _$OwnedDHTRecordPointerCopyWithImpl<$Res, OwnedDHTRecordPointer>; + @useResult + $Res call({Typed recordKey, KeyPair owner}); +} + +/// @nodoc +class _$OwnedDHTRecordPointerCopyWithImpl<$Res, + $Val extends OwnedDHTRecordPointer> + implements $OwnedDHTRecordPointerCopyWith<$Res> { + _$OwnedDHTRecordPointerCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? recordKey = null, + Object? owner = null, + }) { + return _then(_value.copyWith( + recordKey: null == recordKey + ? _value.recordKey + : recordKey // ignore: cast_nullable_to_non_nullable + as Typed, + owner: null == owner + ? _value.owner + : owner // ignore: cast_nullable_to_non_nullable + as KeyPair, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$_OwnedDHTRecordPointerCopyWith<$Res> + implements $OwnedDHTRecordPointerCopyWith<$Res> { + factory _$$_OwnedDHTRecordPointerCopyWith(_$_OwnedDHTRecordPointer value, + $Res Function(_$_OwnedDHTRecordPointer) then) = + __$$_OwnedDHTRecordPointerCopyWithImpl<$Res>; + @override + @useResult + $Res call({Typed recordKey, KeyPair owner}); +} + +/// @nodoc +class __$$_OwnedDHTRecordPointerCopyWithImpl<$Res> + extends _$OwnedDHTRecordPointerCopyWithImpl<$Res, _$_OwnedDHTRecordPointer> + implements _$$_OwnedDHTRecordPointerCopyWith<$Res> { + __$$_OwnedDHTRecordPointerCopyWithImpl(_$_OwnedDHTRecordPointer _value, + $Res Function(_$_OwnedDHTRecordPointer) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? recordKey = null, + Object? owner = null, + }) { + return _then(_$_OwnedDHTRecordPointer( + recordKey: null == recordKey + ? _value.recordKey + : recordKey // ignore: cast_nullable_to_non_nullable + as Typed, + owner: null == owner + ? _value.owner + : owner // ignore: cast_nullable_to_non_nullable + as KeyPair, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_OwnedDHTRecordPointer implements _OwnedDHTRecordPointer { + const _$_OwnedDHTRecordPointer( + {required this.recordKey, required this.owner}); + + factory _$_OwnedDHTRecordPointer.fromJson(Map json) => + _$$_OwnedDHTRecordPointerFromJson(json); + + @override + final Typed recordKey; + @override + final KeyPair owner; + + @override + String toString() { + return 'OwnedDHTRecordPointer(recordKey: $recordKey, owner: $owner)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_OwnedDHTRecordPointer && + (identical(other.recordKey, recordKey) || + other.recordKey == recordKey) && + (identical(other.owner, owner) || other.owner == owner)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, recordKey, owner); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_OwnedDHTRecordPointerCopyWith<_$_OwnedDHTRecordPointer> get copyWith => + __$$_OwnedDHTRecordPointerCopyWithImpl<_$_OwnedDHTRecordPointer>( + this, _$identity); + + @override + Map toJson() { + return _$$_OwnedDHTRecordPointerToJson( + this, + ); + } +} + +abstract class _OwnedDHTRecordPointer implements OwnedDHTRecordPointer { + const factory _OwnedDHTRecordPointer( + {required final Typed recordKey, + required final KeyPair owner}) = _$_OwnedDHTRecordPointer; + + factory _OwnedDHTRecordPointer.fromJson(Map json) = + _$_OwnedDHTRecordPointer.fromJson; + + @override + Typed get recordKey; + @override + KeyPair get owner; + @override + @JsonKey(ignore: true) + _$$_OwnedDHTRecordPointerCopyWith<_$_OwnedDHTRecordPointer> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/veilid_support/dht_support/dht_record_pool.g.dart b/lib/veilid_support/dht_support/dht_record_pool.g.dart new file mode 100644 index 0000000..46ac52b --- /dev/null +++ b/lib/veilid_support/dht_support/dht_record_pool.g.dart @@ -0,0 +1,52 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'dht_record_pool.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$_DHTRecordPoolAllocations _$$_DHTRecordPoolAllocationsFromJson( + Map json) => + _$_DHTRecordPoolAllocations( + childrenByParent: IMap, + ISet>>.fromJson( + json['children_by_parent'] as Map, + (value) => Typed.fromJson(value), + (value) => ISet>.fromJson( + value, (value) => Typed.fromJson(value))), + parentByChild: IMap, + Typed>.fromJson( + json['parent_by_child'] as Map, + (value) => Typed.fromJson(value), + (value) => Typed.fromJson(value)), + ); + +Map _$$_DHTRecordPoolAllocationsToJson( + _$_DHTRecordPoolAllocations instance) => + { + 'children_by_parent': instance.childrenByParent.toJson( + (value) => value.toJson(), + (value) => value.toJson( + (value) => value.toJson(), + ), + ), + 'parent_by_child': instance.parentByChild.toJson( + (value) => value.toJson(), + (value) => value.toJson(), + ), + }; + +_$_OwnedDHTRecordPointer _$$_OwnedDHTRecordPointerFromJson( + Map json) => + _$_OwnedDHTRecordPointer( + recordKey: Typed.fromJson(json['record_key']), + owner: KeyPair.fromJson(json['owner']), + ); + +Map _$$_OwnedDHTRecordPointerToJson( + _$_OwnedDHTRecordPointer instance) => + { + 'record_key': instance.recordKey.toJson(), + 'owner': instance.owner.toJson(), + }; diff --git a/lib/veilid_support/dht_short_array.dart b/lib/veilid_support/dht_support/dht_short_array.dart similarity index 86% rename from lib/veilid_support/dht_short_array.dart rename to lib/veilid_support/dht_support/dht_short_array.dart index 426e850..3260b5e 100644 --- a/lib/veilid_support/dht_short_array.dart +++ b/lib/veilid_support/dht_support/dht_short_array.dart @@ -1,11 +1,11 @@ +import 'dart:async'; import 'dart:typed_data'; import 'package:protobuf/protobuf.dart'; -import 'package:veilid/veilid.dart'; -import '../entities/proto.dart' as proto; -import '../tools/tools.dart'; -import 'veilid_support.dart'; +import '../../entities/proto.dart' as proto; +import '../../tools/tools.dart'; +import '../veilid_support.dart'; class _DHTShortArrayCache { _DHTShortArrayCache() @@ -23,11 +23,11 @@ class _DHTShortArrayCache { } class DHTShortArray { - DHTShortArray({required DHTRecord dhtRecord}) - : _headRecord = dhtRecord, + DHTShortArray._({required DHTRecord headRecord}) + : _headRecord = headRecord, _head = _DHTShortArrayCache() { late final int stride; - switch (dhtRecord.schema) { + switch (headRecord.schema) { case DHTSchemaDFLT(oCnt: final oCnt): stride = oCnt - 1; if (stride <= 0) { @@ -49,13 +49,21 @@ class DHTShortArray { // Cached representation refreshed from head record _DHTShortArrayCache _head; - static Future create(VeilidRoutingContext dhtctx, int stride, - {DHTRecordCrypto? crypto}) async { + static Future create( + {int stride = maxElements, + VeilidRoutingContext? routingContext, + TypedKey? parent, + DHTRecordCrypto? crypto}) async { assert(stride <= maxElements, 'stride too long'); - final dhtRecord = await DHTRecord.create(dhtctx, - schema: DHTSchema.dflt(oCnt: stride + 1), crypto: crypto); + final pool = await DHTRecordPool.instance(); + + final dhtRecord = await pool.create( + parent: parent, + routingContext: routingContext, + schema: DHTSchema.dflt(oCnt: stride + 1), + crypto: crypto); try { - final dhtShortArray = DHTShortArray(dhtRecord: dhtRecord); + final dhtShortArray = DHTShortArray._(headRecord: dhtRecord); return dhtShortArray; } on Exception catch (_) { await dhtRecord.delete(); @@ -63,13 +71,16 @@ class DHTShortArray { } } - static Future openRead( - VeilidRoutingContext dhtctx, TypedKey dhtRecordKey, - {DHTRecordCrypto? crypto}) async { - final dhtRecord = - await DHTRecord.openRead(dhtctx, dhtRecordKey, crypto: crypto); + static Future openRead(TypedKey headRecordKey, + {VeilidRoutingContext? routingContext, + TypedKey? parent, + DHTRecordCrypto? crypto}) async { + final pool = await DHTRecordPool.instance(); + + final dhtRecord = await pool.openRead(headRecordKey, + parent: parent, routingContext: routingContext, crypto: crypto); try { - final dhtShortArray = DHTShortArray(dhtRecord: dhtRecord); + final dhtShortArray = DHTShortArray._(headRecord: dhtRecord); await dhtShortArray._refreshHead(); return dhtShortArray; } on Exception catch (_) { @@ -79,15 +90,17 @@ class DHTShortArray { } static Future openWrite( - VeilidRoutingContext dhtctx, - TypedKey dhtRecordKey, + TypedKey headRecordKey, KeyPair writer, { + VeilidRoutingContext? routingContext, + TypedKey? parent, DHTRecordCrypto? crypto, }) async { - final dhtRecord = - await DHTRecord.openWrite(dhtctx, dhtRecordKey, writer, crypto: crypto); + final pool = await DHTRecordPool.instance(); + final dhtRecord = await pool.openWrite(headRecordKey, writer, + parent: parent, routingContext: routingContext, crypto: crypto); try { - final dhtShortArray = DHTShortArray(dhtRecord: dhtRecord); + final dhtShortArray = DHTShortArray._(headRecord: dhtRecord); await dhtShortArray._refreshHead(); return dhtShortArray; } on Exception catch (_) { @@ -96,6 +109,22 @@ class DHTShortArray { } } + static Future openOwned( + OwnedDHTRecordPointer ownedDHTRecordPointer, { + required TypedKey parent, + VeilidRoutingContext? routingContext, + DHTRecordCrypto? crypto, + }) => + openWrite( + ownedDHTRecordPointer.recordKey, + ownedDHTRecordPointer.owner, + routingContext: routingContext, + parent: parent, + crypto: crypto, + ); + + DHTRecord get record => _headRecord; + //////////////////////////////////////////////////////////////// /// Seralize and write out the current head record, possibly updating it @@ -151,11 +180,21 @@ class DHTShortArray { /// Open a linked record for reading or writing, same as the head record Future _openLinkedRecord(TypedKey recordKey) async { + final pool = await DHTRecordPool.instance(); + final writer = _headRecord.writer; return (writer != null) - ? await DHTRecord.openWrite( - _headRecord.routingContext, recordKey, writer) - : await DHTRecord.openRead(_headRecord.routingContext, recordKey); + ? await pool.openWrite( + recordKey, + writer, + parent: _headRecord.key, + routingContext: _headRecord.routingContext, + ) + : await pool.openRead( + recordKey, + parent: _headRecord.key, + routingContext: _headRecord.routingContext, + ); } /// Validate a new head record @@ -242,7 +281,7 @@ class DHTShortArray { await Future.wait(futures); } - Future scope(Future Function(DHTShortArray) scopeFunction) async { + Future scope(FutureOr Function(DHTShortArray) scopeFunction) async { try { return await scopeFunction(this); } finally { @@ -251,7 +290,7 @@ class DHTShortArray { } Future deleteScope( - Future Function(DHTShortArray) scopeFunction) async { + FutureOr Function(DHTShortArray) scopeFunction) async { try { final out = await scopeFunction(this); await close(); diff --git a/lib/veilid_support/dht_support/dht_support.dart b/lib/veilid_support/dht_support/dht_support.dart new file mode 100644 index 0000000..bdb5747 --- /dev/null +++ b/lib/veilid_support/dht_support/dht_support.dart @@ -0,0 +1,4 @@ +export 'dht_record.dart'; +export 'dht_record_crypto.dart'; +export 'dht_record_pool.dart'; +export 'dht_short_array.dart'; diff --git a/lib/veilid_support/identity_master.dart b/lib/veilid_support/identity_master.dart index 81aa11b..72c5d9b 100644 --- a/lib/veilid_support/identity_master.dart +++ b/lib/veilid_support/identity_master.dart @@ -3,7 +3,6 @@ import 'dart:typed_data'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:veilid/veilid.dart'; import '../entities/identity.dart'; import 'veilid_support.dart'; @@ -11,7 +10,7 @@ import 'veilid_support.dart'; // Identity Master with secrets // Not freezed because we never persist this class in its entirety class IdentityMasterWithSecrets { - IdentityMasterWithSecrets( + IdentityMasterWithSecrets._( {required this.identityMaster, required this.masterSecret, required this.identitySecret}); @@ -19,28 +18,15 @@ class IdentityMasterWithSecrets { SecretKey masterSecret; SecretKey identitySecret; - Future delete() async { - final veilid = await eventualVeilid.future; - final dhtctx = (await veilid.routingContext()) - .withPrivacy() - .withSequencing(Sequencing.ensureOrdered); - await dhtctx.deleteDHTRecord(identityMaster.masterRecordKey); - await dhtctx.deleteDHTRecord(identityMaster.identityRecordKey); - } -} + /// Creates a new master identity and returns it with its secrets + static Future create() async { + final pool = await DHTRecordPool.instance(); -/// Creates a new master identity and returns it with its secrets -Future newIdentityMaster() async { - final veilid = await eventualVeilid.future; - final dhtctx = (await veilid.routingContext()) - .withPrivacy() - .withSequencing(Sequencing.ensureOrdered); - - // IdentityMaster DHT record is public/unencrypted - return (await DHTRecord.create(dhtctx, crypto: const DHTRecordCryptoPublic())) - .deleteScope((masterRec) async { - // Identity record is private - return (await DHTRecord.create(dhtctx)).deleteScope((identityRec) async { + // IdentityMaster DHT record is public/unencrypted + return (await pool.create(crypto: const DHTRecordCryptoPublic())) + .deleteScope((masterRec) async { + // Identity record is private + final identityRec = await pool.create(parent: masterRec.key); // Make IdentityMaster final masterRecordKey = masterRec.key; final masterOwner = masterRec.ownerKeyPair!; @@ -56,7 +42,7 @@ Future newIdentityMaster() async { assert(masterRecordKey.kind == identityRecordKey.kind, 'new master and identity should have same cryptosystem'); - final crypto = await veilid.getCryptoSystem(masterRecordKey.kind); + final crypto = await pool.veilid.getCryptoSystem(masterRecordKey.kind); final identitySignature = await crypto.signWithKeyPair(masterOwner, identitySigBuf.toBytes()); @@ -80,10 +66,16 @@ Future newIdentityMaster() async { // Write empty identity to identity dht key await identityRec.eventualWriteJson(identity); - return IdentityMasterWithSecrets( + return IdentityMasterWithSecrets._( identityMaster: identityMaster, masterSecret: masterOwner.secret, identitySecret: identityOwner.secret); }); - }); + } + + /// Creates a new master identity and returns it with its secrets + Future delete() async { + final pool = await DHTRecordPool.instance(); + await pool.deleteDeep(identityMaster.masterRecordKey); + } } diff --git a/lib/veilid_support/processor.dart b/lib/veilid_support/processor.dart index 748e76c..80cbb60 100644 --- a/lib/veilid_support/processor.dart +++ b/lib/veilid_support/processor.dart @@ -3,12 +3,11 @@ import 'dart:async'; import 'package:veilid/veilid.dart'; import '../log/log.dart'; -import '../providers/providers.dart'; +import '../providers/connection_state.dart'; import 'config.dart'; import 'veilid_log.dart'; class Processor { - Processor(); String _veilidVersion = ''; bool _startedUp = false; diff --git a/lib/veilid_support/veilid_init.dart b/lib/veilid_support/veilid_init.dart index 70da2f0..193afc1 100644 --- a/lib/veilid_support/veilid_init.dart +++ b/lib/veilid_support/veilid_init.dart @@ -1,14 +1,11 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:veilid/veilid.dart'; import 'processor.dart'; import 'veilid_log.dart'; -part 'veilid_init.g.dart'; - Future getVeilidVersion() async { String veilidVersion; try { @@ -72,8 +69,3 @@ Future initializeVeilid() async { // Share the initialized veilid instance to the rest of the app eventualVeilid.complete(Veilid.instance); } - -// Expose the Veilid instance as a FutureProvider -@riverpod -FutureOr veilidInstance(VeilidInstanceRef ref) async => - await eventualVeilid.future; diff --git a/lib/veilid_support/veilid_support.dart b/lib/veilid_support/veilid_support.dart index b9b57bf..7435ada 100644 --- a/lib/veilid_support/veilid_support.dart +++ b/lib/veilid_support/veilid_support.dart @@ -1,6 +1,7 @@ +export 'package:veilid/veilid.dart'; + export 'config.dart'; -export 'dht_record.dart'; -export 'dht_record_crypto.dart'; +export 'dht_support/dht_support.dart'; export 'identity_master.dart'; export 'processor.dart'; export 'table_db.dart';