This commit is contained in:
Christien Rioux 2023-08-01 00:39:50 -04:00
parent 57c366ef91
commit c35056f687
39 changed files with 1382 additions and 662 deletions

View file

@ -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(

View file

@ -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(

View file

@ -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;
// }
}

View file

@ -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

View file

@ -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

View file

@ -112,7 +112,7 @@ class FetchLocalAccountProvider
}
}
String _$localAccountsHash() => r'd6ced0ad7108c1111603235cf394faa5f6bcdae1';
String _$localAccountsHash() => r'a9a1e1765188556858ec982c9e99f780756ade1e';
/// See also [LocalAccounts].
@ProviderFor(LocalAccounts)

View file

@ -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 {

View file

@ -111,7 +111,7 @@ class FetchLoginProvider extends AutoDisposeFutureProvider<UserLogin?> {
}
}
String _$loginsHash() => r'ed9dbe91a248f662ccb0fac6edf5b1892cf2ef92';
String _$loginsHash() => r'5720eaacf858b2e1d69ebf9d2a981173a30f8592';
/// See also [Logins].
@ProviderFor(Logins)

View file

@ -1,5 +0,0 @@
export 'account.dart';
export 'connection_state.dart';
export 'local_accounts.dart';
export 'logins.dart';
export 'window_control.dart';

View 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;

View file

@ -0,0 +1,24 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'veilid_instance.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$veilidInstanceHash() => r'cca5cf288bafc4a051a1713e285f4c1d3ef4b680';
/// See also [veilidInstance].
@ProviderFor(veilidInstance)
final veilidInstanceProvider = AutoDisposeFutureProvider<Veilid>.internal(
veilidInstance,
name: r'veilidInstanceProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$veilidInstanceHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef VeilidInstanceRef = AutoDisposeFutureProviderRef<Veilid>;
// 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