Skip to content

Commit

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

//TODO: similar for changePassword
Future<bool> changePassword(ProtectedValue password, Future<bool> Function() onChangeStarted) async {
if (currentUser.emailHashed == null) throw KeeInvalidStateException();
if (currentUser.email?.isEmpty ?? true) throw KeeInvalidStateException();
if (currentUser.salt?.isEmpty ?? true) throw KeeInvalidStateException();

final key = password.hash;
final newPassKey = await derivePassKey(currentUser.email!, key);

try {
await _userRepo.changePasswordStart(currentUser, newPassKey);

final success = await onChangeStarted();

if (!success) {
throw KeeException('Password change aborted.');
}

await _userRepo.changePasswordFinish(currentUser, newPassKey);
//TODO: What do we do now? emit a new state to force the user to sign in again? may not be needed since we updated
// JWTs... can maybe just do nothing and let the caller pop the navigation or show some feedback to say all worked
// fine. But maybe want to at least emit the currentuser again since it changed? but mutated so nothing will actually happen?
return true;
} on KeeLoginFailedMITMException {
rethrow;

//TODO: All error handling. sample from email change is below...
} on KeeLoginRequiredException {
l.w('Unable to changeEmailAddress due to a 403.');
emit(AccountEmailChangeRequested(currentUser,
'Due to an authentication problem, we were unable to change your email address. Probably it has been too long since you last signed in with your previous email address. We have left you signed in using your old email address but you may find that you are signed out soon. Please sign out and then sign in again with your previous email address and try again when you have enough time to complete the operation within 10 minutes.'));
} on FormatException {
// Local validation
l.i('Unable to changeEmailAddress due to FormatException.');
emit(AccountEmailChangeRequested(
currentUser, 'Please enter the correct password for your existing Kee Vault account.'));
} on KeeInvalidRequestException {
// Local validation should mean this is unlikely to happen outside of malicious acts
l.i('Unable to changeEmailAddress due to 400 response.');
emit(AccountEmailChangeRequested(currentUser,
'Please double check that you have entered the correct password for your existing Kee Vault account. Also check that you have entered a valid email address of no more than 70 characters.'));
} on KeeServerConflictException {
l.i('Unable to changeEmailAddress due to 409 response.');
emit(AccountEmailChangeRequested(currentUser,
'Sorry, that email address is already associated with a different Kee Vault account (or is reserved due to earlier use by a deleted account). Try signing in to that account, and consider importing your exported KDBX file from this account if you wish to transfer your data to the other account. If you have access to the requested email address but are unable to remember your password, you could use the account reset feature to delete the contents of the other account and assign it a new password that you will remember.'));
} on KeeNotFoundException {
l.i('Unable to changeEmailAddress due to 404 response.');
emit(AccountEmailChangeRequested(currentUser, 'We cannot find your account. Have you recently deleted it?'));
} on KeeServiceTransportException catch (e) {
l.w('Unable to changeEmailAddress due to a transport error. Cannot be sure if the request was successful or not. Details: $e');
emit(AccountEmailChangeRequested(currentUser,
'Due to a network failure, we cannot say whether your request succeeded or not. We have left you signed in using your old email address but if the operation did eventually succeed, you may find that you are signed out soon. Please check your new email address for a verification request. It might take a moment to arrive but if it does, that suggests the process did work so just verify your new address, sign out of the app and then sign-in using the new email address. If unsure if it worked, sign in with your previous email address next time and try again when you have a more stable network connection.'));

//} on KeeMaybeOfflineException {
//TODO: confirm if can get a KeeMaybeOfflineException here
// l.i('Unable to authenticate since initial identification failed, probably due to a transport error. App should continue to work offline if user has previously stored their Vault.');
// final prefs = await SharedPreferences.getInstance();
// await prefs.setString('user.current.email', user.email!);
// if (user.id?.isNotEmpty ?? false) {
// await prefs.setString('user.authMaterialUserIdMap.${user.emailHashed}', user.id!);
// }
// emit(AccountAuthenticationBypassed(user));
}
return false;
}

Future<void> changeEmailAddress(String password, String newEmailAddress) async {
l.d('starting the changeEmailAddress procedure');
User user = currentUser;
Expand Down
41 changes: 41 additions & 0 deletions lib/cubit/vault_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1456,6 +1456,47 @@ class VaultCubit extends Cubit<VaultState> {
}
}

Future<void> changeRemoteUserPassword(String password) async {
VaultState s = state;
if (isPasswordChangingSuspended()) {
const message =
'User tried to change password while cubit prevented it. This is extremely unlikely to happen and retrying should resolve the issue.';
l.w(message);
throw Exception(message);
}
if (s is VaultSaving) {
const message = 'Can\'t change password while vault is being saved. Try again later.';
l.e(message);
throw Exception(message);
}
final user = _accountCubit.currentUserIfIdKnown;
if (user == null) {
const message = 'User not known. Cannot change remote user password if we don\'t know this.';
l.e(message);
throw Exception(message);
}
//TODO: Any other situations we want to prevent for remote changing?
if (s is VaultLoaded) {
final protectedValue = ProtectedValue.fromString(password);
await _accountCubit.changePassword(protectedValue, () async {
//TODO: change kdbx password
// 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
// file.setPassword(newPassword, emailAddrParts);
// this.syncFile(file, { skipValidation: true, startedByUser: false, remoteKey: {passwordHash: oldPasswordHash} });
return true;
});

// 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');
}
}

Future<void> autofillMerge(User? user, {bool onlyIfAttemptAlreadyDue = false}) async {
if (onlyIfAttemptAlreadyDue && !autoFillMergeAttemptDue) {
return;
Expand Down
16 changes: 16 additions & 0 deletions lib/user_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,22 @@ class UserRepository {
return;
}

Future<void> changePasswordStart(User user, String newPassKey) async {
if (user.emailHashed == null) throw KeeInvalidStateException();
if (user.email?.isEmpty ?? true) throw KeeInvalidStateException();
if (newPassKey.isEmpty) throw KeeInvalidStateException();
await userService.changePasswordStart(user, newPassKey);
return;
}

Future<void> changePasswordFinish(User user, String newPassKey) async {
if (user.emailHashed == null) throw KeeInvalidStateException();
if (user.email?.isEmpty ?? true) throw KeeInvalidStateException();
if (newPassKey.isEmpty) throw KeeInvalidStateException();
await userService.changePasswordFinish(user, newPassKey);
return;
}

Future<bool> associate(User user, int subscriptionSource, String validationData) async {
return await subscriptionService.associate(user, subscriptionSource, validationData);
}
Expand Down
22 changes: 22 additions & 0 deletions lib/vault_backend/user_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,28 @@ class UserService {
return;
}

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

await _service.postRequest<String>(
'changePasswordStart',
{
'verifier': hex2base64(newVerifier),
},
user.tokens!.identity);
return;
}

Future<void> changePasswordFinish(User user, String newPassKey) async {
final response = await _service.postRequest<String>('changePasswordFinish', {}, user.tokens!.identity);
user.passKey = newPassKey;

final jwts = List<String>.from(json.decode(response.data!)['JWTs']);
await _parseJWTs(user, jwts);
return;
}

Future<void> _parseJWTs(User user, List<String> jwts, {bool notifyListeners = false}) async {
user.tokens = Tokens();

Expand Down
8 changes: 4 additions & 4 deletions lib/widgets/account_create.dart
Original file line number Diff line number Diff line change
Expand Up @@ -677,10 +677,10 @@ class _AccountCreateWidgetState extends State<AccountCreateWidget> {
return false;
}

Future<void> changePassword(String password) async {
final vaultCubit = BlocProvider.of<VaultCubit>(context);
await vaultCubit.changeFreeUserPassword(password);
}
// Future<void> changePassword(String password) async {
// final vaultCubit = BlocProvider.of<VaultCubit>(context);
// await vaultCubit.changeFreeUserPassword(password);
// }

subscribeUser(User user) async {
final accountCubit = BlocProvider.of<AccountCubit>(context);
Expand Down
7 changes: 5 additions & 2 deletions lib/widgets/change_password.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kdbx/kdbx.dart';
import 'package:keevault/cubit/account_cubit.dart';
import '../cubit/vault_cubit.dart';
import '../generated/l10n.dart';
import '../widgets/password_strength.dart';
Expand Down Expand Up @@ -252,8 +253,10 @@ class _ChangePasswordWidgetState extends State<ChangePasswordWidget> {
}

Future<void> changePassword(String password) async {
//TODO: call accountcubit.changePassword if a user is signed in
final accountCubit = BlocProvider.of<AccountCubit>(context);
final vaultCubit = BlocProvider.of<VaultCubit>(context);
await vaultCubit.changeFreeUserPassword(password);
await ((accountCubit.currentUserIfIdKnown == null)
? vaultCubit.changeFreeUserPassword(password)
: vaultCubit.changeRemoteUserPassword(password));
}
}
1 change: 1 addition & 0 deletions lib/widgets/settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ class _SettingsWidgetState extends State<SettingsWidget> with TraceableClientMix
SimpleSettingsTile(
title: str.changeEmail,
onTap: () {
//TODO: lock vault here? Also need to kick user out from this vaultloader widget entirely. probably do something similar when verification grace expires or subscription expires?
BlocProvider.of<AccountCubit>(context).startEmailChange();
},
),
Expand Down

0 comments on commit 742d734

Please sign in to comment.