diff --git a/packages/lib/services/e2ee/EncryptionService.ts b/packages/lib/services/e2ee/EncryptionService.ts index db9a52585..97cff6846 100644 --- a/packages/lib/services/e2ee/EncryptionService.ts +++ b/packages/lib/services/e2ee/EncryptionService.ts @@ -254,6 +254,7 @@ export default class EncryptionService { model.created_time = now; model.updated_time = now; model.source_application = Setting.value('appId'); + model.hasBeenUsed = false; return model; } diff --git a/packages/lib/services/e2ee/types.ts b/packages/lib/services/e2ee/types.ts index 564877acc..7899d1556 100644 --- a/packages/lib/services/e2ee/types.ts +++ b/packages/lib/services/e2ee/types.ts @@ -8,6 +8,7 @@ export interface MasterKeyEntity { content?: string; type_?: number; enabled?: number; + hasBeenUsed?: boolean; } export type RSAKeyPair = any; // Depends on implementation diff --git a/packages/lib/services/synchronizer/syncInfoUtils.test.ts b/packages/lib/services/synchronizer/syncInfoUtils.test.ts index bbdb299e0..a0f813333 100644 --- a/packages/lib/services/synchronizer/syncInfoUtils.test.ts +++ b/packages/lib/services/synchronizer/syncInfoUtils.test.ts @@ -1,6 +1,6 @@ -import { afterAllCleanUp, setupDatabaseAndSynchronizer, switchClient, encryptionService } from '../../testing/test-utils'; +import { afterAllCleanUp, setupDatabaseAndSynchronizer, switchClient, encryptionService, msleep } from '../../testing/test-utils'; import MasterKey from '../../models/MasterKey'; -import { masterKeyEnabled, setMasterKeyEnabled, SyncInfo, syncInfoEquals } from './syncInfoUtils'; +import { masterKeyEnabled, mergeSyncInfos, setMasterKeyEnabled, SyncInfo, syncInfoEquals } from './syncInfoUtils'; describe('syncInfoUtils', function() { @@ -92,4 +92,33 @@ describe('syncInfoUtils', function() { } }); + it('should merge sync target info and takes into account usage of master key - 1', async () => { + const syncInfo1 = new SyncInfo(); + syncInfo1.masterKeys = [{ + id: '1', + content: 'content1', + hasBeenUsed: true, + }]; + syncInfo1.activeMasterKeyId = '1'; + + await msleep(1); + + const syncInfo2 = new SyncInfo(); + syncInfo2.masterKeys = [{ + id: '2', + content: 'content2', + hasBeenUsed: false, + }]; + syncInfo2.activeMasterKeyId = '2'; + + // If one master key has been used and the other not, it should select + // the one that's been used regardless of timestamps. + expect(mergeSyncInfos(syncInfo1, syncInfo2).activeMasterKeyId).toBe('1'); + + // If both master keys have been used it should rely on timestamp + // (latest modified is picked). + syncInfo2.masterKeys[0].hasBeenUsed = true; + expect(mergeSyncInfos(syncInfo1, syncInfo2).activeMasterKeyId).toBe('2'); + }); + }); diff --git a/packages/lib/services/synchronizer/syncInfoUtils.ts b/packages/lib/services/synchronizer/syncInfoUtils.ts index 38a9ccc45..287ad915b 100644 --- a/packages/lib/services/synchronizer/syncInfoUtils.ts +++ b/packages/lib/services/synchronizer/syncInfoUtils.ts @@ -90,11 +90,32 @@ export function localSyncInfoFromState(state: State): SyncInfo { return new SyncInfo(state.settings['syncInfoCache']); } +const mergeActiveMasterKeys = (s1: SyncInfo, s2: SyncInfo, output: SyncInfo) => { + const activeMasterKey1 = getActiveMasterKey(s1); + const activeMasterKey2 = getActiveMasterKey(s2); + let doDefaultAction = false; + + if (activeMasterKey1 && activeMasterKey2) { + if (activeMasterKey1.hasBeenUsed && !activeMasterKey2.hasBeenUsed) { + output.setWithTimestamp(s1, 'activeMasterKeyId'); + } else if (!activeMasterKey1.hasBeenUsed && activeMasterKey2.hasBeenUsed) { + output.setWithTimestamp(s2, 'activeMasterKeyId'); + } else { + doDefaultAction = true; + } + } else { + doDefaultAction = true; + } + + if (doDefaultAction) { + output.setWithTimestamp(s1.keyTimestamp('activeMasterKeyId') > s2.keyTimestamp('activeMasterKeyId') ? s1 : s2, 'activeMasterKeyId'); + } +}; + export function mergeSyncInfos(s1: SyncInfo, s2: SyncInfo): SyncInfo { const output: SyncInfo = new SyncInfo(); output.setWithTimestamp(s1.keyTimestamp('e2ee') > s2.keyTimestamp('e2ee') ? s1 : s2, 'e2ee'); - output.setWithTimestamp(s1.keyTimestamp('activeMasterKeyId') > s2.keyTimestamp('activeMasterKeyId') ? s1 : s2, 'activeMasterKeyId'); output.setWithTimestamp(s1.keyTimestamp('ppk') > s2.keyTimestamp('ppk') ? s1 : s2, 'ppk'); output.version = s1.version > s2.version ? s1.version : s2.version; @@ -110,6 +131,8 @@ export function mergeSyncInfos(s1: SyncInfo, s2: SyncInfo): SyncInfo { } } + mergeActiveMasterKeys(s1, s2, output); + return output; } @@ -154,6 +177,14 @@ export class SyncInfo { this.activeMasterKeyId_ = 'activeMasterKeyId' in s ? s.activeMasterKeyId : { value: '', updatedTime: 0 }; this.masterKeys_ = 'masterKeys' in s ? s.masterKeys : []; this.ppk_ = 'ppk' in s ? s.ppk : { value: null, updatedTime: 0 }; + + // Migration for master keys that didn't have "hasBeenUsed" property - + // in that case we assume they've been used at least once. + for (const mk of this.masterKeys_) { + if (!('hasBeenUsed' in mk) || mk.hasBeenUsed === undefined) { + mk.hasBeenUsed = true; + } + } } public setWithTimestamp(fromSyncInfo: SyncInfo, propName: string) {