This commit is contained in:
Christien Rioux 2023-07-21 21:25:27 -04:00
parent 9d8b609844
commit bc3ed79cc2
23 changed files with 458 additions and 275 deletions

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

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

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

View 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

View file

@ -0,0 +1,3 @@
export 'local_accounts.dart';
export 'connection_state.dart';
export 'logins.dart';