Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
luckyrat committed Jan 17, 2025
1 parent 742d734 commit d146f64
Show file tree
Hide file tree
Showing 12 changed files with 146 additions and 59 deletions.
4 changes: 2 additions & 2 deletions lib/cubit/account_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ class AccountCubit extends Cubit<AccountState> {
emit(AccountEmailChangeRequested(currentUser, null));
}

Future<bool> changePassword(ProtectedValue password, Future<bool> Function() onChangeStarted) async {
Future<bool> changePassword(ProtectedValue password, Future<bool> Function(User user) onChangeStarted) async {
if (currentUser.emailHashed == null) throw KeeInvalidStateException();
if (currentUser.email?.isEmpty ?? true) throw KeeInvalidStateException();
if (currentUser.salt?.isEmpty ?? true) throw KeeInvalidStateException();
Expand All @@ -341,7 +341,7 @@ class AccountCubit extends Cubit<AccountState> {
try {
await _userRepo.changePasswordStart(currentUser, newPassKey);

final success = await onChangeStarted();
final success = await onChangeStarted(currentUser);

if (!success) {
throw KeeException('Password change aborted.');
Expand Down
54 changes: 47 additions & 7 deletions lib/cubit/vault_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -559,10 +559,15 @@ class VaultCubit extends Cubit<VaultState> {
l.i('refresh called during an ongoing upload. Will not refresh now.');
return;
}
if (s is VaultChangingPassword) {
l.i('refresh called during a password change. Will not refresh now.');
return;
}

Credentials creds = s.vault.files.current.credentials;

if (overridePassword != null) {
//TODO: Why does this not update the local kdbx file and QU?
l.d('we have a password explicitly supplied');
final protectedValue = ProtectedValue.fromString(overridePassword);
final key = protectedValue.hash;
Expand Down Expand Up @@ -1059,11 +1064,12 @@ class VaultCubit extends Cubit<VaultState> {
final protectedValue = ProtectedValue.fromString(overridePassword);
final key = protectedValue.hash;
await user.attachKey(key);
creds = Credentials(protectedValue);
final credentialsWithStrength = StrengthAssessedCredentials(protectedValue, user.emailParts);
creds = credentialsWithStrength.credentials;

// If user has supplied the correct password for the upload to succeed, we must make sure the locked data uploaded is encrypted using that new password. remoteMergeTarget and current are identical at this time because user has just supplied a new password through the UI so can't have any outstanding modifications in the current vault file. There must also be no pending files.
updatedLocalFile = LocalVaultFile(await vault.files.copyWithNewCredentials(creds), vault.lastOpenedAt,
vault.persistedAt, vault.uuid, vault.etag, vault.versionId);
updatedLocalFile = LocalVaultFile(await vault.files.copyWithNewCredentials(credentialsWithStrength),
vault.lastOpenedAt, vault.persistedAt, vault.uuid, vault.etag, vault.versionId);
}

String? lastRemoteEtag;
Expand Down Expand Up @@ -1311,6 +1317,9 @@ class VaultCubit extends Cubit<VaultState> {
castState.remotely,
castState.causedByInteraction,
));
} else if (state is VaultChangingPassword) {
final castState = state as VaultChangingPassword;
emit(VaultChangingPassword(castState.vault));
} else if (state is VaultReconcilingUpload) {
final castState = state as VaultReconcilingUpload;
emit(VaultReconcilingUpload(castState.vault, castState.locally, castState.remotely));
Expand Down Expand Up @@ -1475,11 +1484,21 @@ class VaultCubit extends Cubit<VaultState> {
l.e(message);
throw Exception(message);
}
//TODO: Any other situations we want to prevent for remote changing?
if (s is VaultLoaded) {
//TODO: Any other situations we want to prevent for remote changing? e.g. remoteauthrequested
// also need to put same checks into beginChangePasswordIfPossible so we don't give the user a chance to reach this point when we know current state is invalid
if (s is VaultChangingPassword) {
final protectedValue = ProtectedValue.fromString(password);
await _accountCubit.changePassword(protectedValue, () async {
await _accountCubit.changePassword(protectedValue, (User user) async {
//TODO: change kdbx password
l.d('changing KDBX password');
final credentialsWithStrength = StrengthAssessedCredentials(protectedValue, user.emailParts);
s.vault.files.current
..changeCredentials(credentialsWithStrength.credentials)
..header.writeKdfParameters(credentialsWithStrength.createNewKdfParameters());
await save(user);
//TODO: Do we need to refresh manually before we start this process so that we know we have the latest remote changes too?
// Need to protect against situation where remote file gets updated in the mean time and thus we start a sync operation but no longer have matching KDBX passwords so sync fails and thus password change fails. Edge case though so maybe instruct user to not do that and then just throw failure error if it happens. Double-check that this does not lead to permenant problem. user should be able to re-sign in and do it again successfully after sync happens.
l.d('KDBX password changed and uploaded');
// const emailAddrParts = EmailUtil.split(this.account.get('email'));
// const file = this.files.first();
// const oldPasswordHash = file.db.credentials.passwordHash.clone(); // Not certain clone is needed
Expand All @@ -1489,14 +1508,35 @@ class VaultCubit extends Cubit<VaultState> {
});

// l.d('changing KDBX password');
// final protectedValue = ProtectedValue.fromString(password);
// final creds = Credentials(protectedValue);
// s.vault.files.current.changeCredentials(creds);
// await save(null);
// l.d('KDBX password changed');
}
}

void startEmailChange() {
if (currentVaultFile == null || (!currentVaultFile!.files.current.isDirty && _entryCubit.state is! EntryLoaded)) {
//TODO: test if just signing out triggers the request to sign in again automatically or if it is to do with the change to the account cubit
// _accountCubit.startEmailChange();
signout();
} else {
emitError('You must save your changes first!', toast: true);
}
}

bool beginChangePasswordIfPossible() {
if (currentVaultFile == null) {
return false;
}
if (!currentVaultFile!.files.current.isDirty && _entryCubit.state is! EntryLoaded) {
emit(VaultChangingPassword(currentVaultFile!));
return true;
}
emitError('You must save your changes first!', toast: true);
return false;
}

Future<void> autofillMerge(User? user, {bool onlyIfAttemptAlreadyDue = false}) async {
if (onlyIfAttemptAlreadyDue && !autoFillMergeAttemptDue) {
return;
Expand Down
4 changes: 4 additions & 0 deletions lib/cubit/vault_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,7 @@ class VaultUploadCredentialsRequired extends VaultReconcilingUpload {
final bool causedByInteraction;
const VaultUploadCredentialsRequired(super.vault, super.locally, super.remotely, this.causedByInteraction);
}

class VaultChangingPassword extends VaultLoaded {
const VaultChangingPassword(super.vault);
}
8 changes: 5 additions & 3 deletions lib/generated/intl/messages_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ class MessageLookup extends MessageLookupByLibrary {
"Change or Cancel Subscription"),
"changeEmail":
MessageLookupByLibrary.simpleMessage("Change email address"),
"changeEmailConfirmCheckbox": MessageLookupByLibrary.simpleMessage(
"I have read the above warnings, mitigated the risks and wish to continue"),
"changeEmailInfo1": MessageLookupByLibrary.simpleMessage(
"Your email address has a crucial role in the advanced security protections Kee Vault offers. Changing it securely is a far more complex task than for most of the places you might wish to change it. We are happy to finally offer this feature to you but please read the information carefully and don\'t proceed when you are in a rush."),
"changeEmailInfo2": MessageLookupByLibrary.simpleMessage(
Expand All @@ -158,11 +160,9 @@ class MessageLookup extends MessageLookupByLibrary {
"changeEmailInfo3c": MessageLookupByLibrary.simpleMessage(
"3) Copy/paste what you have entered in the email address box and store somewhere like a note on your phone."),
"changeEmailInfo4": MessageLookupByLibrary.simpleMessage(
"If you make a mistake, you should be able to regain access to your Vault but in some cases you may need to create a new Kee Vault subscription and import from your previously exported KDBX file - this can result in additional hassle and costs since your current subscription would not automatically end."),
"If you make a mistake, you should be able to regain access to your Vault but in some cases you may need to create a new Kee Vault subscription and import from your previously exported KDBX file - this can result in additional hassle and costs since your old subscription would not be refundable."),
"changeEmailInfo5": MessageLookupByLibrary.simpleMessage(
"Your password will remain the same throughout the process. If you want to change that too, we first recommend signing in on multiple devices using your new email address and waiting at least an hour."),
"changeEmailInfo6": MessageLookupByLibrary.simpleMessage(
"I have read the above warnings, mitigated the risks and wish to continue"),
"changeEmailPrefs":
MessageLookupByLibrary.simpleMessage("Change email preferences"),
"changePassword":
Expand Down Expand Up @@ -245,6 +245,8 @@ class MessageLookup extends MessageLookupByLibrary {
"done": MessageLookupByLibrary.simpleMessage("Done"),
"downloading": MessageLookupByLibrary.simpleMessage("Downloading"),
"email": MessageLookupByLibrary.simpleMessage("Email"),
"emailChanged":
MessageLookupByLibrary.simpleMessage("Email address changed"),
"emailValidationFail": MessageLookupByLibrary.simpleMessage(
"Not a valid email address. Please try again."),
"emailVerification":
Expand Down
18 changes: 14 additions & 4 deletions lib/generated/l10n.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions lib/l10n/intl_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -389,10 +389,11 @@
"changeEmailInfo3a": "1) Click the Cancel button below, sign in to Kee Vault again, Export your Vault to a KDBX file and store it somewhere safe as a backup.",
"changeEmailInfo3b": "2) Double check you enter the correct email address - you will need to type it exactly to sign in to your account in a moment.",
"changeEmailInfo3c": "3) Copy/paste what you have entered in the email address box and store somewhere like a note on your phone.",
"changeEmailInfo4": "If you make a mistake, you should be able to regain access to your Vault but in some cases you may need to create a new Kee Vault subscription and import from your previously exported KDBX file - this can result in additional hassle and costs since your current subscription would not automatically end.",
"changeEmailInfo4": "If you make a mistake, you should be able to regain access to your Vault but in some cases you may need to create a new Kee Vault subscription and import from your previously exported KDBX file - this can result in additional hassle and costs since your old subscription would not be refundable.",
"changeEmailInfo5": "Your password will remain the same throughout the process. If you want to change that too, we first recommend signing in on multiple devices using your new email address and waiting at least an hour.",
"changeEmailInfo6": "I have read the above warnings, mitigated the risks and wish to continue",
"changeEmailConfirmCheckbox": "I have read the above warnings, mitigated the risks and wish to continue",
"currentEmailAddress": "Current email address",
"newEmailAddress": "New email address"
"newEmailAddress": "New email address",
"emailChanged": "Email address changed"

}
9 changes: 6 additions & 3 deletions lib/password_strength.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,16 +88,19 @@ class StrengthAssessedCredentials {
: strength = fuzzyStrength(password.getText(), emailAddrParts),
credentials = Credentials(password);

KdbxHeader createNewKdbxHeader() {
VarDictionary createNewKdfParameters() {
final argon2Params = Argon2Params.forStrength(strength);
final kdfParameters = VarDictionary([
return VarDictionary([
KdfField.uuid.item(KeyEncrypterKdf.kdfUuidForType(KdfType.Argon2d).toBytes()),
KdfField.salt.item(ByteUtils.randomBytes(argon2Params.saltLength)),
KdfField.parallelism.item(argon2Params.parallelism),
KdfField.iterations.item(argon2Params.iterations),
KdfField.memory.item(argon2Params.memory),
KdfField.version.item(argon2Params.version),
]);
return KdbxHeader.createV4_1()..writeKdfParameters(kdfParameters);
}

KdbxHeader createNewKdbxHeader() {
return KdbxHeader.createV4_1()..writeKdfParameters(createNewKdfParameters());
}
}
13 changes: 6 additions & 7 deletions lib/vault_backend/user_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ class UserService {
return user;
}

// Actually can't ever be false - just throws instead
Future<bool> loginFinish(User user, {List<int>? hashedMasterKey, required bool notifyListeners}) async {
Future<void> loginFinish(User user, {List<int>? hashedMasterKey, required bool notifyListeners}) async {
if (user.loginParameters == null) throw KeeMaybeOfflineException();
if (user.emailHashed?.isEmpty ?? true) throw KeeInvalidStateException();
if (user.salt?.isEmpty ?? true) throw KeeInvalidStateException();
Expand Down Expand Up @@ -80,7 +79,7 @@ class UserService {
user.verificationStatus = srp2.verificationStatus;

await _finishIosIapTransaction(user);
return true;
return;
}

Future<void> _finishIosIapTransaction(User user) async {
Expand Down Expand Up @@ -156,9 +155,9 @@ class UserService {
user.emailHashed!.isNotEmpty &&
user.passKey!.isNotEmpty) {
await loginStart(user);
final loginResult = await loginFinish(user, notifyListeners: notifyListeners);
await loginFinish(user, notifyListeners: notifyListeners);
// Unlike with the refresh operation above, user.verificationStatus is updated as part of the loginFinish function
if (loginResult && user.tokens != null) {
if (user.tokens != null) {
return user.tokens!;
} else {
throw KeeLoginRequiredException();
Expand Down Expand Up @@ -222,7 +221,7 @@ class UserService {
final newPrivateKey = derivePrivateKey(newHexSalt, newEmailHashed, newPassKey);
final newVerifier = deriveVerifier(newPrivateKey);

final oldPrivateKey = derivePrivateKey(user.salt!, user.emailHashed!, oldPassKey);
final oldPrivateKey = derivePrivateKey(base642hex(user.salt!), user.emailHashed!, oldPassKey);
final oldVerifier = deriveVerifier(oldPrivateKey);
final oldVerifierHashed = await hashBytes(Uint8List.fromList(hex.decode(oldVerifier)));

Expand All @@ -240,7 +239,7 @@ class UserService {
}

Future<void> changePasswordStart(User user, String newPassKey) async {
final newPrivateKey = derivePrivateKey(user.salt!, user.emailHashed!, newPassKey);
final newPrivateKey = derivePrivateKey(base642hex(user.salt!), user.emailHashed!, newPassKey);
final newVerifier = deriveVerifier(newPrivateKey);

await _service.postRequest<String>(
Expand Down
15 changes: 7 additions & 8 deletions lib/vault_file.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:keevault/locked_vault_file.dart';

import 'kdbx_argon2_ffi.dart';
import 'kdf_cache.dart';
import 'password_strength.dart';

class VaultFileVersions {
// Current unlocked kdbx must always be available
Expand Down Expand Up @@ -71,21 +72,19 @@ class VaultFileVersions {
// remoteMergeTarget and current are identical at this time because user has just
// supplied a new password through the UI so can't have any outstanding modifications
// in the current vault file. There must also be no pending files.
Future<VaultFileVersions> copyWithNewCredentials(Credentials credentials) async {
// final unlockedFiles = await unlockTwice(this.remoteMergeTargetLocked!);
// final unlockedCurrent = unlockedFiles[0];
//unlockedCurrent.overwriteCredentials(credentials, DateTime.now());
Future<VaultFileVersions> copyWithNewCredentials(StrengthAssessedCredentials credentialsWithStrength) async {
final unlockedFile = await unlock(remoteMergeTargetLocked);
unlockedFile.changeCredentials(credentials);
// final unlockedMergeTarget = unlockedFiles[1];
// unlockedMergeTarget.changeCredentials(credentials);
unlockedFile
..changeCredentials(credentialsWithStrength.credentials)
..header.writeKdfParameters(credentialsWithStrength.createNewKdfParameters());
final kdbxData = await unlockedFile.save();
return VaultFileVersions(
current: _current,
pending: null,
pendingLocked: null,
remoteMergeTarget: null,
remoteMergeTargetLocked: LockedVaultFile(kdbxData, DateTime.now(), credentials, null, null));
remoteMergeTargetLocked:
LockedVaultFile(kdbxData, DateTime.now(), credentialsWithStrength.credentials, null, null));
}

@override
Expand Down
Loading

0 comments on commit d146f64

Please sign in to comment.