Skip to content

Commit 742d734

Browse files
committed
wip
1 parent 84aa3d2 commit 742d734

File tree

7 files changed

+154
-7
lines changed

7 files changed

+154
-7
lines changed

lib/cubit/account_cubit.dart

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,71 @@ class AccountCubit extends Cubit<AccountState> {
330330
emit(AccountEmailChangeRequested(currentUser, null));
331331
}
332332

333-
//TODO: similar for changePassword
333+
Future<bool> changePassword(ProtectedValue password, Future<bool> Function() onChangeStarted) async {
334+
if (currentUser.emailHashed == null) throw KeeInvalidStateException();
335+
if (currentUser.email?.isEmpty ?? true) throw KeeInvalidStateException();
336+
if (currentUser.salt?.isEmpty ?? true) throw KeeInvalidStateException();
337+
338+
final key = password.hash;
339+
final newPassKey = await derivePassKey(currentUser.email!, key);
340+
341+
try {
342+
await _userRepo.changePasswordStart(currentUser, newPassKey);
343+
344+
final success = await onChangeStarted();
345+
346+
if (!success) {
347+
throw KeeException('Password change aborted.');
348+
}
349+
350+
await _userRepo.changePasswordFinish(currentUser, newPassKey);
351+
//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
352+
// JWTs... can maybe just do nothing and let the caller pop the navigation or show some feedback to say all worked
353+
// fine. But maybe want to at least emit the currentuser again since it changed? but mutated so nothing will actually happen?
354+
return true;
355+
} on KeeLoginFailedMITMException {
356+
rethrow;
357+
358+
//TODO: All error handling. sample from email change is below...
359+
} on KeeLoginRequiredException {
360+
l.w('Unable to changeEmailAddress due to a 403.');
361+
emit(AccountEmailChangeRequested(currentUser,
362+
'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.'));
363+
} on FormatException {
364+
// Local validation
365+
l.i('Unable to changeEmailAddress due to FormatException.');
366+
emit(AccountEmailChangeRequested(
367+
currentUser, 'Please enter the correct password for your existing Kee Vault account.'));
368+
} on KeeInvalidRequestException {
369+
// Local validation should mean this is unlikely to happen outside of malicious acts
370+
l.i('Unable to changeEmailAddress due to 400 response.');
371+
emit(AccountEmailChangeRequested(currentUser,
372+
'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.'));
373+
} on KeeServerConflictException {
374+
l.i('Unable to changeEmailAddress due to 409 response.');
375+
emit(AccountEmailChangeRequested(currentUser,
376+
'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.'));
377+
} on KeeNotFoundException {
378+
l.i('Unable to changeEmailAddress due to 404 response.');
379+
emit(AccountEmailChangeRequested(currentUser, 'We cannot find your account. Have you recently deleted it?'));
380+
} on KeeServiceTransportException catch (e) {
381+
l.w('Unable to changeEmailAddress due to a transport error. Cannot be sure if the request was successful or not. Details: $e');
382+
emit(AccountEmailChangeRequested(currentUser,
383+
'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.'));
384+
385+
//} on KeeMaybeOfflineException {
386+
//TODO: confirm if can get a KeeMaybeOfflineException here
387+
// 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.');
388+
// final prefs = await SharedPreferences.getInstance();
389+
// await prefs.setString('user.current.email', user.email!);
390+
// if (user.id?.isNotEmpty ?? false) {
391+
// await prefs.setString('user.authMaterialUserIdMap.${user.emailHashed}', user.id!);
392+
// }
393+
// emit(AccountAuthenticationBypassed(user));
394+
}
395+
return false;
396+
}
397+
334398
Future<void> changeEmailAddress(String password, String newEmailAddress) async {
335399
l.d('starting the changeEmailAddress procedure');
336400
User user = currentUser;

lib/cubit/vault_cubit.dart

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1456,6 +1456,47 @@ class VaultCubit extends Cubit<VaultState> {
14561456
}
14571457
}
14581458

1459+
Future<void> changeRemoteUserPassword(String password) async {
1460+
VaultState s = state;
1461+
if (isPasswordChangingSuspended()) {
1462+
const message =
1463+
'User tried to change password while cubit prevented it. This is extremely unlikely to happen and retrying should resolve the issue.';
1464+
l.w(message);
1465+
throw Exception(message);
1466+
}
1467+
if (s is VaultSaving) {
1468+
const message = 'Can\'t change password while vault is being saved. Try again later.';
1469+
l.e(message);
1470+
throw Exception(message);
1471+
}
1472+
final user = _accountCubit.currentUserIfIdKnown;
1473+
if (user == null) {
1474+
const message = 'User not known. Cannot change remote user password if we don\'t know this.';
1475+
l.e(message);
1476+
throw Exception(message);
1477+
}
1478+
//TODO: Any other situations we want to prevent for remote changing?
1479+
if (s is VaultLoaded) {
1480+
final protectedValue = ProtectedValue.fromString(password);
1481+
await _accountCubit.changePassword(protectedValue, () async {
1482+
//TODO: change kdbx password
1483+
// const emailAddrParts = EmailUtil.split(this.account.get('email'));
1484+
// const file = this.files.first();
1485+
// const oldPasswordHash = file.db.credentials.passwordHash.clone(); // Not certain clone is needed
1486+
// file.setPassword(newPassword, emailAddrParts);
1487+
// this.syncFile(file, { skipValidation: true, startedByUser: false, remoteKey: {passwordHash: oldPasswordHash} });
1488+
return true;
1489+
});
1490+
1491+
// l.d('changing KDBX password');
1492+
// final protectedValue = ProtectedValue.fromString(password);
1493+
// final creds = Credentials(protectedValue);
1494+
// s.vault.files.current.changeCredentials(creds);
1495+
// await save(null);
1496+
// l.d('KDBX password changed');
1497+
}
1498+
}
1499+
14591500
Future<void> autofillMerge(User? user, {bool onlyIfAttemptAlreadyDue = false}) async {
14601501
if (onlyIfAttemptAlreadyDue && !autoFillMergeAttemptDue) {
14611502
return;

lib/user_repository.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,22 @@ class UserRepository {
6262
return;
6363
}
6464

65+
Future<void> changePasswordStart(User user, String newPassKey) async {
66+
if (user.emailHashed == null) throw KeeInvalidStateException();
67+
if (user.email?.isEmpty ?? true) throw KeeInvalidStateException();
68+
if (newPassKey.isEmpty) throw KeeInvalidStateException();
69+
await userService.changePasswordStart(user, newPassKey);
70+
return;
71+
}
72+
73+
Future<void> changePasswordFinish(User user, String newPassKey) async {
74+
if (user.emailHashed == null) throw KeeInvalidStateException();
75+
if (user.email?.isEmpty ?? true) throw KeeInvalidStateException();
76+
if (newPassKey.isEmpty) throw KeeInvalidStateException();
77+
await userService.changePasswordFinish(user, newPassKey);
78+
return;
79+
}
80+
6581
Future<bool> associate(User user, int subscriptionSource, String validationData) async {
6682
return await subscriptionService.associate(user, subscriptionSource, validationData);
6783
}

lib/vault_backend/user_service.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,28 @@ class UserService {
239239
return;
240240
}
241241

242+
Future<void> changePasswordStart(User user, String newPassKey) async {
243+
final newPrivateKey = derivePrivateKey(user.salt!, user.emailHashed!, newPassKey);
244+
final newVerifier = deriveVerifier(newPrivateKey);
245+
246+
await _service.postRequest<String>(
247+
'changePasswordStart',
248+
{
249+
'verifier': hex2base64(newVerifier),
250+
},
251+
user.tokens!.identity);
252+
return;
253+
}
254+
255+
Future<void> changePasswordFinish(User user, String newPassKey) async {
256+
final response = await _service.postRequest<String>('changePasswordFinish', {}, user.tokens!.identity);
257+
user.passKey = newPassKey;
258+
259+
final jwts = List<String>.from(json.decode(response.data!)['JWTs']);
260+
await _parseJWTs(user, jwts);
261+
return;
262+
}
263+
242264
Future<void> _parseJWTs(User user, List<String> jwts, {bool notifyListeners = false}) async {
243265
user.tokens = Tokens();
244266

lib/widgets/account_create.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -677,10 +677,10 @@ class _AccountCreateWidgetState extends State<AccountCreateWidget> {
677677
return false;
678678
}
679679

680-
Future<void> changePassword(String password) async {
681-
final vaultCubit = BlocProvider.of<VaultCubit>(context);
682-
await vaultCubit.changeFreeUserPassword(password);
683-
}
680+
// Future<void> changePassword(String password) async {
681+
// final vaultCubit = BlocProvider.of<VaultCubit>(context);
682+
// await vaultCubit.changeFreeUserPassword(password);
683+
// }
684684

685685
subscribeUser(User user) async {
686686
final accountCubit = BlocProvider.of<AccountCubit>(context);

lib/widgets/change_password.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:collection/collection.dart';
22
import 'package:flutter/material.dart';
33
import 'package:flutter_bloc/flutter_bloc.dart';
44
import 'package:kdbx/kdbx.dart';
5+
import 'package:keevault/cubit/account_cubit.dart';
56
import '../cubit/vault_cubit.dart';
67
import '../generated/l10n.dart';
78
import '../widgets/password_strength.dart';
@@ -252,8 +253,10 @@ class _ChangePasswordWidgetState extends State<ChangePasswordWidget> {
252253
}
253254

254255
Future<void> changePassword(String password) async {
255-
//TODO: call accountcubit.changePassword if a user is signed in
256+
final accountCubit = BlocProvider.of<AccountCubit>(context);
256257
final vaultCubit = BlocProvider.of<VaultCubit>(context);
257-
await vaultCubit.changeFreeUserPassword(password);
258+
await ((accountCubit.currentUserIfIdKnown == null)
259+
? vaultCubit.changeFreeUserPassword(password)
260+
: vaultCubit.changeRemoteUserPassword(password));
258261
}
259262
}

lib/widgets/settings.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ class _SettingsWidgetState extends State<SettingsWidget> with TraceableClientMix
152152
SimpleSettingsTile(
153153
title: str.changeEmail,
154154
onTap: () {
155+
//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?
155156
BlocProvider.of<AccountCubit>(context).startEmailChange();
156157
},
157158
),

0 commit comments

Comments
 (0)