veilidchat/lib/account_manager/repository/account_repository.dart

393 lines
14 KiB
Dart
Raw Normal View History

2024-01-08 21:37:08 -05:00
import 'dart:async';
2023-12-26 20:26:54 -05:00
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
2023-12-27 22:56:24 -05:00
import 'package:veilid_support/veilid_support.dart';
2023-12-26 20:26:54 -05:00
2024-06-11 21:27:20 -04:00
import '../../../proto/proto.dart' as proto;
import '../../tools/tools.dart';
import '../models/models.dart';
2023-12-26 20:26:54 -05:00
const String veilidChatAccountKey = 'com.veilid.veilidchat';
2024-02-13 22:03:26 -05:00
enum AccountRepositoryChange { localAccounts, userLogins, activeLocalAccount }
2023-12-26 20:26:54 -05:00
class AccountRepository {
AccountRepository._()
: _localAccounts = _initLocalAccounts(),
2024-02-13 22:03:26 -05:00
_userLogins = _initUserLogins(),
_activeLocalAccount = _initActiveAccount(),
2024-01-08 21:37:08 -05:00
_streamController =
StreamController<AccountRepositoryChange>.broadcast();
2023-12-26 20:26:54 -05:00
static TableDBValue<IList<LocalAccount>> _initLocalAccounts() => TableDBValue(
tableName: 'local_account_manager',
tableKeyName: 'local_accounts',
valueFromJson: (obj) => obj != null
? IList<LocalAccount>.fromJson(
obj, genericFromJson(LocalAccount.fromJson))
: IList<LocalAccount>(),
2024-04-17 21:31:26 -04:00
valueToJson: (val) => val?.toJson((la) => la.toJson()),
makeInitialValue: IList<LocalAccount>.empty);
2023-12-26 20:26:54 -05:00
2024-02-13 22:03:26 -05:00
static TableDBValue<IList<UserLogin>> _initUserLogins() => TableDBValue(
2023-12-26 20:26:54 -05:00
tableName: 'local_account_manager',
2024-02-13 22:03:26 -05:00
tableKeyName: 'user_logins',
2023-12-26 20:26:54 -05:00
valueFromJson: (obj) => obj != null
2024-02-13 22:03:26 -05:00
? IList<UserLogin>.fromJson(obj, genericFromJson(UserLogin.fromJson))
: IList<UserLogin>(),
2024-04-17 21:31:26 -04:00
valueToJson: (val) => val?.toJson((la) => la.toJson()),
makeInitialValue: IList<UserLogin>.empty);
2024-02-13 22:03:26 -05:00
static TableDBValue<TypedKey?> _initActiveAccount() => TableDBValue(
tableName: 'local_account_manager',
tableKeyName: 'active_local_account',
valueFromJson: (obj) => obj == null ? null : TypedKey.fromJson(obj),
2024-04-17 21:31:26 -04:00
valueToJson: (val) => val?.toJson(),
makeInitialValue: () => null);
2023-12-26 20:26:54 -05:00
Future<void> init() async {
2024-02-14 21:33:15 -05:00
await _localAccounts.get();
await _userLogins.get();
await _activeLocalAccount.get();
2023-12-26 20:26:54 -05:00
}
2024-02-25 10:03:41 -05:00
Future<void> close() async {
2024-04-17 21:31:26 -04:00
await _localAccounts.close();
await _userLogins.close();
await _activeLocalAccount.close();
2024-02-25 10:03:41 -05:00
}
2023-12-26 20:26:54 -05:00
//////////////////////////////////////////////////////////////
/// Public Interface
///
2024-01-08 21:37:08 -05:00
Stream<AccountRepositoryChange> get stream => _streamController.stream;
2023-12-26 20:26:54 -05:00
2024-04-17 21:31:26 -04:00
IList<LocalAccount> getLocalAccounts() => _localAccounts.value;
TypedKey? getActiveLocalAccount() => _activeLocalAccount.value;
IList<UserLogin> getUserLogins() => _userLogins.value;
2024-02-13 22:03:26 -05:00
UserLogin? getActiveUserLogin() {
2024-04-17 21:31:26 -04:00
final activeLocalAccount = _activeLocalAccount.value;
2024-02-13 22:03:26 -05:00
return activeLocalAccount == null
? null
: fetchUserLogin(activeLocalAccount);
}
2023-12-26 20:26:54 -05:00
2024-06-07 14:42:04 -04:00
LocalAccount? fetchLocalAccount(TypedKey accountSuperIdentityRecordKey) {
2024-04-17 21:31:26 -04:00
final localAccounts = _localAccounts.value;
2023-12-26 20:26:54 -05:00
final idx = localAccounts.indexWhere(
2024-06-07 14:42:04 -04:00
(e) => e.superIdentity.recordKey == accountSuperIdentityRecordKey);
2023-12-26 20:26:54 -05:00
if (idx == -1) {
return null;
}
return localAccounts[idx];
}
2024-06-07 14:42:04 -04:00
UserLogin? fetchUserLogin(TypedKey superIdentityRecordKey) {
2024-04-17 21:31:26 -04:00
final userLogins = _userLogins.value;
2023-12-26 20:26:54 -05:00
final idx = userLogins
2024-06-07 14:42:04 -04:00
.indexWhere((e) => e.superIdentityRecordKey == superIdentityRecordKey);
2023-12-26 20:26:54 -05:00
if (idx == -1) {
return null;
}
return userLogins[idx];
}
AccountInfo? getAccountInfo(TypedKey superIdentityRecordKey) {
2024-01-08 21:37:08 -05:00
// Get which local account we want to fetch the profile for
2024-06-07 14:42:04 -04:00
final localAccount = fetchLocalAccount(superIdentityRecordKey);
2024-01-08 21:37:08 -05:00
if (localAccount == null) {
return null;
2024-01-08 21:37:08 -05:00
}
// See if we've logged into this account or if it is locked
2024-06-07 14:42:04 -04:00
final userLogin = fetchUserLogin(superIdentityRecordKey);
2024-01-18 19:44:15 -05:00
if (userLogin == null) {
2024-01-08 21:37:08 -05:00
// Account was locked
return AccountInfo(
status: AccountInfoStatus.accountLocked,
localAccount: localAccount,
userLogin: null,
);
2024-01-08 21:37:08 -05:00
}
// Got account, decrypted and decoded
return AccountInfo(
2024-06-16 22:12:24 -04:00
status: AccountInfoStatus.accountUnlocked,
localAccount: localAccount,
userLogin: userLogin,
2024-01-08 21:37:08 -05:00
);
}
2023-12-26 20:26:54 -05:00
/// Reorder accounts
Future<void> reorderAccount(int oldIndex, int newIndex) async {
final localAccounts = await _localAccounts.get();
final removedItem = Output<LocalAccount>();
final updated = localAccounts
.removeAt(oldIndex, removedItem)
.insert(newIndex, removedItem.value!);
await _localAccounts.set(updated);
2024-01-08 21:37:08 -05:00
_streamController.add(AccountRepositoryChange.localAccounts);
2023-12-26 20:26:54 -05:00
}
2024-06-07 14:42:04 -04:00
/// Creates a new super identity, an identity instance, an account associated
/// with the identity instance, stores the account in the identity key and
/// then logs into that account with no password set at this time
Future<SecretKey> createWithNewSuperIdentity(proto.Profile newProfile) async {
2024-06-07 14:42:04 -04:00
log.debug('Creating super identity');
final wsi = await WritableSuperIdentity.create();
2023-12-27 22:56:24 -05:00
try {
final localAccount = await _newLocalAccount(
2024-06-07 14:42:04 -04:00
superIdentity: wsi.superIdentity,
identitySecret: wsi.identitySecret,
newProfile: newProfile);
2023-12-27 22:56:24 -05:00
// Log in the new account by default with no pin
2024-06-07 14:42:04 -04:00
final ok = await login(
localAccount.superIdentity.recordKey, EncryptionKeyType.none, '');
2023-12-27 22:56:24 -05:00
assert(ok, 'login with none should never fail');
2024-06-10 10:04:03 -04:00
return wsi.superSecret;
2023-12-27 22:56:24 -05:00
} on Exception catch (_) {
2024-06-07 14:42:04 -04:00
await wsi.delete();
2023-12-27 22:56:24 -05:00
rethrow;
}
}
Future<void> editAccountProfile(
TypedKey superIdentityRecordKey, proto.Profile newProfile) async {
log.debug('Editing profile for $superIdentityRecordKey');
final localAccounts = await _localAccounts.get();
final newLocalAccounts = localAccounts.replaceFirstWhere(
(x) => x.superIdentity.recordKey == superIdentityRecordKey,
(localAccount) => localAccount!.copyWith(name: newProfile.name));
await _localAccounts.set(newLocalAccounts);
_streamController.add(AccountRepositoryChange.localAccounts);
}
/// Remove an account and wipe the messages for this account from this device
Future<bool> deleteLocalAccount(TypedKey superIdentityRecordKey) async {
await logout(superIdentityRecordKey);
final localAccounts = await _localAccounts.get();
final newLocalAccounts = localAccounts.removeWhere(
(la) => la.superIdentity.recordKey == superIdentityRecordKey);
await _localAccounts.set(newLocalAccounts);
_streamController.add(AccountRepositoryChange.localAccounts);
// TO DO: wipe messages
return true;
}
/// Import an account from another VeilidChat instance
/// Recover an account with the master identity secret
/// Delete an account from all devices
Future<void> switchToAccount(TypedKey? superIdentityRecordKey) async {
final activeLocalAccount = await _activeLocalAccount.get();
if (activeLocalAccount == superIdentityRecordKey) {
// Nothing to do
return;
}
if (superIdentityRecordKey != null) {
// Assert the specified record key can be found, will throw if not
final _ = _userLogins.value.firstWhere(
(ul) => ul.superIdentityRecordKey == superIdentityRecordKey);
}
await _activeLocalAccount.set(superIdentityRecordKey);
_streamController.add(AccountRepositoryChange.activeLocalAccount);
}
//////////////////////////////////////////////////////////////
/// Internal Implementation
2024-06-07 14:42:04 -04:00
/// Creates a new Account associated with the current instance of the identity
2023-12-26 20:26:54 -05:00
/// Adds a logged-out LocalAccount to track its existence on this device
2023-12-27 22:56:24 -05:00
Future<LocalAccount> _newLocalAccount(
2024-06-07 14:42:04 -04:00
{required SuperIdentity superIdentity,
2023-12-26 20:26:54 -05:00
required SecretKey identitySecret,
required proto.Profile newProfile,
2023-12-26 20:26:54 -05:00
EncryptionKeyType encryptionKeyType = EncryptionKeyType.none,
String encryptionKey = ''}) async {
2024-03-01 16:25:56 -05:00
log.debug('Creating new local account');
2023-12-26 20:26:54 -05:00
final localAccounts = await _localAccounts.get();
// Add account with profile to DHT
2024-06-07 14:42:04 -04:00
await superIdentity.currentInstance.addAccount(
superRecordKey: superIdentity.recordKey,
secretKey: identitySecret,
2023-12-26 20:26:54 -05:00
accountKey: veilidChatAccountKey,
createAccountCallback: (parent) async {
// Make empty contact list
2024-03-01 16:25:56 -05:00
log.debug('Creating contacts list');
final contactList = await (await DHTShortArray.create(
debugName: 'AccountRepository::_newLocalAccount::Contacts',
parent: parent))
2024-03-24 12:13:27 -04:00
.scope((r) async => r.recordPointer);
2023-12-26 20:26:54 -05:00
// Make empty contact invitation record list
2024-03-01 16:25:56 -05:00
log.debug('Creating contact invitation records list');
final contactInvitationRecords = await (await DHTShortArray.create(
debugName:
'AccountRepository::_newLocalAccount::ContactInvitations',
parent: parent))
.scope((r) async => r.recordPointer);
2023-12-26 20:26:54 -05:00
// Make empty chat record list
2024-03-01 16:25:56 -05:00
log.debug('Creating chat records list');
final chatRecords = await (await DHTShortArray.create(
debugName: 'AccountRepository::_newLocalAccount::Chats',
parent: parent))
2024-03-24 12:13:27 -04:00
.scope((r) async => r.recordPointer);
2023-12-26 20:26:54 -05:00
// Make account object
final account = proto.Account()
..profile = newProfile
2023-12-26 20:26:54 -05:00
..contactList = contactList.toProto()
..contactInvitationRecords = contactInvitationRecords.toProto()
..chatList = chatRecords.toProto();
2024-06-07 14:42:04 -04:00
return account.writeToBuffer();
2023-12-26 20:26:54 -05:00
});
// Encrypt identitySecret with key
2023-12-27 22:56:24 -05:00
final identitySecretBytes = await encryptionKeyType.encryptSecretToBytes(
secret: identitySecret,
2024-06-07 14:42:04 -04:00
cryptoKind: superIdentity.currentInstance.recordKey.kind,
2023-12-27 22:56:24 -05:00
encryptionKey: encryptionKey,
);
2023-12-26 20:26:54 -05:00
// 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(
2024-06-07 14:42:04 -04:00
superIdentity: superIdentity,
2023-12-26 20:26:54 -05:00
identitySecretBytes: identitySecretBytes,
encryptionKeyType: encryptionKeyType,
biometricsEnabled: false,
hiddenAccount: false,
name: newProfile.name,
2023-12-26 20:26:54 -05:00
);
// Add local account object to internal store
final newLocalAccounts = localAccounts.add(localAccount);
await _localAccounts.set(newLocalAccounts);
2024-01-08 21:37:08 -05:00
_streamController.add(AccountRepositoryChange.localAccounts);
2023-12-26 20:26:54 -05:00
// Return local account object
return localAccount;
}
Future<bool> _decryptedLogin(
2024-06-07 14:42:04 -04:00
SuperIdentity superIdentity, SecretKey identitySecret) async {
2024-02-16 11:46:42 -05:00
// Verify identity secret works and return the valid cryptosystem
2024-06-07 14:42:04 -04:00
final cs = await superIdentity.currentInstance
.validateIdentitySecret(identitySecret);
2023-12-26 20:26:54 -05:00
// Read the identity key to get the account keys
2024-06-07 14:42:04 -04:00
final accountRecordInfoList = await superIdentity.currentInstance
.readAccount(
superRecordKey: superIdentity.recordKey,
secretKey: identitySecret,
accountKey: veilidChatAccountKey);
2024-02-16 11:46:42 -05:00
if (accountRecordInfoList.length > 1) {
throw IdentityException.limitExceeded;
} else if (accountRecordInfoList.isEmpty) {
throw IdentityException.noAccount;
}
final accountRecordInfo = accountRecordInfoList.single;
2023-12-26 20:26:54 -05:00
// Add to user logins and select it
2024-02-13 22:03:26 -05:00
final userLogins = await _userLogins.get();
2023-12-26 20:26:54 -05:00
final now = Veilid.instance.now();
2024-02-13 22:03:26 -05:00
final newUserLogins = userLogins.replaceFirstWhere(
2024-06-07 14:42:04 -04:00
(ul) => ul.superIdentityRecordKey == superIdentity.recordKey,
2024-02-13 22:03:26 -05:00
(ul) => ul != null
? ul.copyWith(lastActive: now)
: UserLogin(
2024-06-07 14:42:04 -04:00
superIdentityRecordKey: superIdentity.recordKey,
2024-02-13 22:03:26 -05:00
identitySecret:
TypedSecret(kind: cs.kind(), value: identitySecret),
accountRecordInfo: accountRecordInfo,
lastActive: now),
addIfNotFound: true);
await _userLogins.set(newUserLogins);
2024-06-07 14:42:04 -04:00
await _activeLocalAccount.set(superIdentity.recordKey);
2024-01-08 21:37:08 -05:00
2024-03-05 15:29:02 -05:00
_streamController
..add(AccountRepositoryChange.userLogins)
..add(AccountRepositoryChange.activeLocalAccount);
2023-12-26 20:26:54 -05:00
return true;
}
2024-06-07 14:42:04 -04:00
Future<bool> login(TypedKey accountSuperRecordKey,
2023-12-26 20:26:54 -05:00
EncryptionKeyType encryptionKeyType, String encryptionKey) async {
final localAccounts = await _localAccounts.get();
// Get account, throws if not found
final localAccount = localAccounts.firstWhere(
2024-06-07 14:42:04 -04:00
(la) => la.superIdentity.recordKey == accountSuperRecordKey);
2023-12-26 20:26:54 -05:00
// Log in with this local account
// Derive key from password
if (localAccount.encryptionKeyType != encryptionKeyType) {
throw Exception('Wrong authentication type');
}
2023-12-27 22:56:24 -05:00
final identitySecret =
await localAccount.encryptionKeyType.decryptSecretFromBytes(
2023-12-26 20:26:54 -05:00
secretBytes: localAccount.identitySecretBytes,
2024-06-07 14:42:04 -04:00
cryptoKind: localAccount.superIdentity.currentInstance.recordKey.kind,
2023-12-26 20:26:54 -05:00
encryptionKey: encryptionKey,
);
// Validate this secret with the identity public key and log in
2024-06-07 14:42:04 -04:00
return _decryptedLogin(localAccount.superIdentity, identitySecret);
2023-12-26 20:26:54 -05:00
}
Future<void> logout(TypedKey? accountMasterRecordKey) async {
2024-01-08 21:37:08 -05:00
// Resolve which user to log out
2024-02-13 22:03:26 -05:00
final activeLocalAccount = await _activeLocalAccount.get();
final logoutUser = accountMasterRecordKey ?? activeLocalAccount;
2023-12-26 20:26:54 -05:00
if (logoutUser == null) {
2024-01-08 21:37:08 -05:00
log.error('missing user in logout: $accountMasterRecordKey');
2023-12-26 20:26:54 -05:00
return;
}
2024-01-08 21:37:08 -05:00
2024-02-13 22:03:26 -05:00
final logoutUserLogin = fetchUserLogin(logoutUser);
if (logoutUserLogin == null) {
// Already logged out
return;
2024-01-08 21:37:08 -05:00
}
// Remove user from active logins list
2024-02-13 22:03:26 -05:00
final newUserLogins = (await _userLogins.get())
2024-06-07 14:42:04 -04:00
.removeWhere((ul) => ul.superIdentityRecordKey == logoutUser);
2024-02-13 22:03:26 -05:00
await _userLogins.set(newUserLogins);
2024-01-08 21:37:08 -05:00
_streamController.add(AccountRepositoryChange.userLogins);
}
//////////////////////////////////////////////////////////////
/// Fields
2024-02-25 10:03:41 -05:00
static AccountRepository instance = AccountRepository._();
2024-02-25 10:03:41 -05:00
final TableDBValue<IList<LocalAccount>> _localAccounts;
final TableDBValue<IList<UserLogin>> _userLogins;
final TableDBValue<TypedKey?> _activeLocalAccount;
final StreamController<AccountRepositoryChange> _streamController;
2023-12-26 20:26:54 -05:00
}