mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-08-03 11:49:04 -04:00
xfer
This commit is contained in:
parent
9d8b609844
commit
bc3ed79cc2
23 changed files with 458 additions and 275 deletions
16
lib/providers/connection_state.dart
Normal file
16
lib/providers/connection_state.dart
Normal file
|
@ -0,0 +1,16 @@
|
|||
import '../tools/tools.dart';
|
||||
|
||||
enum ConnectionState {
|
||||
detached,
|
||||
detaching,
|
||||
attaching,
|
||||
attachedWeak,
|
||||
attachedGood,
|
||||
attachedStrong,
|
||||
fullyAttached,
|
||||
overAttached,
|
||||
}
|
||||
|
||||
ExternalStreamState<ConnectionState> globalConnectionState =
|
||||
ExternalStreamState<ConnectionState>(ConnectionState.detached);
|
||||
var globalConnectionStateProvider = globalConnectionState.provider();
|
177
lib/providers/local_accounts.dart
Normal file
177
lib/providers/local_accounts.dart
Normal file
|
@ -0,0 +1,177 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
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;
|
||||
|
||||
part 'local_accounts.g.dart';
|
||||
|
||||
// Local account manager
|
||||
@riverpod
|
||||
class LocalAccounts extends _$LocalAccounts
|
||||
with AsyncTableDBBacked<IList<LocalAccount>> {
|
||||
//////////////////////////////////////////////////////////////
|
||||
/// AsyncTableDBBacked
|
||||
@override
|
||||
String tableName() => "local_account_manager";
|
||||
@override
|
||||
String tableKeyName() => "local_accounts";
|
||||
@override
|
||||
IList<LocalAccount> reviveJson(Object? obj) => obj != null
|
||||
? IList<LocalAccount>.fromJson(
|
||||
obj, genericFromJson(LocalAccount.fromJson))
|
||||
: IList<LocalAccount>();
|
||||
|
||||
/// Get all local account information
|
||||
@override
|
||||
FutureOr<IList<LocalAccount>> build() async {
|
||||
return await load();
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
/// Mutators and Selectors
|
||||
|
||||
/// Creates a new master identity and returns it with its secrets
|
||||
Future<IdentityMasterWithSecrets> 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Creates a new account associated with master identity
|
||||
Future<LocalAccount> newAccount(
|
||||
IdentityMaster identityMaster,
|
||||
SecretKey identitySecret,
|
||||
EncryptionKeyType encryptionKeyType,
|
||||
String encryptionKey,
|
||||
proto.Account account) async {
|
||||
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.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: identitySecretSaltBytes,
|
||||
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.openWrite(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 newAccountRecordInfo = AccountRecordInfo(
|
||||
key: accountRec.key(), owner: accountRec.ownerKeyPair()!);
|
||||
|
||||
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
|
||||
final newLocalAccounts = localAccounts.add(localAccount);
|
||||
await store(newLocalAccounts);
|
||||
state = AsyncValue.data(newLocalAccounts);
|
||||
|
||||
// Return local account object
|
||||
return localAccount;
|
||||
}
|
||||
|
||||
/// Import an account from another VeilidChat instance
|
||||
|
||||
/// Recover an account with the master identity secret
|
||||
}
|
25
lib/providers/local_accounts.g.dart
Normal file
25
lib/providers/local_accounts.g.dart
Normal file
|
@ -0,0 +1,25 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'local_accounts.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$localAccountsHash() => r'41f70b78e71c8b3faa2cd1f2a96084fbb373324c';
|
||||
|
||||
/// See also [LocalAccounts].
|
||||
@ProviderFor(LocalAccounts)
|
||||
final localAccountsProvider = AutoDisposeAsyncNotifierProvider<LocalAccounts,
|
||||
IList<LocalAccount>>.internal(
|
||||
LocalAccounts.new,
|
||||
name: r'localAccountsProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$localAccountsHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$LocalAccounts = AutoDisposeAsyncNotifier<IList<LocalAccount>>;
|
||||
// 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
|
170
lib/providers/logins.dart
Normal file
170
lib/providers/logins.dart
Normal file
|
@ -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<ActiveLogins> {
|
||||
//////////////////////////////////////////////////////////////
|
||||
/// AsyncTableDBBacked
|
||||
@override
|
||||
String tableName() => "local_account_manager";
|
||||
@override
|
||||
String tableKeyName() => "active_logins";
|
||||
@override
|
||||
ActiveLogins reviveJson(Object? obj) => obj != null
|
||||
? ActiveLogins.fromJson(obj as Map<String, dynamic>)
|
||||
: ActiveLogins.empty();
|
||||
|
||||
/// Get all local account information
|
||||
@override
|
||||
FutureOr<ActiveLogins> build() async {
|
||||
return await load();
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
/// Mutators and Selectors
|
||||
|
||||
Future<void> 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<bool> 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<bool> 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<void> 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<void> 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);
|
||||
}
|
||||
}
|
24
lib/providers/logins.g.dart
Normal file
24
lib/providers/logins.g.dart
Normal file
|
@ -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<Logins, ActiveLogins>.internal(
|
||||
Logins.new,
|
||||
name: r'loginsProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$loginsHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$Logins = AutoDisposeAsyncNotifier<ActiveLogins>;
|
||||
// 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
|
3
lib/providers/providers.dart
Normal file
3
lib/providers/providers.dart
Normal file
|
@ -0,0 +1,3 @@
|
|||
export 'local_accounts.dart';
|
||||
export 'connection_state.dart';
|
||||
export 'logins.dart';
|
Loading…
Add table
Add a link
Reference in a new issue