big identity refactor

This commit is contained in:
Christien Rioux 2024-06-07 14:42:04 -04:00
parent bae58d5f5c
commit ddc02f6771
46 changed files with 2143 additions and 1300 deletions

View file

@ -14,14 +14,18 @@ class ActiveAccountInfo {
});
//
TypedKey get superIdentityRecordKey => localAccount.superIdentity.recordKey;
TypedKey get accountRecordKey =>
userLogin.accountRecordInfo.accountRecord.recordKey;
KeyPair get conversationWriter {
final identityKey = localAccount.identityMaster.identityPublicKey;
final identitySecret = userLogin.identitySecret;
return KeyPair(key: identityKey, secret: identitySecret.value);
}
TypedKey get identityTypedPublicKey =>
localAccount.superIdentity.currentInstance.typedPublicKey;
PublicKey get identityPublicKey =>
localAccount.superIdentity.currentInstance.publicKey;
SecretKey get identitySecretKey => userLogin.identitySecret.value;
KeyPair get identityWriter =>
KeyPair(key: identityPublicKey, secret: identitySecretKey);
Future<VeilidCryptoSystem> get identityCryptoSystem =>
localAccount.superIdentity.currentInstance.cryptoSystem;
Future<VeilidCrypto> makeConversationCrypto(
TypedKey remoteIdentityPublicKey) async {

View file

@ -11,25 +11,31 @@ part 'local_account.freezed.dart';
// Local Accounts are stored in a table locally and not backed by a DHT key
// and represents the accounts that have been added/imported
// on the current device.
// Stores a copy of the IdentityMaster associated with the account
// Stores a copy of the most recent SuperIdentity associated with the account
// and the identitySecretKey optionally encrypted by an unlock code
// This is the root of the account information tree for VeilidChat
//
@freezed
class LocalAccount with _$LocalAccount {
const factory LocalAccount({
// The master key record for the account, containing the identityPublicKey
required IdentityMaster identityMaster,
// The encrypted identity secret that goes with
// The super identity key record for the account,
// containing the publicKey in the currentIdentity
required SuperIdentity superIdentity,
// The encrypted currentIdentity secret that goes with
// the identityPublicKey with appended salt
@Uint8ListJsonConverter() required Uint8List identitySecretBytes,
// The kind of encryption input used on the account
required EncryptionKeyType encryptionKeyType,
// If account is not hidden, password can be retrieved via
required bool biometricsEnabled,
// Keep account hidden unless account password is entered
// (tries all hidden accounts with auth method (no biometrics))
required bool hiddenAccount,
// Display name for account until it is unlocked
required String name,
}) = _LocalAccount;

View file

@ -20,9 +20,10 @@ LocalAccount _$LocalAccountFromJson(Map<String, dynamic> json) {
/// @nodoc
mixin _$LocalAccount {
// The master key record for the account, containing the identityPublicKey
IdentityMaster get identityMaster =>
throw _privateConstructorUsedError; // The encrypted identity secret that goes with
// The super identity key record for the account,
// containing the publicKey in the currentIdentity
SuperIdentity get superIdentity =>
throw _privateConstructorUsedError; // The encrypted currentIdentity secret that goes with
// the identityPublicKey with appended salt
@Uint8ListJsonConverter()
Uint8List get identitySecretBytes =>
@ -49,14 +50,14 @@ abstract class $LocalAccountCopyWith<$Res> {
_$LocalAccountCopyWithImpl<$Res, LocalAccount>;
@useResult
$Res call(
{IdentityMaster identityMaster,
{SuperIdentity superIdentity,
@Uint8ListJsonConverter() Uint8List identitySecretBytes,
EncryptionKeyType encryptionKeyType,
bool biometricsEnabled,
bool hiddenAccount,
String name});
$IdentityMasterCopyWith<$Res> get identityMaster;
$SuperIdentityCopyWith<$Res> get superIdentity;
}
/// @nodoc
@ -72,7 +73,7 @@ class _$LocalAccountCopyWithImpl<$Res, $Val extends LocalAccount>
@pragma('vm:prefer-inline')
@override
$Res call({
Object? identityMaster = null,
Object? superIdentity = null,
Object? identitySecretBytes = null,
Object? encryptionKeyType = null,
Object? biometricsEnabled = null,
@ -80,10 +81,10 @@ class _$LocalAccountCopyWithImpl<$Res, $Val extends LocalAccount>
Object? name = null,
}) {
return _then(_value.copyWith(
identityMaster: null == identityMaster
? _value.identityMaster
: identityMaster // ignore: cast_nullable_to_non_nullable
as IdentityMaster,
superIdentity: null == superIdentity
? _value.superIdentity
: superIdentity // ignore: cast_nullable_to_non_nullable
as SuperIdentity,
identitySecretBytes: null == identitySecretBytes
? _value.identitySecretBytes
: identitySecretBytes // ignore: cast_nullable_to_non_nullable
@ -109,9 +110,9 @@ class _$LocalAccountCopyWithImpl<$Res, $Val extends LocalAccount>
@override
@pragma('vm:prefer-inline')
$IdentityMasterCopyWith<$Res> get identityMaster {
return $IdentityMasterCopyWith<$Res>(_value.identityMaster, (value) {
return _then(_value.copyWith(identityMaster: value) as $Val);
$SuperIdentityCopyWith<$Res> get superIdentity {
return $SuperIdentityCopyWith<$Res>(_value.superIdentity, (value) {
return _then(_value.copyWith(superIdentity: value) as $Val);
});
}
}
@ -125,7 +126,7 @@ abstract class _$$LocalAccountImplCopyWith<$Res>
@override
@useResult
$Res call(
{IdentityMaster identityMaster,
{SuperIdentity superIdentity,
@Uint8ListJsonConverter() Uint8List identitySecretBytes,
EncryptionKeyType encryptionKeyType,
bool biometricsEnabled,
@ -133,7 +134,7 @@ abstract class _$$LocalAccountImplCopyWith<$Res>
String name});
@override
$IdentityMasterCopyWith<$Res> get identityMaster;
$SuperIdentityCopyWith<$Res> get superIdentity;
}
/// @nodoc
@ -147,7 +148,7 @@ class __$$LocalAccountImplCopyWithImpl<$Res>
@pragma('vm:prefer-inline')
@override
$Res call({
Object? identityMaster = null,
Object? superIdentity = null,
Object? identitySecretBytes = null,
Object? encryptionKeyType = null,
Object? biometricsEnabled = null,
@ -155,10 +156,10 @@ class __$$LocalAccountImplCopyWithImpl<$Res>
Object? name = null,
}) {
return _then(_$LocalAccountImpl(
identityMaster: null == identityMaster
? _value.identityMaster
: identityMaster // ignore: cast_nullable_to_non_nullable
as IdentityMaster,
superIdentity: null == superIdentity
? _value.superIdentity
: superIdentity // ignore: cast_nullable_to_non_nullable
as SuperIdentity,
identitySecretBytes: null == identitySecretBytes
? _value.identitySecretBytes
: identitySecretBytes // ignore: cast_nullable_to_non_nullable
@ -187,7 +188,7 @@ class __$$LocalAccountImplCopyWithImpl<$Res>
@JsonSerializable()
class _$LocalAccountImpl implements _LocalAccount {
const _$LocalAccountImpl(
{required this.identityMaster,
{required this.superIdentity,
@Uint8ListJsonConverter() required this.identitySecretBytes,
required this.encryptionKeyType,
required this.biometricsEnabled,
@ -197,10 +198,11 @@ class _$LocalAccountImpl implements _LocalAccount {
factory _$LocalAccountImpl.fromJson(Map<String, dynamic> json) =>
_$$LocalAccountImplFromJson(json);
// The master key record for the account, containing the identityPublicKey
// The super identity key record for the account,
// containing the publicKey in the currentIdentity
@override
final IdentityMaster identityMaster;
// The encrypted identity secret that goes with
final SuperIdentity superIdentity;
// The encrypted currentIdentity secret that goes with
// the identityPublicKey with appended salt
@override
@Uint8ListJsonConverter()
@ -221,7 +223,7 @@ class _$LocalAccountImpl implements _LocalAccount {
@override
String toString() {
return 'LocalAccount(identityMaster: $identityMaster, identitySecretBytes: $identitySecretBytes, encryptionKeyType: $encryptionKeyType, biometricsEnabled: $biometricsEnabled, hiddenAccount: $hiddenAccount, name: $name)';
return 'LocalAccount(superIdentity: $superIdentity, identitySecretBytes: $identitySecretBytes, encryptionKeyType: $encryptionKeyType, biometricsEnabled: $biometricsEnabled, hiddenAccount: $hiddenAccount, name: $name)';
}
@override
@ -229,8 +231,8 @@ class _$LocalAccountImpl implements _LocalAccount {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$LocalAccountImpl &&
(identical(other.identityMaster, identityMaster) ||
other.identityMaster == identityMaster) &&
(identical(other.superIdentity, superIdentity) ||
other.superIdentity == superIdentity) &&
const DeepCollectionEquality()
.equals(other.identitySecretBytes, identitySecretBytes) &&
(identical(other.encryptionKeyType, encryptionKeyType) ||
@ -246,7 +248,7 @@ class _$LocalAccountImpl implements _LocalAccount {
@override
int get hashCode => Object.hash(
runtimeType,
identityMaster,
superIdentity,
const DeepCollectionEquality().hash(identitySecretBytes),
encryptionKeyType,
biometricsEnabled,
@ -269,7 +271,7 @@ class _$LocalAccountImpl implements _LocalAccount {
abstract class _LocalAccount implements LocalAccount {
const factory _LocalAccount(
{required final IdentityMaster identityMaster,
{required final SuperIdentity superIdentity,
@Uint8ListJsonConverter() required final Uint8List identitySecretBytes,
required final EncryptionKeyType encryptionKeyType,
required final bool biometricsEnabled,
@ -279,9 +281,10 @@ abstract class _LocalAccount implements LocalAccount {
factory _LocalAccount.fromJson(Map<String, dynamic> json) =
_$LocalAccountImpl.fromJson;
@override // The master key record for the account, containing the identityPublicKey
IdentityMaster get identityMaster;
@override // The encrypted identity secret that goes with
@override // The super identity key record for the account,
// containing the publicKey in the currentIdentity
SuperIdentity get superIdentity;
@override // The encrypted currentIdentity secret that goes with
// the identityPublicKey with appended salt
@Uint8ListJsonConverter()
Uint8List get identitySecretBytes;

View file

@ -8,7 +8,7 @@ part of 'local_account.dart';
_$LocalAccountImpl _$$LocalAccountImplFromJson(Map<String, dynamic> json) =>
_$LocalAccountImpl(
identityMaster: IdentityMaster.fromJson(json['identity_master']),
superIdentity: SuperIdentity.fromJson(json['super_identity']),
identitySecretBytes: const Uint8ListJsonConverter()
.fromJson(json['identity_secret_bytes']),
encryptionKeyType:
@ -20,7 +20,7 @@ _$LocalAccountImpl _$$LocalAccountImplFromJson(Map<String, dynamic> json) =>
Map<String, dynamic> _$$LocalAccountImplToJson(_$LocalAccountImpl instance) =>
<String, dynamic>{
'identity_master': instance.identityMaster.toJson(),
'super_identity': instance.superIdentity.toJson(),
'identity_secret_bytes':
const Uint8ListJsonConverter().toJson(instance.identitySecretBytes),
'encryption_key_type': instance.encryptionKeyType.toJson(),

View file

@ -7,12 +7,13 @@ part 'user_login.g.dart';
// Represents a currently logged in account
// User logins are stored in the user_logins tablestore table
// indexed by the accountMasterKey
// indexed by the accountSuperIdentityRecordKey
@freezed
class UserLogin with _$UserLogin {
const factory UserLogin({
// Master record key for the user used to index the local accounts table
required TypedKey accountMasterRecordKey,
// SuperIdentity record key for the user
// used to index the local accounts table
required TypedKey superIdentityRecordKey,
// The identity secret as unlocked from the local accounts table
required TypedSecret identitySecret,
// The account record key, owner key and secret pulled from the identity

View file

@ -20,8 +20,9 @@ UserLogin _$UserLoginFromJson(Map<String, dynamic> json) {
/// @nodoc
mixin _$UserLogin {
// Master record key for the user used to index the local accounts table
Typed<FixedEncodedString43> get accountMasterRecordKey =>
// SuperIdentity record key for the user
// used to index the local accounts table
Typed<FixedEncodedString43> get superIdentityRecordKey =>
throw _privateConstructorUsedError; // The identity secret as unlocked from the local accounts table
Typed<FixedEncodedString43> get identitySecret =>
throw _privateConstructorUsedError; // The account record key, owner key and secret pulled from the identity
@ -41,7 +42,7 @@ abstract class $UserLoginCopyWith<$Res> {
_$UserLoginCopyWithImpl<$Res, UserLogin>;
@useResult
$Res call(
{Typed<FixedEncodedString43> accountMasterRecordKey,
{Typed<FixedEncodedString43> superIdentityRecordKey,
Typed<FixedEncodedString43> identitySecret,
AccountRecordInfo accountRecordInfo,
Timestamp lastActive});
@ -62,15 +63,15 @@ class _$UserLoginCopyWithImpl<$Res, $Val extends UserLogin>
@pragma('vm:prefer-inline')
@override
$Res call({
Object? accountMasterRecordKey = null,
Object? superIdentityRecordKey = null,
Object? identitySecret = null,
Object? accountRecordInfo = null,
Object? lastActive = null,
}) {
return _then(_value.copyWith(
accountMasterRecordKey: null == accountMasterRecordKey
? _value.accountMasterRecordKey
: accountMasterRecordKey // ignore: cast_nullable_to_non_nullable
superIdentityRecordKey: null == superIdentityRecordKey
? _value.superIdentityRecordKey
: superIdentityRecordKey // ignore: cast_nullable_to_non_nullable
as Typed<FixedEncodedString43>,
identitySecret: null == identitySecret
? _value.identitySecret
@ -105,7 +106,7 @@ abstract class _$$UserLoginImplCopyWith<$Res>
@override
@useResult
$Res call(
{Typed<FixedEncodedString43> accountMasterRecordKey,
{Typed<FixedEncodedString43> superIdentityRecordKey,
Typed<FixedEncodedString43> identitySecret,
AccountRecordInfo accountRecordInfo,
Timestamp lastActive});
@ -125,15 +126,15 @@ class __$$UserLoginImplCopyWithImpl<$Res>
@pragma('vm:prefer-inline')
@override
$Res call({
Object? accountMasterRecordKey = null,
Object? superIdentityRecordKey = null,
Object? identitySecret = null,
Object? accountRecordInfo = null,
Object? lastActive = null,
}) {
return _then(_$UserLoginImpl(
accountMasterRecordKey: null == accountMasterRecordKey
? _value.accountMasterRecordKey
: accountMasterRecordKey // ignore: cast_nullable_to_non_nullable
superIdentityRecordKey: null == superIdentityRecordKey
? _value.superIdentityRecordKey
: superIdentityRecordKey // ignore: cast_nullable_to_non_nullable
as Typed<FixedEncodedString43>,
identitySecret: null == identitySecret
? _value.identitySecret
@ -155,7 +156,7 @@ class __$$UserLoginImplCopyWithImpl<$Res>
@JsonSerializable()
class _$UserLoginImpl implements _UserLogin {
const _$UserLoginImpl(
{required this.accountMasterRecordKey,
{required this.superIdentityRecordKey,
required this.identitySecret,
required this.accountRecordInfo,
required this.lastActive});
@ -163,9 +164,10 @@ class _$UserLoginImpl implements _UserLogin {
factory _$UserLoginImpl.fromJson(Map<String, dynamic> json) =>
_$$UserLoginImplFromJson(json);
// Master record key for the user used to index the local accounts table
// SuperIdentity record key for the user
// used to index the local accounts table
@override
final Typed<FixedEncodedString43> accountMasterRecordKey;
final Typed<FixedEncodedString43> superIdentityRecordKey;
// The identity secret as unlocked from the local accounts table
@override
final Typed<FixedEncodedString43> identitySecret;
@ -178,7 +180,7 @@ class _$UserLoginImpl implements _UserLogin {
@override
String toString() {
return 'UserLogin(accountMasterRecordKey: $accountMasterRecordKey, identitySecret: $identitySecret, accountRecordInfo: $accountRecordInfo, lastActive: $lastActive)';
return 'UserLogin(superIdentityRecordKey: $superIdentityRecordKey, identitySecret: $identitySecret, accountRecordInfo: $accountRecordInfo, lastActive: $lastActive)';
}
@override
@ -186,8 +188,8 @@ class _$UserLoginImpl implements _UserLogin {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$UserLoginImpl &&
(identical(other.accountMasterRecordKey, accountMasterRecordKey) ||
other.accountMasterRecordKey == accountMasterRecordKey) &&
(identical(other.superIdentityRecordKey, superIdentityRecordKey) ||
other.superIdentityRecordKey == superIdentityRecordKey) &&
(identical(other.identitySecret, identitySecret) ||
other.identitySecret == identitySecret) &&
(identical(other.accountRecordInfo, accountRecordInfo) ||
@ -198,7 +200,7 @@ class _$UserLoginImpl implements _UserLogin {
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, accountMasterRecordKey,
int get hashCode => Object.hash(runtimeType, superIdentityRecordKey,
identitySecret, accountRecordInfo, lastActive);
@JsonKey(ignore: true)
@ -217,7 +219,7 @@ class _$UserLoginImpl implements _UserLogin {
abstract class _UserLogin implements UserLogin {
const factory _UserLogin(
{required final Typed<FixedEncodedString43> accountMasterRecordKey,
{required final Typed<FixedEncodedString43> superIdentityRecordKey,
required final Typed<FixedEncodedString43> identitySecret,
required final AccountRecordInfo accountRecordInfo,
required final Timestamp lastActive}) = _$UserLoginImpl;
@ -225,8 +227,9 @@ abstract class _UserLogin implements UserLogin {
factory _UserLogin.fromJson(Map<String, dynamic> json) =
_$UserLoginImpl.fromJson;
@override // Master record key for the user used to index the local accounts table
Typed<FixedEncodedString43> get accountMasterRecordKey;
@override // SuperIdentity record key for the user
// used to index the local accounts table
Typed<FixedEncodedString43> get superIdentityRecordKey;
@override // The identity secret as unlocked from the local accounts table
Typed<FixedEncodedString43> get identitySecret;
@override // The account record key, owner key and secret pulled from the identity

View file

@ -8,8 +8,8 @@ part of 'user_login.dart';
_$UserLoginImpl _$$UserLoginImplFromJson(Map<String, dynamic> json) =>
_$UserLoginImpl(
accountMasterRecordKey: Typed<FixedEncodedString43>.fromJson(
json['account_master_record_key']),
superIdentityRecordKey: Typed<FixedEncodedString43>.fromJson(
json['super_identity_record_key']),
identitySecret:
Typed<FixedEncodedString43>.fromJson(json['identity_secret']),
accountRecordInfo:
@ -19,7 +19,7 @@ _$UserLoginImpl _$$UserLoginImplFromJson(Map<String, dynamic> json) =>
Map<String, dynamic> _$$UserLoginImplToJson(_$UserLoginImpl instance) =>
<String, dynamic>{
'account_master_record_key': instance.accountMasterRecordKey.toJson(),
'super_identity_record_key': instance.superIdentityRecordKey.toJson(),
'identity_secret': instance.identitySecret.toJson(),
'account_record_info': instance.accountRecordInfo.toJson(),
'last_active': instance.lastActive.toJson(),

View file

@ -87,30 +87,30 @@ class AccountRepository {
: fetchUserLogin(activeLocalAccount);
}
LocalAccount? fetchLocalAccount(TypedKey accountMasterRecordKey) {
LocalAccount? fetchLocalAccount(TypedKey accountSuperIdentityRecordKey) {
final localAccounts = _localAccounts.value;
final idx = localAccounts.indexWhere(
(e) => e.identityMaster.masterRecordKey == accountMasterRecordKey);
(e) => e.superIdentity.recordKey == accountSuperIdentityRecordKey);
if (idx == -1) {
return null;
}
return localAccounts[idx];
}
UserLogin? fetchUserLogin(TypedKey accountMasterRecordKey) {
UserLogin? fetchUserLogin(TypedKey superIdentityRecordKey) {
final userLogins = _userLogins.value;
final idx = userLogins
.indexWhere((e) => e.accountMasterRecordKey == accountMasterRecordKey);
.indexWhere((e) => e.superIdentityRecordKey == superIdentityRecordKey);
if (idx == -1) {
return null;
}
return userLogins[idx];
}
AccountInfo getAccountInfo(TypedKey? accountMasterRecordKey) {
AccountInfo getAccountInfo(TypedKey? superIdentityRecordKey) {
// Get active account if we have one
final activeLocalAccount = getActiveLocalAccount();
if (accountMasterRecordKey == null) {
if (superIdentityRecordKey == null) {
if (activeLocalAccount == null) {
// No user logged in
return const AccountInfo(
@ -118,12 +118,12 @@ class AccountRepository {
active: false,
activeAccountInfo: null);
}
accountMasterRecordKey = activeLocalAccount;
superIdentityRecordKey = activeLocalAccount;
}
final active = accountMasterRecordKey == activeLocalAccount;
final active = superIdentityRecordKey == activeLocalAccount;
// Get which local account we want to fetch the profile for
final localAccount = fetchLocalAccount(accountMasterRecordKey);
final localAccount = fetchLocalAccount(superIdentityRecordKey);
if (localAccount == null) {
// account does not exist
return AccountInfo(
@ -133,7 +133,7 @@ class AccountRepository {
}
// See if we've logged into this account or if it is locked
final userLogin = fetchUserLogin(accountMasterRecordKey);
final userLogin = fetchUserLogin(superIdentityRecordKey);
if (userLogin == null) {
// Account was locked
return AccountInfo(
@ -165,33 +165,32 @@ class AccountRepository {
_streamController.add(AccountRepositoryChange.localAccounts);
}
/// Creates a new master identity, an account associated with the master
/// identity, stores the account in the identity key and then logs into
/// that account with no password set at this time
Future<void> createWithNewMasterIdentity(
NewProfileSpec newProfileSpec) async {
log.debug('Creating master identity');
final imws = await IdentityMasterWithSecrets.create();
/// 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<void> createWithNewSuperIdentity(NewProfileSpec newProfileSpec) async {
log.debug('Creating super identity');
final wsi = await WritableSuperIdentity.create();
try {
final localAccount = await _newLocalAccount(
identityMaster: imws.identityMaster,
identitySecret: imws.identitySecret,
superIdentity: wsi.superIdentity,
identitySecret: wsi.identitySecret,
newProfileSpec: newProfileSpec);
// Log in the new account by default with no pin
final ok = await login(localAccount.identityMaster.masterRecordKey,
EncryptionKeyType.none, '');
final ok = await login(
localAccount.superIdentity.recordKey, EncryptionKeyType.none, '');
assert(ok, 'login with none should never fail');
} on Exception catch (_) {
await imws.delete();
await wsi.delete();
rethrow;
}
}
/// Creates a new Account associated with master identity
/// Creates a new Account associated with the current instance of the identity
/// Adds a logged-out LocalAccount to track its existence on this device
Future<LocalAccount> _newLocalAccount(
{required IdentityMaster identityMaster,
{required SuperIdentity superIdentity,
required SecretKey identitySecret,
required NewProfileSpec newProfileSpec,
EncryptionKeyType encryptionKeyType = EncryptionKeyType.none,
@ -201,8 +200,9 @@ class AccountRepository {
final localAccounts = await _localAccounts.get();
// Add account with profile to DHT
await identityMaster.addAccountToIdentity(
identitySecret: identitySecret,
await superIdentity.currentInstance.addAccount(
superRecordKey: superIdentity.recordKey,
secretKey: identitySecret,
accountKey: veilidChatAccountKey,
createAccountCallback: (parent) async {
// Make empty contact list
@ -235,13 +235,13 @@ class AccountRepository {
..contactList = contactList.toProto()
..contactInvitationRecords = contactInvitationRecords.toProto()
..chatList = chatRecords.toProto();
return account;
return account.writeToBuffer();
});
// Encrypt identitySecret with key
final identitySecretBytes = await encryptionKeyType.encryptSecretToBytes(
secret: identitySecret,
cryptoKind: identityMaster.identityRecordKey.kind,
cryptoKind: superIdentity.currentInstance.recordKey.kind,
encryptionKey: encryptionKey,
);
@ -250,7 +250,7 @@ class AccountRepository {
// 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,
superIdentity: superIdentity,
identitySecretBytes: identitySecretBytes,
encryptionKeyType: encryptionKeyType,
biometricsEnabled: false,
@ -269,12 +269,12 @@ class AccountRepository {
}
/// Remove an account and wipe the messages for this account from this device
Future<bool> deleteLocalAccount(TypedKey accountMasterRecordKey) async {
await logout(accountMasterRecordKey);
Future<bool> deleteLocalAccount(TypedKey superIdentityRecordKey) async {
await logout(superIdentityRecordKey);
final localAccounts = await _localAccounts.get();
final newLocalAccounts = localAccounts.removeWhere(
(la) => la.identityMaster.masterRecordKey == accountMasterRecordKey);
(la) => la.superIdentity.recordKey == superIdentityRecordKey);
await _localAccounts.set(newLocalAccounts);
_streamController.add(AccountRepositoryChange.localAccounts);
@ -290,31 +290,35 @@ class AccountRepository {
/// Delete an account from all devices
Future<void> switchToAccount(TypedKey? accountMasterRecordKey) async {
Future<void> switchToAccount(TypedKey? superIdentityRecordKey) async {
final activeLocalAccount = await _activeLocalAccount.get();
if (activeLocalAccount == accountMasterRecordKey) {
if (activeLocalAccount == superIdentityRecordKey) {
// Nothing to do
return;
}
if (accountMasterRecordKey != null) {
if (superIdentityRecordKey != null) {
// Assert the specified record key can be found, will throw if not
final _ = _userLogins.value.firstWhere(
(ul) => ul.accountMasterRecordKey == accountMasterRecordKey);
(ul) => ul.superIdentityRecordKey == superIdentityRecordKey);
}
await _activeLocalAccount.set(accountMasterRecordKey);
await _activeLocalAccount.set(superIdentityRecordKey);
_streamController.add(AccountRepositoryChange.activeLocalAccount);
}
Future<bool> _decryptedLogin(
IdentityMaster identityMaster, SecretKey identitySecret) async {
SuperIdentity superIdentity, SecretKey identitySecret) async {
// Verify identity secret works and return the valid cryptosystem
final cs = await identityMaster.validateIdentitySecret(identitySecret);
final cs = await superIdentity.currentInstance
.validateIdentitySecret(identitySecret);
// Read the identity key to get the account keys
final accountRecordInfoList = await identityMaster.readAccountsFromIdentity(
identitySecret: identitySecret, accountKey: veilidChatAccountKey);
final accountRecordInfoList = await superIdentity.currentInstance
.readAccount(
superRecordKey: superIdentity.recordKey,
secretKey: identitySecret,
accountKey: veilidChatAccountKey);
if (accountRecordInfoList.length > 1) {
throw IdentityException.limitExceeded;
} else if (accountRecordInfoList.isEmpty) {
@ -326,11 +330,11 @@ class AccountRepository {
final userLogins = await _userLogins.get();
final now = Veilid.instance.now();
final newUserLogins = userLogins.replaceFirstWhere(
(ul) => ul.accountMasterRecordKey == identityMaster.masterRecordKey,
(ul) => ul.superIdentityRecordKey == superIdentity.recordKey,
(ul) => ul != null
? ul.copyWith(lastActive: now)
: UserLogin(
accountMasterRecordKey: identityMaster.masterRecordKey,
superIdentityRecordKey: superIdentity.recordKey,
identitySecret:
TypedSecret(kind: cs.kind(), value: identitySecret),
accountRecordInfo: accountRecordInfo,
@ -338,7 +342,7 @@ class AccountRepository {
addIfNotFound: true);
await _userLogins.set(newUserLogins);
await _activeLocalAccount.set(identityMaster.masterRecordKey);
await _activeLocalAccount.set(superIdentity.recordKey);
_streamController
..add(AccountRepositoryChange.userLogins)
@ -347,13 +351,13 @@ class AccountRepository {
return true;
}
Future<bool> login(TypedKey accountMasterRecordKey,
Future<bool> login(TypedKey accountSuperRecordKey,
EncryptionKeyType encryptionKeyType, String encryptionKey) async {
final localAccounts = await _localAccounts.get();
// Get account, throws if not found
final localAccount = localAccounts.firstWhere(
(la) => la.identityMaster.masterRecordKey == accountMasterRecordKey);
(la) => la.superIdentity.recordKey == accountSuperRecordKey);
// Log in with this local account
@ -365,12 +369,12 @@ class AccountRepository {
final identitySecret =
await localAccount.encryptionKeyType.decryptSecretFromBytes(
secretBytes: localAccount.identitySecretBytes,
cryptoKind: localAccount.identityMaster.identityRecordKey.kind,
cryptoKind: localAccount.superIdentity.currentInstance.recordKey.kind,
encryptionKey: encryptionKey,
);
// Validate this secret with the identity public key and log in
return _decryptedLogin(localAccount.identityMaster, identitySecret);
return _decryptedLogin(localAccount.superIdentity, identitySecret);
}
Future<void> logout(TypedKey? accountMasterRecordKey) async {
@ -390,20 +394,20 @@ class AccountRepository {
// Remove user from active logins list
final newUserLogins = (await _userLogins.get())
.removeWhere((ul) => ul.accountMasterRecordKey == logoutUser);
.removeWhere((ul) => ul.superIdentityRecordKey == logoutUser);
await _userLogins.set(newUserLogins);
_streamController.add(AccountRepositoryChange.userLogins);
}
Future<DHTRecord> openAccountRecord(UserLogin userLogin) async {
final localAccount = fetchLocalAccount(userLogin.accountMasterRecordKey)!;
final localAccount = fetchLocalAccount(userLogin.superIdentityRecordKey)!;
// Record not yet open, do it
final pool = DHTRecordPool.instance;
final record = await pool.openRecordOwned(
userLogin.accountRecordInfo.accountRecord,
debugName: 'AccountRepository::openAccountRecord::AccountRecord',
parent: localAccount.identityMaster.identityRecordKey);
parent: localAccount.superIdentity.currentInstance.recordKey);
return record;
}

View file

@ -1,16 +1,17 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:go_router/go_router.dart';
import '../../../layout/default_app_bar.dart';
import '../../../theme/theme.dart';
import '../../../tools/tools.dart';
import '../../../veilid_processor/veilid_processor.dart';
import '../../account_manager.dart';
import '../../layout/default_app_bar.dart';
import '../../theme/theme.dart';
import '../../tools/tools.dart';
import '../../veilid_processor/veilid_processor.dart';
import '../account_manager.dart';
class NewAccountPage extends StatefulWidget {
const NewAccountPage({super.key});
@ -36,63 +37,76 @@ class NewAccountPageState extends State<NewAccountPage> {
}
Widget _newAccountForm(BuildContext context,
{required Future<void> Function(GlobalKey<FormBuilderState>)
onSubmit}) =>
FormBuilder(
key: _formKey,
child: ListView(
children: [
Text(translate('new_account_page.header'))
.textStyle(context.headlineSmall)
.paddingSymmetric(vertical: 16),
FormBuilderTextField(
autofocus: true,
name: formFieldName,
decoration:
InputDecoration(labelText: translate('account.form_name')),
maxLength: 64,
// The validator receives the text that the user has entered.
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(),
]),
textInputAction: TextInputAction.next,
),
FormBuilderTextField(
name: formFieldPronouns,
maxLength: 64,
decoration: InputDecoration(
labelText: translate('account.form_pronouns')),
textInputAction: TextInputAction.next,
),
Row(children: [
const Spacer(),
Text(translate('new_account_page.instructions'))
.toCenter()
.flexible(flex: 6),
const Spacer(),
]).paddingSymmetric(vertical: 4),
ElevatedButton(
onPressed: () async {
if (_formKey.currentState?.saveAndValidate() ?? false) {
setState(() {
isInAsyncCall = true;
});
try {
await onSubmit(_formKey);
} finally {
if (mounted) {
{required Future<void> Function(GlobalKey<FormBuilderState>) onSubmit}) {
final networkReady = context
.watch<ConnectionStateCubit>()
.state
.asData
?.value
.isPublicInternetReady ??
false;
final canSubmit = networkReady;
return FormBuilder(
key: _formKey,
child: ListView(
children: [
Text(translate('new_account_page.header'))
.textStyle(context.headlineSmall)
.paddingSymmetric(vertical: 16),
FormBuilderTextField(
autofocus: true,
name: formFieldName,
decoration:
InputDecoration(labelText: translate('account.form_name')),
maxLength: 64,
// The validator receives the text that the user has entered.
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(),
]),
textInputAction: TextInputAction.next,
),
FormBuilderTextField(
name: formFieldPronouns,
maxLength: 64,
decoration:
InputDecoration(labelText: translate('account.form_pronouns')),
textInputAction: TextInputAction.next,
),
Row(children: [
const Spacer(),
Text(translate('new_account_page.instructions'))
.toCenter()
.flexible(flex: 6),
const Spacer(),
]).paddingSymmetric(vertical: 4),
ElevatedButton(
onPressed: !canSubmit
? null
: () async {
if (_formKey.currentState?.saveAndValidate() ?? false) {
setState(() {
isInAsyncCall = false;
isInAsyncCall = true;
});
try {
await onSubmit(_formKey);
} finally {
if (mounted) {
setState(() {
isInAsyncCall = false;
});
}
}
}
}
}
},
child: Text(translate('new_account_page.create')),
).paddingSymmetric(vertical: 4).alignAtCenterRight(),
],
),
);
},
child: Text(translate(!networkReady
? 'button.waiting_for_network'
: 'new_account_page.create')),
).paddingSymmetric(vertical: 4).alignAtCenterRight(),
],
),
);
}
@override
Widget build(BuildContext context) {
@ -127,7 +141,7 @@ class NewAccountPageState extends State<NewAccountPage> {
NewProfileSpec(name: name, pronouns: pronouns);
await AccountRepository.instance
.createWithNewMasterIdentity(newProfileSpec);
.createWithNewSuperIdentity(newProfileSpec);
} on Exception catch (e) {
if (context.mounted) {
await showErrorModal(context, translate('new_account_page.error'),

View file

@ -1,2 +1,2 @@
export 'new_account_page/new_account_page.dart';
export 'new_account_page.dart';
export 'profile_widget.dart';