diff --git a/lib/account_manager/repository/account_repository/account_repository.dart b/lib/account_manager/repository/account_repository/account_repository.dart index 0ce8ce7..40ce315 100644 --- a/lib/account_manager/repository/account_repository/account_repository.dart +++ b/lib/account_manager/repository/account_repository/account_repository.dart @@ -299,17 +299,18 @@ class AccountRepository { Future _decryptedLogin( IdentityMaster identityMaster, SecretKey identitySecret) async { - final cs = await Veilid.instance - .getCryptoSystem(identityMaster.identityRecordKey.kind); - final keyOk = await cs.validateKeyPair( - identityMaster.identityPublicKey, identitySecret); - if (!keyOk) { - throw Exception('Identity is corrupted'); - } + // Verify identity secret works and return the valid cryptosystem + final cs = await identityMaster.validateIdentitySecret(identitySecret); // Read the identity key to get the account keys - final accountRecordInfo = await identityMaster.readAccountFromIdentity( + final accountRecordInfoList = await identityMaster.readAccountsFromIdentity( identitySecret: identitySecret, accountKey: veilidChatAccountKey); + if (accountRecordInfoList.length > 1) { + throw IdentityException.limitExceeded; + } else if (accountRecordInfoList.isEmpty) { + throw IdentityException.noAccount; + } + final accountRecordInfo = accountRecordInfoList.single; // Add to user logins and select it final userLogins = await _userLogins.get(); diff --git a/packages/veilid_support/lib/dht_support/src/dht_record.dart b/packages/veilid_support/lib/dht_support/src/dht_record.dart index f7f66bd..d9a1337 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record.dart @@ -199,7 +199,7 @@ class DHTRecord { // See if the encrypted data returned is exactly the same // if so, shortcut and don't bother decrypting it - if (newValueData.data == encryptedNewValue) { + if (newValueData.data.equals(encryptedNewValue)) { if (isUpdated) { addLocalValueChange(newValue, subkey); } @@ -243,7 +243,7 @@ class DHTRecord { // The encrypted data returned should be exactly the same // as what we are trying to set, // otherwise we still need to keep trying to set the value - } while (newValueData.data != encryptedNewValue); + } while (!newValueData.data.equals(encryptedNewValue)); final isUpdated = newValueData.seq != lastSeq; if (isUpdated) { diff --git a/packages/veilid_support/lib/src/identity.dart b/packages/veilid_support/lib/src/identity.dart index 5a77e2e..70dc295 100644 --- a/packages/veilid_support/lib/src/identity.dart +++ b/packages/veilid_support/lib/src/identity.dart @@ -10,6 +10,20 @@ import '../dht_support/dht_support.dart'; part 'identity.freezed.dart'; part 'identity.g.dart'; +// Identity errors +enum IdentityException implements Exception { + readError('identity could not be read'), + noAccount('no account record info'), + limitExceeded('too many items for the limit'), + invalid('identity is corrupted or secret is invalid'); + + const IdentityException(this.message); + final String message; + + @override + String toString() => 'IdentityException($name): $message'; +} + // AccountOwnerInfo is the key and owner info for the account dht key that is // stored in the identity key @freezed @@ -82,6 +96,12 @@ extension IdentityMasterExtension on IdentityMaster { await (await pool.openRead(masterRecordKey)).delete(); } + Future get identityCrypto => + Veilid.instance.getCryptoSystem(identityRecordKey.kind); + + Future get masterCrypto => + Veilid.instance.getCryptoSystem(masterRecordKey.kind); + KeyPair identityWriter(SecretKey secret) => KeyPair(key: identityPublicKey, secret: secret); @@ -91,7 +111,17 @@ extension IdentityMasterExtension on IdentityMaster { TypedKey identityPublicTypedKey() => TypedKey(kind: identityRecordKey.kind, value: identityPublicKey); - Future readAccountFromIdentity( + Future validateIdentitySecret( + SecretKey identitySecret) async { + final cs = await identityCrypto; + final keyOk = await cs.validateKeyPair(identityPublicKey, identitySecret); + if (!keyOk) { + throw IdentityException.invalid; + } + return cs; + } + + Future> readAccountsFromIdentity( {required SharedSecret identitySecret, required String accountKey}) async { // Read the identity key to get the account keys @@ -100,23 +130,19 @@ extension IdentityMasterExtension on IdentityMaster { final identityRecordCrypto = await DHTRecordCryptoPrivate.fromSecret( identityRecordKey.kind, identitySecret); - late final AccountRecordInfo accountRecordInfo; + late final List 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'); + throw IdentityException.readError; } final accountRecords = IMapOfSets.from(identity.accountRecords); final vcAccounts = accountRecords.get(accountKey); - if (vcAccounts.length != 1) { - // No account, or multiple accounts somehow associated with identity - throw StateError('no single account record info'); - } - accountRecordInfo = vcAccounts.first; + accountRecordInfo = vcAccounts.toList(); }); return accountRecordInfo; @@ -128,6 +154,7 @@ extension IdentityMasterExtension on IdentityMaster { required SharedSecret identitySecret, required String accountKey, required Future Function(TypedKey parent) createAccountCallback, + int maxAccounts = 1, }) async { final pool = DHTRecordPool.instance; @@ -153,11 +180,14 @@ extension IdentityMasterExtension on IdentityMaster { await identityRec.eventualUpdateJson(Identity.fromJson, (oldIdentity) async { + if (oldIdentity == null) { + throw IdentityException.readError; + } final oldAccountRecords = IMapOfSets.from(oldIdentity.accountRecords); - // Only allow one account per identity for veilidchat - if (oldAccountRecords.get(accountKey).isNotEmpty) { - throw StateError('Only one account per key in identity'); + + if (oldAccountRecords.get(accountKey).length >= maxAccounts) { + throw IdentityException.limitExceeded; } final accountRecords = oldAccountRecords .add(accountKey, newAccountRecordInfo) diff --git a/packages/veilid_support/lib/src/table_db.dart b/packages/veilid_support/lib/src/table_db.dart index f6a69b7..1e09fc4 100644 --- a/packages/veilid_support/lib/src/table_db.dart +++ b/packages/veilid_support/lib/src/table_db.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:async_tools/async_tools.dart'; import 'package:veilid/veilid.dart'; Future tableScope( @@ -67,25 +68,26 @@ class TableDBValue extends TableDBBacked { _tableKeyName = tableKeyName, _streamController = StreamController.broadcast(); - T? get value => _value; - T get requireValue => _value!; + AsyncData? get value => _value; + T get requireValue => _value!.value; Stream get stream => _streamController.stream; Future get() async { final val = _value; if (val != null) { - return val; + return val.value; } final loadedValue = await load(); - return _value = loadedValue; + _value = AsyncData(loadedValue); + return loadedValue; } Future set(T newVal) async { - _value = await store(newVal); + _value = AsyncData(await store(newVal)); _streamController.add(newVal); } - T? _value; + AsyncData? _value; final String _tableName; final String _tableKeyName; final T Function(Object? obj) _valueFromJson; diff --git a/packages/veilid_support/lib/veilid_support.dart b/packages/veilid_support/lib/veilid_support.dart index f873397..f9e9293 100644 --- a/packages/veilid_support/lib/veilid_support.dart +++ b/packages/veilid_support/lib/veilid_support.dart @@ -9,6 +9,7 @@ export 'dht_support/dht_support.dart'; export 'src/config.dart'; export 'src/identity.dart'; export 'src/json_tools.dart'; +export 'src/memory_tools.dart'; export 'src/protobuf_tools.dart'; export 'src/table_db.dart'; export 'src/veilid_log.dart';