diff --git a/lib/repositories/local_account_repository.dart b/lib/repositories/local_account_repository.dart new file mode 100644 index 0000000..07ced9f --- /dev/null +++ b/lib/repositories/local_account_repository.dart @@ -0,0 +1,28 @@ +import 'package:veilid/veilid.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import '../entities/entities.dart'; +import '../entities/proto.dart' as proto; + +import 'local_account_repository_impl.dart'; + +part 'local_account_repository.g.dart'; + +// Local account manager +abstract class LocalAccountRepository { + /// Creates a new master identity and returns it with its secrets + Future newIdentityMaster(); + + /// Creates a new account associated with master identity + Future newAccount( + IdentityMaster identityMaster, + SecretKey identitySecret, + EncryptionKeyType encryptionKeyType, + String encryptionKey, + proto.Account account); +} + +@riverpod +Future localAccountManager( + LocalAccountManagerRef ref) async { + return await LocalAccountRepositoryImpl.open(); +} diff --git a/lib/repositories/local_account_repository.g.dart b/lib/repositories/local_account_repository.g.dart new file mode 100644 index 0000000..b837ac2 --- /dev/null +++ b/lib/repositories/local_account_repository.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'local_account_repository.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$localAccountManagerHash() => + r'0ecdc84f868edb33f6e62bfe116f18568b424267'; + +/// See also [localAccountManager]. +@ProviderFor(localAccountManager) +final localAccountManagerProvider = + AutoDisposeFutureProvider.internal( + localAccountManager, + name: r'localAccountManagerProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$localAccountManagerHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef LocalAccountManagerRef + = AutoDisposeFutureProviderRef; +// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions diff --git a/lib/repositories/local_account_repository_impl.dart b/lib/repositories/local_account_repository_impl.dart new file mode 100644 index 0000000..7b2dee8 --- /dev/null +++ b/lib/repositories/local_account_repository_impl.dart @@ -0,0 +1,159 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:veilid/veilid.dart'; + +import '../tools/tools.dart'; +import '../entities/entities.dart'; +import '../entities/proto.dart' as proto; + +import 'local_account_repository.dart'; + +// Local account manager +class LocalAccountRepositoryImpl extends LocalAccountRepository { + IList _localAccounts; + + static const localAccountManagerTable = "local_account_manager"; + static const localAccountsKey = "local_accounts"; + + LocalAccountRepositoryImpl._({required IList localAccounts}) + : _localAccounts = localAccounts; + + /// Gets or creates a local account manager + static Future open() async { + // Load accounts from tabledb + final localAccounts = + await tableScope(localAccountManagerTable, (tdb) async { + final localAccountsJson = await tdb.loadStringJson(0, localAccountsKey); + return localAccountsJson != null + ? IList.fromJson( + localAccountsJson, genericFromJson(LocalAccount.fromJson)) + : IList(); + }); + + return LocalAccountRepositoryImpl._(localAccounts: localAccounts); + } + + /// Store things back to storage + Future flush() async { + await tableScope(localAccountManagerTable, (tdb) async { + await tdb.storeStringJson(0, localAccountsKey, _localAccounts); + }); + } + + /// Creates a new master identity and returns it with its secrets + @override + Future newIdentityMaster() async { + final crypto = await Veilid.instance.bestCryptoSystem(); + final dhtctx = (await Veilid.instance.routingContext()) + .withPrivacy() + .withSequencing(Sequencing.ensureOrdered); + + return (await DHTRecord.create(dhtctx)).deleteScope((masterRec) async { + return (await DHTRecord.create(dhtctx)).deleteScope((identityRec) async { + // Make IdentityMaster + final masterRecordKey = masterRec.key(); + final masterOwner = masterRec.ownerKeyPair()!; + final masterSigBuf = masterRecordKey.decode() + ..addAll(masterOwner.key.decode()); + + final identityRecordKey = identityRec.key(); + final identityOwner = identityRec.ownerKeyPair()!; + final identitySigBuf = identityRecordKey.decode() + ..addAll(identityOwner.key.decode()); + + final identitySignature = + await crypto.signWithKeyPair(masterOwner, identitySigBuf); + final masterSignature = + await crypto.signWithKeyPair(identityOwner, masterSigBuf); + + final identityMaster = IdentityMaster( + identityRecordKey: identityRecordKey, + identityPublicKey: identityOwner.key, + masterRecordKey: masterRecordKey, + masterPublicKey: masterOwner.key, + identitySignature: identitySignature, + masterSignature: masterSignature); + + // Write identity master to master dht key + await masterRec.eventualWriteJson(identityMaster); + + // Make empty identity + const identity = Identity(accountRecords: IMapConst({})); + + // Write empty identity to identity dht key + await identityRec.eventualWriteJson(identity); + + return IdentityMasterWithSecrets( + identityMaster: identityMaster, + masterSecret: masterOwner.secret, + identitySecret: identityOwner.secret); + }); + }); + } + + /// Creates a new account associated with master identity + @override + Future newAccount( + IdentityMaster identityMaster, + SecretKey identitySecret, + EncryptionKeyType encryptionKeyType, + String encryptionKey, + proto.Account account) async { + // Encrypt identitySecret with key + final cs = await Veilid.instance.bestCryptoSystem(); + final ekbytes = Uint8List.fromList(utf8.encode(encryptionKey)); + final nonce = await cs.randomNonce(); + final eksalt = nonce.decode(); + SharedSecret sharedSecret = await cs.deriveSharedSecret(ekbytes, eksalt); + final identitySecretBytes = + await cs.cryptNoAuth(identitySecret.decode(), nonce, sharedSecret); + + // Create local account object + final localAccount = LocalAccount( + identityMaster: identityMaster, + identitySecretKeyBytes: identitySecretBytes, + identitySecretSaltBytes: eksalt, + encryptionKeyType: encryptionKeyType, + biometricsEnabled: false, + hiddenAccount: false, + ); + + /////// Add account with profile to DHT + + // Create private routing context + final dhtctx = (await Veilid.instance.routingContext()) + .withPrivacy() + .withSequencing(Sequencing.ensureOrdered); + + // Open identity key for writing + (await DHTRecord.open(dhtctx, identityMaster.identityRecordKey, + identityMaster.identityWriter(identitySecret))) + .scope((identityRec) async { + // Create new account to insert into identity + (await DHTRecord.create(dhtctx)).deleteScope((accountRec) async { + // Write account key + await accountRec.eventualWriteProtobuf(account); + + // Update identity key to include account + final newAccountRecordOwner = accountRec.ownerKeyPair()!; + final newAccountRecordInfo = AccountRecordInfo( + key: accountRec.key(), owner: newAccountRecordOwner); + + await identityRec.eventualUpdateJson(Identity.fromJson, + (oldIdentity) async { + final accountRecords = IMapOfSets.from(oldIdentity.accountRecords) + .add("VeilidChat", newAccountRecordInfo) + .asIMap(); + return oldIdentity.copyWith(accountRecords: accountRecords); + }); + }); + }); + + // Add local account object to internal store + + // Return local account object + return localAccount; + } +} diff --git a/lib/repositories/repositories.dart b/lib/repositories/repositories.dart new file mode 100644 index 0000000..c20fa81 --- /dev/null +++ b/lib/repositories/repositories.dart @@ -0,0 +1 @@ +export 'local_account_repository.dart'; diff --git a/lib/state/local_account_manager.dart b/lib/state/local_account_manager.dart deleted file mode 100644 index f2b4011..0000000 --- a/lib/state/local_account_manager.dart +++ /dev/null @@ -1,217 +0,0 @@ -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 'package:veilidchat/tools/tools.dart'; - -import '../entities/entities.dart'; -import '../entities/proto.dart' as proto; - -part 'local_account_manager.g.dart'; - -// Local account manager -class LocalAccountManager { - final VeilidTableDB _localAccountsTable; - final IList _localAccounts; - - const LocalAccountManager( - {required VeilidTableDB localAccountsTable, - required IList localAccounts}) - : _localAccountsTable = localAccountsTable, - _localAccounts = localAccounts; - - /// Gets or creates a local account manager - static Future open() async { - final localAccountsTable = - await Veilid.instance.openTableDB("local_account_manager", 1); - final localAccounts = - (await localAccountsTable.loadStringJson(0, "local_accounts") ?? - const IListConst([])) as IList; - return LocalAccountManager( - localAccountsTable: localAccountsTable, localAccounts: localAccounts); - } - - /// Flush things to storage - Future flush() async {} - - /// Creates a new master identity and returns it with its secrets - Future newIdentityMaster() async { - final dhtctx = (await Veilid.instance.routingContext()) - .withPrivacy() - .withSequencing(Sequencing.ensureOrdered); - DHTRecordDescriptor? masterRec; - DHTRecordDescriptor? identityRec; - try { - masterRec = await dhtctx.createDHTRecord(const DHTSchema.dflt(oCnt: 1)); - identityRec = await dhtctx.createDHTRecord(const DHTSchema.dflt(oCnt: 1)); - final crypto = await Veilid.instance.bestCryptoSystem(); - assert(masterRec.key.kind == crypto.kind()); - assert(identityRec.key.kind == crypto.kind()); - - // IdentityMaster - final masterRecordKey = masterRec.key; - final masterPublicKey = masterRec.owner; - final masterSecret = masterRec.ownerSecret!; - final masterSigBuf = masterRecordKey.decode() - ..addAll(masterPublicKey.decode()); - - final identityRecordKey = identityRec.key; - final identityPublicKey = identityRec.owner; - final identitySecret = identityRec.ownerSecret!; - final identitySigBuf = identityRecordKey.decode() - ..addAll(identityPublicKey.decode()); - - final identitySignature = - await crypto.sign(masterPublicKey, masterSecret, identitySigBuf); - final masterSignature = - await crypto.sign(identityPublicKey, identitySecret, masterSigBuf); - - final identityMaster = IdentityMaster( - identityRecordKey: identityRecordKey, - identityPublicKey: identityPublicKey, - masterRecordKey: masterRecordKey, - masterPublicKey: masterPublicKey, - identitySignature: identitySignature, - masterSignature: masterSignature); - - // Write identity master to master dht key - final identityMasterBytes = - Uint8List.fromList(utf8.encode(jsonEncode(identityMaster))); - await dhtctx.setDHTValue(masterRecordKey, 0, identityMasterBytes); - - // Write empty identity to identity dht key - const identity = Identity(accountKeyPairs: {}); - final identityBytes = - Uint8List.fromList(utf8.encode(jsonEncode(identity))); - await dhtctx.setDHTValue(identityRecordKey, 0, identityBytes); - - return IdentityMasterWithSecrets( - identityMaster: identityMaster, - masterSecret: masterSecret, - identitySecret: identitySecret); - } catch (e) { - if (masterRec != null) { - await dhtctx.deleteDHTRecord(masterRec.key); - } - if (identityRec != null) { - await dhtctx.deleteDHTRecord(identityRec.key); - } - rethrow; - } - } - - /// Adds a 'VeilidChat' account record to an identity key - Future updateIdentityKey( - VeilidRoutingContext dhtctx, - TypedKey identityRecordKey, - TypedKey accountRecordKey, - KeyPair accountRecordOwner) async { - // Account record to add - final accountRecordInfo = - AccountRecordInfo(key: accountRecordKey, owner: accountRecordOwner); - - // Eventually update identity key - eventuallyConsistentUpdate( - dhtctx, - identityRecordKey, - 0, - true, - jsonUpdate(Identity.fromJson, (oldObj) async { - final accountRecords = IMapOfSets.from(oldObj.accountRecords) - .add("VeilidChat", accountRecordInfo) - .asIMap(); - return oldObj.copyWith(accountRecords: accountRecords); - })); - - // Get existing identity key - ValueData? identityValueData = - await dhtctx.getDHTValue(identityRecordKey, 0, true); - do { - if (identityValueData == null) { - throw const FormatException("Identity does not exist"); - } - - var identity = identityValueData.readJsonData(Identity.fromJson); - // Update identity key to include account - final accountRecords = IMapOfSets.from(identity.accountRecords) - .add("VeilidChat", accountRecordInfo) - .asIMap(); - identity = identity.copyWith(accountRecords: accountRecords); - - final identityBytes = jsonEncodeBytes(identity); - identityValueData = - await dhtctx.setDHTValue(identityRecordKey, 0, identityBytes); - } while (identityValueData != null); - } - - /// Creates a new account associated with master identity - Future newAccount( - IdentityMaster identityMaster, - SecretKey identitySecret, - EncryptionKeyType encryptionKeyType, - String encryptionKey, - proto.Account account) async { - // Encrypt identitySecret with key - final cs = await Veilid.instance.bestCryptoSystem(); - final ekbytes = Uint8List.fromList(utf8.encode(encryptionKey)); - final nonce = await cs.randomNonce(); - final eksalt = nonce.decode(); - SharedSecret sharedSecret = await cs.deriveSharedSecret(ekbytes, eksalt); - final identitySecretBytes = - await cs.cryptNoAuth(identitySecret.decode(), nonce, sharedSecret); - - // Create local account object - final localAccount = LocalAccount( - identityMaster: identityMaster, - identitySecretKeyBytes: identitySecretBytes, - identitySecretSaltBytes: eksalt, - encryptionKeyType: encryptionKeyType, - biometricsEnabled: false, - hiddenAccount: false, - ); - - // Add account with profile to DHT - final dhtctx = (await Veilid.instance.routingContext()) - .withPrivacy() - .withSequencing(Sequencing.ensureOrdered); - DHTRecordDescriptor? identityRec; - DHTRecordDescriptor? accountRec; - try { - identityRec = await dhtctx.openDHTRecord(identityMaster.identityRecordKey, - identityMaster.identityWriter(identitySecret)); - accountRec = await dhtctx.createDHTRecord(const DHTSchema.dflt(oCnt: 1)); - final crypto = await Veilid.instance.bestCryptoSystem(); - assert(identityRec.key.kind == crypto.kind()); - assert(accountRec.key.kind == crypto.kind()); - - // Write account key - assert(await dhtctx.setDHTValue( - accountRec.key, 0, account.writeToBuffer()) == - null); - - // Update identity key to include account - await updateIdentityKey(dhtctx, identityRec.key, accountRec.key, - KeyPair(key: accountRec.owner, secret: accountRec.ownerSecret!)); - } catch (e) { - if (identityRec != null) { - await dhtctx.closeDHTRecord(identityRec.key); - } - if (accountRec != null) { - await dhtctx.deleteDHTRecord(accountRec.key); - } - rethrow; - } - - // Add local account object to internal store - - // Return local account object - return localAccount; - } -} - -@riverpod -Future localAccountManager(LocalAccountManagerRef ref) { - return LocalAccountManager.open(); -} diff --git a/lib/tools/dht_record.dart b/lib/tools/dht_record.dart index c477e53..9faf15a 100644 --- a/lib/tools/dht_record.dart +++ b/lib/tools/dht_record.dart @@ -1,3 +1,4 @@ +import 'package:protobuf/protobuf.dart'; import 'package:veilid/veilid.dart'; import 'dart:typed_data'; import 'tools.dart'; @@ -38,6 +39,49 @@ class DHTRecord { int _subkey(int subkey) => (subkey == -1) ? _defaultSubkey : subkey; + TypedKey key() { + return _recordDescriptor.key; + } + + PublicKey owner() { + return _recordDescriptor.owner; + } + + KeyPair? ownerKeyPair() { + final ownerSecret = _recordDescriptor.ownerSecret; + if (ownerSecret == null) { + return null; + } + return KeyPair(key: _recordDescriptor.owner, secret: ownerSecret); + } + + Future close() async { + await _dhtctx.closeDHTRecord(_recordDescriptor.key); + } + + Future delete() async { + await _dhtctx.deleteDHTRecord(_recordDescriptor.key); + } + + Future scope(Future Function(DHTRecord) scopeFunction) async { + try { + return await scopeFunction(this); + } finally { + close(); + } + } + + Future deleteScope(Future Function(DHTRecord) scopeFunction) async { + try { + return await scopeFunction(this); + } catch (_) { + delete(); + rethrow; + } finally { + close(); + } + } + Future get({int subkey = -1, bool forceRefresh = false}) async { ValueData? valueData = await _dhtctx.getDHTValue( _recordDescriptor.key, _subkey(subkey), false); @@ -101,9 +145,21 @@ class DHTRecord { return eventualWriteBytes(jsonEncodeBytes(newValue), subkey: subkey); } + Future eventualWriteProtobuf(T newValue, + {int subkey = -1}) { + return eventualWriteBytes(newValue.writeToBuffer(), subkey: subkey); + } + Future eventualUpdateJson( T Function(Map) fromJson, Future Function(T) update, {int subkey = -1}) { return eventualUpdateBytes(jsonUpdate(fromJson, update), subkey: subkey); } + + Future eventualUpdateProtobuf( + T Function(List) fromBuffer, Future Function(T) update, + {int subkey = -1}) { + return eventualUpdateBytes(protobufUpdate(fromBuffer, update), + subkey: subkey); + } } diff --git a/lib/tools/json_tools.dart b/lib/tools/json_tools.dart index dca950f..a575253 100644 --- a/lib/tools/json_tools.dart +++ b/lib/tools/json_tools.dart @@ -1,4 +1,3 @@ -// import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:veilid/veilid.dart'; import 'dart:typed_data'; import 'dart:convert'; @@ -28,3 +27,10 @@ Future Function(Uint8List) jsonUpdate( return jsonUpdateBytes(fromJson, oldBytes, update); }; } + +T Function(Object?) genericFromJson( + T Function(Map) fromJsonMap) { + return (Object? json) { + return fromJsonMap(json as Map); + }; +} diff --git a/lib/tools/protobuf_tools.dart b/lib/tools/protobuf_tools.dart new file mode 100644 index 0000000..a2a1046 --- /dev/null +++ b/lib/tools/protobuf_tools.dart @@ -0,0 +1,19 @@ +import 'package:protobuf/protobuf.dart'; +import 'dart:typed_data'; + +Future protobufUpdateBytes( + T Function(List) fromBuffer, + Uint8List oldBytes, + Future Function(T) update) async { + T oldObj = fromBuffer(oldBytes); + T newObj = await update(oldObj); + return Uint8List.fromList(newObj.writeToBuffer()); +} + +Future Function(Uint8List) + protobufUpdate( + T Function(List) fromBuffer, Future Function(T) update) { + return (Uint8List oldBytes) { + return protobufUpdateBytes(fromBuffer, oldBytes, update); + }; +} diff --git a/lib/tools/table_db.dart b/lib/tools/table_db.dart new file mode 100644 index 0000000..0d646e3 --- /dev/null +++ b/lib/tools/table_db.dart @@ -0,0 +1,30 @@ +import 'package:veilid/veilid.dart'; + +Future tableScope( + String name, Future Function(VeilidTableDB tdb) callback, + {int columnCount = 1}) async { + VeilidTableDB tableDB = await Veilid.instance.openTableDB(name, columnCount); + try { + return await callback(tableDB); + } finally { + tableDB.close(); + } +} + +Future transactionScope( + VeilidTableDB tdb, + Future Function(VeilidTableDBTransaction tdbt) callback, +) async { + VeilidTableDBTransaction tdbt = tdb.transact(); + try { + final ret = await callback(tdbt); + if (!tdbt.isDone()) { + await tdbt.commit(); + } + return ret; + } finally { + if (!tdbt.isDone()) { + await tdbt.rollback(); + } + } +} diff --git a/lib/tools/tools.dart b/lib/tools/tools.dart index b25ec3b..98d01e5 100644 --- a/lib/tools/tools.dart +++ b/lib/tools/tools.dart @@ -2,3 +2,5 @@ export 'external_stream_state.dart'; export 'dht_record.dart'; export 'json_tools.dart'; export 'phono_byte.dart'; +export 'protobuf_tools.dart'; +export 'table_db.dart';