mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2024-10-01 06:55:46 -04:00
dht work
This commit is contained in:
parent
57c366ef91
commit
c35056f687
25
lib/components/contact_invitation_display.dart
Normal file
25
lib/components/contact_invitation_display.dart
Normal file
@ -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<LocalAccount>('account', account));
|
||||
}
|
||||
}
|
@ -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<AccountRecordInfo> 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<void> 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -21,8 +21,7 @@ AccountRecordInfo _$AccountRecordInfoFromJson(Map<String, dynamic> json) {
|
||||
/// @nodoc
|
||||
mixin _$AccountRecordInfo {
|
||||
// Top level account keys and secrets
|
||||
Typed<FixedEncodedString43> get key => throw _privateConstructorUsedError;
|
||||
KeyPair get owner => throw _privateConstructorUsedError;
|
||||
OwnedDHTRecordPointer get accountRecord => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> 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<FixedEncodedString43> 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<FixedEncodedString43>,
|
||||
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<FixedEncodedString43> 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<FixedEncodedString43>,
|
||||
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<String, dynamic> json) =>
|
||||
_$$_AccountRecordInfoFromJson(json);
|
||||
|
||||
// Top level account keys and secrets
|
||||
@override
|
||||
final Typed<FixedEncodedString43> 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<FixedEncodedString43> key,
|
||||
required final KeyPair owner}) = _$_AccountRecordInfo;
|
||||
{required final OwnedDHTRecordPointer accountRecord}) =
|
||||
_$_AccountRecordInfo;
|
||||
|
||||
factory _AccountRecordInfo.fromJson(Map<String, dynamic> json) =
|
||||
_$_AccountRecordInfo.fromJson;
|
||||
|
||||
@override // Top level account keys and secrets
|
||||
Typed<FixedEncodedString43> get key;
|
||||
@override
|
||||
KeyPair get owner;
|
||||
OwnedDHTRecordPointer get accountRecord;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$_AccountRecordInfoCopyWith<_$_AccountRecordInfo> get copyWith =>
|
||||
|
@ -8,15 +8,13 @@ part of 'identity.dart';
|
||||
|
||||
_$_AccountRecordInfo _$$_AccountRecordInfoFromJson(Map<String, dynamic> json) =>
|
||||
_$_AccountRecordInfo(
|
||||
key: Typed<FixedEncodedString43>.fromJson(json['key']),
|
||||
owner: KeyPair.fromJson(json['owner']),
|
||||
accountRecord: OwnedDHTRecordPointer.fromJson(json['account_record']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$_AccountRecordInfoToJson(
|
||||
_$_AccountRecordInfo instance) =>
|
||||
<String, dynamic>{
|
||||
'key': instance.key.toJson(),
|
||||
'owner': instance.owner.toJson(),
|
||||
'account_record': instance.accountRecord.toJson(),
|
||||
};
|
||||
|
||||
_$_Identity _$$_IdentityFromJson(Map<String, dynamic> json) => _$_Identity(
|
||||
|
@ -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
|
||||
|
@ -22,12 +22,10 @@ LocalAccount _$LocalAccountFromJson(Map<String, dynamic> 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
|
||||
|
@ -9,10 +9,8 @@ part of 'local_account.dart';
|
||||
_$_LocalAccount _$$_LocalAccountFromJson(Map<String, dynamic> 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<String, dynamic> json) =>
|
||||
Map<String, dynamic> _$$_LocalAccountToJson(_$_LocalAccount instance) =>
|
||||
<String, dynamic>{
|
||||
'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,
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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<CryptoKey>(1, _omitFieldNames ? '' : 'key', subBuilder: CryptoKey.create)
|
||||
..aOM<CryptoKey>(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<KeyPair> createRepeated() => $pb.PbList<KeyPair>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static KeyPair getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<KeyPair>(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<TypedKey>(1, _omitFieldNames ? '' : 'recordKey', subBuilder: TypedKey.create)
|
||||
..aOM<CryptoKey>(2, _omitFieldNames ? '' : 'ownerKey', subBuilder: CryptoKey.create)
|
||||
..aOM<CryptoKey>(3, _omitFieldNames ? '' : 'ownerSecret', subBuilder: CryptoKey.create)
|
||||
..aOM<KeyPair>(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 {
|
||||
|
@ -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 = {
|
||||
|
@ -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;
|
||||
|
@ -24,6 +24,8 @@ mixin _$UserLogin {
|
||||
Typed<FixedEncodedString43> get accountMasterRecordKey =>
|
||||
throw _privateConstructorUsedError; // The identity secret as unlocked from the local accounts table
|
||||
Typed<FixedEncodedString43> 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<FixedEncodedString43> accountMasterRecordKey,
|
||||
Typed<FixedEncodedString43> 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<FixedEncodedString43>,
|
||||
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<FixedEncodedString43> accountMasterRecordKey,
|
||||
Typed<FixedEncodedString43> 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<FixedEncodedString43>,
|
||||
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<String, dynamic> json) =>
|
||||
@ -140,13 +168,16 @@ class _$_UserLogin implements _UserLogin {
|
||||
// The identity secret as unlocked from the local accounts table
|
||||
@override
|
||||
final Typed<FixedEncodedString43> 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<FixedEncodedString43> accountMasterRecordKey,
|
||||
required final Typed<FixedEncodedString43> identitySecret,
|
||||
required final AccountRecordInfo accountRecordInfo,
|
||||
required final Timestamp lastActive}) = _$_UserLogin;
|
||||
|
||||
factory _UserLogin.fromJson(Map<String, dynamic> json) =
|
||||
@ -194,6 +228,8 @@ abstract class _UserLogin implements UserLogin {
|
||||
Typed<FixedEncodedString43> get accountMasterRecordKey;
|
||||
@override // The identity secret as unlocked from the local accounts table
|
||||
Typed<FixedEncodedString43> 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
|
||||
|
@ -11,6 +11,8 @@ _$_UserLogin _$$_UserLoginFromJson(Map<String, dynamic> json) => _$_UserLogin(
|
||||
json['account_master_record_key']),
|
||||
identitySecret:
|
||||
Typed<FixedEncodedString43>.fromJson(json['identity_secret']),
|
||||
accountRecordInfo:
|
||||
AccountRecordInfo.fromJson(json['account_record_info']),
|
||||
lastActive: Timestamp.fromJson(json['last_active']),
|
||||
);
|
||||
|
||||
@ -18,6 +20,7 @@ Map<String, dynamic> _$$_UserLoginToJson(_$_UserLogin instance) =>
|
||||
<String, dynamic>{
|
||||
'account_master_record_key': instance.accountMasterRecordKey.toJson(),
|
||||
'identity_secret': instance.identitySecret.toJson(),
|
||||
'account_record_info': instance.accountRecordInfo.toJson(),
|
||||
'last_active': instance.lastActive.toJson(),
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
|
@ -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<AccountPage> {
|
||||
|
||||
// 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<AccountPage> {
|
||||
// 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<AccountPage> {
|
||||
// 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:
|
||||
|
@ -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<MainPager>
|
||||
return bottomBarItems;
|
||||
}
|
||||
|
||||
Future<void> _onNewContactInvitation(BuildContext context) async {
|
||||
Scaffold.of(context).showBottomSheet<void>((context) => SizedBox(
|
||||
height: 200, child: Center(child: ContactInvitationDisplay())));
|
||||
}
|
||||
|
||||
Future<void> _onNewChat(BuildContext context) async {
|
||||
//
|
||||
}
|
||||
|
||||
Future<void> _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<MainPager>
|
||||
),
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
@ -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<NewAccountPage> {
|
||||
});
|
||||
}
|
||||
|
||||
/// 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<void> 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
|
||||
|
@ -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<AccountInfo> 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<AccountInfo> 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(
|
||||
|
@ -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<AccountInfo>;
|
||||
|
||||
/// 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<AsyncValue<AccountInfo>> {
|
||||
/// 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<FixedEncodedString43> accountMasterRecordKey,
|
||||
}) {
|
||||
@ -73,9 +85,15 @@ class FetchAccountFamily extends Family<AsyncValue<AccountInfo>> {
|
||||
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<AccountInfo> {
|
||||
/// 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(
|
||||
|
@ -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<proto.ContactRequestRecord> newContactRequest(
|
||||
proto.EncryptionKind encryptionKind,
|
||||
String encryptionKey,
|
||||
) async {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
class ContactRequestRecordsParams {
|
||||
ContactRequestRecordsParams({required this.contactRequestsDHTListKey});
|
||||
TypedKey contactRequestsDHTListKey;
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<ContactRequestRecords?> 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;
|
||||
// }
|
||||
}
|
@ -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<ContactRequestRecords?>;
|
||||
|
||||
/// See also [fetchContactRequestRecords].
|
||||
@ProviderFor(fetchContactRequestRecords)
|
||||
const fetchContactRequestRecordsProvider = FetchContactRequestRecordsFamily();
|
||||
|
||||
/// See also [fetchContactRequestRecords].
|
||||
class FetchContactRequestRecordsFamily
|
||||
extends Family<AsyncValue<ContactRequestRecords?>> {
|
||||
/// 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<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'fetchContactRequestRecordsProvider';
|
||||
}
|
||||
|
||||
/// See also [fetchContactRequestRecords].
|
||||
class FetchContactRequestRecordsProvider
|
||||
extends AutoDisposeFutureProvider<ContactRequestRecords?> {
|
||||
/// 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
|
@ -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<LocalAccount> newAccount(
|
||||
{required IdentityMaster identityMaster,
|
||||
required SecretKey identitySecret,
|
||||
required proto.Account account,
|
||||
/// Make encrypted identitySecret
|
||||
Future<Uint8List> _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<LocalAccount> 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<bool> deleteAccount(TypedKey accountMasterRecordKey) async {
|
||||
Future<bool> 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
|
||||
|
@ -112,7 +112,7 @@ class FetchLocalAccountProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$localAccountsHash() => r'd6ced0ad7108c1111603235cf394faa5f6bcdae1';
|
||||
String _$localAccountsHash() => r'a9a1e1765188556858ec982c9e99f780756ade1e';
|
||||
|
||||
/// See also [LocalAccounts].
|
||||
@ProviderFor(LocalAccounts)
|
||||
|
@ -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<ActiveLogins> {
|
||||
state = AsyncValue.data(updated);
|
||||
}
|
||||
|
||||
Future<bool> loginWithNone(TypedKey accountMasterRecordKey) async {
|
||||
Future<bool> _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<bool> 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<ActiveLogins> {
|
||||
}
|
||||
|
||||
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<bool> loginWithPasswordOrPin(
|
||||
@ -112,39 +121,21 @@ class Logins extends _$Logins with AsyncTableDBBacked<ActiveLogins> {
|
||||
}
|
||||
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<void> logout(TypedKey? accountMasterRecordKey) async {
|
||||
|
@ -111,7 +111,7 @@ class FetchLoginProvider extends AutoDisposeFutureProvider<UserLogin?> {
|
||||
}
|
||||
}
|
||||
|
||||
String _$loginsHash() => r'ed9dbe91a248f662ccb0fac6edf5b1892cf2ef92';
|
||||
String _$loginsHash() => r'5720eaacf858b2e1d69ebf9d2a981173a30f8592';
|
||||
|
||||
/// See also [Logins].
|
||||
@ProviderFor(Logins)
|
||||
|
@ -1,5 +0,0 @@
|
||||
export 'account.dart';
|
||||
export 'connection_state.dart';
|
||||
export 'local_accounts.dart';
|
||||
export 'logins.dart';
|
||||
export 'window_control.dart';
|
9
lib/providers/veilid_instance.dart
Normal file
9
lib/providers/veilid_instance.dart
Normal file
@ -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<Veilid> veilidInstance(VeilidInstanceRef ref) async =>
|
||||
await eventualVeilid.future;
|
@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'veilid_init.dart';
|
||||
part of 'veilid_instance.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
@ -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<int, int> _subkeySeqCache;
|
||||
DHTRecordCrypto crypto;
|
||||
|
||||
static Future<DHTRecord> 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<DHTRecord> 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<DHTRecord> 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<void> 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<void> 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<T> scope<T>(Future<T> Function(DHTRecord) scopeFunction) async {
|
||||
Future<T> scope<T>(FutureOr<T> Function(DHTRecord) scopeFunction) async {
|
||||
try {
|
||||
return await scopeFunction(this);
|
||||
} finally {
|
||||
@ -101,7 +75,8 @@ class DHTRecord {
|
||||
}
|
||||
}
|
||||
|
||||
Future<T> deleteScope<T>(Future<T> Function(DHTRecord) scopeFunction) async {
|
||||
Future<T> deleteScope<T>(
|
||||
FutureOr<T> 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<Uint8List?> 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<void> 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);
|
@ -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<Uint8List> encrypt(Uint8List data, int subkey);
|
272
lib/veilid_support/dht_support/dht_record_pool.dart
Normal file
272
lib/veilid_support/dht_support/dht_record_pool.dart
Normal file
@ -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<TypedKey, ISet<TypedKey>> childrenByParent,
|
||||
required IMap<TypedKey, TypedKey> parentByChild,
|
||||
}) = _DHTRecordPoolAllocations;
|
||||
|
||||
factory DHTRecordPoolAllocations.fromJson(dynamic json) =>
|
||||
_$DHTRecordPoolAllocationsFromJson(json as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
/// 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<String, dynamic>);
|
||||
}
|
||||
|
||||
class DHTRecordPool with AsyncTableDBBacked<DHTRecordPoolAllocations> {
|
||||
DHTRecordPool._(Veilid veilid, VeilidRoutingContext routingContext)
|
||||
: _state = DHTRecordPoolAllocations(
|
||||
childrenByParent: IMap(), parentByChild: IMap()),
|
||||
_opened = <TypedKey, DHTRecord>{},
|
||||
_routingContext = routingContext,
|
||||
_veilid = veilid;
|
||||
|
||||
// Persistent DHT record list
|
||||
DHTRecordPoolAllocations _state;
|
||||
// Which DHT records are currently open
|
||||
final Map<TypedKey, DHTRecord> _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<DHTRecordPool> 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<void> deleteDeep(TypedKey parent) async {
|
||||
// Collect all dependencies
|
||||
final allDeps = <TypedKey>[];
|
||||
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 = <Future<void>>[];
|
||||
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<TypedKey>();
|
||||
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<DHTRecord> 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<DHTRecord> 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<DHTRecord> 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<DHTRecord> 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,
|
||||
);
|
||||
}
|
357
lib/veilid_support/dht_support/dht_record_pool.freezed.dart
Normal file
357
lib/veilid_support/dht_support/dht_record_pool.freezed.dart
Normal file
@ -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>(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<String, dynamic> json) {
|
||||
return _DHTRecordPoolAllocations.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$DHTRecordPoolAllocations {
|
||||
IMap<Typed<FixedEncodedString43>, ISet<Typed<FixedEncodedString43>>>
|
||||
get childrenByParent => throw _privateConstructorUsedError;
|
||||
IMap<Typed<FixedEncodedString43>, Typed<FixedEncodedString43>>
|
||||
get parentByChild => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$DHTRecordPoolAllocationsCopyWith<DHTRecordPoolAllocations> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $DHTRecordPoolAllocationsCopyWith<$Res> {
|
||||
factory $DHTRecordPoolAllocationsCopyWith(DHTRecordPoolAllocations value,
|
||||
$Res Function(DHTRecordPoolAllocations) then) =
|
||||
_$DHTRecordPoolAllocationsCopyWithImpl<$Res, DHTRecordPoolAllocations>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{IMap<Typed<FixedEncodedString43>, ISet<Typed<FixedEncodedString43>>>
|
||||
childrenByParent,
|
||||
IMap<Typed<FixedEncodedString43>, Typed<FixedEncodedString43>>
|
||||
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<Typed<FixedEncodedString43>,
|
||||
ISet<Typed<FixedEncodedString43>>>,
|
||||
parentByChild: null == parentByChild
|
||||
? _value.parentByChild
|
||||
: parentByChild // ignore: cast_nullable_to_non_nullable
|
||||
as IMap<Typed<FixedEncodedString43>, Typed<FixedEncodedString43>>,
|
||||
) 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<Typed<FixedEncodedString43>, ISet<Typed<FixedEncodedString43>>>
|
||||
childrenByParent,
|
||||
IMap<Typed<FixedEncodedString43>, Typed<FixedEncodedString43>>
|
||||
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<Typed<FixedEncodedString43>,
|
||||
ISet<Typed<FixedEncodedString43>>>,
|
||||
parentByChild: null == parentByChild
|
||||
? _value.parentByChild
|
||||
: parentByChild // ignore: cast_nullable_to_non_nullable
|
||||
as IMap<Typed<FixedEncodedString43>, Typed<FixedEncodedString43>>,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$_DHTRecordPoolAllocations implements _DHTRecordPoolAllocations {
|
||||
const _$_DHTRecordPoolAllocations(
|
||||
{required this.childrenByParent, required this.parentByChild});
|
||||
|
||||
factory _$_DHTRecordPoolAllocations.fromJson(Map<String, dynamic> json) =>
|
||||
_$$_DHTRecordPoolAllocationsFromJson(json);
|
||||
|
||||
@override
|
||||
final IMap<Typed<FixedEncodedString43>, ISet<Typed<FixedEncodedString43>>>
|
||||
childrenByParent;
|
||||
@override
|
||||
final IMap<Typed<FixedEncodedString43>, Typed<FixedEncodedString43>>
|
||||
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<String, dynamic> toJson() {
|
||||
return _$$_DHTRecordPoolAllocationsToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _DHTRecordPoolAllocations implements DHTRecordPoolAllocations {
|
||||
const factory _DHTRecordPoolAllocations(
|
||||
{required final IMap<Typed<FixedEncodedString43>,
|
||||
ISet<Typed<FixedEncodedString43>>>
|
||||
childrenByParent,
|
||||
required final IMap<Typed<FixedEncodedString43>,
|
||||
Typed<FixedEncodedString43>>
|
||||
parentByChild}) = _$_DHTRecordPoolAllocations;
|
||||
|
||||
factory _DHTRecordPoolAllocations.fromJson(Map<String, dynamic> json) =
|
||||
_$_DHTRecordPoolAllocations.fromJson;
|
||||
|
||||
@override
|
||||
IMap<Typed<FixedEncodedString43>, ISet<Typed<FixedEncodedString43>>>
|
||||
get childrenByParent;
|
||||
@override
|
||||
IMap<Typed<FixedEncodedString43>, Typed<FixedEncodedString43>>
|
||||
get parentByChild;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$_DHTRecordPoolAllocationsCopyWith<_$_DHTRecordPoolAllocations>
|
||||
get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
OwnedDHTRecordPointer _$OwnedDHTRecordPointerFromJson(
|
||||
Map<String, dynamic> json) {
|
||||
return _OwnedDHTRecordPointer.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$OwnedDHTRecordPointer {
|
||||
Typed<FixedEncodedString43> get recordKey =>
|
||||
throw _privateConstructorUsedError;
|
||||
KeyPair get owner => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$OwnedDHTRecordPointerCopyWith<OwnedDHTRecordPointer> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $OwnedDHTRecordPointerCopyWith<$Res> {
|
||||
factory $OwnedDHTRecordPointerCopyWith(OwnedDHTRecordPointer value,
|
||||
$Res Function(OwnedDHTRecordPointer) then) =
|
||||
_$OwnedDHTRecordPointerCopyWithImpl<$Res, OwnedDHTRecordPointer>;
|
||||
@useResult
|
||||
$Res call({Typed<FixedEncodedString43> 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<FixedEncodedString43>,
|
||||
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<FixedEncodedString43> 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<FixedEncodedString43>,
|
||||
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<String, dynamic> json) =>
|
||||
_$$_OwnedDHTRecordPointerFromJson(json);
|
||||
|
||||
@override
|
||||
final Typed<FixedEncodedString43> 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<String, dynamic> toJson() {
|
||||
return _$$_OwnedDHTRecordPointerToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _OwnedDHTRecordPointer implements OwnedDHTRecordPointer {
|
||||
const factory _OwnedDHTRecordPointer(
|
||||
{required final Typed<FixedEncodedString43> recordKey,
|
||||
required final KeyPair owner}) = _$_OwnedDHTRecordPointer;
|
||||
|
||||
factory _OwnedDHTRecordPointer.fromJson(Map<String, dynamic> json) =
|
||||
_$_OwnedDHTRecordPointer.fromJson;
|
||||
|
||||
@override
|
||||
Typed<FixedEncodedString43> get recordKey;
|
||||
@override
|
||||
KeyPair get owner;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$_OwnedDHTRecordPointerCopyWith<_$_OwnedDHTRecordPointer> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
52
lib/veilid_support/dht_support/dht_record_pool.g.dart
Normal file
52
lib/veilid_support/dht_support/dht_record_pool.g.dart
Normal file
@ -0,0 +1,52 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'dht_record_pool.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$_DHTRecordPoolAllocations _$$_DHTRecordPoolAllocationsFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$_DHTRecordPoolAllocations(
|
||||
childrenByParent: IMap<Typed<FixedEncodedString43>,
|
||||
ISet<Typed<FixedEncodedString43>>>.fromJson(
|
||||
json['children_by_parent'] as Map<String, dynamic>,
|
||||
(value) => Typed<FixedEncodedString43>.fromJson(value),
|
||||
(value) => ISet<Typed<FixedEncodedString43>>.fromJson(
|
||||
value, (value) => Typed<FixedEncodedString43>.fromJson(value))),
|
||||
parentByChild: IMap<Typed<FixedEncodedString43>,
|
||||
Typed<FixedEncodedString43>>.fromJson(
|
||||
json['parent_by_child'] as Map<String, dynamic>,
|
||||
(value) => Typed<FixedEncodedString43>.fromJson(value),
|
||||
(value) => Typed<FixedEncodedString43>.fromJson(value)),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$_DHTRecordPoolAllocationsToJson(
|
||||
_$_DHTRecordPoolAllocations instance) =>
|
||||
<String, dynamic>{
|
||||
'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<String, dynamic> json) =>
|
||||
_$_OwnedDHTRecordPointer(
|
||||
recordKey: Typed<FixedEncodedString43>.fromJson(json['record_key']),
|
||||
owner: KeyPair.fromJson(json['owner']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$_OwnedDHTRecordPointerToJson(
|
||||
_$_OwnedDHTRecordPointer instance) =>
|
||||
<String, dynamic>{
|
||||
'record_key': instance.recordKey.toJson(),
|
||||
'owner': instance.owner.toJson(),
|
||||
};
|
@ -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<DHTShortArray> create(VeilidRoutingContext dhtctx, int stride,
|
||||
{DHTRecordCrypto? crypto}) async {
|
||||
static Future<DHTShortArray> 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<DHTShortArray> openRead(
|
||||
VeilidRoutingContext dhtctx, TypedKey dhtRecordKey,
|
||||
{DHTRecordCrypto? crypto}) async {
|
||||
final dhtRecord =
|
||||
await DHTRecord.openRead(dhtctx, dhtRecordKey, crypto: crypto);
|
||||
static Future<DHTShortArray> 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<DHTShortArray> 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<DHTShortArray> 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<DHTRecord> _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<T> scope<T>(Future<T> Function(DHTShortArray) scopeFunction) async {
|
||||
Future<T> scope<T>(FutureOr<T> Function(DHTShortArray) scopeFunction) async {
|
||||
try {
|
||||
return await scopeFunction(this);
|
||||
} finally {
|
||||
@ -251,7 +290,7 @@ class DHTShortArray {
|
||||
}
|
||||
|
||||
Future<T> deleteScope<T>(
|
||||
Future<T> Function(DHTShortArray) scopeFunction) async {
|
||||
FutureOr<T> Function(DHTShortArray) scopeFunction) async {
|
||||
try {
|
||||
final out = await scopeFunction(this);
|
||||
await close();
|
4
lib/veilid_support/dht_support/dht_support.dart
Normal file
4
lib/veilid_support/dht_support/dht_support.dart
Normal file
@ -0,0 +1,4 @@
|
||||
export 'dht_record.dart';
|
||||
export 'dht_record_crypto.dart';
|
||||
export 'dht_record_pool.dart';
|
||||
export 'dht_short_array.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<void> 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<IdentityMasterWithSecrets> create() async {
|
||||
final pool = await DHTRecordPool.instance();
|
||||
|
||||
/// Creates a new master identity and returns it with its secrets
|
||||
Future<IdentityMasterWithSecrets> 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<IdentityMasterWithSecrets> 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<IdentityMasterWithSecrets> 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<void> delete() async {
|
||||
final pool = await DHTRecordPool.instance();
|
||||
await pool.deleteDeep(identityMaster.masterRecordKey);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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<String> getVeilidVersion() async {
|
||||
String veilidVersion;
|
||||
try {
|
||||
@ -72,8 +69,3 @@ Future<void> 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<Veilid> veilidInstance(VeilidInstanceRef ref) async =>
|
||||
await eventualVeilid.future;
|
||||
|
@ -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';
|
||||
|
Loading…
Reference in New Issue
Block a user