mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
All: Fixes #8254: Improve selection of active E2EE key
This commit is contained in:
parent
03cfef6a8d
commit
577aa519e0
@ -1,6 +1,6 @@
|
|||||||
import { afterAllCleanUp, setupDatabaseAndSynchronizer, switchClient, encryptionService, expectNotThrow, expectThrow, kvStore } from '../../testing/test-utils';
|
import { afterAllCleanUp, setupDatabaseAndSynchronizer, switchClient, encryptionService, expectNotThrow, expectThrow, kvStore, msleep } from '../../testing/test-utils';
|
||||||
import MasterKey from '../../models/MasterKey';
|
import MasterKey from '../../models/MasterKey';
|
||||||
import { migrateMasterPassword, resetMasterPassword, showMissingMasterKeyMessage, updateMasterPassword } from './utils';
|
import { activeMasterKeySanityCheck, migrateMasterPassword, resetMasterPassword, showMissingMasterKeyMessage, updateMasterPassword } from './utils';
|
||||||
import { localSyncInfo, masterKeyById, masterKeyEnabled, setActiveMasterKeyId, setMasterKeyEnabled, setPpk } from '../synchronizer/syncInfoUtils';
|
import { localSyncInfo, masterKeyById, masterKeyEnabled, setActiveMasterKeyId, setMasterKeyEnabled, setPpk } from '../synchronizer/syncInfoUtils';
|
||||||
import Setting from '../../models/Setting';
|
import Setting from '../../models/Setting';
|
||||||
import { generateKeyPair, ppkPasswordIsValid } from './ppk';
|
import { generateKeyPair, ppkPasswordIsValid } from './ppk';
|
||||||
@ -147,4 +147,36 @@ describe('e2ee/utils', () => {
|
|||||||
expect(localSyncInfo().ppk.publicKey).not.toBe(previousPpk.publicKey);
|
expect(localSyncInfo().ppk.publicKey).not.toBe(previousPpk.publicKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should fix active key selection issues - 1', async () => {
|
||||||
|
const masterPassword1 = '111111';
|
||||||
|
Setting.setValue('encryption.masterPassword', masterPassword1);
|
||||||
|
const mk1 = await MasterKey.save(await encryptionService().generateMasterKey(masterPassword1));
|
||||||
|
await msleep(1);
|
||||||
|
await MasterKey.save(await encryptionService().generateMasterKey(masterPassword1));
|
||||||
|
await msleep(1);
|
||||||
|
const mk3 = await MasterKey.save(await encryptionService().generateMasterKey(masterPassword1));
|
||||||
|
setActiveMasterKeyId(mk1.id);
|
||||||
|
setMasterKeyEnabled(mk1.id, false);
|
||||||
|
|
||||||
|
activeMasterKeySanityCheck();
|
||||||
|
|
||||||
|
const syncInfo = localSyncInfo();
|
||||||
|
expect(syncInfo.activeMasterKeyId).toBe(mk3.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fix active key selection issues - 2', async () => {
|
||||||
|
// Should not do anything if the active key is already an enabled one.
|
||||||
|
const masterPassword1 = '111111';
|
||||||
|
Setting.setValue('encryption.masterPassword', masterPassword1);
|
||||||
|
const mk1 = await MasterKey.save(await encryptionService().generateMasterKey(masterPassword1));
|
||||||
|
await msleep(1);
|
||||||
|
await MasterKey.save(await encryptionService().generateMasterKey(masterPassword1));
|
||||||
|
setActiveMasterKeyId(mk1.id);
|
||||||
|
|
||||||
|
activeMasterKeySanityCheck();
|
||||||
|
|
||||||
|
const syncInfo = localSyncInfo();
|
||||||
|
expect(syncInfo.activeMasterKeyId).toBe(mk1.id);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -4,7 +4,7 @@ import MasterKey from '../../models/MasterKey';
|
|||||||
import Setting from '../../models/Setting';
|
import Setting from '../../models/Setting';
|
||||||
import { MasterKeyEntity } from './types';
|
import { MasterKeyEntity } from './types';
|
||||||
import EncryptionService from './EncryptionService';
|
import EncryptionService from './EncryptionService';
|
||||||
import { getActiveMasterKey, getActiveMasterKeyId, localSyncInfo, masterKeyEnabled, saveLocalSyncInfo, setEncryptionEnabled, SyncInfo } from '../synchronizer/syncInfoUtils';
|
import { getActiveMasterKey, getActiveMasterKeyId, localSyncInfo, masterKeyEnabled, saveLocalSyncInfo, setActiveMasterKeyId, setEncryptionEnabled, SyncInfo } from '../synchronizer/syncInfoUtils';
|
||||||
import JoplinError from '../../JoplinError';
|
import JoplinError from '../../JoplinError';
|
||||||
import { generateKeyPair, pkReencryptPrivateKey, ppkPasswordIsValid } from './ppk';
|
import { generateKeyPair, pkReencryptPrivateKey, ppkPasswordIsValid } from './ppk';
|
||||||
import KvStore from '../KvStore';
|
import KvStore from '../KvStore';
|
||||||
@ -131,6 +131,8 @@ export async function findMasterKeyPassword(service: EncryptionService, masterKe
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function loadMasterKeysFromSettings(service: EncryptionService) {
|
export async function loadMasterKeysFromSettings(service: EncryptionService) {
|
||||||
|
activeMasterKeySanityCheck();
|
||||||
|
|
||||||
const masterKeys = await MasterKey.all();
|
const masterKeys = await MasterKey.all();
|
||||||
const activeMasterKeyId = getActiveMasterKeyId();
|
const activeMasterKeyId = getActiveMasterKeyId();
|
||||||
|
|
||||||
@ -153,6 +155,35 @@ export async function loadMasterKeysFromSettings(service: EncryptionService) {
|
|||||||
logger.info(`Loaded master keys: ${service.loadedMasterKeysCount()}`);
|
logger.info(`Loaded master keys: ${service.loadedMasterKeysCount()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In some rare cases (normally should no longer be possible), a disabled master
|
||||||
|
// key end up being the active one (the one used to encrypt data). This sanity
|
||||||
|
// check resolves this by making an enabled key the active one.
|
||||||
|
export const activeMasterKeySanityCheck = () => {
|
||||||
|
const syncInfo = localSyncInfo();
|
||||||
|
const activeMasterKeyId = syncInfo.activeMasterKeyId;
|
||||||
|
const enabledMasterKeys = syncInfo.masterKeys.filter(mk => masterKeyEnabled(mk));
|
||||||
|
if (!enabledMasterKeys.length) return;
|
||||||
|
|
||||||
|
if (enabledMasterKeys.find(mk => mk.id === activeMasterKeyId)) {
|
||||||
|
logger.info('activeMasterKeySanityCheck: Active key is an enabled key - nothing to do');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('activeMasterKeySanityCheck: Active key is **not** an enabled key - selecting a different key as the active key...');
|
||||||
|
|
||||||
|
const latestMasterKey = enabledMasterKeys.reduce((acc: MasterKeyEntity, current: MasterKeyEntity) => {
|
||||||
|
if (current.created_time > acc.created_time) {
|
||||||
|
return current;
|
||||||
|
} else {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info('activeMasterKeySanityCheck: Selected new active key:', latestMasterKey);
|
||||||
|
|
||||||
|
setActiveMasterKeyId(latestMasterKey.id);
|
||||||
|
};
|
||||||
|
|
||||||
export function showMissingMasterKeyMessage(syncInfo: SyncInfo, notLoadedMasterKeys: string[]) {
|
export function showMissingMasterKeyMessage(syncInfo: SyncInfo, notLoadedMasterKeys: string[]) {
|
||||||
if (!syncInfo.masterKeys.length) return false;
|
if (!syncInfo.masterKeys.length) return false;
|
||||||
|
|
||||||
|
@ -120,4 +120,38 @@ describe('syncInfoUtils', () => {
|
|||||||
expect(mergeSyncInfos(syncInfo1, syncInfo2).activeMasterKeyId).toBe('2');
|
expect(mergeSyncInfos(syncInfo1, syncInfo2).activeMasterKeyId).toBe('2');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should merge sync target info, but should not make a disabled key the active one', async () => {
|
||||||
|
const syncInfo1 = new SyncInfo();
|
||||||
|
syncInfo1.masterKeys = [{
|
||||||
|
id: '1',
|
||||||
|
content: 'content1',
|
||||||
|
hasBeenUsed: true,
|
||||||
|
enabled: 0,
|
||||||
|
}];
|
||||||
|
syncInfo1.activeMasterKeyId = '1';
|
||||||
|
|
||||||
|
await msleep(1);
|
||||||
|
|
||||||
|
const syncInfo2 = new SyncInfo();
|
||||||
|
syncInfo2.masterKeys = [{
|
||||||
|
id: '2',
|
||||||
|
content: 'content2',
|
||||||
|
enabled: 1,
|
||||||
|
hasBeenUsed: false,
|
||||||
|
}];
|
||||||
|
syncInfo2.activeMasterKeyId = '2';
|
||||||
|
|
||||||
|
// Normally, if one master key has been used (1) and the other not (2),
|
||||||
|
// it should select the one that's been used regardless of timestamps.
|
||||||
|
// **However**, if the key 1 has been disabled by user, it should
|
||||||
|
// **not** be picked as the active one. Instead it should use key 2,
|
||||||
|
// because it's still enabled.
|
||||||
|
expect(mergeSyncInfos(syncInfo1, syncInfo2).activeMasterKeyId).toBe('2');
|
||||||
|
|
||||||
|
// If both key are disabled, we go back to the original logic, where we
|
||||||
|
// select the key that's been used.
|
||||||
|
syncInfo2.masterKeys[0].enabled = 0;
|
||||||
|
expect(mergeSyncInfos(syncInfo1, syncInfo2).activeMasterKeyId).toBe('1');
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -115,13 +115,20 @@ export function localSyncInfoFromState(state: State): SyncInfo {
|
|||||||
// has already been used to encrypt data. In this case, at the moment we compare
|
// has already been used to encrypt data. In this case, at the moment we compare
|
||||||
// local and remote sync info (before synchronising the data), key1.hasBeenUsed
|
// local and remote sync info (before synchronising the data), key1.hasBeenUsed
|
||||||
// is true, but key2.hasBeenUsed is false.
|
// is true, but key2.hasBeenUsed is false.
|
||||||
|
//
|
||||||
|
// 2023-05-30: Additionally, if one key is enabled and the other is not, we
|
||||||
|
// always pick the enabled one regardless of usage.
|
||||||
const mergeActiveMasterKeys = (s1: SyncInfo, s2: SyncInfo, output: SyncInfo) => {
|
const mergeActiveMasterKeys = (s1: SyncInfo, s2: SyncInfo, output: SyncInfo) => {
|
||||||
const activeMasterKey1 = getActiveMasterKey(s1);
|
const activeMasterKey1 = getActiveMasterKey(s1);
|
||||||
const activeMasterKey2 = getActiveMasterKey(s2);
|
const activeMasterKey2 = getActiveMasterKey(s2);
|
||||||
let doDefaultAction = false;
|
let doDefaultAction = false;
|
||||||
|
|
||||||
if (activeMasterKey1 && activeMasterKey2) {
|
if (activeMasterKey1 && activeMasterKey2) {
|
||||||
if (activeMasterKey1.hasBeenUsed && !activeMasterKey2.hasBeenUsed) {
|
if (masterKeyEnabled(activeMasterKey1) && !masterKeyEnabled(activeMasterKey2)) {
|
||||||
|
output.setWithTimestamp(s1, 'activeMasterKeyId');
|
||||||
|
} else if (!masterKeyEnabled(activeMasterKey1) && masterKeyEnabled(activeMasterKey2)) {
|
||||||
|
output.setWithTimestamp(s2, 'activeMasterKeyId');
|
||||||
|
} else if (activeMasterKey1.hasBeenUsed && !activeMasterKey2.hasBeenUsed) {
|
||||||
output.setWithTimestamp(s1, 'activeMasterKeyId');
|
output.setWithTimestamp(s1, 'activeMasterKeyId');
|
||||||
} else if (!activeMasterKey1.hasBeenUsed && activeMasterKey2.hasBeenUsed) {
|
} else if (!activeMasterKey1.hasBeenUsed && activeMasterKey2.hasBeenUsed) {
|
||||||
output.setWithTimestamp(s2, 'activeMasterKeyId');
|
output.setWithTimestamp(s2, 'activeMasterKeyId');
|
||||||
|
Loading…
Reference in New Issue
Block a user