From bc3ed79cc2fd6d79075d5ef9117c4819eea9ddfc Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 21 Jul 2023 21:25:27 -0400 Subject: [PATCH] xfer --- lib/entities/user_login.dart | 4 +- lib/entities/user_login.freezed.dart | 34 ++-- lib/entities/user_login.g.dart | 4 +- .../connection_state.dart | 0 .../local_accounts.dart | 77 ++++---- .../local_accounts.g.dart | 2 +- lib/providers/logins.dart | 170 ++++++++++++++++++ lib/providers/logins.g.dart | 24 +++ lib/providers/providers.dart | 3 + lib/repositories/repositories.dart | 3 - lib/state/active_logins_state.dart | 107 ----------- lib/state/active_logins_state.g.dart | 25 --- lib/state/state.dart | 2 - lib/tools/dht_record_encryption.dart | 53 ------ lib/tools/json_tools.dart | 1 - lib/tools/tools.dart | 3 - lib/{tools => veilid_support}/dht_record.dart | 58 +++--- lib/veilid_support/dht_record_crypto.dart | 73 ++++++++ lib/veilid_support/identity_master.dart | 58 ++++++ lib/veilid_support/processor.dart | 2 +- lib/{tools => veilid_support}/table_db.dart | 22 +++ lib/veilid_support/tools.dart | 3 - lib/veilid_support/veilid_support.dart | 5 +- 23 files changed, 458 insertions(+), 275 deletions(-) rename lib/{state => providers}/connection_state.dart (100%) rename lib/{repositories => providers}/local_accounts.dart (69%) rename lib/{repositories => providers}/local_accounts.g.dart (92%) create mode 100644 lib/providers/logins.dart create mode 100644 lib/providers/logins.g.dart create mode 100644 lib/providers/providers.dart delete mode 100644 lib/repositories/repositories.dart delete mode 100644 lib/state/active_logins_state.dart delete mode 100644 lib/state/active_logins_state.g.dart delete mode 100644 lib/state/state.dart delete mode 100644 lib/tools/dht_record_encryption.dart rename lib/{tools => veilid_support}/dht_record.dart (80%) create mode 100644 lib/veilid_support/dht_record_crypto.dart create mode 100644 lib/veilid_support/identity_master.dart rename lib/{tools => veilid_support}/table_db.dart (55%) delete mode 100644 lib/veilid_support/tools.dart diff --git a/lib/entities/user_login.dart b/lib/entities/user_login.dart index 9e9c73c..2853634 100644 --- a/lib/entities/user_login.dart +++ b/lib/entities/user_login.dart @@ -11,7 +11,7 @@ part 'user_login.g.dart'; @freezed class UserLogin with _$UserLogin { const factory UserLogin({ - // Master public key for the user used to index the local accounts table + // Master record key for the user used to index the local accounts table required TypedKey accountMasterKey, // The identity secret as unlocked from the local accounts table required TypedSecret secretKey, @@ -30,7 +30,7 @@ class ActiveLogins with _$ActiveLogins { const factory ActiveLogins({ // The list of current logged in accounts required IList userLogins, - // The current selected account indexed by master key + // The current selected account indexed by master record key TypedKey? activeUserLogin, }) = _ActiveLogins; diff --git a/lib/entities/user_login.freezed.dart b/lib/entities/user_login.freezed.dart index 6ea708e..27c6983 100644 --- a/lib/entities/user_login.freezed.dart +++ b/lib/entities/user_login.freezed.dart @@ -20,8 +20,8 @@ UserLogin _$UserLoginFromJson(Map json) { /// @nodoc mixin _$UserLogin { -// Master public key for the user used to index the local accounts table - Typed get accountMasterKey => +// Master record key for the user used to index the local accounts table + Typed get accountMasterRecordKey => throw _privateConstructorUsedError; // The identity secret as unlocked from the local accounts table Typed get secretKey => throw _privateConstructorUsedError; // The time this login was most recently used @@ -63,7 +63,7 @@ class _$UserLoginCopyWithImpl<$Res, $Val extends UserLogin> }) { return _then(_value.copyWith( accountMasterKey: null == accountMasterKey - ? _value.accountMasterKey + ? _value.accountMasterRecordKey : accountMasterKey // ignore: cast_nullable_to_non_nullable as Typed, secretKey: null == secretKey @@ -107,8 +107,8 @@ class __$$_UserLoginCopyWithImpl<$Res> Object? lastActive = null, }) { return _then(_$_UserLogin( - accountMasterKey: null == accountMasterKey - ? _value.accountMasterKey + accountMasterRecordKey: null == accountMasterKey + ? _value.accountMasterRecordKey : accountMasterKey // ignore: cast_nullable_to_non_nullable as Typed, secretKey: null == secretKey @@ -127,16 +127,16 @@ class __$$_UserLoginCopyWithImpl<$Res> @JsonSerializable() class _$_UserLogin implements _UserLogin { const _$_UserLogin( - {required this.accountMasterKey, + {required this.accountMasterRecordKey, required this.secretKey, required this.lastActive}); factory _$_UserLogin.fromJson(Map json) => _$$_UserLoginFromJson(json); -// Master public key for the user used to index the local accounts table +// Master record key for the user used to index the local accounts table @override - final Typed accountMasterKey; + final Typed accountMasterRecordKey; // The identity secret as unlocked from the local accounts table @override final Typed secretKey; @@ -146,7 +146,7 @@ class _$_UserLogin implements _UserLogin { @override String toString() { - return 'UserLogin(accountMasterKey: $accountMasterKey, secretKey: $secretKey, lastActive: $lastActive)'; + return 'UserLogin(accountMasterKey: $accountMasterRecordKey, secretKey: $secretKey, lastActive: $lastActive)'; } @override @@ -154,8 +154,8 @@ class _$_UserLogin implements _UserLogin { return identical(this, other) || (other.runtimeType == runtimeType && other is _$_UserLogin && - (identical(other.accountMasterKey, accountMasterKey) || - other.accountMasterKey == accountMasterKey) && + (identical(other.accountMasterRecordKey, accountMasterRecordKey) || + other.accountMasterRecordKey == accountMasterRecordKey) && (identical(other.secretKey, secretKey) || other.secretKey == secretKey) && (identical(other.lastActive, lastActive) || @@ -165,7 +165,7 @@ class _$_UserLogin implements _UserLogin { @JsonKey(ignore: true) @override int get hashCode => - Object.hash(runtimeType, accountMasterKey, secretKey, lastActive); + Object.hash(runtimeType, accountMasterRecordKey, secretKey, lastActive); @JsonKey(ignore: true) @override @@ -190,8 +190,8 @@ abstract class _UserLogin implements UserLogin { factory _UserLogin.fromJson(Map json) = _$_UserLogin.fromJson; - @override // Master public key for the user used to index the local accounts table - Typed get accountMasterKey; + @override // Master record key for the user used to index the local accounts table + Typed get accountMasterRecordKey; @override // The identity secret as unlocked from the local accounts table Typed get secretKey; @override // The time this login was most recently used @@ -210,7 +210,7 @@ ActiveLogins _$ActiveLoginsFromJson(Map json) { mixin _$ActiveLogins { // The list of current logged in accounts IList get userLogins => - throw _privateConstructorUsedError; // The current selected account indexed by master key + throw _privateConstructorUsedError; // The current selected account indexed by master record key Typed? get activeUserLogin => throw _privateConstructorUsedError; @@ -311,7 +311,7 @@ class _$_ActiveLogins implements _ActiveLogins { // The list of current logged in accounts @override final IList userLogins; -// The current selected account indexed by master key +// The current selected account indexed by master record key @override final Typed? activeUserLogin; @@ -360,7 +360,7 @@ abstract class _ActiveLogins implements ActiveLogins { @override // The list of current logged in accounts IList get userLogins; - @override // The current selected account indexed by master key + @override // The current selected account indexed by master record key Typed? get activeUserLogin; @override @JsonKey(ignore: true) diff --git a/lib/entities/user_login.g.dart b/lib/entities/user_login.g.dart index 5207f85..acff4e4 100644 --- a/lib/entities/user_login.g.dart +++ b/lib/entities/user_login.g.dart @@ -7,7 +7,7 @@ part of 'user_login.dart'; // ************************************************************************** _$_UserLogin _$$_UserLoginFromJson(Map json) => _$_UserLogin( - accountMasterKey: + accountMasterRecordKey: Typed.fromJson(json['account_master_key']), secretKey: Typed.fromJson(json['secret_key']), lastActive: Timestamp.fromJson(json['last_active']), @@ -15,7 +15,7 @@ _$_UserLogin _$$_UserLoginFromJson(Map json) => _$_UserLogin( Map _$$_UserLoginToJson(_$_UserLogin instance) => { - 'account_master_key': instance.accountMasterKey.toJson(), + 'account_master_key': instance.accountMasterRecordKey.toJson(), 'secret_key': instance.secretKey.toJson(), 'last_active': instance.lastActive.toJson(), }; diff --git a/lib/state/connection_state.dart b/lib/providers/connection_state.dart similarity index 100% rename from lib/state/connection_state.dart rename to lib/providers/connection_state.dart diff --git a/lib/repositories/local_accounts.dart b/lib/providers/local_accounts.dart similarity index 69% rename from lib/repositories/local_accounts.dart rename to lib/providers/local_accounts.dart index dd090c1..ed5268b 100644 --- a/lib/repositories/local_accounts.dart +++ b/lib/providers/local_accounts.dart @@ -7,6 +7,7 @@ import 'package:veilid/veilid.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../tools/tools.dart'; +import '../veilid_support/veilid_support.dart'; import '../entities/entities.dart'; import '../entities/proto.dart' as proto; @@ -14,31 +15,28 @@ part 'local_accounts.g.dart'; // Local account manager @riverpod -abstract class LocalAccounts extends _$LocalAccounts { - static const localAccountManagerTable = "local_account_manager"; - static const localAccountsKey = "local_accounts"; +class LocalAccounts extends _$LocalAccounts + with AsyncTableDBBacked> { + ////////////////////////////////////////////////////////////// + /// AsyncTableDBBacked + @override + String tableName() => "local_account_manager"; + @override + String tableKeyName() => "local_accounts"; + @override + IList reviveJson(Object? obj) => obj != null + ? IList.fromJson( + obj, genericFromJson(LocalAccount.fromJson)) + : IList(); /// Get all local account information @override FutureOr> build() 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 localAccounts; + return await load(); } - /// Store things back to storage - Future flush(IList localAccounts) async { - await tableScope(localAccountManagerTable, (tdb) async { - await tdb.storeStringJson(0, localAccountsKey, localAccounts); - }); - } + ////////////////////////////////////////////////////////////// + /// Mutators and Selectors /// Creates a new master identity and returns it with its secrets Future newIdentityMaster() async { @@ -47,7 +45,11 @@ abstract class LocalAccounts extends _$LocalAccounts { .withPrivacy() .withSequencing(Sequencing.ensureOrdered); - return (await DHTRecord.create(dhtctx)).deleteScope((masterRec) async { + // 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 { // Make IdentityMaster final masterRecordKey = masterRec.key(); @@ -100,19 +102,31 @@ abstract class LocalAccounts extends _$LocalAccounts { final localAccounts = state.requireValue; // 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); + 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.instance + .getCryptoSystem(identityMaster.identityRecordKey.kind); + final ekbytes = Uint8List.fromList(utf8.encode(encryptionKey)); + final nonce = await cs.randomNonce(); + identitySecretSaltBytes = nonce.decode(); + SharedSecret sharedSecret = + await cs.deriveSharedSecret(ekbytes, identitySecretSaltBytes); + identitySecretBytes = + await cs.cryptNoAuth(identitySecret.decode(), nonce, sharedSecret); + } // Create local account object final localAccount = LocalAccount( identityMaster: identityMaster, identitySecretKeyBytes: identitySecretBytes, - identitySecretSaltBytes: eksalt, + identitySecretSaltBytes: identitySecretSaltBytes, encryptionKeyType: encryptionKeyType, biometricsEnabled: false, hiddenAccount: false, @@ -126,7 +140,7 @@ abstract class LocalAccounts extends _$LocalAccounts { .withSequencing(Sequencing.ensureOrdered); // Open identity key for writing - (await DHTRecord.open(dhtctx, identityMaster.identityRecordKey, + (await DHTRecord.openWrite(dhtctx, identityMaster.identityRecordKey, identityMaster.identityWriter(identitySecret))) .scope((identityRec) async { // Create new account to insert into identity @@ -135,9 +149,8 @@ abstract class LocalAccounts extends _$LocalAccounts { await accountRec.eventualWriteProtobuf(account); // Update identity key to include account - final newAccountRecordOwner = accountRec.ownerKeyPair()!; final newAccountRecordInfo = AccountRecordInfo( - key: accountRec.key(), owner: newAccountRecordOwner); + key: accountRec.key(), owner: accountRec.ownerKeyPair()!); await identityRec.eventualUpdateJson(Identity.fromJson, (oldIdentity) async { @@ -151,7 +164,7 @@ abstract class LocalAccounts extends _$LocalAccounts { // Add local account object to internal store final newLocalAccounts = localAccounts.add(localAccount); - await flush(newLocalAccounts); + await store(newLocalAccounts); state = AsyncValue.data(newLocalAccounts); // Return local account object diff --git a/lib/repositories/local_accounts.g.dart b/lib/providers/local_accounts.g.dart similarity index 92% rename from lib/repositories/local_accounts.g.dart rename to lib/providers/local_accounts.g.dart index 8bac7a7..1a94454 100644 --- a/lib/repositories/local_accounts.g.dart +++ b/lib/providers/local_accounts.g.dart @@ -6,7 +6,7 @@ part of 'local_accounts.dart'; // RiverpodGenerator // ************************************************************************** -String _$localAccountsHash() => r'27e90f03be60aa9f3d534456b5b697de669a20fe'; +String _$localAccountsHash() => r'41f70b78e71c8b3faa2cd1f2a96084fbb373324c'; /// See also [LocalAccounts]. @ProviderFor(LocalAccounts) diff --git a/lib/providers/logins.dart b/lib/providers/logins.dart new file mode 100644 index 0000000..09fb1e9 --- /dev/null +++ b/lib/providers/logins.dart @@ -0,0 +1,170 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:veilid/veilid.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:veilidchat/providers/repositories.dart'; + +import '../veilid_support/veilid_support.dart'; +import '../entities/entities.dart'; + +part 'logins.g.dart'; + +// Local account manager +@riverpod +class Logins extends _$Logins with AsyncTableDBBacked { + ////////////////////////////////////////////////////////////// + /// AsyncTableDBBacked + @override + String tableName() => "local_account_manager"; + @override + String tableKeyName() => "active_logins"; + @override + ActiveLogins reviveJson(Object? obj) => obj != null + ? ActiveLogins.fromJson(obj as Map) + : ActiveLogins.empty(); + + /// Get all local account information + @override + FutureOr build() async { + return await load(); + } + + ////////////////////////////////////////////////////////////// + /// Mutators and Selectors + + Future setActiveUserLogin(TypedKey accountMasterKey) async { + final current = state.requireValue; + for (final userLogin in current.userLogins) { + if (userLogin.accountMasterRecordKey == accountMasterKey) { + state = AsyncValue.data( + current.copyWith(activeUserLogin: accountMasterKey)); + return; + } + } + throw Exception("User not found"); + } + + Future loginWithNone(TypedKey accountMasterRecordKey) async { + final localAccounts = ref.read(localAccountsProvider).requireValue; + + // Get account, throws if not found + final localAccount = localAccounts.firstWhere( + (la) => la.identityMaster.masterRecordKey == accountMasterRecordKey); + + // Log in with this local account + + // Derive key from password + if (localAccount.encryptionKeyType != EncryptionKeyType.none) { + throw Exception("Wrong authentication type"); + } + + final identitySecret = + SecretKey.fromBytes(localAccount.identitySecretKeyBytes); + + // Validate this secret with the identity public key + final cs = await Veilid.instance + .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.instance.now(); + final updated = current.copyWith( + userLogins: current.userLogins.replaceFirstWhere( + (ul) => ul.accountMasterRecordKey == accountMasterRecordKey, + (ul) => ul != null + ? ul.copyWith(lastActive: now) + : UserLogin( + accountMasterKey: accountMasterRecordKey, + secretKey: + TypedSecret(kind: cs.kind(), value: identitySecret), + lastActive: now), + addIfNotFound: true), + activeUserLogin: accountMasterRecordKey); + await store(updated); + state = AsyncValue.data(updated); + + return true; + } + + Future loginWithPasswordOrPin( + TypedKey accountMasterRecordKey, String encryptionKey) async { + final localAccounts = ref.read(localAccountsProvider).requireValue; + + // Get account, throws if not found + final localAccount = localAccounts.firstWhere( + (la) => la.identityMaster.masterRecordKey == accountMasterRecordKey); + + // Log in with this local account + + // Derive key from password + if (localAccount.encryptionKeyType != EncryptionKeyType.password || + localAccount.encryptionKeyType != EncryptionKeyType.pin) { + throw Exception("Wrong authentication type"); + } + final cs = await Veilid.instance + .getCryptoSystem(localAccount.identityMaster.identityRecordKey.kind); + final ekbytes = Uint8List.fromList(utf8.encode(encryptionKey)); + final eksalt = localAccount.identitySecretSaltBytes; + final nonce = Nonce.fromBytes(eksalt); + SharedSecret sharedSecret = await cs.deriveSharedSecret(ekbytes, eksalt); + final identitySecret = SecretKey.fromBytes(await cs.cryptNoAuth( + localAccount.identitySecretKeyBytes, nonce, sharedSecret)); + + // Validate this secret with the identity public key + final keyOk = await cs.validateKeyPair( + localAccount.identityMaster.identityPublicKey, identitySecret); + if (!keyOk) { + return false; + } + + // Add to user logins and select it + final current = state.requireValue; + final now = Veilid.instance.now(); + final updated = current.copyWith( + userLogins: current.userLogins.replaceFirstWhere( + (ul) => ul.accountMasterRecordKey == accountMasterRecordKey, + (ul) => ul != null + ? ul.copyWith(lastActive: now) + : UserLogin( + accountMasterKey: accountMasterRecordKey, + secretKey: + TypedSecret(kind: cs.kind(), value: identitySecret), + lastActive: now), + addIfNotFound: true), + activeUserLogin: accountMasterRecordKey); + await store(updated); + state = AsyncValue.data(updated); + + return true; + } + + Future logout() async { + final current = state.requireValue; + if (current.activeUserLogin == null) { + return; + } + final updated = current.copyWith( + activeUserLogin: null, + userLogins: current.userLogins.removeWhere( + (ul) => ul.accountMasterRecordKey == current.activeUserLogin)); + await store(updated); + state = AsyncValue.data(updated); + } + + Future switchToAccount(TypedKey accountMasterRecordKey) async { + final current = state.requireValue; + final userLogin = current.userLogins.firstWhere( + (ul) => ul.accountMasterRecordKey == accountMasterRecordKey); + final updated = + current.copyWith(activeUserLogin: userLogin.accountMasterRecordKey); + await store(updated); + state = AsyncValue.data(updated); + } +} diff --git a/lib/providers/logins.g.dart b/lib/providers/logins.g.dart new file mode 100644 index 0000000..7d597e5 --- /dev/null +++ b/lib/providers/logins.g.dart @@ -0,0 +1,24 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'logins.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$loginsHash() => r'379514b8b6623d59cb89b4f128be5953d5ea62a6'; + +/// See also [Logins]. +@ProviderFor(Logins) +final loginsProvider = + AutoDisposeAsyncNotifierProvider.internal( + Logins.new, + name: r'loginsProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$loginsHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$Logins = AutoDisposeAsyncNotifier; +// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions diff --git a/lib/providers/providers.dart b/lib/providers/providers.dart new file mode 100644 index 0000000..3f9bde7 --- /dev/null +++ b/lib/providers/providers.dart @@ -0,0 +1,3 @@ +export 'local_accounts.dart'; +export 'connection_state.dart'; +export 'logins.dart'; diff --git a/lib/repositories/repositories.dart b/lib/repositories/repositories.dart deleted file mode 100644 index 3c533cd..0000000 --- a/lib/repositories/repositories.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'local_accounts.dart'; - -// xxx rename this probably \ No newline at end of file diff --git a/lib/state/active_logins_state.dart b/lib/state/active_logins_state.dart deleted file mode 100644 index 7ae3c37..0000000 --- a/lib/state/active_logins_state.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'dart:async'; - -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:veilid/veilid.dart'; -import '../entities/entities.dart'; - -part 'active_logins_state.g.dart'; - -@riverpod -class ActiveLoginsState extends _$ActiveLoginsState { - VeilidTableDB? _userLoginsTable; - ActiveLogins _activeLogins; - - ActiveLoginsState() : _activeLogins = ActiveLogins.empty(); - - @override - FutureOr build() async { - _userLoginsTable ??= await Veilid.instance.openTableDB("login_state", 1); - _activeLogins = - (await _userLoginsTable!.loadStringJson(0, "active_logins") ?? - ActiveLogins.empty()) as ActiveLogins; - _persistenceRefreshLogic(); - return _activeLogins; - } - - /// Log out of active user - Future logout() async { - // If no user is active, then logout does nothing - if (_activeLogins.activeUserLogin == null) { - return; - } - - // Remove userlogin and set the active user to logged out - final newUserLogins = _activeLogins.userLogins.removeWhere( - (ul) => _activeLogins.activeUserLogin == ul.accountMasterKey); - _activeLogins = _activeLogins.copyWith( - activeUserLogin: null, userLogins: newUserLogins); - - // Report changed state - state = AsyncValue.data(_activeLogins); - } - - /// Log all users - Future logoutAll() async { - // If no user is active, then logout does nothing - if (_activeLogins.activeUserLogin == null) { - return; - } - - // Remove all userlogins and set the active user to logged out - _activeLogins = ActiveLogins.empty(); - - // Report changed state - state = AsyncValue.data(_activeLogins); - } - - /// Log out specific user identified by its master public key - Future logoutUser(TypedKey user) async { - // Remove userlogin and set the active user to logged out - final newUserLogins = _activeLogins.userLogins - .removeWhere((ul) => user == ul.accountMasterKey); - final newActiveUserLogin = _activeLogins.activeUserLogin == user - ? null - : _activeLogins.activeUserLogin; - _activeLogins = ActiveLogins( - userLogins: newUserLogins, activeUserLogin: newActiveUserLogin); - - // Report changed state - state = AsyncValue.data(_activeLogins); - } - - /// Attempt a login and if successful make that user active - Future login(String publicKey, String password) async { - state = await AsyncValue.guard(() async { - return Future.delayed( - networkRoundTripTime, - () => _dummyUser, - ); - }); - } - - /// Internal method used to listen authentication state changes. - /// When the auth object is in a loading state, nothing happens. - /// When the auth object is in a error state, we choose to remove the token - /// Otherwise, we expect the current auth value to be reflected in our persistence API - void _persistenceRefreshLogic() { - ref.listenSelf((_, next) { - if (next.isLoading) return; - if (next.hasError) { - sharedPreferences.remove(_sharedPrefsKey); - return; - } - - final val = next.requireValue; - final isAuthenticated = val == null; - - isAuthenticated - ? sharedPreferences.remove(_sharedPrefsKey) - : sharedPreferences.setString(_sharedPrefsKey, val.publicKey); - }); - } -} - -class UnauthorizedException implements Exception { - final String message; - const UnauthorizedException(this.message); -} diff --git a/lib/state/active_logins_state.g.dart b/lib/state/active_logins_state.g.dart deleted file mode 100644 index 1eeaad2..0000000 --- a/lib/state/active_logins_state.g.dart +++ /dev/null @@ -1,25 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'active_logins_state.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$activeLoginsStateHash() => r'9b8795055e21f15f8fbf13534365725591311cf4'; - -/// See also [ActiveLoginsState]. -@ProviderFor(ActiveLoginsState) -final activeLoginsStateProvider = - AutoDisposeAsyncNotifierProvider.internal( - ActiveLoginsState.new, - name: r'activeLoginsStateProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$activeLoginsStateHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$ActiveLoginsState = AutoDisposeAsyncNotifier; -// 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/state/state.dart b/lib/state/state.dart deleted file mode 100644 index af7417d..0000000 --- a/lib/state/state.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'connection_state.dart'; -export 'active_logins_state.dart'; diff --git a/lib/tools/dht_record_encryption.dart b/lib/tools/dht_record_encryption.dart deleted file mode 100644 index 9af778a..0000000 --- a/lib/tools/dht_record_encryption.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'dart:async'; - -import 'package:veilid/veilid.dart'; -import 'dart:typed_data'; -import 'tools.dart'; - -typedef DHTRecordEncryptionFactory = DHTRecordEncryption Function(DHTRecord); - -abstract class DHTRecordEncryption { - factory DHTRecordEncryption.private(DHTRecord record) { - return _DHTRecordEncryptionPrivate(record); - } - factory DHTRecordEncryption.public(DHTRecord record) { - return _DHTRecordEncryptionPublic(record); - } - - FutureOr encrypt(Uint8List data, int subkey); - FutureOr decrypt(Uint8List data, int subkey); -} - -//////////////////////////////////// -/// Private DHT Record: Encrypted with the owner's secret key -class _DHTRecordEncryptionPrivate implements DHTRecordEncryption { - _DHTRecordEncryptionPrivate(DHTRecord record) { - // xxx derive key from record - } - - @override - FutureOr encrypt(Uint8List data, int subkey) {} - - @override - FutureOr decrypt(Uint8List data, int subkey) { - // - } -} - -//////////////////////////////////// -/// Public DHT Record: No encryption -class _DHTRecordEncryptionPublic implements DHTRecordEncryption { - _DHTRecordEncryptionPublic(DHTRecord record) { - // - } - - @override - FutureOr encrypt(Uint8List data, int subkey) { - return data; - } - - @override - FutureOr decrypt(Uint8List data, int subkey) { - return data; - } -} diff --git a/lib/tools/json_tools.dart b/lib/tools/json_tools.dart index 6d9b85a..3dba0c9 100644 --- a/lib/tools/json_tools.dart +++ b/lib/tools/json_tools.dart @@ -1,4 +1,3 @@ -import 'package:veilid/veilid.dart'; import 'dart:typed_data'; import 'dart:convert'; diff --git a/lib/tools/tools.dart b/lib/tools/tools.dart index b67d206..394aa5a 100644 --- a/lib/tools/tools.dart +++ b/lib/tools/tools.dart @@ -1,7 +1,4 @@ export 'external_stream_state.dart'; -export 'dht_record.dart'; -export 'dht_record_encryption.dart'; export 'json_tools.dart'; export 'phono_byte.dart'; export 'protobuf_tools.dart'; -export 'table_db.dart'; diff --git a/lib/tools/dht_record.dart b/lib/veilid_support/dht_record.dart similarity index 80% rename from lib/tools/dht_record.dart rename to lib/veilid_support/dht_record.dart index 5a8335f..a8f8b0e 100644 --- a/lib/tools/dht_record.dart +++ b/lib/veilid_support/dht_record.dart @@ -1,60 +1,68 @@ +import 'dart:typed_data'; + import 'package:protobuf/protobuf.dart'; import 'package:veilid/veilid.dart'; -import 'dart:typed_data'; -import 'tools.dart'; + +import 'veilid_support.dart'; +import '../tools/tools.dart'; class DHTRecord { final VeilidRoutingContext _dhtctx; final DHTRecordDescriptor _recordDescriptor; final int _defaultSubkey; final KeyPair? _writer; - late final DHTRecordEncryption _encryption; + DHTRecordCrypto _crypto; static Future create(VeilidRoutingContext dhtctx, {DHTSchema schema = const DHTSchema.dflt(oCnt: 1), int defaultSubkey = 0, - DHTRecordEncryptionFactory crypto = DHTRecordEncryption.private}) async { + DHTRecordCrypto? crypto}) async { DHTRecordDescriptor recordDescriptor = await dhtctx.createDHTRecord(schema); final rec = DHTRecord( dhtctx: dhtctx, recordDescriptor: recordDescriptor, defaultSubkey: defaultSubkey, - writer: recordDescriptor.ownerKeyPair()); - - rec._encryption = crypto(rec); + writer: recordDescriptor.ownerKeyPair(), + crypto: crypto ?? + await DHTRecordCryptoPrivate.fromTypedKeyPair( + recordDescriptor.ownerTypedKeyPair()!)); return rec; } static Future openRead( VeilidRoutingContext dhtctx, TypedKey recordKey, - {int defaultSubkey = 0, - DHTRecordEncryptionFactory crypto = DHTRecordEncryption.private}) async { + {int defaultSubkey = 0, DHTRecordCrypto? crypto}) async { DHTRecordDescriptor recordDescriptor = await dhtctx.openDHTRecord(recordKey, null); final rec = DHTRecord( dhtctx: dhtctx, recordDescriptor: recordDescriptor, defaultSubkey: defaultSubkey, - writer: null); + writer: null, + crypto: crypto ?? const DHTRecordCryptoPublic()); - rec._encryption = crypto(rec); return rec; } static Future openWrite( - VeilidRoutingContext dhtctx, TypedKey recordKey, KeyPair writer, - {int defaultSubkey = 0, - DHTRecordEncryptionFactory crypto = DHTRecordEncryption.private}) async { + VeilidRoutingContext dhtctx, + TypedKey recordKey, + KeyPair writer, { + int defaultSubkey = 0, + DHTRecordCrypto? crypto, + }) async { DHTRecordDescriptor recordDescriptor = await dhtctx.openDHTRecord(recordKey, writer); final rec = DHTRecord( dhtctx: dhtctx, recordDescriptor: recordDescriptor, defaultSubkey: defaultSubkey, - writer: writer); - rec._encryption = crypto(rec); + writer: writer, + crypto: crypto ?? + await DHTRecordCryptoPrivate.fromTypedKeyPair( + TypedKeyPair.fromKeyPair(recordKey.kind, writer))); return rec; } @@ -62,11 +70,13 @@ class DHTRecord { {required VeilidRoutingContext dhtctx, required DHTRecordDescriptor recordDescriptor, int defaultSubkey = 0, - KeyPair? writer}) + KeyPair? writer, + DHTRecordCrypto crypto = const DHTRecordCryptoPublic()}) : _dhtctx = dhtctx, _recordDescriptor = recordDescriptor, _defaultSubkey = defaultSubkey, - _writer = writer; + _writer = writer, + _crypto = crypto; int subkeyOrDefault(int subkey) => (subkey == -1) ? _defaultSubkey : subkey; @@ -86,6 +96,10 @@ class DHTRecord { return _writer; } + void setCrypto(DHTRecordCrypto crypto) { + _crypto = crypto; + } + Future close() async { await _dhtctx.closeDHTRecord(_recordDescriptor.key); } @@ -120,7 +134,7 @@ class DHTRecord { if (valueData == null) { return null; } - return _encryption.decrypt(valueData.data, subkey); + return _crypto.decrypt(valueData.data, subkey); } Future getJson(T Function(Map) fromJson, @@ -134,7 +148,7 @@ class DHTRecord { Future eventualWriteBytes(Uint8List newValue, {int subkey = -1}) async { subkey = subkeyOrDefault(subkey); - newValue = await _encryption.encrypt(newValue, subkey); + newValue = await _crypto.encrypt(newValue, subkey); // Get existing identity key ValueData? valueData; do { @@ -165,9 +179,9 @@ class DHTRecord { } // Update the data - final oldData = await _encryption.decrypt(valueData.data, subkey); + final oldData = await _crypto.decrypt(valueData.data, subkey); final updatedData = await update(oldData); - final newData = await _encryption.encrypt(updatedData, subkey); + final newData = await _crypto.encrypt(updatedData, subkey); // Set it back valueData = diff --git a/lib/veilid_support/dht_record_crypto.dart b/lib/veilid_support/dht_record_crypto.dart new file mode 100644 index 0000000..2b9ff7f --- /dev/null +++ b/lib/veilid_support/dht_record_crypto.dart @@ -0,0 +1,73 @@ +import 'dart:async'; + +import 'package:veilid/veilid.dart'; +import 'dart:typed_data'; + +abstract class DHTRecordCrypto { + FutureOr encrypt(Uint8List data, int subkey); + FutureOr decrypt(Uint8List data, int subkey); +} + +//////////////////////////////////// +/// Private DHT Record: Encrypted for a specific symmetric key +class DHTRecordCryptoPrivate implements DHTRecordCrypto { + final VeilidCryptoSystem _cryptoSystem; + final SharedSecret _secretKey; + + DHTRecordCryptoPrivate._( + VeilidCryptoSystem cryptoSystem, SharedSecret secretKey) + : _cryptoSystem = cryptoSystem, + _secretKey = secretKey; + + static Future fromTypedKeyPair( + TypedKeyPair typedKeyPair) async { + final cryptoSystem = + await Veilid.instance.getCryptoSystem(typedKeyPair.kind); + final secretKey = typedKeyPair.secret; + return DHTRecordCryptoPrivate._(cryptoSystem, secretKey); + } + + static Future fromSecret( + CryptoKind kind, SharedSecret secretKey) async { + final cryptoSystem = await Veilid.instance.getCryptoSystem(kind); + return DHTRecordCryptoPrivate._(cryptoSystem, secretKey); + } + + @override + FutureOr encrypt(Uint8List data, int subkey) async { + // generate nonce + final nonce = await _cryptoSystem.randomNonce(); + // crypt and append nonce + return (await _cryptoSystem.cryptNoAuth(data, nonce, _secretKey)) + ..addAll(nonce.decode()); + } + + @override + FutureOr decrypt(Uint8List data, int subkey) async { + // split off nonce from end + if (data.length <= Nonce.decodedLength()) { + throw const FormatException("not enough data to decrypt"); + } + final nonce = + Nonce.fromBytes(data.sublist(data.length - Nonce.decodedLength())); + final encryptedData = data.sublist(0, data.length - Nonce.decodedLength()); + // decrypt + return await _cryptoSystem.cryptNoAuth(encryptedData, nonce, _secretKey); + } +} + +//////////////////////////////////// +/// Public DHT Record: No encryption +class DHTRecordCryptoPublic implements DHTRecordCrypto { + const DHTRecordCryptoPublic(); + + @override + FutureOr encrypt(Uint8List data, int subkey) { + return data; + } + + @override + FutureOr decrypt(Uint8List data, int subkey) { + return data; + } +} diff --git a/lib/veilid_support/identity_master.dart b/lib/veilid_support/identity_master.dart new file mode 100644 index 0000000..2ab93ba --- /dev/null +++ b/lib/veilid_support/identity_master.dart @@ -0,0 +1,58 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:veilid/veilid.dart'; + +import '../entities/identity.dart'; +import 'veilid_support.dart'; + +/// Creates a new master identity and returns it with its secrets +Future newIdentityMaster() async { + final crypto = await Veilid.instance.bestCryptoSystem(); + final dhtctx = (await Veilid.instance.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 { + // 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); + }); + }); +} diff --git a/lib/veilid_support/processor.dart b/lib/veilid_support/processor.dart index 14d86a3..c306a09 100644 --- a/lib/veilid_support/processor.dart +++ b/lib/veilid_support/processor.dart @@ -3,7 +3,7 @@ import 'package:veilid/veilid.dart'; import 'config.dart'; import 'veilid_log.dart'; import '../log/log.dart'; -import '../state/state.dart'; +import '../providers/providers.dart'; class Processor { String _veilidVersion = ""; diff --git a/lib/tools/table_db.dart b/lib/veilid_support/table_db.dart similarity index 55% rename from lib/tools/table_db.dart rename to lib/veilid_support/table_db.dart index 0d646e3..f7f1873 100644 --- a/lib/tools/table_db.dart +++ b/lib/veilid_support/table_db.dart @@ -28,3 +28,25 @@ Future transactionScope( } } } + +abstract mixin class AsyncTableDBBacked { + String tableName(); + String tableKeyName(); + T reviveJson(Object? obj); + + /// Load things from storage + Future load() async { + final obj = await tableScope(tableName(), (tdb) async { + final objJson = await tdb.loadStringJson(0, tableKeyName()); + return reviveJson(objJson); + }); + return obj; + } + + /// Store things to storage + Future store(T obj) async { + await tableScope(tableName(), (tdb) async { + await tdb.storeStringJson(0, tableKeyName(), obj); + }); + } +} diff --git a/lib/veilid_support/tools.dart b/lib/veilid_support/tools.dart deleted file mode 100644 index 3959330..0000000 --- a/lib/veilid_support/tools.dart +++ /dev/null @@ -1,3 +0,0 @@ -import 'package:veilid/veilid_encoding.dart'; - -//xxx \ No newline at end of file diff --git a/lib/veilid_support/veilid_support.dart b/lib/veilid_support/veilid_support.dart index 482701b..78c1deb 100644 --- a/lib/veilid_support/veilid_support.dart +++ b/lib/veilid_support/veilid_support.dart @@ -1,5 +1,8 @@ export 'config.dart'; export 'processor.dart'; -export 'tools.dart'; export 'veilid_log.dart'; export 'init.dart'; +export 'dht_record.dart'; +export 'dht_record_crypto.dart'; +export 'table_db.dart'; +export 'identity_master.dart';