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:
Henry HeinoandLaurent Cozic authored and GitHub committed 2026-10-06 15:42:30 +01:00
1 parent 9e4ca948be
commit ab241bd8d6
9 files changed
+165 -27

No files matched your search

@@ -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> {
+15 -1
View File
@@ -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]);
+30 -6
View File
@@ -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 {