Skip to content

Commit c14ac5e

Browse files
committed
wip
1 parent a114324 commit c14ac5e

File tree

8 files changed

+141
-28
lines changed

8 files changed

+141
-28
lines changed

lib/cubit/account_cubit.dart

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -348,9 +348,6 @@ class AccountCubit extends Cubit<AccountState> {
348348
}
349349

350350
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?
354351
return true;
355352
} on KeeLoginFailedMITMException {
356353
rethrow;

lib/cubit/vault_cubit.dart

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'package:keevault/cubit/generator_profiles_cubit.dart';
1212
import 'package:keevault/extension_methods.dart';
1313
import 'package:keevault/local_vault_repository.dart';
1414
import 'package:keevault/locked_vault_file.dart';
15+
import 'package:keevault/password_mismatch_recovery_situation.dart';
1516
import 'package:keevault/password_strength.dart';
1617
import 'package:keevault/credentials/quick_unlocker.dart';
1718
import 'package:keevault/vault_backend/exceptions.dart';
@@ -29,16 +30,6 @@ import 'account_cubit.dart';
2930

3031
part 'vault_state.dart';
3132

32-
enum PasswordMismatchRecoverySituation {
33-
None,
34-
RemoteUserDiffers,
35-
RemoteFileDiffers,
36-
37-
//TODO: Probably can never know that these are the situation - can't tell the difference between user typing the wrong password and can't try the remote file password anyway until the service password is correct. Maybe delete these enum values?
38-
//RemoteUserAndFileDiffers,
39-
//AllThreeAreDifferent
40-
}
41-
4233
class VaultCubit extends Cubit<VaultState> {
4334
final RemoteVaultRepository _remoteVaultRepo;
4435
final LocalVaultRepository _localVaultRepo;
@@ -574,6 +565,18 @@ class VaultCubit extends Cubit<VaultState> {
574565
l.i('refresh called during an ongoing upload. Will not refresh now.');
575566
return;
576567
}
568+
if (s is VaultRefreshing) {
569+
l.i('refresh called during an ongoing refresh operation. Will not start a new refresh now.');
570+
return;
571+
}
572+
if (s is VaultRefreshCredentialsRequired && recovery == PasswordMismatchRecoverySituation.None) {
573+
l.i('refresh called during an ongoing refresh credentials repair operation. Will not refresh now.');
574+
return;
575+
}
576+
if (s is VaultUploadCredentialsRequired && recovery == PasswordMismatchRecoverySituation.None) {
577+
l.i('refresh called during an ongoing upload credentials repair operation. Will not refresh now.');
578+
return;
579+
}
577580
if (s is VaultChangingPassword) {
578581
l.i('refresh called during a password change. Will not refresh now.');
579582
return;
@@ -666,15 +669,27 @@ class VaultCubit extends Cubit<VaultState> {
666669
credentialsOverrideWithStrength.credentials,
667670
DateTime.now().add(Duration(days: requireFullPasswordPeriod)).millisecondsSinceEpoch,
668671
await updatedLocalFile.files.current.kdfCacheKey);
669-
}
670672

671-
if (tempLockedFile == null) {
672-
l.d("Latest remote file not changed so didn't download it");
673-
safeEmitLoaded(updatedLocalFile ?? s.vault);
674-
if (KeeVaultPlatform.isIOS) {
675-
await autofillMerge(user, onlyIfAttemptAlreadyDue: true);
673+
if (tempLockedFile == null) {
674+
l.d("Latest remote file not changed so didn't download it");
675+
safeEmitLoaded(updatedLocalFile);
676+
//TODO: save and upload LK to become RK. need to provide any override passwords/status?
677+
await save(user);
678+
if (KeeVaultPlatform.isIOS) {
679+
await autofillMerge(user, onlyIfAttemptAlreadyDue: true);
680+
}
681+
return;
682+
}
683+
//TODO: else... probably already handled by next step? need to check we use the updatedlocalfile as our local one since that now has the correct kdbx password.
684+
} else {
685+
if (tempLockedFile == null) {
686+
l.d("Latest remote file not changed so didn't download it");
687+
safeEmitLoaded(s.vault);
688+
if (KeeVaultPlatform.isIOS) {
689+
await autofillMerge(user, onlyIfAttemptAlreadyDue: true);
690+
}
691+
return;
676692
}
677-
return;
678693
}
679694
lockedFile = tempLockedFile;
680695
} on KdbxInvalidKeyException {
@@ -721,18 +736,23 @@ class VaultCubit extends Cubit<VaultState> {
721736
try {
722737
file = await RemoteVaultFile.unlock(lockedFile);
723738
successfulCredentials = lockedFile.credentials;
739+
// no password problem or situation ???
724740
} on KdbxInvalidKeyException {
725741
// Try again with the other password or let the higher catch statement handle this
726742
// We know we tried the remote creds first so if they are different to local, we should try those too.
727743
if (credsRemote != credsLocal) {
728744
final lockedFileWithLocalCreds = lockedFile.copyWith(credentials: credsLocal);
729745
file = await RemoteVaultFile.unlock(lockedFileWithLocalCreds);
730746
successfulCredentials = lockedFileWithLocalCreds.credentials;
747+
// 2 or 3?
748+
// if (updatedLocalFile != null) 3 else 2?
731749
} else {
732750
rethrow;
733751
}
734752
}
735753

754+
// RemoteFileDiffers only time and safe time to save the credentials? we are saying whatever creds worked to open the remote file is what we will save as the credentials needed to open the local file.
755+
// updatedLocalFile must be null if we are in remotefilediffers mode
736756
if (recovery == PasswordMismatchRecoverySituation.RemoteFileDiffers && successfulCredentials != null) {
737757
l.d('Updating QU with newly successful KDBX password');
738758
final requireFullPasswordPeriod =
@@ -747,7 +767,7 @@ class VaultCubit extends Cubit<VaultState> {
747767
await prefs.setString('user.${user.email}.lastRemoteEtag', file.etag!);
748768
await prefs.setString('user.${user.email}.lastRemoteVersionId', file.versionId!);
749769
emit(VaultUpdatingLocalFromRemote(updatedLocalFile ?? s.vault));
750-
final newFile = await update(user, s.vault, file);
770+
final newFile = await update(user, updatedLocalFile ?? s.vault, file);
751771
if (newFile != null) {
752772
await emitVaultLoaded(newFile, user, immediateRemoteRefresh: false, safe: true);
753773
}
@@ -774,7 +794,7 @@ class VaultCubit extends Cubit<VaultState> {
774794
l.d('refresh called during an import. Will not refresh now.');
775795
return;
776796
}
777-
throw Exception('Vault not loaded when refresh called');
797+
l.w('Vault not loaded when refresh called');
778798
}
779799
}
780800

@@ -1610,8 +1630,10 @@ class VaultCubit extends Cubit<VaultState> {
16101630

16111631
void startEmailChange() {
16121632
if (currentVaultFile == null || (!currentVaultFile!.files.current.isDirty && _entryCubit.state is! EntryLoaded)) {
1613-
//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
1614-
// _accountCubit.startEmailChange();
1633+
//TODO: test if just signing out triggers the request to sign in again automatically or if it is
1634+
// to do with the change to the account cubit
1635+
// result: Yes, signout results in immediate re-signin so cubit account request can never work. need to do something different to sign-out. maybe forget the user or sign out the account level user too? would be good to remeember their email address though since really we only want to sign them out from the vault, not their user account.
1636+
_accountCubit.startEmailChange();
16151637
signout();
16161638
} else {
16171639
emitError('You must save your changes first!', toast: true);
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
enum PasswordMismatchRecoverySituation {
2+
None,
3+
RemoteUserDiffers,
4+
RemoteFileDiffers,
5+
6+
//TODO: Probably can never know that these are the situation - can't tell the difference between user typing the wrong password and can't try the remote file password anyway until the service password is correct. Maybe delete these enum values?
7+
//RemoteUserAndFileDiffers,
8+
//AllThreeAreDifferent
9+
}
10+
11+
/*
12+
13+
We try to automatically resolve all mismatched password situations here.
14+
15+
There are 3 current passwords we need to align - they generally always are aligned but crashes or bugs, particularly during a password change procedure, can cause them to get out of sync.
16+
17+
The 3 passwords are:
18+
Local KDBX file - the remoteMargeTarget unless current is newer
19+
Remote KDBX file
20+
Remote user authentication password
21+
22+
We'll need to align on the remote authentication password so that the user doesn't get stuck in a constant battle between conflicting local passwords on multiple devices.
23+
24+
Slightly old tokens being used to upload modified RK could result in state 1 or 2.
25+
26+
p1 p2 and p3 have no specific temporal ordering. Depending on how we ended up in one of these states, any of those could be considered "newest".
27+
28+
Thus the possible failure modes are:
29+
30+
1) RemoteUserDiffers
31+
32+
LK = p1
33+
RK = p1
34+
RU = p2
35+
36+
Symptom:
37+
User will be unable to download or upload the RK file
38+
39+
Solution:
40+
User provides an override password which we use for authentication.
41+
Need to use non-override password to unlock RK. But we don't know in advance if we are in this state or 3. So we assume 3 and actually progress towards state 2. Ideally we resolve in the same step by just trying again with p1 but in worst case user could enter a password again.
42+
If RK is newer, download it, change its password to p2, change LK password to p2, merge it, store result as LK and upload as RK.
43+
If RK is not newer, change LK password to p2, store result as LK and upload as RK.
44+
45+
46+
2) RemoteFileDiffers
47+
48+
LK = p1
49+
RK = p2
50+
RU = p1
51+
52+
Symptom:
53+
User will be unable to unlock the RK file while merging for upload or downloading for refresh/syncing.
54+
55+
Solution:
56+
User provides an override password which we use for unlocking the RK.
57+
Need to use non-override password to download/upload RK. So we need to know that RU worked when getting p2 from the user.
58+
If RK is newer, download it, change its password to p1, merge it, store result as LK and upload as RK.
59+
If RK is not newer, upload LK as RK.
60+
61+
62+
3) RemoteUserAndFileDiffers
63+
64+
LK = p1
65+
RK = p2
66+
RU = p2
67+
68+
Symptom:
69+
User will be unable to download or modify the RK file, nor unlock it using their local password
70+
71+
Solution:
72+
User provides an override password which we use for authentication and to unlock RK.
73+
If RK is newer, download it, change LK password to p2, merge it, store result as LK and upload as RK.
74+
If RK is not newer, change LK password to p2, store result as LK and upload as RK.
75+
76+
4) AllThreeAreDifferent
77+
78+
LK = p1
79+
RK = p2
80+
RU = p3
81+
82+
Solution:
83+
Ignore. We hope that existing support for other situations will allow the user to muddle through to a resolution in multiple steps.
84+
85+
86+
87+
Generally, we will need to track whether it was the RK or RU that failed. That lets us determine between states 1,2 and 3+4. We know we are in 3+4 if trying to resolve 3 fails at either the access request or RK unlocking stage. Once the user enters p3, we can change LK with that password and thus effectively reduce the problem to state 1 (which user can then resolve by entering what was p2 in this scenario). We won't implement all of this edge case but expect at least that forcibly killing the app at the right time will allow the user to recover.
88+
*/

lib/vault_file.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ class VaultFileVersions {
7979
..header.writeKdfParameters(credentialsWithStrength.createNewKdfParameters());
8080
final kdbxData = await unlockedFile.save();
8181
return VaultFileVersions(
82-
current: _current,
82+
current: unlockedFile,
8383
pending: null,
8484
pendingLocked: null,
8585
remoteMergeTarget: null,

lib/widgets/account_email_change.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import 'package:email_validator/email_validator.dart';
44
import 'package:flutter/material.dart';
55
import 'package:flutter_bloc/flutter_bloc.dart';
66
import 'package:keevault/cubit/account_cubit.dart';
7+
import '../config/app.dart';
8+
import '../config/routes.dart';
79
import '../generated/l10n.dart';
810

911
typedef SubmitCallback = Future<void> Function(String string);
@@ -22,6 +24,7 @@ class _AccountEmailChangeWidgetState extends State<AccountEmailChangeWidget> {
2224
// We always sign the user out. Might be nice to send them back to their Vault if that's where they came from (i.e. they have a validated and active account) but might be hard to do securely so will ignore that edge case initially.
2325
final accountCubit = BlocProvider.of<AccountCubit>(context);
2426
await accountCubit.signout();
27+
await AppConfig.router.navigateTo(AppConfig.navigatorKey.currentContext!, Routes.root, clearStack: true);
2528
}
2629

2730
Future<void> changeEmailAddress(String password, String newEmailAddress) async {

lib/widgets/root.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ class RootWidgetState extends State<RootWidget> {
3636
await BlocProvider.of<AutofillCubit>(context).refresh();
3737
await accountCubit.startup();
3838
final AccountState state = accountCubit.state;
39-
if (state is AccountChosen) {
39+
//TODO: need more exceptions like for email verification etc?
40+
if (state is AccountChosen && state is! AccountEmailChangeRequested) {
4041
await vaultCubit.startup(state.user, null);
4142
} else if (state is AccountLocalOnly) {
4243
await vaultCubit.startupFreeMode(null);

lib/widgets/vault.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'package:keevault/cubit/account_cubit.dart';
1010
import 'package:keevault/cubit/app_settings_cubit.dart';
1111
import 'package:keevault/cubit/autofill_cubit.dart';
1212
import 'package:keevault/cubit/filter_cubit.dart';
13+
import 'package:keevault/password_mismatch_recovery_situation.dart';
1314
import 'package:keevault/vault_backend/exceptions.dart';
1415
import 'package:keevault/widgets/vault_password_credentials.dart';
1516

@@ -56,7 +57,7 @@ class _VaultWidgetState extends State<VaultWidget> with WidgetsBindingObserver {
5657
final user = BlocProvider.of<AccountCubit>(context).currentUser;
5758
final VaultState vaultState = BlocProvider.of<VaultCubit>(context).state;
5859
PasswordMismatchRecoverySituation recovery = PasswordMismatchRecoverySituation.None;
59-
if (vaultState is VaultUploadCredentialsRequired) {
60+
if (vaultState is VaultRefreshCredentialsRequired) {
6061
recovery = vaultState.recovery;
6162
}
6263
await BlocProvider.of<VaultCubit>(context).refresh(user, overridePasswordRemote: password, recovery: recovery);

lib/widgets/vault_loader.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,8 @@ class VaultLoaderState extends State<VaultLoaderWidget> {
153153
}
154154
return LoadingSpinner(tooltip: 'Unknown app state: ${state.toString()}');
155155
}, listener: (context, state) async {
156-
if (state is VaultLoaded) {
156+
//TODO: why do we need the refreshing exception? do we need any others? does this fix any autofill reliability issues too?
157+
if (state is VaultLoaded && state is! VaultRefreshing) {
157158
final AutofillState autofillState = BlocProvider.of<AutofillCubit>(context).state;
158159
final filterContext = BlocProvider.of<FilterCubit>(context);
159160
final interactionContext = BlocProvider.of<InteractionCubit>(context);

0 commit comments

Comments
 (0)