mirror of
https://github.com/laurent22/joplin.git
synced 2026-10-25 02:47:28 +00:00
Desktop, Mobile: Resolves #16710: Improve E2EE UI when a key is stored in an unknown format (#16711)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
This commit is contained in:
1 parent
9e4ca948be
commit
ab241bd8d6
9 files changed
+165
-27
No files matched your search
@@ -5,10 +5,10 @@ import { _ } from '@joplin/lib/locale';
|
||||
import time from '@joplin/lib/time';
|
||||
import shim, { MessageBoxType } from '@joplin/lib/shim';
|
||||
import dialogs from '../dialogs';
|
||||
import { decryptedStatText, determineKeyPassword, dontReencryptData, enableEncryptionConfirmationMessages, onSavePasswordClick, onToggleEnabledClick, reencryptData, upgradeMasterKey, useInputPasswords, useNeedMasterPassword, usePasswordChecker, useStats, useToggleShowDisabledMasterKeys } from '@joplin/lib/components/EncryptionConfigScreen/utils';
|
||||
import { decryptedStatText, determineKeyPassword, dontReencryptData, enableEncryptionConfirmationMessages, onSavePasswordClick, onToggleEnabledClick, reencryptData, upgradeMasterKey, useHasUnknownEncryptionMethodKeysMessage, useInputPasswords, useNeedMasterPassword, usePasswordChecker, useStats, useToggleShowDisabledMasterKeys } from '@joplin/lib/components/EncryptionConfigScreen/utils';
|
||||
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
|
||||
import { getEncryptionEnabled, localSyncInfoSelector, masterKeyEnabled } from '@joplin/lib/services/synchronizer/syncInfoUtils';
|
||||
import { getDefaultMasterKey, getMasterPasswordStatusMessage, masterPasswordIsValid, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
|
||||
import { getDefaultMasterKey, getMasterPasswordStatusMessage, isKnownEncryptionMethod, masterPasswordIsValid, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
|
||||
import Button, { ButtonLevel } from '../Button/Button';
|
||||
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
@@ -76,6 +76,7 @@ export const EncryptionConfigScreen = (props: Props) => {
|
||||
const { passwordChecks, masterPasswordKeys, masterPasswordStatus } = usePasswordChecker(props.masterKeys, props.activeMasterKeyId, props.masterPassword, props.passwords);
|
||||
const { showDisabledMasterKeys, toggleShowDisabledMasterKeys } = useToggleShowDisabledMasterKeys();
|
||||
const needMasterPassword = useNeedMasterPassword(passwordChecks, props.masterKeys);
|
||||
const unknownEncryptionMethodMessage = useHasUnknownEncryptionMethodKeysMessage(props.masterKeys);
|
||||
|
||||
useEffect(() => {
|
||||
const wasOpen = wasMasterPasswordDialogOpen.current;
|
||||
@@ -173,6 +174,7 @@ export const EncryptionConfigScreen = (props: Props) => {
|
||||
const password = inputPasswords[mk.id] ? inputPasswords[mk.id] : '';
|
||||
const isActive = props.activeMasterKeyId === mk.id;
|
||||
const activeIcon = isActive ? '✔' : '';
|
||||
const knownKeyType = isKnownEncryptionMethod(mk.encryption_method);
|
||||
const passwordOk = passwordChecks[mk.id] === true ? '✔' : '❌';
|
||||
|
||||
const renderPasswordInput = (masterKeyId: string) => {
|
||||
@@ -207,7 +209,9 @@ export const EncryptionConfigScreen = (props: Props) => {
|
||||
<td style={theme.textStyle}>{mk.id}<br/>{_('Source: ')}{mk.source_application}</td>
|
||||
<td style={theme.textStyle}>{_('Created: ')}{time.formatMsToLocal(mk.created_time)}<br/>{_('Updated: ')}{time.formatMsToLocal(mk.updated_time)}</td>
|
||||
{renderPasswordInput(mk.id)}
|
||||
<td style={theme.textStyle}>{passwordOk}</td>
|
||||
<td
|
||||
style={theme.textStyle}
|
||||
>{knownKeyType ? passwordOk : <span title={_('Unknown key format')}>❓</span>}</td>
|
||||
<td style={theme.textStyle}>
|
||||
<button style={theme.buttonStyle} onClick={() => onToggleEnabledClick(mk)}>{masterKeyEnabled(mk) ? _('Disable') : _('Enable')}</button>
|
||||
</td>
|
||||
@@ -456,7 +460,7 @@ export const EncryptionConfigScreen = (props: Props) => {
|
||||
const buttonTitle = CommandService.instance().label('openMasterPasswordDialog');
|
||||
|
||||
const needPasswordMessage = !needMasterPassword ? null : (
|
||||
<p className="needpassword">
|
||||
<p className="warning">
|
||||
{_('Your password is needed to decrypt some of your data.')}
|
||||
<br/>
|
||||
{_('Please click on "%s" to proceed, or set the passwords in the "%s" list below.', buttonTitle, _('Encryption keys'))}
|
||||
@@ -468,12 +472,19 @@ export const EncryptionConfigScreen = (props: Props) => {
|
||||
</p>
|
||||
);
|
||||
|
||||
const needUpgradeAppMessage = !!unknownEncryptionMethodMessage && (
|
||||
<p className='warning'>
|
||||
{unknownEncryptionMethodMessage}
|
||||
</p>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="section">
|
||||
<div className="manage-password-section">
|
||||
<h2>{_('Master password')}</h2>
|
||||
<p className="status"><span>{_('Master password:')}</span> <span className="bold">{getMasterPasswordStatusMessage(masterPasswordStatus)}</span></p>
|
||||
{needPasswordMessage}
|
||||
{needUpgradeAppMessage}
|
||||
<Button className="managebutton" level={needMasterPassword ? ButtonLevel.Primary : ButtonLevel.Secondary} onClick={onManageMasterPassword} title={buttonTitle} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.manage-password-section > .needpassword {
|
||||
.manage-password-section > .warning {
|
||||
color: var(--joplin-color-warn);
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import Logger from '@joplin/utils/Logger';
|
||||
import { AppState } from '../app.reducer';
|
||||
import { localSyncInfoFromState } from '@joplin/lib/services/synchronizer/syncInfoUtils';
|
||||
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
|
||||
import { showMissingMasterKeyMessage } from '@joplin/lib/services/e2ee/utils';
|
||||
import { showMissingMasterKeyMessage, showUnknownKeyFormatBanner } from '@joplin/lib/services/e2ee/utils';
|
||||
import shouldShowMissingPasswordWarning from '@joplin/lib/components/shared/config/shouldShowMissingPasswordWarning';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
@@ -32,6 +32,7 @@ interface Props {
|
||||
processingShareInvitationResponse: boolean;
|
||||
shareInvitations: ShareInvitation[];
|
||||
hasDisabledSyncItems: boolean;
|
||||
showUnknownKeyFormatMessage: boolean;
|
||||
showMissingMasterKeyMessage: boolean;
|
||||
mustUpgradeAppMessage: string;
|
||||
syncTargetAppMinVersion: string;
|
||||
@@ -225,6 +226,12 @@ const WarningBanner: React.FC<Props> = props => {
|
||||
} else {
|
||||
msg = renderNotificationMessage(props.mustUpgradeAppMessage);
|
||||
}
|
||||
} else if (props.showUnknownKeyFormatMessage) {
|
||||
msg = renderNotificationMessage(
|
||||
_('One or more encryption keys are stored in an unknown format.'),
|
||||
_('Manage'),
|
||||
onViewEncryptionConfigScreen,
|
||||
);
|
||||
} else if (props.shouldSwitchToAppleSiliconVersion) {
|
||||
msg = renderNotificationMessage(
|
||||
_('You are running the Intel version of Joplin on an Apple Silicon processor. Download the Apple Silicon one for better performance.'),
|
||||
@@ -283,6 +290,7 @@ const mapStateToProps = (state: AppState) => {
|
||||
hasDisabledSyncItems: state.hasDisabledSyncItems,
|
||||
hasDisabledEncryptionItems: state.hasDisabledEncryptionItems,
|
||||
showMissingMasterKeyMessage: showMissingMasterKeyMessage(syncInfo, state.notLoadedMasterKeys),
|
||||
showUnknownKeyFormatMessage: showUnknownKeyFormatBanner(syncInfo),
|
||||
showNeedUpgradingMasterKeyMessage: showNeedUpgradingEnabledMasterKeyMessage,
|
||||
showShouldReencryptMessage: state.settings['encryption.shouldReencrypt'] >= Setting.SHOULD_REENCRYPT_YES,
|
||||
shouldUpgradeSyncTarget: state.settings['sync.upgradeState'] === Setting.SYNC_UPGRADE_STATE_SHOULD_DO,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Platform } from 'react-native';
|
||||
import { AppState } from '../../utils/types';
|
||||
import WarningBox, { WarningBoxTarget } from './WarningBox';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { showMissingMasterKeyMessage } from '@joplin/lib/services/e2ee/utils';
|
||||
import { showMissingMasterKeyMessage, showUnknownKeyFormatBanner } from '@joplin/lib/services/e2ee/utils';
|
||||
import { localSyncInfoFromState } from '@joplin/lib/services/synchronizer/syncInfoUtils';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { ShareInvitation, ShareUserStatus } from '@joplin/lib/services/share/reducer';
|
||||
@@ -20,6 +20,7 @@ const logger = Logger.create('WarningBanner');
|
||||
interface Props {
|
||||
themeId: number;
|
||||
showMissingMasterKeyMessage: boolean;
|
||||
showUnknownKeyFormatMessage: boolean;
|
||||
hasDisabledSyncItems: boolean;
|
||||
shouldUpgradeSyncTarget: boolean;
|
||||
showShouldUpgradeSyncTargetMessage: boolean|undefined;
|
||||
@@ -110,6 +111,11 @@ const WarningBannerComponent: React.FC<Props> = props => {
|
||||
if (props.showMissingMasterKeyMessage) {
|
||||
warningComps.push(renderWarningBox('missingDecryptionPassword', _('Press to set the decryption password.'), { screen: 'EncryptionConfig' }));
|
||||
}
|
||||
if (props.showUnknownKeyFormatMessage) {
|
||||
warningComps.push(
|
||||
renderWarningBox('unknownKeyFormat', _('One or more encryption keys are stored in an unknown format. Press for more info.'), { screen: 'EncryptionConfig' }),
|
||||
);
|
||||
}
|
||||
if (props.hasDisabledSyncItems) {
|
||||
warningComps.push(renderWarningBox('cannotSync', _('Some items cannot be synchronised. Press for more info.'), { screen: 'Status' }));
|
||||
}
|
||||
@@ -157,6 +163,10 @@ const isSyncLoginRoute = (state: AppState) => {
|
||||
return false;
|
||||
};
|
||||
|
||||
const isEncryptionConfigRoute = (state: AppState) => {
|
||||
return state.route?.routeName === 'EncryptionConfig';
|
||||
};
|
||||
|
||||
export default connect((state: AppState) => {
|
||||
const syncInfo = localSyncInfoFromState(state);
|
||||
|
||||
@@ -166,7 +176,8 @@ export default connect((state: AppState) => {
|
||||
noteSelectionEnabled: state.noteSelectionEnabled,
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
notesParentType: state.notesParentType,
|
||||
showMissingMasterKeyMessage: showMissingMasterKeyMessage(syncInfo, state.notLoadedMasterKeys),
|
||||
showMissingMasterKeyMessage: !isEncryptionConfigRoute(state) && showMissingMasterKeyMessage(syncInfo, state.notLoadedMasterKeys),
|
||||
showUnknownKeyFormatMessage: !isEncryptionConfigRoute(state) && showUnknownKeyFormatBanner(syncInfo),
|
||||
hasDisabledSyncItems: state.hasDisabledSyncItems,
|
||||
shouldUpgradeSyncTarget: state.settings['sync.upgradeState'] === Setting.SYNC_UPGRADE_STATE_SHOULD_DO,
|
||||
mustUpgradeAppMessage: state.mustUpgradeAppMessage,
|
||||
|
||||
@@ -6,15 +6,22 @@ import { themeStyle } from '../global-style';
|
||||
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import time from '@joplin/lib/time';
|
||||
import { decryptedStatText, enableEncryptionConfirmationMessages, onSavePasswordClick, useInputMasterPassword, useInputPasswords, usePasswordChecker, useStats } from '@joplin/lib/components/EncryptionConfigScreen/utils';
|
||||
import { decryptedStatText, enableEncryptionConfirmationMessages, onSavePasswordClick, useHasUnknownEncryptionMethodKeysMessage, useInputMasterPassword, useInputPasswords, usePasswordChecker, useStats } from '@joplin/lib/components/EncryptionConfigScreen/utils';
|
||||
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
|
||||
import { State } from '@joplin/lib/reducer';
|
||||
import { localSyncInfoSelector, masterKeyEnabled } from '@joplin/lib/services/synchronizer/syncInfoUtils';
|
||||
import { getDefaultMasterKey, setupAndDisableEncryption, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
|
||||
import { getDefaultMasterKey, getMasterPasswordStatusMessage, isKnownEncryptionMethod, setupAndDisableEncryption, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Divider, List } from 'react-native-paper';
|
||||
import shim from '@joplin/lib/shim';
|
||||
|
||||
const allMasterPasswordKeysStoredInUnknownFormat = (keyIds: string[], allKeys: MasterKeyEntity[]) => {
|
||||
return keyIds.length > 0 && keyIds.every(id => {
|
||||
const masterKey = allKeys.find(key => key.id === id);
|
||||
return !!masterKey && !isKnownEncryptionMethod(masterKey.encryption_method);
|
||||
});
|
||||
};
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
masterKeys: MasterKeyEntity[];
|
||||
@@ -30,7 +37,7 @@ const EncryptionConfigScreen = (props: Props) => {
|
||||
const [passwordPromptAnswer, setPasswordPromptAnswer] = useState('');
|
||||
const [passwordPromptConfirmAnswer, setPasswordPromptConfirmAnswer] = useState('');
|
||||
const stats = useStats();
|
||||
const { passwordChecks, masterPasswordKeys } = usePasswordChecker(props.masterKeys, props.activeMasterKeyId, props.masterPassword, props.passwords);
|
||||
const { passwordChecks, masterPasswordKeys, masterPasswordStatus } = usePasswordChecker(props.masterKeys, props.activeMasterKeyId, props.masterPassword, props.passwords);
|
||||
const { inputPasswords, onInputPasswordChange } = useInputPasswords(props.passwords);
|
||||
const { inputMasterPassword, onMasterPasswordSave, onMasterPasswordChange } = useInputMasterPassword(props.masterKeys, props.activeMasterKeyId);
|
||||
const [showDisabledKeys, setShowDisabledKeys] = useState(false);
|
||||
@@ -69,6 +76,27 @@ const EncryptionConfigScreen = (props: Props) => {
|
||||
fontSize: theme.fontSize,
|
||||
color: theme.color,
|
||||
},
|
||||
warningText: {
|
||||
...theme.normalText,
|
||||
color: theme.color,
|
||||
},
|
||||
warningTextBold: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
warningBox: {
|
||||
marginBottom: theme.itemMarginBottom,
|
||||
backgroundColor: theme.warningBackgroundColor,
|
||||
paddingTop: 5,
|
||||
paddingBottom: 5,
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
},
|
||||
masterPasswordLabel: {
|
||||
...theme.normalText,
|
||||
flex: 0,
|
||||
flexBasis: 'auto',
|
||||
marginRight: 5,
|
||||
},
|
||||
normalTextInput: {
|
||||
margin: 10,
|
||||
color: theme.color,
|
||||
@@ -77,12 +105,16 @@ const EncryptionConfigScreen = (props: Props) => {
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
flexBasis: 'auto',
|
||||
padding: theme.margin,
|
||||
},
|
||||
disabledContainer: {
|
||||
paddingLeft: theme.margin,
|
||||
paddingRight: theme.margin,
|
||||
},
|
||||
unknownKeyFormatMessage: {
|
||||
paddingTop: theme.margin,
|
||||
},
|
||||
});
|
||||
}, [theme]);
|
||||
|
||||
@@ -141,6 +173,9 @@ const EncryptionConfigScreen = (props: Props) => {
|
||||
accessibilityRole='header'
|
||||
>{_('Master Key %s', mk.id.substr(0, 6))}</Text>
|
||||
<Text style={styles.normalText}>{_('Created: %s', time.formatMsToLocal(mk.created_time))}</Text>
|
||||
{!isKnownEncryptionMethod(mk.encryption_method) && (
|
||||
<Text style={styles.normalText}>{_('Status: Unsupported key format')}</Text>
|
||||
)}
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Text style={{ flex: 0, fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{_('Password:')}</Text>
|
||||
{renderPasswordInput(mk.id)}
|
||||
@@ -255,11 +290,16 @@ const EncryptionConfigScreen = (props: Props) => {
|
||||
borderBottomColor: theme.dividerColor,
|
||||
};
|
||||
|
||||
if (passwordChecks['master']) {
|
||||
if (
|
||||
passwordChecks['master'] ||
|
||||
allMasterPasswordKeysStoredInUnknownFormat(
|
||||
Object.keys(masterPasswordKeys), props.masterKeys,
|
||||
)
|
||||
) {
|
||||
return (
|
||||
<View style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Text style={{ ...styles.normalText, flex: 0, marginRight: 5 }}>{_('Master password:')}</Text>
|
||||
<Text style={{ ...styles.normalText, fontWeight: 'bold' }}>{_('Loaded')}</Text>
|
||||
<Text style={styles.masterPasswordLabel}>{_('Master password:')}</Text>
|
||||
<Text style={{ ...styles.normalText, fontWeight: 'bold' }}>{getMasterPasswordStatusMessage(masterPasswordStatus)}</Text>
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
@@ -360,23 +400,32 @@ const EncryptionConfigScreen = (props: Props) => {
|
||||
</View>
|
||||
</List.Accordion> : null;
|
||||
|
||||
const unknownKeyFormatText = useHasUnknownEncryptionMethodKeysMessage(props.masterKeys);
|
||||
const unknownKeyFormatMessage = !!unknownKeyFormatText && <View style={styles.warningBox}>
|
||||
<Text
|
||||
style={styles.warningText}
|
||||
><Text style={styles.warningTextBold}>{_('Warning: ')}</Text>{unknownKeyFormatText}</Text>
|
||||
</View>;
|
||||
|
||||
return (
|
||||
<View style={rootStyle}>
|
||||
<ScreenHeader title={_('Encryption Config')} />
|
||||
<ScrollView>
|
||||
<View style={styles.container}>
|
||||
<View style={{ backgroundColor: theme.warningBackgroundColor, paddingTop: 5, paddingBottom: 5, paddingLeft: 10, paddingRight: 10 }}>
|
||||
<Text>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</Text>
|
||||
<View style={styles.warningBox}>
|
||||
<Text style={styles.warningText}>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
void Linking.openURL('https://joplinapp.org/help/apps/sync/e2ee');
|
||||
}}
|
||||
accessibilityRole='link'
|
||||
>
|
||||
<Text>https://joplinapp.org/help/apps/sync/e2ee</Text>
|
||||
<Text style={styles.warningText}>https://joplinapp.org/help/apps/sync/e2ee</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{unknownKeyFormatMessage}
|
||||
|
||||
<Text
|
||||
style={styles.titleText}
|
||||
accessibilityRole='header'
|
||||
|
||||
@@ -3,7 +3,7 @@ import { _ } from '../../locale';
|
||||
import BaseItem, { EncryptedItemsStats } from '../../models/BaseItem';
|
||||
import useAsyncEffect, { AsyncEffectEvent } from '../../hooks/useAsyncEffect';
|
||||
import { MasterKeyEntity } from '../../services/e2ee/types';
|
||||
import { findMasterKeyPassword, getMasterPasswordStatus, loadMasterKeysFromSettings, masterPasswordIsValid, MasterPasswordStatus } from '../../services/e2ee/utils';
|
||||
import { findMasterKeyPassword, getMasterPasswordStatus, isKnownEncryptionMethod, loadMasterKeysFromSettings, masterPasswordIsValid, MasterPasswordStatus } from '../../services/e2ee/utils';
|
||||
import EncryptionService from '../../services/e2ee/EncryptionService';
|
||||
import { masterKeyEnabled, setMasterKeyEnabled } from '../../services/synchronizer/syncInfoUtils';
|
||||
import MasterKey from '../../models/MasterKey';
|
||||
@@ -212,11 +212,29 @@ export const useNeedMasterPassword = (passwordChecks: PasswordChecks, masterKeys
|
||||
const mk = masterKeys.find(mk => mk.id === mkId);
|
||||
if (!mk) continue;
|
||||
if (!masterKeyEnabled(mk)) continue;
|
||||
if (!valid) return true;
|
||||
if (!valid && isKnownEncryptionMethod(mk.encryption_method)) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const useHasUnknownEncryptionMethodKeys = (masterKeys: MasterKeyEntity[]) => {
|
||||
for (const mk of masterKeys) {
|
||||
if (mk && masterKeyEnabled(mk) && !isKnownEncryptionMethod(mk.encryption_method)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const useHasUnknownEncryptionMethodKeysMessage = (masterKeys: MasterKeyEntity[]) => {
|
||||
const hasUnknownEncryptionMethodKeys = useHasUnknownEncryptionMethodKeys(masterKeys);
|
||||
if (!hasUnknownEncryptionMethodKeys) return '';
|
||||
return [
|
||||
_('One or more encryption keys are stored in an unknown format.'),
|
||||
_('These keys were most likely created by a newer version of Joplin. Please check that Joplin has been updated to the latest version.'),
|
||||
].join('\n\n');
|
||||
};
|
||||
|
||||
export const determineKeyPassword = (masterKeyId: string, masterPasswordKeys: PasswordChecks, masterPassword: string, passwords: Record<string, string>): string => {
|
||||
if (masterPasswordKeys[masterKeyId]) return masterPassword;
|
||||
return passwords[masterKeyId];
|
||||
|
||||
@@ -286,7 +286,10 @@ export default class EncryptionService {
|
||||
}
|
||||
|
||||
public masterKeysThatNeedUpgrading(masterKeys: MasterKeyEntity[]) {
|
||||
return MasterKey.allWithoutEncryptionMethod(masterKeys, [this.defaultMasterKeyEncryptionMethod_, EncryptionMethod.Custom]);
|
||||
const keysWithNonDefaultMethods = MasterKey.allWithoutEncryptionMethod(masterKeys, [
|
||||
this.defaultMasterKeyEncryptionMethod_, EncryptionMethod.Custom,
|
||||
]);
|
||||
return keysWithNonDefaultMethods.filter(key => key.encryption_method in EncryptionMethod);
|
||||
}
|
||||
|
||||
public async reencryptMasterKey(model: MasterKeyEntity, decryptionPassword: string, encryptionPassword: string, decryptOptions: EncryptOptions = null, encryptOptions: EncryptOptions = null): Promise<MasterKeyEntity> {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { afterAllCleanUp, setupDatabaseAndSynchronizer, switchClient, encryptionService, expectNotThrow, expectThrow, kvStore, msleep } from '../../testing/test-utils';
|
||||
import MasterKey from '../../models/MasterKey';
|
||||
import { activeMasterKeySanityCheck, migrateMasterPassword, migratePpk, resetMasterPassword, showMissingMasterKeyMessage, updateMasterPassword } from './utils';
|
||||
import { activeMasterKeySanityCheck, migrateMasterPassword, migratePpk, resetMasterPassword, showMissingMasterKeyMessage, showUnknownKeyFormatBanner, updateMasterPassword } from './utils';
|
||||
import { localSyncInfo, masterKeyById, masterKeyEnabled, saveLocalSyncInfo, setActiveMasterKeyId, setMasterKeyEnabled, setPpk } from '../synchronizer/syncInfoUtils';
|
||||
import Setting from '../../models/Setting';
|
||||
import { generateKeyPair, generateKeyPairWithAlgorithm, getPpkAlgorithm, ppkPasswordIsValid, testing__setPpkMigrations_ } from './ppk/ppk';
|
||||
import { PublicKeyAlgorithm } from './types';
|
||||
import { EncryptionMethod } from './EncryptionService';
|
||||
|
||||
describe('e2ee/utils', () => {
|
||||
|
||||
@@ -43,6 +44,19 @@ describe('e2ee/utils', () => {
|
||||
expect(showMissingMasterKeyMessage(syncInfo, [mk1.id, mk2.id])).toBe(false);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[EncryptionMethod.KeyV1, false],
|
||||
[1234, true],
|
||||
[{ toString: 'throws if converted to string with String(...)' }, true],
|
||||
])('should decide whether to show the unknown key format banner (encryption method: %j, should show: %j)', async (encryptionMethod, shouldShow) => {
|
||||
await MasterKey.save({
|
||||
...await encryptionService().generateMasterKey('111111'),
|
||||
encryption_method: encryptionMethod as unknown as number,
|
||||
});
|
||||
|
||||
expect(showUnknownKeyFormatBanner(localSyncInfo())).toBe(shouldShow);
|
||||
});
|
||||
|
||||
it('should do ppk migration', async () => {
|
||||
const { reset } = testing__setPpkMigrations_([PublicKeyAlgorithm.RsaV1, PublicKeyAlgorithm.RsaV2]);
|
||||
|
||||
|
||||
@@ -3,13 +3,15 @@ import BaseItem from '../../models/BaseItem';
|
||||
import MasterKey from '../../models/MasterKey';
|
||||
import Setting from '../../models/Setting';
|
||||
import { MasterKeyEntity } from './types';
|
||||
import EncryptionService from './EncryptionService';
|
||||
import EncryptionService, { EncryptionMethod } from './EncryptionService';
|
||||
import { getActiveMasterKey, getActiveMasterKeyId, localSyncInfo, masterKeyEnabled, saveLocalSyncInfo, setActiveMasterKeyId, setEncryptionEnabled, SyncInfo } from '../synchronizer/syncInfoUtils';
|
||||
import JoplinError from '../../JoplinError';
|
||||
import { generateKeyPair, pkReencryptPrivateKey, ppkPasswordIsValid, shouldUpdatePpk } from './ppk/ppk';
|
||||
import KvStore from '../KvStore';
|
||||
import Folder from '../../models/Folder';
|
||||
import ShareService from '../share/ShareService';
|
||||
import { hasOwnProperty } from '@joplin/utils/object';
|
||||
import { _ } from '../../locale';
|
||||
|
||||
const logger = Logger.create('e2ee/utils');
|
||||
|
||||
@@ -135,6 +137,11 @@ export const migratePpk = async () => {
|
||||
saveLocalSyncInfo(syncInfo);
|
||||
};
|
||||
|
||||
export const isKnownEncryptionMethod = (method: EncryptionMethod|number) => {
|
||||
if (typeof method !== 'number') return false;
|
||||
return hasOwnProperty(EncryptionMethod, String(method));
|
||||
};
|
||||
|
||||
// All master keys normally should be decrypted with the master password, however
|
||||
// previously any master key could be encrypted with any password, so to support
|
||||
// this legacy case, we first check if the MK decrypts with the master password.
|
||||
@@ -202,8 +209,18 @@ export const activeMasterKeySanityCheck = () => {
|
||||
setActiveMasterKeyId(latestMasterKey.id);
|
||||
};
|
||||
|
||||
export const showUnknownKeyFormatBanner = (syncInfo: SyncInfo) => {
|
||||
for (const masterKey of syncInfo.masterKeys) {
|
||||
if (masterKeyEnabled(masterKey) && !isKnownEncryptionMethod(masterKey.encryption_method)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export function showMissingMasterKeyMessage(syncInfo: SyncInfo, notLoadedMasterKeys: string[]) {
|
||||
if (!syncInfo.masterKeys.length) return false;
|
||||
if (showUnknownKeyFormatBanner(syncInfo)) return false;
|
||||
|
||||
notLoadedMasterKeys = notLoadedMasterKeys.slice();
|
||||
|
||||
@@ -341,12 +358,18 @@ export enum MasterPasswordStatus {
|
||||
NotSet = 2,
|
||||
Invalid = 3,
|
||||
Valid = 4,
|
||||
Unsupported = 5,
|
||||
}
|
||||
|
||||
export async function getMasterPasswordStatus(password: string = null): Promise<MasterPasswordStatus> {
|
||||
password = password === null ? getMasterPassword(false) : password;
|
||||
if (!password) return MasterPasswordStatus.NotSet;
|
||||
|
||||
const defaultKey = getDefaultMasterKey();
|
||||
if (defaultKey && !isKnownEncryptionMethod(defaultKey.encryption_method)) {
|
||||
return MasterPasswordStatus.Unsupported;
|
||||
}
|
||||
|
||||
const isValid = await masterPasswordIsValid(password);
|
||||
return isValid ? MasterPasswordStatus.Valid : MasterPasswordStatus.Invalid;
|
||||
}
|
||||
@@ -357,11 +380,12 @@ export async function checkHasMasterPasswordEncryptedData(syncInfo: SyncInfo = n
|
||||
}
|
||||
|
||||
const masterPasswordStatusMessages = {
|
||||
[MasterPasswordStatus.Unknown]: 'Checking...',
|
||||
[MasterPasswordStatus.Loaded]: 'Loaded',
|
||||
[MasterPasswordStatus.NotSet]: 'Not set',
|
||||
[MasterPasswordStatus.Valid]: '✓ ' + 'Valid',
|
||||
[MasterPasswordStatus.Invalid]: '❌ ' + 'Invalid',
|
||||
[MasterPasswordStatus.Unknown]: _('Checking...'),
|
||||
[MasterPasswordStatus.Loaded]: _('Loaded'),
|
||||
[MasterPasswordStatus.NotSet]: _('Not set'),
|
||||
[MasterPasswordStatus.Valid]: `✓ ${_('Valid')}`,
|
||||
[MasterPasswordStatus.Invalid]: `❌ ${_('Invalid')}`,
|
||||
[MasterPasswordStatus.Unsupported]: `❌ ${_('Unsupported key format')}`,
|
||||
};
|
||||
|
||||
export function getMasterPasswordStatusMessage(status: MasterPasswordStatus): string {
|
||||
|
||||
Reference in new issue
Block a user