From dcd5a8d975984990cddc03e5bf204adc66a88e14 Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 12 Aug 2021 16:54:10 +0100 Subject: [PATCH] All: Improved E2EE usability by making its state a property of the sync target (#5276) --- .eslintignore | 12 + .gitignore | 12 + packages/app-cli/app/command-e2ee.ts | 10 +- packages/app-cli/app/command-sync.js | 1 + packages/app-cli/tests/MdToHtml.ts | 2 +- packages/app-cli/tests/MdToMd.ts | 3 +- .../79a7c378c40a4538bdab7a6d46e7883e | 1 + .../ac3adde7d7d642a48355553923dd2cde | 1 + .../3/e2ee/.sync/readme.txt | 1 + .../3/e2ee/.sync/version.txt | 1 + .../e2ee/00dceec04659436196bae6b56eea10ad.md | 26 ++ .../e2ee/04761c7a9930415f95ce98dac1706411.md | 26 ++ .../e2ee/07cd0925745b4441b898288f000c93d8.md | 10 + .../e2ee/11e9256883664948970fc5f78d0556bb.md | 10 + .../e2ee/1692f8857934461d8c24c8bf68c3fedf.md | 11 + .../e2ee/376c1a3fe5ce4fc885e344b52b9f37b8.md | 11 + .../e2ee/4a754e4afb6147d1a70114596d02184f.md | 11 + .../e2ee/6865d0c2562e4d8ba88464e75006d443.md | 26 ++ .../e2ee/79a7c378c40a4538bdab7a6d46e7883e.md | 15 ++ .../e2ee/8897f538ba4343cfba78eafd6c71a29c.md | 11 + .../e2ee/a83fbcc450ab44679a785f5f265b5427.md | 26 ++ .../e2ee/ac3adde7d7d642a48355553923dd2cde.md | 15 ++ .../e2ee/affcd54c42bc4f49829013c6522d3a6f.md | 11 + .../e2ee/ba876a7c0d2e44fc8105887c2a3fe9d0.md | 26 ++ .../e2ee/c5a09550eca84955bd4df85345299e5c.md | 11 + .../e2ee/c9e46ce958bb4bd5a3d0cfb65855d9d2.md | 11 + .../e2ee/d7f300c742bb44338563ddbd6fb285c3.md | 11 + .../e2ee/df92d4e526b84c0d9d294c40790b6ee1.md | 11 + .../e2ee/ea596f46d526439490c9e1a32e35fab3.md | 11 + .../syncTargetSnapshots/3/e2ee/info.json | 1 + .../3/e2ee/locks/.gitignore | 4 + .../3/e2ee/temp/.gitignore | 4 + .../933cf209b0094d43884c03149f034128 | Bin 0 -> 2720 bytes .../b1947d6f70314ab180b343e90f1b4660 | Bin 0 -> 2720 bytes .../3/normal/.sync/readme.txt | 1 + .../3/normal/.sync/version.txt | 1 + .../0394074009f8454e86cbf935a03da973.md | 11 + .../052789e9393649ae85427a7c17bfd263.md | 12 + .../1e55a346b1c3444996c3c9f52d4adc47.md | 12 + .../24a091b928cd4d70968514d4bddb79ad.md | 11 + .../264606b4634f4fe58dda0e58a0dc64d9.md | 30 +++ .../5186fc36e6e44e14bd9dff172d7c41b2.md | 13 + .../59e44f93f28242529fbbca804bdd25c5.md | 28 +++ .../5c0b421ac1e645e48dbdba4ed5328327.md | 28 +++ .../625eb45b911248028fecd94bdca86f61.md | 13 + .../6b3b51468e8e44f1ba1b69e4d7e71613.md | 28 +++ .../8436cd9c58824ca58ed146d7bd519dfd.md | 30 +++ .../8ce22808466b43008754086e29acccb5.md | 13 + .../933cf209b0094d43884c03149f034128.md | 17 ++ .../a9d36e4398c74610b44a690b3a263fec.md | 11 + .../b1947d6f70314ab180b343e90f1b4660.md | 17 ++ .../bd25634be15c4005913bca01e3229305.md | 11 + .../c227e85585674332badfbc09c50907ec.md | 13 + .../e2f6cc04e2c94066b3104e0f478d50c5.md | 11 + .../f2bd86db0eeb47da8255e4bfb14cac16.md | 13 + .../syncTargetSnapshots/3/normal/info.json | 1 + .../3/normal/locks/.gitignore | 4 + .../3/normal/temp/.gitignore | 4 + packages/app-desktop/app.ts | 12 +- .../gui/EncryptionConfigScreen.tsx | 31 +-- .../app-desktop/gui/MainScreen/MainScreen.tsx | 7 +- packages/app-desktop/gui/Navigator.jsx | 2 +- packages/app-desktop/gui/ShareNoteDialog.tsx | 4 +- packages/app-desktop/runForTesting.sh | 17 +- .../app-mobile/components/screen-header.js | 5 +- .../components/screens/encryption-config.tsx | 37 +-- packages/app-mobile/root.tsx | 17 +- packages/lib/BaseApplication.ts | 23 +- packages/lib/Synchronizer.ts | 99 ++++++-- .../shared/encryption-config-shared.ts | 228 +++++++++-------- packages/lib/fsDriver.test.ts | 10 +- packages/lib/import-enex-md-gen.test.ts | 2 +- packages/lib/markdownUtils2.test.ts | 6 +- packages/lib/models/BaseItem.ts | 8 +- packages/lib/models/MasterKey.test.ts | 34 +++ packages/lib/models/MasterKey.ts | 71 +++++- packages/lib/models/Resource.ts | 3 +- packages/lib/models/Setting.ts | 25 +- packages/lib/package.json | 2 +- packages/lib/reducer.ts | 6 +- packages/lib/services/CommandService.test.ts | 3 +- .../lib/services/EncryptionService.test.ts | 41 ++- packages/lib/services/EncryptionService.ts | 146 +++-------- packages/lib/services/ResourceService.test.ts | 14 +- packages/lib/services/SettingUtils.ts | 8 + packages/lib/services/e2ee/utils.ts | 92 +++++++ packages/lib/services/rest/Api.test.ts | 3 +- .../synchronizer/ItemUploader.test.ts | 2 +- .../services/synchronizer/MigrationHandler.ts | 57 +++-- .../Synchronizer.conflicts.test.ts | 7 +- .../synchronizer/Synchronizer.e2ee.test.ts | 72 +++--- .../Synchronizer.resources.test.ts | 9 +- .../Synchronizer.revisions.test.ts | 10 +- .../synchronizer/Synchronizer.tags.test.ts | 9 +- .../synchronizer/gui/useSyncTargetUpgrade.ts | 3 +- .../lib/services/synchronizer/migrations/3.ts | 13 + .../services/synchronizer/syncInfoUtils.ts | 235 ++++++++++++++++++ .../synchronizer_LockHandler.test.ts | 4 +- .../synchronizer_MigrationHandler.test.ts | 200 +++++++++------ packages/lib/testing/syncTargetUtils.ts | 30 +-- .../lib/testing/test-utils-synchronizer.ts | 2 +- packages/lib/testing/test-utils.ts | 41 ++- 102 files changed, 1775 insertions(+), 551 deletions(-) create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/.resource/79a7c378c40a4538bdab7a6d46e7883e create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/.resource/ac3adde7d7d642a48355553923dd2cde create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/.sync/readme.txt create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/.sync/version.txt create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/00dceec04659436196bae6b56eea10ad.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/04761c7a9930415f95ce98dac1706411.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/07cd0925745b4441b898288f000c93d8.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/11e9256883664948970fc5f78d0556bb.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/1692f8857934461d8c24c8bf68c3fedf.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/376c1a3fe5ce4fc885e344b52b9f37b8.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/4a754e4afb6147d1a70114596d02184f.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/6865d0c2562e4d8ba88464e75006d443.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/79a7c378c40a4538bdab7a6d46e7883e.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/8897f538ba4343cfba78eafd6c71a29c.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/a83fbcc450ab44679a785f5f265b5427.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/ac3adde7d7d642a48355553923dd2cde.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/affcd54c42bc4f49829013c6522d3a6f.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/ba876a7c0d2e44fc8105887c2a3fe9d0.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/c5a09550eca84955bd4df85345299e5c.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/c9e46ce958bb4bd5a3d0cfb65855d9d2.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/d7f300c742bb44338563ddbd6fb285c3.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/df92d4e526b84c0d9d294c40790b6ee1.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/ea596f46d526439490c9e1a32e35fab3.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/info.json create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/locks/.gitignore create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/temp/.gitignore create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/normal/.resource/933cf209b0094d43884c03149f034128 create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/normal/.resource/b1947d6f70314ab180b343e90f1b4660 create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/normal/.sync/readme.txt create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/normal/.sync/version.txt create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/normal/0394074009f8454e86cbf935a03da973.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/normal/052789e9393649ae85427a7c17bfd263.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/normal/1e55a346b1c3444996c3c9f52d4adc47.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/normal/24a091b928cd4d70968514d4bddb79ad.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/normal/264606b4634f4fe58dda0e58a0dc64d9.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/normal/5186fc36e6e44e14bd9dff172d7c41b2.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/normal/59e44f93f28242529fbbca804bdd25c5.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/normal/5c0b421ac1e645e48dbdba4ed5328327.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/normal/625eb45b911248028fecd94bdca86f61.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/normal/6b3b51468e8e44f1ba1b69e4d7e71613.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/normal/8436cd9c58824ca58ed146d7bd519dfd.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/normal/8ce22808466b43008754086e29acccb5.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/normal/933cf209b0094d43884c03149f034128.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/normal/a9d36e4398c74610b44a690b3a263fec.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/normal/b1947d6f70314ab180b343e90f1b4660.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/normal/bd25634be15c4005913bca01e3229305.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/normal/c227e85585674332badfbc09c50907ec.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/normal/e2f6cc04e2c94066b3104e0f478d50c5.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/normal/f2bd86db0eeb47da8255e4bfb14cac16.md create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/normal/info.json create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/normal/locks/.gitignore create mode 100644 packages/app-cli/tests/support/syncTargetSnapshots/3/normal/temp/.gitignore create mode 100644 packages/lib/models/MasterKey.test.ts create mode 100644 packages/lib/services/e2ee/utils.ts create mode 100644 packages/lib/services/synchronizer/migrations/3.ts create mode 100644 packages/lib/services/synchronizer/syncInfoUtils.ts diff --git a/.eslintignore b/.eslintignore index 18b451fc64..b3341f001c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -978,6 +978,9 @@ packages/lib/models/ItemChange.js.map packages/lib/models/MasterKey.d.ts packages/lib/models/MasterKey.js packages/lib/models/MasterKey.js.map +packages/lib/models/MasterKey.test.d.ts +packages/lib/models/MasterKey.test.js +packages/lib/models/MasterKey.test.js.map packages/lib/models/Migration.d.ts packages/lib/models/Migration.js packages/lib/models/Migration.js.map @@ -1173,6 +1176,9 @@ packages/lib/services/database/types.js.map packages/lib/services/debug/populateDatabase.d.ts packages/lib/services/debug/populateDatabase.js packages/lib/services/debug/populateDatabase.js.map +packages/lib/services/e2ee/utils.d.ts +packages/lib/services/e2ee/utils.js +packages/lib/services/e2ee/utils.js.map packages/lib/services/interop/InteropService.d.ts packages/lib/services/interop/InteropService.js packages/lib/services/interop/InteropService.js.map @@ -1500,6 +1506,12 @@ packages/lib/services/synchronizer/migrations/1.js.map packages/lib/services/synchronizer/migrations/2.d.ts packages/lib/services/synchronizer/migrations/2.js packages/lib/services/synchronizer/migrations/2.js.map +packages/lib/services/synchronizer/migrations/3.d.ts +packages/lib/services/synchronizer/migrations/3.js +packages/lib/services/synchronizer/migrations/3.js.map +packages/lib/services/synchronizer/syncInfoUtils.d.ts +packages/lib/services/synchronizer/syncInfoUtils.js +packages/lib/services/synchronizer/syncInfoUtils.js.map packages/lib/services/synchronizer/synchronizer_LockHandler.test.d.ts packages/lib/services/synchronizer/synchronizer_LockHandler.test.js packages/lib/services/synchronizer/synchronizer_LockHandler.test.js.map diff --git a/.gitignore b/.gitignore index 3d1af7200d..387ffae349 100644 --- a/.gitignore +++ b/.gitignore @@ -963,6 +963,9 @@ packages/lib/models/ItemChange.js.map packages/lib/models/MasterKey.d.ts packages/lib/models/MasterKey.js packages/lib/models/MasterKey.js.map +packages/lib/models/MasterKey.test.d.ts +packages/lib/models/MasterKey.test.js +packages/lib/models/MasterKey.test.js.map packages/lib/models/Migration.d.ts packages/lib/models/Migration.js packages/lib/models/Migration.js.map @@ -1158,6 +1161,9 @@ packages/lib/services/database/types.js.map packages/lib/services/debug/populateDatabase.d.ts packages/lib/services/debug/populateDatabase.js packages/lib/services/debug/populateDatabase.js.map +packages/lib/services/e2ee/utils.d.ts +packages/lib/services/e2ee/utils.js +packages/lib/services/e2ee/utils.js.map packages/lib/services/interop/InteropService.d.ts packages/lib/services/interop/InteropService.js packages/lib/services/interop/InteropService.js.map @@ -1485,6 +1491,12 @@ packages/lib/services/synchronizer/migrations/1.js.map packages/lib/services/synchronizer/migrations/2.d.ts packages/lib/services/synchronizer/migrations/2.js packages/lib/services/synchronizer/migrations/2.js.map +packages/lib/services/synchronizer/migrations/3.d.ts +packages/lib/services/synchronizer/migrations/3.js +packages/lib/services/synchronizer/migrations/3.js.map +packages/lib/services/synchronizer/syncInfoUtils.d.ts +packages/lib/services/synchronizer/syncInfoUtils.js +packages/lib/services/synchronizer/syncInfoUtils.js.map packages/lib/services/synchronizer/synchronizer_LockHandler.test.d.ts packages/lib/services/synchronizer/synchronizer_LockHandler.test.js packages/lib/services/synchronizer/synchronizer_LockHandler.test.js.map diff --git a/packages/app-cli/app/command-e2ee.ts b/packages/app-cli/app/command-e2ee.ts index df75104ef4..cc2f6e2605 100644 --- a/packages/app-cli/app/command-e2ee.ts +++ b/packages/app-cli/app/command-e2ee.ts @@ -6,6 +6,8 @@ import BaseItem from '@joplin/lib/models/BaseItem'; import Setting from '@joplin/lib/models/Setting'; import shim from '@joplin/lib/shim'; import * as pathUtils from '@joplin/lib/path-utils'; +import { getEncryptionEnabled } from '@joplin/lib/services/synchronizer/syncInfoUtils'; +import { generateMasterKeyAndEnableEncryption, loadMasterKeysFromSettings, setupAndDisableEncryption } from '@joplin/lib/services/e2ee/utils'; const imageType = require('image-type'); const readChunk = require('read-chunk'); @@ -39,7 +41,7 @@ class Command extends BaseCommand { return false; } Setting.setObjectValue('encryption.passwordCache', masterKeyId, password); - await EncryptionService.instance().loadMasterKeysFromSettings(); + await loadMasterKeysFromSettings(EncryptionService.instance()); return true; }; @@ -93,12 +95,12 @@ class Command extends BaseCommand { } } - await EncryptionService.instance().generateMasterKeyAndEnableEncryption(password); + await generateMasterKeyAndEnableEncryption(EncryptionService.instance(), password); return; } if (args.command === 'disable') { - await EncryptionService.instance().disableEncryption(); + await setupAndDisableEncryption(EncryptionService.instance()); return; } @@ -115,7 +117,7 @@ class Command extends BaseCommand { } if (args.command === 'status') { - this.stdout(_('Encryption is: %s', Setting.value('encryption.enabled') ? _('Enabled') : _('Disabled'))); + this.stdout(_('Encryption is: %s', getEncryptionEnabled() ? _('Enabled') : _('Disabled'))); return; } diff --git a/packages/app-cli/app/command-sync.js b/packages/app-cli/app/command-sync.js index 586b679bda..2219141fe7 100644 --- a/packages/app-cli/app/command-sync.js +++ b/packages/app-cli/app/command-sync.js @@ -187,6 +187,7 @@ class Command extends BaseCommand { try { const migrationHandler = new MigrationHandler( sync.api(), + reg.db(), sync.lockHandler(), Setting.value('appType'), Setting.value('clientId') diff --git a/packages/app-cli/tests/MdToHtml.ts b/packages/app-cli/tests/MdToHtml.ts index bad64d0a5c..8c69d6b11c 100644 --- a/packages/app-cli/tests/MdToHtml.ts +++ b/packages/app-cli/tests/MdToHtml.ts @@ -1,7 +1,7 @@ import MdToHtml from '@joplin/renderer/MdToHtml'; const os = require('os'); const { filename } = require('@joplin/lib/path-utils'); -const { setupDatabaseAndSynchronizer, switchClient } = require('@joplin/lib/testing/test-utils.js'); +import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils'; import shim from '@joplin/lib/shim'; const { themeStyle } = require('@joplin/lib/theme'); diff --git a/packages/app-cli/tests/MdToMd.ts b/packages/app-cli/tests/MdToMd.ts index 5627081490..31afd3cf71 100644 --- a/packages/app-cli/tests/MdToMd.ts +++ b/packages/app-cli/tests/MdToMd.ts @@ -1,7 +1,6 @@ const mdImporterService = require('@joplin/lib/services/interop/InteropService_Importer_Md').default; const Note = require('@joplin/lib/models/Note').default; -const { setupDatabaseAndSynchronizer, switchClient } = require('@joplin/lib/testing/test-utils.js'); - +import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils'; const importer = new mdImporterService(); diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/.resource/79a7c378c40a4538bdab7a6d46e7883e b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/.resource/79a7c378c40a4538bdab7a6d46e7883e new file mode 100644 index 0000000000..8d7b2b1082 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/.resource/79a7c378c40a4538bdab7a6d46e7883e @@ -0,0 +1 @@ +JED01000022051f3b6b71948c4f5d909d1af6588c78bb00137c{"iv":"76ZUxZ3WWvKusZE4H7pKfg==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"tVgmTCWSasM=","ct":"a5z7foE3EVBW8WV90J6fB06hXrjIGOEQx3S3JE266Qiqz60TZPOv4JV/X6KgSz0Es5e8bIoxDMq+9z3rYebV+FU3BHK9aPeOqC2UQOvDfeof30kJY/IlnBq/3UT1whLvJB8WKMJy52BACK2njUcFGRQcrTkcvrRSPJ8hwZAm9FzF0AI4fW4nNAH4uOT31MmI6jAcGjKaiAmUhkoxEvzM5coeR7layx3vm5KqQMGSXs/gSnzvLlLW3rgDYK6pYEQMc4eV812NnbKeYfteLmR+5hJ8LwFnTyfnKlISRRGrxCe09UANkqgncvGvpaRxmikJHAO3iCV2H8kG1f/hHpdy90FLp7GT/wTsLuts0cT37wsjSJdeT8Ug5OX/Wp6e/MWLyVH7mQ2C2t3rKEyFYtVpKfV9N64Q1BNaRSOtkP9PwHYzu+K/tcwQ2TQyoLL1hiCzb4r/qu+b6mhDklSO3p1thGO0DIeXF/qKJTreXUhux+5SnkqgLzqoysTKZNFASTa1MFNYFeBXvK9bWBNVxxqnpmMGnYz+aGF5GhOyp+cQ4djGsnQnf95Vz9h+mlxG6STF27BcUyNzSGO4g3rLtf2Cv/jiQh7zcs6xqcSyn1qeCwHoKYAajLdGXeHwG1IzxQPoV7af4RJhR+RW8TIElFg2Ay2zPaFecYSwc1AU4hGPmdJk8cNy1IbWe657acidl0mSeXQFsz9YHBM5+GWtbNsWkGya5B0GgXYeLRw9rss28SBD22kGQemUEh8p4EjII0+K/r9pwGnYr4f0SgjaB/T9O1zTqa9ujJA3yorvKAR4JB8al2nxFgT9nQPLkQux21MatI1q3m4O4MP9ijCpk2HXpcRCUouW8k8latk7C1uk/QiOTVrbmxjqRj7H16Xh4AexlP0YkCt7VM0Sfr7iOWWnxGeEUGm1Z7A012FtDWXal2Dk5lj1YWPSWuq5mQhd9tNClMf6zKvdnETr0PZ8uoK1shiWUO8ftpyUeeqBZc0zEY9BWdzFms5ermRmFLdtxz/Rfdmvkszr2jO1PXDBrDjc/lCxs05VjxP95d37wNlhjzTX0Ga5pzZRc7ZzISzguHL5W5fboS9xAQSrtjZByhwZ4l5IzN+T3TgpSJqs2ZebdEWeNJpjOTsFhorJ2Y9YLn/6lYmxMUPbeXSwF0swMdxi66zztCDL4n3XIy1f6yGs1Ga88X7odZlv+iCslos2nFB8WDeyTfSHFpwD6z57L8mdMZq50vZpPhjldexgEW/E+8ag1iCrx3u0efEP95EnViFDKpLpxFApnCSlQywFeDSlsQD8NVuBarA3/GDFJOBvb6MPgfH8cnFZw8XMa2NgMAASz6TuYMVVf/7h8M0syuXU1QuO0flh3mdbeborur2Lq+Gjp4oZ6+w7p4GFyW4tcQ27Zkx/OXfV3sC02eUvchGKogYn5w7ykOE/XDLCmHgLDUZrloco1OsbkAW3vxqdnq4LUmKB503eOc0IIrI7nxyXEyRApPKkxNAS/8+cF6uhbgQcgFbFMtW0PU8pAwa09lGFUwFiAA5Lb9m4fl7ojbZpR4Rli4g6yYRmx8BmA33SEZmgfdZLNJXizUeRa1JDVPFdkQib9sv6LRGeuJ2COjYn2+51BFAVGT6Z1vgPbOeNwY8wHotDVQUxzGElm+vgFlgxQ8JbJXwwBjH8swGdNLu6sSvbCro09KJUPGbCOmZj0duF0MCttSCOeWkcC3O/LyCxnQPmpoeN6nb5q6HfQhG37H3f1qrIRUHHOc/+LyCgdyEfOdistGhTCRxNp/E7wKeQXhZe4KV19tuJwPwlsmXzDA4kyhxnDyjnZoSmJz0FFF3C6qTlEZCTOQ5aIDWdslfW5ilu39drSvd5/ntlLDUTBhfbX56wrh367aoDdGADv23U4gqgAsQ+1YHrz8P2Cb2PjzXakWBmGrV4+QCglJtzw+YH38oxMcg8/BTYFgBtr3dCQfqgocZoj+2XoEOrUEvxPTXD4NDJxjhH0Hoz+n40oUWAbicwi+/dWOins124VbU+r1QAskpVQuHLIqNztETIUl1a7n7DzjMfg0VxDgD++YWegUfCmyn2kBBHaYKt6Q8WFXnuqC2XXLU/vpVqv9Bs57YVwfdv735TvWn3p5OEwgKSur988fS0T7G2/CvywCkW/Mxq4RW6zhVlJOLI0p9b/N/02Sf56Fqx+nhi4GwMN7Ru9B3KnYFKEBJbjjFkxVe+afoCuWSh4dESlDDE/+vgsQhdghQXef3UsbOaYufI+3PQcQs+4SUgjZEyFEmJXcXQMb364/KPuMcGW7gmriW+AiywL8It85f73bEurMv7xEbK97fVTnOligmAFdFM9LZcIfWhMzg/mU+jSS9pYTs6IdAwfS8RT1wsfjul8gMOT/Ny4saV/QOG9fdz8Mp5jxace4RYErQtdr33U8KKb7i7qQQaAeqKSO8PxSFsCdjxsvGMRakAQfJ22EtL0pFPtw3ZjxMeJWhqnag/lxpIsXFvXraDwMcFtTVGkPKQkvXZbgk5T5PXe4QZkEnkAilbQECyZk9HWJZlCziJk3qAedhbN0lkyiVMPBGNMwAFq3+DjYGL6fSmvlPc5cHSAe1WT7+pilkPa2AGr8uQgq39VQQIaWL59bLYxoPZr0S2Ns3m/gz3hXZZb8TVgNHF87NKHUQIATJ4PLFv+Ek4DbXLJLIS0moWu/apm+wHp7Y9Vdj8Mk4mBN4hJqwCbYNpMhp6uBPcCdVTSjycZXToul3wTr6/92DSWh5Xph0GvnT0ig1B85nTifbgMOQJa6HpLHWvGLc2fmIGBhFI9eSb+WY+N8Rg7/qmLa5dARsozhnFMtonpEizaTsy0+9/FPaEmy7fuM5q92bhJgcrs+5nP/dG7zlo+IAwZL2/fEzVY5zY7/XZ+3khH8gWR0Ra3KKhPeM58tgwhKXXBy8BP+matrSkYqvY7+49gxBcxX8mOopyTH6lBMNCqzG8cqRWcpTEVBOpiiI/bYjNXS9mzhu3ZJwukaD4Wkuy/7stNl5CvOYZ5zhV9m1l9d/IjU05V8Ep2SmJ0Jw92VIduc9VRpMyyCLyRRP/TngxrpWkc9rZMwZS0V7LdZHCMboPGL/FSj2s9hrEXNafKr38hqy3HJMOnIoA+lpY51VluSlUr8DT6dkkZdy3d8TVqmm12pDhXAh5t3kyNPBFIRLEmZ1lZyqVlRexHy8bI2/RetjXHLMV/KqxsDidmgbsRkmN+4fm7/0WcMgl1ryiP5gvVDCbwBsyeI9x88D14CAeuFXTk0v1ExHAqzaqf3ISiHH3pDGE4R+9dz2a1h1jyvUyoBMpbT9yW6mPpBUZJ4918LYkRtNGrD2B3drsXmbGSn7fCQ8Uh8/SfoRPqVEkmPuNp6Lpyr9tbYaoWIvpXSh9LivkVhM0UkWa8sU2IMcb39VkVEqJkgRqZ70bSchmWm1GzeyqlYQSAOmaK9HvOzR7YpS84WN7F97ZwNXzZ3VJKMZAG/t/qauNnb92tsJ9/AtcbEC3r1eVXmmkuKr9Q3irYgLSL3ovSK9BXqKueIyoSqUJMqnCjdXkrjnkurUooahal5RmEQtVCkqtT+ZQelCFuxR9s2aic1ZTWFkV7aHtd53ainLT/hDwgjNOXR/Txt5Tx1q5hS741LzC7xV60TUbqOhHumZZeNhPTVfRbFzjloH8KitgVw0doEfJQZ950Oulf2Usd/fw/EVtynntfEHQtHJl/OrOeEX13dZM65+tf2mmVwnk69/hbMcESSghhWsh6hFYI9YWZGIT0agcI68WoOOGlPooHmUwm5LNLKe4227eMckld5Z68B4w+RKJsJUGVC135PtrSg7PCw4GK2uHh6K9bvSXNaBNCWm1Ral7rc+eEO1j1T3XDtTd3IyiHaxAe6U5Uihv8znNR8TVkhejmxVejr9RKB4rMauWjn+/E33GzwWYPhOQl6AT0BWtC3qJ7IbjlwQC96nRQEyyYsg/o3j7FlE3t1Pge5vc21rQblNbLS6ttspq71efEIyHl2vbJee1goTzHlFirj1xAueZsko9rrPPqI0YWu729dg3W9n8DPKf7yJAp/TkGUryurqoz1rBCdUV4e8rBaMoziv8Op3leqYedI+dvEthWSjXY10kxpyM3PILRoJXuwe4ClQwfNYeBxeBJ3YK8+CjzeVnmYkafI1W3KX7ZGAO812NBukL847NdqUVo49lC7xAY5cbvIs6QNeMOlZGcbF9HojUBQ5IrxbEAJ2W9iBcVy25yCGsbWUmntATormLNCHY3jrMsob7/mdTfXOd9EYj7OOuBzMiqInFB3fCjk2A0TjlDW8BAHAgUZQGtZMYvqh5aPRKlF9GBRt3618zfzzii/NOBrmEawq0ZcUyOedokel2vgeCmdW6Kb0ZNayM9IkSCbxeaA2eT4dz9TbQeY5+SX+OmLzdCIYSxR7BUlcLrJHBaykCEpt6BscsLFzbNA4thTBAizR10R4NImLTSWjpbb49of14R5f53iGj+ZXHQA8ABQBIN1uunW0UspWYxqfyyTmw5PRQP+S7ljssUWly8oR7mH/lDF0vKgq1p12797aWUyuSrqozI71g6agmart1fwmCGgCtqRZXeBk7h+lS0zLRGw6Ew5IeZtLt0KMOL6Em2roeRR+ORQoxrLDkYlnwtz24p9BpyZZLyMMg3nkS5PvTahP/Zf5JJ4yHPoMJLe6H/JsW33XsbZwHlS9k/CE5d00vnQM1qmeE69wuSCVvk9Cc5Icyz3ZrSt3sTWbPNA1pmIi8xUT4SGaoo5jQlvEfkGEPmrsF5v9A6rpHyz7T1FRaeuncHCSyN3uRSsr5GjKhEDQ2Uocofx+/ynjmvHy4bCA7m3E="} \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/.resource/ac3adde7d7d642a48355553923dd2cde b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/.resource/ac3adde7d7d642a48355553923dd2cde new file mode 100644 index 0000000000..061a8be3fc --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/.resource/ac3adde7d7d642a48355553923dd2cde @@ -0,0 +1 @@ +JED01000022051f3b6b71948c4f5d909d1af6588c78bb00137c{"iv":"xMfC9Mk0Cs5OsgN8+fU6jA==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"tVgmTCWSasM=","ct":"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"} \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/.sync/readme.txt b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/.sync/readme.txt new file mode 100644 index 0000000000..e46819c443 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/.sync/readme.txt @@ -0,0 +1 @@ +2020-07-16: In the new sync format, the version number is stored in /info.json. However, for backward compatibility, we need to keep the old version.txt file here, otherwise old clients will automatically recreate it, and assume a sync target version 1. So we keep it here but set its value to "2", so that old clients know that they need to be upgraded. This directory can be removed after a year or so, once we are confident that all clients have been upgraded to recent versions. \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/.sync/version.txt b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/.sync/version.txt new file mode 100644 index 0000000000..d8263ee986 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/.sync/version.txt @@ -0,0 +1 @@ +2 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/00dceec04659436196bae6b56eea10ad.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/00dceec04659436196bae6b56eea10ad.md new file mode 100644 index 0000000000..723c8e2678 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/00dceec04659436196bae6b56eea10ad.md @@ -0,0 +1,26 @@ +id: 00dceec04659436196bae6b56eea10ad +parent_id: 8897f538ba4343cfba78eafd6c71a29c +created_time: +updated_time: 2021-08-07T17:03:37.267Z +is_conflict: +latitude: +longitude: +altitude: +author: +source_url: +is_todo: +todo_due: +todo_completed: +source: +source_application: +application_data: +order: +user_created_time: +user_updated_time: +encryption_cipher_text: JED01000022051f3b6b71948c4f5d909d1af6588c78bb000504{"iv":"ROL4MNnDG2BpjKv0QO+qNQ==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"tVgmTCWSasM=","ct":"QsbumZxIFu1k1SxbJopf+NuhX5rhAjwDXOcwA4lRCNs3dsode2FIYV6hlCShrZ6FI+o/O72ecbZm1gvCGiIT6sG3Jl+snaXuT+B72/ibZrATs1ESJu0maQH+LrNG+TFcHFx5SbKKqsuCkQchWleDGsnjume08EkqhGwgDKC6FMGsCipWtsVxvOoZc1Khvf7PBmUYUSWaLLwPaawhMQPzm8m8ZMQMpU3shsY6FJZWhfCYqTomVey9DreKDLYe85Mgu+eCwnQDnZzd1/V7Xqi0gseJFxjBaqaKeO888ClpI6ew4bcL03KmtmSVhIkHTBo9R1wCA+Z33KWjT9JD2swhoi1SJNT0Hm6j1soCSLLnNWsiD/cnr4v41+7UUUZ++QDb79P/Dw1065tAjlVZm6Vmp1yT3X/Fl2ORzb6+DnhUMbEjw5tHIrFFtpYlcnoBjE4OaUIq/Hkhm9Q1L8HA1/6090x/4DVYmmQ617kIO7AvnpWy3TD+qKENcFr5gzXKUzWkQnfjZF7Z2si0sFdTRToj1wf72tiVvf24YYIdSxMTO3n2eXaahP4yjOcCQGnRy+QrUOwZn2+7nHa/THaSn98+PYcagAKmQccSQWYfmMc8wR3toLFQJKWZ/BP0ixX9v/D8uEihQMXlQXFxIVoAoltiRs3zkjR45RLSEQHfjzWsXxW6F1275Gd2weHl3T06sj+4kIAkaBCyarOSCfSCBCqSWaR3vNSW6ZmSjPH1KKAI1r8mkhK1KgQjZtoh78L+hOmkUjCMSK97mfq6Im26hY62pqx9bS5PFPSdO6HQs30mVtvqk3TIJXDKuwyvgA2YZoHxXg571IFDRW6Z6A0rS5p1XBmwUL4XoWAS2PJFfEs9FES28yCosp1/jVc0lE6Q587qn3T7Tz65TtZ6uyxz+O5aUiHjVXuBPELbm7dN6E/jMHaTYkmmDAhe268b+R4Fc3hmFsZmE4g/kCFaTGUZBufdQjZGJInKDecBfK/v3sD6zP8F2gebEOCcePnEiZ9tGAF/bhR0b7+LhMj7qxfaEuatxjAUjNuD0wgykVzsBiLw3EYnBWjtacOsH0iLUcdoZEatTEApT4zPhLjjEjtGbp0kmVj/p9hkWwxj+nuRA6/h/tIdhN9jGuNxca5IsONZ"} +encryption_applied: 1 +markup_language: +is_shared: +share_id: +conflict_original_id: +type_: 1 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/04761c7a9930415f95ce98dac1706411.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/04761c7a9930415f95ce98dac1706411.md new file mode 100644 index 0000000000..da3ccfa837 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/04761c7a9930415f95ce98dac1706411.md @@ -0,0 +1,26 @@ +id: 04761c7a9930415f95ce98dac1706411 +parent_id: 376c1a3fe5ce4fc885e344b52b9f37b8 +created_time: +updated_time: 2021-08-07T17:03:37.157Z +is_conflict: +latitude: +longitude: +altitude: +author: +source_url: +is_todo: +todo_due: +todo_completed: +source: +source_application: +application_data: +order: +user_created_time: +user_updated_time: +encryption_cipher_text: JED01000022051f3b6b71948c4f5d909d1af6588c78bb0004ac{"iv":"lewZXUA2jDkYdCad/AI54w==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"tVgmTCWSasM=","ct":"yd4oVd062nvTUBHM/kj14xPa6bexLz0xMexWMJlf7zziyswtRNgtA/5S9FbvxUHuzn065mcqjv3MoyvQVgisyH7jiTlHlbnU8JoXIFmwIeK7b3wer2UJrU/xbtICS2e/wrkX4v/uy1/cxyIIDcdjeFWv2v3gUMQVbSZ60r6Tzi8CdqcwhwAd/uVyHoUk2X8zt7on3vYL5ZOcy9TiS23s9Hzer64bJF31LFzer0TBqyV1voGURyeKBhmj+dAogoL4C1qTcebnagM8O2LosD3ZUxNh1gghRPX0BpIjeo3G4rJL9k62O9LsXL3aw/doY4vl32V2mOtXbiV/cU5yutDafxZHDC3VBb4A1RT5F/CeohFWq1CWrqYGINKbc+Mjoadu4oDtL3sR9CIDbktK7KGpmOdGd8hcbrLdRdI8ORbE831m1e5gmm7zc7tA42GvWLhGor0bk/vIJULlzEC9dUWBD8Hm97mpfdweRzKFGcdDGNo2SsSVTd4SSEviSF2q/Jamv9gsaV/kY6LuCFOj/JdhqMtAFR3cVH7fPqlbjbNPmEW8xvHjOLqCFyNFHn6uzJhTI6D+CtRgu1CKFxNozFZ/0C77boWe3EvUGIM40YGqr+GPhPD2qB9JbvrZMOtMQu99St/lxevKT7vni35lwpiTIp729CpuyS152YO6wTxVJD/ugZm/4TBUkf2croRw7i49IxZbCHRpj7fnLBiQ93qKm0Ud7Zh1CuIv3LSpDLNwdHp73/rAxom80P8ix9Md/KM587pxLLZIZmbxI2cmVPwcop2sxStVffu7OWE7NcVUJIfIroAE3UV22+TGvlOe3mrnKYCeMuZfButvmL+TyYHCJxtFK2o5Nofkk/tqgkHm4tu2maJ22djtg1Fl3DL8iR+no75M4IZY3quAiSNMh2jJruIl1nBT7sRwczjdVbLz5fUMjoY72fmGE2Y3aiPGEfZk2KtXFBHOo+S1ulrc7A6GN8yyLRoE8Jff5iL2MCBzhDWq0ABHhU+eKv9k6BPD75CmoPR5VbK2LYy9no0ae+HazqD0IjTaNXEdiLmD"} +encryption_applied: 1 +markup_language: +is_shared: +share_id: +conflict_original_id: +type_: 1 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/07cd0925745b4441b898288f000c93d8.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/07cd0925745b4441b898288f000c93d8.md new file mode 100644 index 0000000000..d49928d399 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/07cd0925745b4441b898288f000c93d8.md @@ -0,0 +1,10 @@ +id: 07cd0925745b4441b898288f000c93d8 +created_time: +updated_time: 2021-08-07T17:03:37.155Z +user_created_time: +user_updated_time: +encryption_cipher_text: JED01000022051f3b6b71948c4f5d909d1af6588c78bb00027c{"iv":"TcXBDKbu+tIXL2vhzwYb7w==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"tVgmTCWSasM=","ct":"7JdThQsMepqu2l0gBd4ptjb98vLGMO4Hn3Mrsbepml9JkqgFKX8Bz7l9Bpn/zhQ7zInSIr89p/n55D93Y44t7ymvXUx3etAPGdDz6dzPklceagDwGWeO+KLD3nKWOfLPL7DSsNnNgZecFN3IJrgw87Kc89hoBz6IXoupdPrN1lIrxFdzC3DeXbE0ai9SnoY1yuipAdatEp+gkuKOyPyPCvHd7ud9JRc1SeI4mmjynuNxWi4ts3JtSTl7qw717FAN4UbVGRxRfI2fuGYxTVeeajJDtRDCDCYe0RfY1nuw3aqsiBnhAdvRcTxN2KZZSmIej5wFtACZ5zcFWQbVD54i8VTT7cTYjAtN4VqJmYvvsGz1uKamMQ/x6yYzRl3ByOes12MZjQnZpf2gGp9xCC52XcySzeu+vmCIllwR46ulbTXR5UhYUJuOlzbkujMXkvsAL4t/Sn4OjLq/mZpN13t1RSBA9ezBarBldqj3ZZ6uMdDISYIQOICe"} +encryption_applied: 1 +is_shared: +parent_id: +type_: 5 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/11e9256883664948970fc5f78d0556bb.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/11e9256883664948970fc5f78d0556bb.md new file mode 100644 index 0000000000..877b9c7546 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/11e9256883664948970fc5f78d0556bb.md @@ -0,0 +1,10 @@ +id: 11e9256883664948970fc5f78d0556bb +created_time: +updated_time: 2021-08-07T17:03:37.146Z +user_created_time: +user_updated_time: +encryption_cipher_text: JED01000022051f3b6b71948c4f5d909d1af6588c78bb00027c{"iv":"XRG1E4Do018pOt/1YCqZHg==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"tVgmTCWSasM=","ct":"jINP1I0ejfdHxBLZInMCi8XUjGIgoSTgPIHipMZl5DsWhGKOLFkQWiNE8nneOAbp6RcT9627JViDrVqANn5U8vfA1MNucPQlQJpBRBZmePzO+KsKcBf7rsz90+jCFDbAGBXi5b94ckUfxijXcwNQqj8IdTSVNheb1e/FfvL3O8qamPANbJ6NxPEMYLSn0HMDkLKa/Lk3FVhybrvoqdipFW2zgGhLslP+gaEs4uk7USjHgmUdDOIQAy8gqxPVidymGslPgqRPVwTbWPRv2LH3J8S+2PNX6HeT/D+gsX/ewMXuCCg2WXWy2s++yFqwiiiLCP779gHMs4zN5nHLvMvH+tT7GQu0wGJRvFWpj9ZbFap3/2Itr0BIZwHIA/zlquUqkgSK026F7YicKJG7IG/DnFbPisLRshjNcBJ3P8RhGQuU2rFqPJvwoeXfoV3KN2fCPj0irVBcbub/99trx8vv3OUlJuEV3teQ37gjUCQccdIMmL7bAc18"} +encryption_applied: 1 +is_shared: +parent_id: +type_: 5 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/1692f8857934461d8c24c8bf68c3fedf.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/1692f8857934461d8c24c8bf68c3fedf.md new file mode 100644 index 0000000000..41566dd7a9 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/1692f8857934461d8c24c8bf68c3fedf.md @@ -0,0 +1,11 @@ +id: 1692f8857934461d8c24c8bf68c3fedf +note_id: 6865d0c2562e4d8ba88464e75006d443 +tag_id: 07cd0925745b4441b898288f000c93d8 +created_time: +updated_time: 2021-08-07T17:03:37.156Z +user_created_time: +user_updated_time: +encryption_cipher_text: JED01000022051f3b6b71948c4f5d909d1af6588c78bb0002d8{"iv":"LGP+1mTtj3U+EjJ0n2ljUg==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"tVgmTCWSasM=","ct":"x5nE3YtB8spmTBk2HNa/GgUsKBply9ZkqCtnhPgDFBj5JeFL447YYL/ysDx4n3I/rdGXSXZvl0xS0U4+ieRlU7HcRGCdNTWdyVtbVaaBsw5J+8WPLU6AgpxtiayUU5R9b0TwxC2reFhxWWeZmXim5eeXr/cjVBAVK5i1f5zBr4xhApnQ8bpk22ymhFsV8lzQgX7Jquagg1EhZ2h1vTXVCXsHQ7lVuJ9u26GDimoFjzjUpHvIoO/f4jZqigHFqdcY5zWNgL3JMOxMNH4mnyQVRyQNNfqrNwmanhRHUfmUunoZwuUYYN5v82hLz7L4OmaNXQiF4QGy37iJ7HOqfheeZu5sicChr7bsBc6DQEAX71ttbIz15vl7DSMgCdKxg7hgsBgYbSDnSGDHeaMUJYR+pd9/fhwR/iH9C3vwCMklW99e477lOo/j20s65vdMXTIogsSYHCtFZAOPIlOkOPjodMMIHyFeslHWhRIPJ58Csqa8szcK6Rawdwd2bkAqUHqk1HXjzZiply6Fy8egJ860cAZTQJ+C6nmX7Fub8d7ALvFiPif3x2s6jP1zxiS6y8u2Jsx8HcUo4lfLDQ=="} +encryption_applied: 1 +is_shared: +type_: 6 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/376c1a3fe5ce4fc885e344b52b9f37b8.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/376c1a3fe5ce4fc885e344b52b9f37b8.md new file mode 100644 index 0000000000..54089fac76 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/376c1a3fe5ce4fc885e344b52b9f37b8.md @@ -0,0 +1,11 @@ +id: 376c1a3fe5ce4fc885e344b52b9f37b8 +created_time: +updated_time: 2021-08-07T17:03:37.026Z +user_created_time: +user_updated_time: +encryption_cipher_text: JED01000022051f3b6b71948c4f5d909d1af6588c78bb000298{"iv":"ejXvrI4W38XZ4yVBrF6ylw==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"tVgmTCWSasM=","ct":"BH9UWW0HJBgQMijTTCkBcRlHGkkyblnMXTfRvYQ8YrGzu1IFlEAmT5kuPmvBWXeM4eDQwOooyE6J6P4m39iuobm1UwxTEeuQWjhwGGfVJYqcMWoqCC1pJPoacpu+MPUZAswd8omD+hBAw2CL4IvMwKYHqGCsaFaVrUex99ksRgAvF4JClSaTrMofUVTNdheVP4ORlFDS97EbR/dF62CZLyrgGGDuwxVONR/EvnRZlUsNYCt5aq/RDrMUdZz3E7RqVDdQ/jLCQc/HH4lY8281QM8RCygMuJP45ZSWfVhkUT2u7Z99Jl96mc5nhSxndRjFaRtkawUSF8nbM3LQQOgHXmmDsxyB4oKE0kvUyosnUNygZNqcKjmn0Tqmuk9eIntJH9ZPe+zdMvWEIuhamXWjmBHJpFSGv0VGKw6xTeALXwk0MXV1bII7sE4vz9Bx1pO4lm1h0nKL0Dxbz/egRAV+TiANjMz5a+uoW7E+QCtspO9fE9pUj5eclKvtMdVJUeRvbIPC8KOLCIP3oBA="} +encryption_applied: 1 +parent_id: +is_shared: +share_id: +type_: 2 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/4a754e4afb6147d1a70114596d02184f.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/4a754e4afb6147d1a70114596d02184f.md new file mode 100644 index 0000000000..adfea0d2a5 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/4a754e4afb6147d1a70114596d02184f.md @@ -0,0 +1,11 @@ +id: 4a754e4afb6147d1a70114596d02184f +note_id: 6865d0c2562e4d8ba88464e75006d443 +tag_id: 11e9256883664948970fc5f78d0556bb +created_time: +updated_time: 2021-08-07T17:03:37.152Z +user_created_time: +user_updated_time: +encryption_cipher_text: JED01000022051f3b6b71948c4f5d909d1af6588c78bb0002d8{"iv":"/eUAJAOvToqE2IUjYsqAMw==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"tVgmTCWSasM=","ct":"CtDJBXr0lQRwJcVZk3Wq+R9R2VtrMdNb4AaMrOwLe/gTv/O5ya2zaOyKUyKEVTV88pws6X6sgBMPgaOKwC9FUrXMlymAMj4Tglj/gqZ6Hje7KD4b4/z0T91ZuJAZPQhdhPiY6YrxetkQmzDyF0Sm9CUKcVuTtBKu5uYcnguzPIn4L2YeZwFPAaf5GIPl/9EzebLE4c1+8W33r8NWQ+CPPf3QTkGjsKc+NaYfmnGJP8moPbtbp4eNtnaLWi+XLEK4VmQ1WbV3907209zy+8bg2GB84ymUBz0vkxb8CXwWObTl/7a3U4LxAiq7FY8mvz3YgM4RyBY2F/MIxG+T3drdkh00515qjJEYe58TOcA+3aGMqwg3Ad/1hQi8TUVODps25fiae7PBrgn+GQWio4+uaGWBiZILWxldgWNqSgNjMxRCrbEECdHNys2KtTaKU7BppBa1q2/+31a+BZaYsvn+cUNfbNrV7AhqPUFU/1yOh+gueaI65xNx5O7glzkJAGItHgoyOBYvgiYz9n6BsEF0L8uZUXsIuu+pXVmtMrM3CHEN61J1DOGm2W0q5WGQDCPZhqcBOOh50NH3Gw=="} +encryption_applied: 1 +is_shared: +type_: 6 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/6865d0c2562e4d8ba88464e75006d443.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/6865d0c2562e4d8ba88464e75006d443.md new file mode 100644 index 0000000000..6817681564 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/6865d0c2562e4d8ba88464e75006d443.md @@ -0,0 +1,26 @@ +id: 6865d0c2562e4d8ba88464e75006d443 +parent_id: 376c1a3fe5ce4fc885e344b52b9f37b8 +created_time: +updated_time: 2021-08-07T17:03:37.151Z +is_conflict: +latitude: +longitude: +altitude: +author: +source_url: +is_todo: +todo_due: +todo_completed: +source: +source_application: +application_data: +order: +user_created_time: +user_updated_time: +encryption_cipher_text: JED01000022051f3b6b71948c4f5d909d1af6588c78bb0004ac{"iv":"ZBBxavFRsrhjBtL5jXaTXA==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"tVgmTCWSasM=","ct":"GZoeIryLSfn96nstg32eBM2vW4aw7oVn+pqIAhRjoOmyOTvkEAUTULgMVmgZDFwZuZMIete7Zkl2QA/VUlI/yu9Qurss7xNedf3CrYjSs0NVMVV9KmlZbX7NNivm01KXgM/02VOBkKCQf1ZtJUp15EwkgQxg+v6UjDktRtmguV3IWccEdJpretMnqfZIoxztmxMygOeGXObpmIwAbXmJ2VMsw638ZU3PH9ClCypo5ZBOC7kuxBzo4XfvhOvQ34Bf4amGSinNIsYOW5Pjt1G+qfBFBUFhIB0QFF0GQJUHDd3hl13YITjSaJQCQiIRsPb+ECOreIPiq8ngyQ6edZPjBWVAIacmyQoK2pUGfxuNJVr9EojJV4hrTnYMRVLRm9mkStYP4IJpg9NLUALIDrb5ScTXB3hkYNSnkEdn7Nk9CvJ1g9jQP6UyhnM6HVSfAZyRF3p2/E5wMPIGWazMCQWzlPoIMnq6YzHz9BRRZkdeBepD9mOIUCw1r4R0anS9/21AcoAb0lrMxELcUCdhQfH9grroyL9AERgoRb1U1i7TnCU3dSFUhTzncEUys45TMRUDG0ulvCDSSOZEev7xHWu7Su2vUjiekxELDzZCCYumDNnGN/QRq5HhTvFQ2Asc2q4GJlnDPdiYbXM4Xf8ziIoBHjjwEo9fQu+5KsVzoEwq4QJyHlCFBUv850GWUR89C5hrxO9AJJW28RMqZSzDFcWCodaFith9yvuhIf/R/6d4CTXaBqUBUc+WTklXPhvv6lzdnELziG79BiLwDcofZVF9cKK7FZWdbYhpWF/EW5X/whNxZYW8nGxSR5LgJhQ/eJSCHs5CSXCRkCYpVLJ7tLNKxirQznDvuvpdG39uwrpkT2Cyg6enBoFpHdeb5kWRiplAlEuHLL/SoOqIPd/2aL7yuloKzlN/mRUW9uwuAr3b4Brui9pOIWt3Izy9mdOoH3MsnqV2Py1yBJmiZ1M95bx9UNdVlJE9ubIbVDuz0eDqAx5zHfb77daQ16S4CnEjWBEpeDQuHCLzaZ/pq/L1U/DQrAA+rhK/XN89C59C"} +encryption_applied: 1 +markup_language: +is_shared: +share_id: +conflict_original_id: +type_: 1 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/79a7c378c40a4538bdab7a6d46e7883e.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/79a7c378c40a4538bdab7a6d46e7883e.md new file mode 100644 index 0000000000..1047d4be59 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/79a7c378c40a4538bdab7a6d46e7883e.md @@ -0,0 +1,15 @@ +id: 79a7c378c40a4538bdab7a6d46e7883e +mime: +filename: +created_time: +updated_time: 2021-08-07T17:03:37.265Z +user_created_time: +user_updated_time: +file_extension: +encryption_cipher_text: JED01000022051f3b6b71948c4f5d909d1af6588c78bb000320{"iv":"HPYbdXS7n2xrY/DqEsUiGw==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"tVgmTCWSasM=","ct":"xO8BaMJjMZS9PSqdcld/Cu2CFQPQQhqxFoyLeQfj5kjH9Jv/O9ztM92FY0ycmhO7v+cx+E/45AFLUdAJlggC8hotYbAvmnUdU08EA/dU0NwxBnWwpWJSBzirXpKP76DUj+hIAxxaXZtu7ISIcvEANwIiMJj9lgPiVoxXfPMPJV9B8dtMdwX+8vqxZxhz5gkOva7T1NJlExlLCErALz1Vrim44bP9WpFbDH5Zihcww5VWE6y1yNbYPZprakiwNFQXQct4Ykm3taBdh3bOFcr4GLBHIccl4VSojmdIOGjCtGyZPBnaXNCPeza75NM2TTsbSEblsuPU49RBLrbICghBVj5wsZpZENf/6A3/syV6zG3TtA+5rCkLv/XH1R8kiJe9G1pHqatXsOjpIb0PVzuYl5OYmQByxmnLK0xhGTASPHKERFi6knKoml3vvy3UJsWRXtBSLk3bp1pJc9GlH2C/YKOenrOninT8SlNjzl2fKn5TOI8977Tw/zc5myrmlfiogf77cqgYKkCU1TSvKpblGvkWdoAewhkHX1DIWKp0DxB3xpToBFu4+Z6X3B+Yb5BGhNFMYibQLGAnQ1b8GWarFiYdChPqRfKV0NoiI7e0mObpvF0x7V7qKZvXN5vubJbq5Fo/X+qAVRTJlTd9cQXLDdA="} +encryption_applied: 1 +encryption_blob_encrypted: +size: +is_shared: +share_id: +type_: 4 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/8897f538ba4343cfba78eafd6c71a29c.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/8897f538ba4343cfba78eafd6c71a29c.md new file mode 100644 index 0000000000..b219a58cc0 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/8897f538ba4343cfba78eafd6c71a29c.md @@ -0,0 +1,11 @@ +id: 8897f538ba4343cfba78eafd6c71a29c +created_time: +updated_time: 2021-08-07T17:03:37.161Z +user_created_time: +user_updated_time: +encryption_cipher_text: JED01000022051f3b6b71948c4f5d909d1af6588c78bb000298{"iv":"hC0Y3d2VM7c49TbONsnUVA==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"tVgmTCWSasM=","ct":"1gD9XcIdebdX5jVkdBVqM8Sf/5ms4d8D4t61Va8BxakyO+xCdZtKUNzPpSDvu+B6lJozt9knQNC2EouuJ0itfWUSmSI+1OVL2lbt8/MJHrhQQFG5JPGcJ06aeuL1dntfRJPpxX4KeDB0Dzh6Zv8qfuosYRjwOfAFf7J+w61Q6eQTBoSi1jfLx12mXEru3JfLOCMzRHtZH+xhghA0+LofM4J2q6Pr9XwIHQ8JMaaDCYV2jhpNUQlEHKlQo1M1ftHDSq0vb7m028c6KH/p0XvqH5rNx0M77nM1FRRmoj1pUgOfzszn0VNbKWi+OmBMWPS0yEruUKznneR35/0ZDFR+eRSHeEsJNbw/uz2NddC1q2Jdj26x/Gl3Sg6fpk2jUrXCNXidyVYWXBnYbK9jNJqIQz3uUQtCxe+lFH7ezZxX2b2gp60gXMluCbUomc8zbmaZHqM6kSAil+P6QUcpUhpMkfS5tjJRvLQ0Gj0/nbRiYjLWaCT5eA/IETU6DDZtrgUFsU1LCCmW5i0EzbU="} +encryption_applied: 1 +parent_id: +is_shared: +share_id: +type_: 2 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/a83fbcc450ab44679a785f5f265b5427.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/a83fbcc450ab44679a785f5f265b5427.md new file mode 100644 index 0000000000..3275828bb7 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/a83fbcc450ab44679a785f5f265b5427.md @@ -0,0 +1,26 @@ +id: a83fbcc450ab44679a785f5f265b5427 +parent_id: affcd54c42bc4f49829013c6522d3a6f +created_time: +updated_time: 2021-08-07T17:03:37.143Z +is_conflict: +latitude: +longitude: +altitude: +author: +source_url: +is_todo: +todo_due: +todo_completed: +source: +source_application: +application_data: +order: +user_created_time: +user_updated_time: +encryption_cipher_text: JED01000022051f3b6b71948c4f5d909d1af6588c78bb000504{"iv":"Vq3cOPpdY23SvcyUjH0rWg==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"tVgmTCWSasM=","ct":"gz0HU2L6HJH0kt20N431NYJQkWdWU3x/MlwO35NNboGvnfYQPU7bQIDtfNf4l/8Wx3LVNM2XqYSmbgzaF5dASvZUmk4Qh+szQUOk60iaxkqKHSm+85Q3O4RTT16aPh8MtJFlJAWrWI0SrNmgHUIcrxpgy//GSG/XIBVSxSGKIaung1bdTy0N4bm6RJjluEwsoPFi6SXwypbM1ZalGVaAEoNNYQZzaDqqGhe/EUpAC8iAQehYeSNf0PqVerx+AonOrlQ7yElLcPtpnadTlCVy0rc1suL0dD3mdRZrFT2Tn/rhb3tzmxM3jZWPl/VzFMCfJkSA0g/TV6r3V7jo/zlYxRs4RvCg87T10fwB5fBsUvmou9bt4wt4mR4YbQxEhlX/QCez4joN80mKu39vblDiiQt5WBzUR0k2D2+1iDvrhitXUVRPj+zdLSDiTDZYzen1TBnkknPZcablVHVCoHDsB1q0mwbtfR7cf/EqigIPOzF7S6I98Zf/ngWt1XPYnzz1pzG4vmSlKb/Ir7fY5KKpZFpuhoneQAhx4K7gX5LXAwABU/Py3DJJox5bSFuUMQN3ImN3BeZN54zsZGjJXXo0r4XGB0XOB/h+x054xflgrFIZb4QtmNbRu2DS7NCmVqlbsUigoh3MyddyLl2PZ5uyQ+YOgFZKUK+kJ7ctC/Cxv5RCRd5gjc9F+Wrn5bg+DziyIlO29q3tRKtIqWN2j7C13hVE3A87SKt2EAXazjMI6vW4jC2jtp5UWrpD6p89oNbtFzwKF0G/aHOOxmwRtV1UWGTXbvwF6PrfU2iHXl72+4At7wV4K74cty15QSZvg/N5T0inHL07q4t11ynvNHMuNQQr9eKyMUrPjOIlOlel7tYxt4H3Qmco0Nb4Oi35ZcRClmWSIVGKcz3cBLtGAYXZrG5fbSPqFa4Kf0vwjeIcdE8GpdVAeoUuw/Wj4DQU/RJncV6GTRTVJKZgC/Wreu8dob5ZkUuiogYehNa4xUupfPLuS3dAICx+/0QWEaFrnl3CXhaHH4I9Z25CUh/eqb7XrvXtaWJ1vEq27JxRIWSgNHqj4fI5l5fBvbPgYAwqllJ2aTLYqfY7/qBAm/2ph/g7EOceqC4AFqGExVF/h7UD4vZUh98YA64ZOD0EeICQ"} +encryption_applied: 1 +markup_language: +is_shared: +share_id: +conflict_original_id: +type_: 1 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/ac3adde7d7d642a48355553923dd2cde.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/ac3adde7d7d642a48355553923dd2cde.md new file mode 100644 index 0000000000..dab4b63494 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/ac3adde7d7d642a48355553923dd2cde.md @@ -0,0 +1,15 @@ +id: ac3adde7d7d642a48355553923dd2cde +mime: +filename: +created_time: +updated_time: 2021-08-07T17:03:37.141Z +user_created_time: +user_updated_time: +file_extension: +encryption_cipher_text: JED01000022051f3b6b71948c4f5d909d1af6588c78bb000320{"iv":"kw4oH3NXPNlqBs9OHb+3BA==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"tVgmTCWSasM=","ct":"noSUeSw/bQ7CvsURbo2Bs0k+MHplFE2FCRjlfpBNqdDXKzcqGGTjPNH/5r9Ulw+rwXtzC0Td1MjEQ83YOphuoljqQEM7UD4wFDYGvOFQxFScCBGs89FU7aMB9uGre71eL1LlXvjDnX/a2p0ZV6qyp55Wp7DPBYe/tQJAxpdvbOXn271cHmiGMPrhV1ML2PJR24eEDwqU5nMMcqf+FpgvBo/KP3xrWwPh1MZy0AMtRPwrhr7xBE8XKFdPwJChKLMMTCqY3w6Loi28AJWvXgV3S0aqZQ4tFgw8IaWwTdxLXUVxO6D1vTQ98rEFp4f0PI0dJULlaVzXxnrczyHl/V5F9IYGaeiNth+6MJc6USkpQ3F+CgBpE3EOKcfojBE9EKa6LZcs4Kg+F8qGH8XdewgfcqMtwKt8qef+f0yIlVjcGNNOd0vlzEEIhtVhn5AlEzzwgu9875RsrMqS4yikfZkELbJ1dqdcffQS3KUINUzuHjxSKjAuDE/qk78pGetoCzlEr5ipeAvymyci6EbbZj1bWbmKXP8yiPlxNXwkydTmDo7FH4tHWzMq362qOFxoXw1Gu2eqqQMpSfhvCCQn+Li/GkFsDXd76zHJU/9J0Cqc3CQbrVFBFPIgGQ8LBauK/mUqi3uxZ5YF+DR0U2BODme+I/Y="} +encryption_applied: 1 +encryption_blob_encrypted: +size: +is_shared: +share_id: +type_: 4 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/affcd54c42bc4f49829013c6522d3a6f.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/affcd54c42bc4f49829013c6522d3a6f.md new file mode 100644 index 0000000000..2829183f4e --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/affcd54c42bc4f49829013c6522d3a6f.md @@ -0,0 +1,11 @@ +id: affcd54c42bc4f49829013c6522d3a6f +created_time: +updated_time: 2021-08-07T17:03:37.029Z +user_created_time: +user_updated_time: +encryption_cipher_text: JED01000022051f3b6b71948c4f5d909d1af6588c78bb0002c8{"iv":"6aYSqc7Qkh4097CegpcyhQ==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"tVgmTCWSasM=","ct":"YIEVEXEs8FLByd5Hdzw01atV/QwNudmMGFwh7Ve6grYsb62ezrGE0lsNuXG6U08JGiSE+vMnalY2OhixxdtOuAXkjVYsvndkSt75/BERHfJZ7OogFTHko3jpR661JfdzrQ2RqBX1S9R0dPYCnP7uZM8+phNxBDt/JwGFtxmQvPo+5ZeeRZC+gnKCWg2H+C1SiImqCkiNkppdi9Ul6OytBUMGoJkuet4+t0hth+mrJcCckshJTeqUYOA2+X4Xy9bOg6WKbGocKJsGyAnNnXKFaxamDa58xXYEfyyaMNCZaBk4yHK9L4jcez21Fy7lr7KQdQOInfY+KnbQsl8UTGAgU8SmR//hrmkAW3jtHcHQBuFkcM0lyXREloIcc6eFkuqfkqI4YK6ySqbZ5qdAGz7WNAPTx39EFxgmMKvYFf49m6JAQEKxMkgYG0eT686j9rsITrUZbGQwyclXZ2mHPehZ2eiQE+XXsZawgDb1Jb21turolUKTU/TXc8ObpUfW1NUmRElVtkeuNfEAmf3iAn8/qKTQQ/33oqnNugdpN9tuxZM7K+e3JR3FhXt2eJrPLA=="} +encryption_applied: 1 +parent_id: 376c1a3fe5ce4fc885e344b52b9f37b8 +is_shared: +share_id: +type_: 2 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/ba876a7c0d2e44fc8105887c2a3fe9d0.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/ba876a7c0d2e44fc8105887c2a3fe9d0.md new file mode 100644 index 0000000000..810750d8d9 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/ba876a7c0d2e44fc8105887c2a3fe9d0.md @@ -0,0 +1,26 @@ +id: ba876a7c0d2e44fc8105887c2a3fe9d0 +parent_id: affcd54c42bc4f49829013c6522d3a6f +created_time: +updated_time: 2021-08-07T17:03:37.150Z +is_conflict: +latitude: +longitude: +altitude: +author: +source_url: +is_todo: +todo_due: +todo_completed: +source: +source_application: +application_data: +order: +user_created_time: +user_updated_time: +encryption_cipher_text: JED01000022051f3b6b71948c4f5d909d1af6588c78bb0004ac{"iv":"lyq7Vk+7IxfwXTfLbAQUFA==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"tVgmTCWSasM=","ct":"vqREg9i2rlzQYKCnH/K2fqheqlXY7N6KfjKyKN33fOob3+urWS1TZWbecom0jnkNflsCmaFatt7awDuOccMIE8YkHG7FaVMnWVWkVRGvFlMD13xQXpVbEEHuIxTUEnBj8ey4quavW8PU6Q9K2KPIZoCDRN1c89ydGoYXSkF8FvaSgz8Di/tvpd/uYzhsYPqsNlGXY+Q4Uivna4fhL0S84cCE9J63JqdNseETPoOlk/bmv6fnAsEBPNJhLlAHHu6FR7IMXtneJfmP5WSZEY5v8yVqQf29xrhwbnwuipm4U3CxWH3u+C3UYSDoyJKW/dZQLDz2gZsNWE7NYMir5OJXIW0Hq/03zy1XmOX5EZyP977WbEEaQBQgSsL2BOJzaw1mrOWFwu46pWeZ6U3AMbRkxry5tgBzWwjkdqdsfPnbHzNjFA/pkSVZVqEPq1puf5mRhdNtn3RdwRN/XK/6GCj67qcwMnDcGvwjKicMKvZVy1igcaqlLWJdmXJYoUuu1UsvpiSPVPDUhTu8hNZ/l0pTBIaViVg5YsxTeUdRyqA/ItM79IDho4udSpNhUxx1SvRfT7MAcCcHHzeUjMhuCqlWzqztBQrk2SpvDd2LEf9sTDRxaJd3mVDlzWPmMrXJ/x27l7Y1PPV0AkYuPjMOl1TzCZTIw/8HTnOr1kKVURHImbZEmE3d8SzowLq3R4Mx1WnlCIZKq0P1brmD0NJZHX6Fx0iD23HX6YVy8JWejzPtKyU6RYR3VBk+LyyTkkYmKjPkgJKO1lJ8h5JqvJubh48WWU6VAZLikfaXy6F3+/syba8fgfU7/KPz3JiVxG6E/mBL+etwziF8UfhRoLx+k5SHWHOhk5+VbJY/VZxh2Mijjs2Rsoj9os8Ic+Q9UXiA/TCOaqkW/kuTor+Nd0bxVvfxwcsCOI6TijFdmkfw3r5C3I5C+MCDo3375NCWYlL5yuWv3LfdKaDd7Gns10xi0lAY2Jv/gpdDM48bN4Hzh17MfNVau5J9oxUN6P8NeZMhXnzwwohVV5lW9qVLByavWMlECMoAvuq7KK7h1MV0"} +encryption_applied: 1 +markup_language: +is_shared: +share_id: +conflict_original_id: +type_: 1 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/c5a09550eca84955bd4df85345299e5c.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/c5a09550eca84955bd4df85345299e5c.md new file mode 100644 index 0000000000..48e983ad27 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/c5a09550eca84955bd4df85345299e5c.md @@ -0,0 +1,11 @@ +id: c5a09550eca84955bd4df85345299e5c +note_id: a83fbcc450ab44679a785f5f265b5427 +tag_id: 11e9256883664948970fc5f78d0556bb +created_time: +updated_time: 2021-08-07T17:03:37.148Z +user_created_time: +user_updated_time: +encryption_cipher_text: JED01000022051f3b6b71948c4f5d909d1af6588c78bb0002d8{"iv":"QTeh2mfqgHTR5Brrz/jmnA==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"tVgmTCWSasM=","ct":"HcosQRm4ww6Nvq13nc2rchWSLuoDU0NMG36paBQ1/1rttyb25US3uZjq0ahhYpofWfTdfPdf/lWYgRWqikBHP6l8xchPcKSwfZ03iAz3TBIGbnnDySW8NDtxUEgIraNpqFj8vsZc/xrT+vrOJef6TqFW0c/MeiQOEIL6UEaVTm8H+6UhEDg3ikPEhcwe9+3vM3V7eLKTRZYkIRFbQ+lOiHGWIbsHg/kuxmYEuKkuLna3cWb8ioV6TqP/5C90OMsexxUohyI5DWiohqebQv1cxKe4J7YrrMtCptNfHXA0/Oe8qSKLrWktFkHtaUVKlL7mASnpuY47x2WJJU200FExOcOpmBOjxZqo2JDML75hq2ZWYQz6KfhVE1Pru1yIuOzIhkJepa6edmuVpMfKuVMp+q1fa4wfdhp5giIgUKesqq/SGeNwxzH7kygStJxKrTRuuZ16rahxI4+modOEiV/henqD1gPH6OuuganjceqJZX7QBFxlcrmHes4TghDE+11Qjcq7jvW3TaMtpR1L2PtwMO60PvrSiQjG9T3QpIKXRZih6IyLUjJICwyQtHAzxGkeSXO6dFYtIdej8Q=="} +encryption_applied: 1 +is_shared: +type_: 6 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/c9e46ce958bb4bd5a3d0cfb65855d9d2.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/c9e46ce958bb4bd5a3d0cfb65855d9d2.md new file mode 100644 index 0000000000..71d58531d9 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/c9e46ce958bb4bd5a3d0cfb65855d9d2.md @@ -0,0 +1,11 @@ +id: c9e46ce958bb4bd5a3d0cfb65855d9d2 +note_id: 00dceec04659436196bae6b56eea10ad +tag_id: 07cd0925745b4441b898288f000c93d8 +created_time: +updated_time: 2021-08-07T17:03:37.269Z +user_created_time: +user_updated_time: +encryption_cipher_text: JED01000022051f3b6b71948c4f5d909d1af6588c78bb0002d8{"iv":"Ks7In3VN+ukwOfMigv5aGg==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"tVgmTCWSasM=","ct":"5PCqJZzapvNh8PwCONuOoEFRS+dY0RKHBCZrgEd5rsBNCDHyW9H9GL7s/CtwYGJDgdRAtT5qCghNlTQa5t9cKE/N+jl+YbgWGdyRHv6BibzWHFd3z5mVWw4yr7jZLf7mb/3z+iiHc5kWsVbiKBlmkWROaVCspI3cwjBNyGCs8204vT6KwYC/OGji21V3gMHrnT7EuwaqvoXxQtYzH8ck8InQz/fQwVRZyFKVEIBcBclqSskpZ/JmXDvMqqq+bqiRRvcrqV6HhHmieOVmxMXwdSmSnRSgybwKjCGD63nmkCQFDvEAt9TvpNqESMDuL+q9DCW9FtZ0SHTfzm6eyBzeV6GdPUuMQVnGKMOhjs+CPZZ1cnEGPYt4fMgZdkuYf57b+Z0BCFC7eTdOhQBQdBtKryUoFp/J7Tg4MXbMCKIH2ZBPxPSAk30nNZbNBynidLOLBAzUFcBhxbjspqdqp3KldYNoqiQNP1j7sgta3e9z9Tf85Jpptfv49NMnCNgPojVXg6bgb5vuDO1QVUmiknhlK1BPjP7UsYFpW/n9OLrKUlTMTKQ1mPA0JkkrsJchB8GFEHPPRb8LJyHycw=="} +encryption_applied: 1 +is_shared: +type_: 6 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/d7f300c742bb44338563ddbd6fb285c3.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/d7f300c742bb44338563ddbd6fb285c3.md new file mode 100644 index 0000000000..39c182145c --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/d7f300c742bb44338563ddbd6fb285c3.md @@ -0,0 +1,11 @@ +id: d7f300c742bb44338563ddbd6fb285c3 +created_time: +updated_time: 2021-08-07T17:03:37.028Z +user_created_time: +user_updated_time: +encryption_cipher_text: JED01000022051f3b6b71948c4f5d909d1af6588c78bb0002c8{"iv":"/TciaFYKNHcgOGewTRhZ7Q==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"tVgmTCWSasM=","ct":"SDEcAeYxBnBVeHf+cPjW5pCSfGkSkHKZTQ50r3TnG1kdORfFV3Xgvchnjc10AdJxZs2QgMT7PoKioF+M9vWJbBbttiEZWVD2AdnnpwUtg3pYRenNvVmZFJ2TRc+llUrKoitgZ4iFUdeW6FD6zGRnXU+06apcmQFDZtHpqFEsFpIaoghYqV1SsjLEAt4sBKQPAFNpK3rJoQoVqVyqglMMAv3Z6UGSb2R8bkY6JxcJi86e1sJ9o/rovmco8cUhAWWa2TtZXvkFn36rTWM64caHW/BNGsM+xTiyvoLyNpWygsNSoRL1gzcLBn6WChdcK0UOaiQZHnWVeeHuMpcpR6lqyaFbf+g8yMtxRGhh4es9eG8F/rimV4aBcRnSdirPXJhLmw5H0OLaXtZOfy2RSYm7P7VqQ9BnYgRbqiZ3f/Bd0izciiekxIskvLWOK2hjc09YnaR0JjNQqsnuV4q9sgCnJkkKwXXv+k6WGlZFUTac4HUM3x+NIFa6yCHAt0D57uaiSJppmFG7ypR7xSTAKuNfGLHBaPlkyMFKDimIKf7RSjnpw1Ptkgbh3vxzg/royQ=="} +encryption_applied: 1 +parent_id: 376c1a3fe5ce4fc885e344b52b9f37b8 +is_shared: +share_id: +type_: 2 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/df92d4e526b84c0d9d294c40790b6ee1.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/df92d4e526b84c0d9d294c40790b6ee1.md new file mode 100644 index 0000000000..8384aa7743 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/df92d4e526b84c0d9d294c40790b6ee1.md @@ -0,0 +1,11 @@ +id: df92d4e526b84c0d9d294c40790b6ee1 +created_time: +updated_time: 2021-08-07T17:03:37.161Z +user_created_time: +user_updated_time: +encryption_cipher_text: JED01000022051f3b6b71948c4f5d909d1af6588c78bb000298{"iv":"0o/EgX9FH6llgRkC/uubsA==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"tVgmTCWSasM=","ct":"xXtHkQ8VT2bM2qWPZStrYtmijFVZEP6qk4qxH+jFTcEw1N7pMh+OOTgZIru7ZaviAXdUSpScKdfuU85t0chsQzsxY+Eg+7QH5P0EQe62WLHubHujyjEi1ogbKpXaXEehjFZDwWc2MVEy3CuQ83K/1h0Y8wcND4W5pXrD2sniIXFiLg68uEAGmTRmbfaUaPfI1u8zBdLE97wmpA7gsXjsV5Rudb30gpHHM9iGwTU5HiXu9vgwufVv2FP/a6zlKmMWIRXlzkIOEtQPghGIOuFoGPQRyKPu3ab86cb5wU2xuXPIPn74eXY/X+sUUa2jZZPPqMWEtXIXF6DA2C52HRsaZ8J5d+TD7eq3vRjbFwYSd4F5lFdnq8SQGZyq4fdZqFF7cf0fFmOGmu+Z6JwYP2rJdVHgDuF8J0+VdkGCo/pZvROn3keUSZ+cl1VNp3M23xaCi5NgLD+ZEVlw+PVbJpd5hpg151zk6vWM7jpNuzdT4zgtdhoeyybInnvaXyRKULZP4FESMCgy+lJfEtA="} +encryption_applied: 1 +parent_id: +is_shared: +share_id: +type_: 2 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/ea596f46d526439490c9e1a32e35fab3.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/ea596f46d526439490c9e1a32e35fab3.md new file mode 100644 index 0000000000..1ca4d3c246 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/ea596f46d526439490c9e1a32e35fab3.md @@ -0,0 +1,11 @@ +id: ea596f46d526439490c9e1a32e35fab3 +note_id: 04761c7a9930415f95ce98dac1706411 +tag_id: 07cd0925745b4441b898288f000c93d8 +created_time: +updated_time: 2021-08-07T17:03:37.159Z +user_created_time: +user_updated_time: +encryption_cipher_text: JED01000022051f3b6b71948c4f5d909d1af6588c78bb0002d8{"iv":"TjqK1/ZKKBVuSEEQmJbvmQ==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"tVgmTCWSasM=","ct":"N7WRYFvCp57jTg60kTvaYsff+IETBz0oJqpQ9Wvb6YedeILeOg8x/vVTQnkhzinDnMggW3BKO69DNHh/laOmJ0CNrihDtt/EVsL3oULu2cFj0Kp7WSwSjyhL25ahvoibToJ9x4d/ERE+yPpw7qiYkTQquGEwQGtgPsZfMrvujWA7CeRvjbGVAfVLjWiuu0ByByG80SqrWX9/XJoI4hfqxkraouwL+KclFWqPCsq/m8y1OfGwjVEnu9cR9FwIK5+F9SEJnrdyx84/gUVm0N2RgUQ+O48Udp2MK1qXpxtq2IX11t///d7ex9lkWHkqteIcHhyISTXCoHso7pLOLnkmJBtGZlp4B5EmoSguw0GJJhDB7ZmJrn5UoBIh/xtq8/Yuqr0/qV8US3xUPUWqQg18UFPTKZyB2S4dGp7oUwK/tBfPGDq1B7sfYWwQkW7t9F1vgQfBPNvLkjDcsU8wq1xpK4FfIo6RRZNcitJLuIBS3wQ4Mvmf2MmbMz90R9qkqk2ibOLcs2jh8x4WfIgdCZjfmUKVQMu7DLV4Aca6IO6uXycaZcikHaGW03uyNTSfHISLzQ5ahOifHQYCdQ=="} +encryption_applied: 1 +is_shared: +type_: 6 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/info.json b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/info.json new file mode 100644 index 0000000000..247f648d02 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/info.json @@ -0,0 +1 @@ +{"version":3,"e2ee":{"value":true,"updatedTime":1628355817270},"activeMasterKeyId":{"value":"1f3b6b71948c4f5d909d1af6588c78bb","updatedTime":1628355817333},"masterKeys":[{"checksum":"","encryption_method":4,"content":"{\"iv\":\"J248v74XaIuzE2PuLfZhsA==\",\"v\":1,\"iter\":10000,\"ks\":256,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"rlIYz9E3bBU=\",\"ct\":\"xe684JZcA0wirB6aaBZd4rBdRQwjA/EqomPOiNCfV+hApf0ZajSTHirPf9UblaJQ9q/sjvs4v2I/yT+NsKB9WNsEegh9dKaXYlfzLbXCW84hBwAxAgBJf9IrtviOD0hS4ArhT3vwNYqZAvUlY7t8E68ntVaNl/4XoIDUHIto9Q9zDQqZd43KJxI7mkPD/pBHtW+OEPYzXtMGDTaV1iKJp2Tnu1w2ENCC8iN/aIwclT8Xp0uy1K3pvfck/FMO8yS4IX4tg6CC342o07DFla7ufeMzjys23j0O+N+mHOlZjumkMdMsRwyE5+JYoG5s7J2f3s3RkIBiBx/HCEP3ecQzaKMSpx5k7Nl9lobYbp9bP12pJSG72ixMGm9+S21b1cQw7geyOxsoaXoe54BsMnvtuey2tFxm3nwk5aBr4F2OWzeulRDGuOlScJkoV8x4C1j6V4qd77Rvf3hkxoHXzIZtir9wy8Iq3EuLuxv07rD0wnNj26XZzAfwN1xT7zLVwjZltNPQ3PKEEVHbKcROXDT1qFpXKeCsCGNWQCHbER7A7WFRj1VW3YbjntJg5JGNBmi3Jiry/AuAb6FYtGYC/J4jQ107WH8PezcbC9oic6ipblgmCHfmKm01sBc0b7U1rE2vWk8vHGlSC2W/LX/5aEwaeVf5YZVxsZ3MWHk3ufEbYF46toL+7as58Q==\"}","created_time":1628355817331,"updated_time":1628355817331,"source_application":"net.cozic.joplintest-cli","id":"1f3b6b71948c4f5d909d1af6588c78bb"}]} \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/locks/.gitignore b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/locks/.gitignore new file mode 100644 index 0000000000..5e7d2734cf --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/locks/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/temp/.gitignore b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/temp/.gitignore new file mode 100644 index 0000000000..5e7d2734cf --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/e2ee/temp/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/.resource/933cf209b0094d43884c03149f034128 b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/.resource/933cf209b0094d43884c03149f034128 new file mode 100644 index 0000000000000000000000000000000000000000..b258679de6bf0f62a6ae9c65f3b8e85d1dba3c50 GIT binary patch literal 2720 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 literal 0 HcmV?d00001 diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/.resource/b1947d6f70314ab180b343e90f1b4660 b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/.resource/b1947d6f70314ab180b343e90f1b4660 new file mode 100644 index 0000000000000000000000000000000000000000..b258679de6bf0f62a6ae9c65f3b8e85d1dba3c50 GIT binary patch literal 2720 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 literal 0 HcmV?d00001 diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/.sync/readme.txt b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/.sync/readme.txt new file mode 100644 index 0000000000..e46819c443 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/.sync/readme.txt @@ -0,0 +1 @@ +2020-07-16: In the new sync format, the version number is stored in /info.json. However, for backward compatibility, we need to keep the old version.txt file here, otherwise old clients will automatically recreate it, and assume a sync target version 1. So we keep it here but set its value to "2", so that old clients know that they need to be upgraded. This directory can be removed after a year or so, once we are confident that all clients have been upgraded to recent versions. \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/.sync/version.txt b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/.sync/version.txt new file mode 100644 index 0000000000..d8263ee986 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/.sync/version.txt @@ -0,0 +1 @@ +2 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/0394074009f8454e86cbf935a03da973.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/0394074009f8454e86cbf935a03da973.md new file mode 100644 index 0000000000..79688a4e36 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/0394074009f8454e86cbf935a03da973.md @@ -0,0 +1,11 @@ +id: 0394074009f8454e86cbf935a03da973 +note_id: 59e44f93f28242529fbbca804bdd25c5 +tag_id: 1e55a346b1c3444996c3c9f52d4adc47 +created_time: 2021-08-07T17:03:33.718Z +updated_time: 2021-08-07T17:03:33.718Z +user_created_time: 2021-08-07T17:03:33.718Z +user_updated_time: 2021-08-07T17:03:33.718Z +encryption_cipher_text: +encryption_applied: 0 +is_shared: 0 +type_: 6 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/052789e9393649ae85427a7c17bfd263.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/052789e9393649ae85427a7c17bfd263.md new file mode 100644 index 0000000000..6b2ca3846d --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/052789e9393649ae85427a7c17bfd263.md @@ -0,0 +1,12 @@ +tag1 + +id: 052789e9393649ae85427a7c17bfd263 +created_time: 2021-08-07T17:03:33.706Z +updated_time: 2021-08-07T17:03:33.706Z +user_created_time: 2021-08-07T17:03:33.706Z +user_updated_time: 2021-08-07T17:03:33.706Z +encryption_cipher_text: +encryption_applied: 0 +is_shared: 0 +parent_id: +type_: 5 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/1e55a346b1c3444996c3c9f52d4adc47.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/1e55a346b1c3444996c3c9f52d4adc47.md new file mode 100644 index 0000000000..82472880b2 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/1e55a346b1c3444996c3c9f52d4adc47.md @@ -0,0 +1,12 @@ +tag2 + +id: 1e55a346b1c3444996c3c9f52d4adc47 +created_time: 2021-08-07T17:03:33.717Z +updated_time: 2021-08-07T17:03:33.717Z +user_created_time: 2021-08-07T17:03:33.717Z +user_updated_time: 2021-08-07T17:03:33.717Z +encryption_cipher_text: +encryption_applied: 0 +is_shared: 0 +parent_id: +type_: 5 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/24a091b928cd4d70968514d4bddb79ad.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/24a091b928cd4d70968514d4bddb79ad.md new file mode 100644 index 0000000000..89b6ef943f --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/24a091b928cd4d70968514d4bddb79ad.md @@ -0,0 +1,11 @@ +id: 24a091b928cd4d70968514d4bddb79ad +note_id: 264606b4634f4fe58dda0e58a0dc64d9 +tag_id: 1e55a346b1c3444996c3c9f52d4adc47 +created_time: 2021-08-07T17:03:33.835Z +updated_time: 2021-08-07T17:03:33.835Z +user_created_time: 2021-08-07T17:03:33.835Z +user_updated_time: 2021-08-07T17:03:33.835Z +encryption_cipher_text: +encryption_applied: 0 +is_shared: 0 +type_: 6 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/264606b4634f4fe58dda0e58a0dc64d9.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/264606b4634f4fe58dda0e58a0dc64d9.md new file mode 100644 index 0000000000..cdc5a7c8aa --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/264606b4634f4fe58dda0e58a0dc64d9.md @@ -0,0 +1,30 @@ +note5 + +![photo.jpg](:/933cf209b0094d43884c03149f034128) + +id: 264606b4634f4fe58dda0e58a0dc64d9 +parent_id: 625eb45b911248028fecd94bdca86f61 +created_time: 2021-08-07T17:03:33.725Z +updated_time: 2021-08-07T17:03:33.833Z +is_conflict: 0 +latitude: 0.00000000 +longitude: 0.00000000 +altitude: 0.0000 +author: +source_url: +is_todo: 0 +todo_due: 0 +todo_completed: 0 +source: joplin +source_application: net.cozic.joplintest-cli +application_data: +order: 1628355813725 +user_created_time: 2021-08-07T17:03:33.725Z +user_updated_time: 2021-08-07T17:03:33.833Z +encryption_cipher_text: +encryption_applied: 0 +markup_language: 1 +is_shared: 0 +share_id: +conflict_original_id: +type_: 1 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/5186fc36e6e44e14bd9dff172d7c41b2.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/5186fc36e6e44e14bd9dff172d7c41b2.md new file mode 100644 index 0000000000..55f1e0ba6b --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/5186fc36e6e44e14bd9dff172d7c41b2.md @@ -0,0 +1,13 @@ +subFolder2 + +id: 5186fc36e6e44e14bd9dff172d7c41b2 +created_time: 2021-08-07T17:03:33.591Z +updated_time: 2021-08-07T17:03:33.591Z +user_created_time: 2021-08-07T17:03:33.591Z +user_updated_time: 2021-08-07T17:03:33.591Z +encryption_cipher_text: +encryption_applied: 0 +parent_id: c227e85585674332badfbc09c50907ec +is_shared: 0 +share_id: +type_: 2 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/59e44f93f28242529fbbca804bdd25c5.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/59e44f93f28242529fbbca804bdd25c5.md new file mode 100644 index 0000000000..d92748c05d --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/59e44f93f28242529fbbca804bdd25c5.md @@ -0,0 +1,28 @@ +note3 + +id: 59e44f93f28242529fbbca804bdd25c5 +parent_id: c227e85585674332badfbc09c50907ec +created_time: 2021-08-07T17:03:33.711Z +updated_time: 2021-08-07T17:03:33.711Z +is_conflict: 0 +latitude: 0.00000000 +longitude: 0.00000000 +altitude: 0.0000 +author: +source_url: +is_todo: 0 +todo_due: 0 +todo_completed: 0 +source: joplin +source_application: net.cozic.joplintest-cli +application_data: +order: 1628355813711 +user_created_time: 2021-08-07T17:03:33.711Z +user_updated_time: 2021-08-07T17:03:33.711Z +encryption_cipher_text: +encryption_applied: 0 +markup_language: 1 +is_shared: 0 +share_id: +conflict_original_id: +type_: 1 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/5c0b421ac1e645e48dbdba4ed5328327.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/5c0b421ac1e645e48dbdba4ed5328327.md new file mode 100644 index 0000000000..71cce5ec4d --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/5c0b421ac1e645e48dbdba4ed5328327.md @@ -0,0 +1,28 @@ +note4 + +id: 5c0b421ac1e645e48dbdba4ed5328327 +parent_id: c227e85585674332badfbc09c50907ec +created_time: 2021-08-07T17:03:33.720Z +updated_time: 2021-08-07T17:03:33.720Z +is_conflict: 0 +latitude: 0.00000000 +longitude: 0.00000000 +altitude: 0.0000 +author: +source_url: +is_todo: 0 +todo_due: 0 +todo_completed: 0 +source: joplin +source_application: net.cozic.joplintest-cli +application_data: +order: 1628355813719 +user_created_time: 2021-08-07T17:03:33.720Z +user_updated_time: 2021-08-07T17:03:33.720Z +encryption_cipher_text: +encryption_applied: 0 +markup_language: 1 +is_shared: 0 +share_id: +conflict_original_id: +type_: 1 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/625eb45b911248028fecd94bdca86f61.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/625eb45b911248028fecd94bdca86f61.md new file mode 100644 index 0000000000..9d6af47317 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/625eb45b911248028fecd94bdca86f61.md @@ -0,0 +1,13 @@ +folder3 + +id: 625eb45b911248028fecd94bdca86f61 +created_time: 2021-08-07T17:03:33.723Z +updated_time: 2021-08-07T17:03:33.723Z +user_created_time: 2021-08-07T17:03:33.723Z +user_updated_time: 2021-08-07T17:03:33.723Z +encryption_cipher_text: +encryption_applied: 0 +parent_id: +is_shared: 0 +share_id: +type_: 2 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/6b3b51468e8e44f1ba1b69e4d7e71613.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/6b3b51468e8e44f1ba1b69e4d7e71613.md new file mode 100644 index 0000000000..0d56ce51c5 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/6b3b51468e8e44f1ba1b69e4d7e71613.md @@ -0,0 +1,28 @@ +note2 + +id: 6b3b51468e8e44f1ba1b69e4d7e71613 +parent_id: 5186fc36e6e44e14bd9dff172d7c41b2 +created_time: 2021-08-07T17:03:33.710Z +updated_time: 2021-08-07T17:03:33.710Z +is_conflict: 0 +latitude: 0.00000000 +longitude: 0.00000000 +altitude: 0.0000 +author: +source_url: +is_todo: 0 +todo_due: 0 +todo_completed: 0 +source: joplin +source_application: net.cozic.joplintest-cli +application_data: +order: 1628355813710 +user_created_time: 2021-08-07T17:03:33.710Z +user_updated_time: 2021-08-07T17:03:33.710Z +encryption_cipher_text: +encryption_applied: 0 +markup_language: 1 +is_shared: 0 +share_id: +conflict_original_id: +type_: 1 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/8436cd9c58824ca58ed146d7bd519dfd.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/8436cd9c58824ca58ed146d7bd519dfd.md new file mode 100644 index 0000000000..0b7b0efc03 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/8436cd9c58824ca58ed146d7bd519dfd.md @@ -0,0 +1,30 @@ +note1 + +![photo.jpg](:/b1947d6f70314ab180b343e90f1b4660) + +id: 8436cd9c58824ca58ed146d7bd519dfd +parent_id: 5186fc36e6e44e14bd9dff172d7c41b2 +created_time: 2021-08-07T17:03:33.592Z +updated_time: 2021-08-07T17:03:33.703Z +is_conflict: 0 +latitude: 0.00000000 +longitude: 0.00000000 +altitude: 0.0000 +author: +source_url: +is_todo: 0 +todo_due: 0 +todo_completed: 0 +source: joplin +source_application: net.cozic.joplintest-cli +application_data: +order: 1628355813592 +user_created_time: 2021-08-07T17:03:33.592Z +user_updated_time: 2021-08-07T17:03:33.703Z +encryption_cipher_text: +encryption_applied: 0 +markup_language: 1 +is_shared: 0 +share_id: +conflict_original_id: +type_: 1 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/8ce22808466b43008754086e29acccb5.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/8ce22808466b43008754086e29acccb5.md new file mode 100644 index 0000000000..67678cf843 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/8ce22808466b43008754086e29acccb5.md @@ -0,0 +1,13 @@ +subFolder1 + +id: 8ce22808466b43008754086e29acccb5 +created_time: 2021-08-07T17:03:33.590Z +updated_time: 2021-08-07T17:03:33.590Z +user_created_time: 2021-08-07T17:03:33.590Z +user_updated_time: 2021-08-07T17:03:33.590Z +encryption_cipher_text: +encryption_applied: 0 +parent_id: c227e85585674332badfbc09c50907ec +is_shared: 0 +share_id: +type_: 2 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/933cf209b0094d43884c03149f034128.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/933cf209b0094d43884c03149f034128.md new file mode 100644 index 0000000000..21f890906a --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/933cf209b0094d43884c03149f034128.md @@ -0,0 +1,17 @@ +photo.jpg + +id: 933cf209b0094d43884c03149f034128 +mime: image/jpeg +filename: +created_time: 2021-08-07T17:03:33.831Z +updated_time: 2021-08-07T17:03:33.831Z +user_created_time: 2021-08-07T17:03:33.831Z +user_updated_time: 2021-08-07T17:03:33.831Z +file_extension: jpg +encryption_cipher_text: +encryption_applied: 0 +encryption_blob_encrypted: 0 +size: 2720 +is_shared: 0 +share_id: +type_: 4 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/a9d36e4398c74610b44a690b3a263fec.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/a9d36e4398c74610b44a690b3a263fec.md new file mode 100644 index 0000000000..3d818136d6 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/a9d36e4398c74610b44a690b3a263fec.md @@ -0,0 +1,11 @@ +id: a9d36e4398c74610b44a690b3a263fec +note_id: 8436cd9c58824ca58ed146d7bd519dfd +tag_id: 052789e9393649ae85427a7c17bfd263 +created_time: 2021-08-07T17:03:33.709Z +updated_time: 2021-08-07T17:03:33.709Z +user_created_time: 2021-08-07T17:03:33.709Z +user_updated_time: 2021-08-07T17:03:33.709Z +encryption_cipher_text: +encryption_applied: 0 +is_shared: 0 +type_: 6 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/b1947d6f70314ab180b343e90f1b4660.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/b1947d6f70314ab180b343e90f1b4660.md new file mode 100644 index 0000000000..fa96039175 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/b1947d6f70314ab180b343e90f1b4660.md @@ -0,0 +1,17 @@ +photo.jpg + +id: b1947d6f70314ab180b343e90f1b4660 +mime: image/jpeg +filename: +created_time: 2021-08-07T17:03:33.701Z +updated_time: 2021-08-07T17:03:33.701Z +user_created_time: 2021-08-07T17:03:33.701Z +user_updated_time: 2021-08-07T17:03:33.701Z +file_extension: jpg +encryption_cipher_text: +encryption_applied: 0 +encryption_blob_encrypted: 0 +size: 2720 +is_shared: 0 +share_id: +type_: 4 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/bd25634be15c4005913bca01e3229305.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/bd25634be15c4005913bca01e3229305.md new file mode 100644 index 0000000000..1925668e03 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/bd25634be15c4005913bca01e3229305.md @@ -0,0 +1,11 @@ +id: bd25634be15c4005913bca01e3229305 +note_id: 5c0b421ac1e645e48dbdba4ed5328327 +tag_id: 1e55a346b1c3444996c3c9f52d4adc47 +created_time: 2021-08-07T17:03:33.721Z +updated_time: 2021-08-07T17:03:33.721Z +user_created_time: 2021-08-07T17:03:33.721Z +user_updated_time: 2021-08-07T17:03:33.721Z +encryption_cipher_text: +encryption_applied: 0 +is_shared: 0 +type_: 6 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/c227e85585674332badfbc09c50907ec.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/c227e85585674332badfbc09c50907ec.md new file mode 100644 index 0000000000..bd97623faa --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/c227e85585674332badfbc09c50907ec.md @@ -0,0 +1,13 @@ +folder1 + +id: c227e85585674332badfbc09c50907ec +created_time: 2021-08-07T17:03:33.589Z +updated_time: 2021-08-07T17:03:33.589Z +user_created_time: 2021-08-07T17:03:33.589Z +user_updated_time: 2021-08-07T17:03:33.589Z +encryption_cipher_text: +encryption_applied: 0 +parent_id: +is_shared: 0 +share_id: +type_: 2 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/e2f6cc04e2c94066b3104e0f478d50c5.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/e2f6cc04e2c94066b3104e0f478d50c5.md new file mode 100644 index 0000000000..d8ab499c8c --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/e2f6cc04e2c94066b3104e0f478d50c5.md @@ -0,0 +1,11 @@ +id: e2f6cc04e2c94066b3104e0f478d50c5 +note_id: 59e44f93f28242529fbbca804bdd25c5 +tag_id: 052789e9393649ae85427a7c17bfd263 +created_time: 2021-08-07T17:03:33.713Z +updated_time: 2021-08-07T17:03:33.713Z +user_created_time: 2021-08-07T17:03:33.713Z +user_updated_time: 2021-08-07T17:03:33.713Z +encryption_cipher_text: +encryption_applied: 0 +is_shared: 0 +type_: 6 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/f2bd86db0eeb47da8255e4bfb14cac16.md b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/f2bd86db0eeb47da8255e4bfb14cac16.md new file mode 100644 index 0000000000..b40b093d21 --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/f2bd86db0eeb47da8255e4bfb14cac16.md @@ -0,0 +1,13 @@ +folder2 + +id: f2bd86db0eeb47da8255e4bfb14cac16 +created_time: 2021-08-07T17:03:33.722Z +updated_time: 2021-08-07T17:03:33.722Z +user_created_time: 2021-08-07T17:03:33.722Z +user_updated_time: 2021-08-07T17:03:33.722Z +encryption_cipher_text: +encryption_applied: 0 +parent_id: +is_shared: 0 +share_id: +type_: 2 \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/info.json b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/info.json new file mode 100644 index 0000000000..719cc0029c --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/info.json @@ -0,0 +1 @@ +{"version":3,"e2ee":{"value":false,"updatedTime":0},"activeMasterKeyId":{"value":"","updatedTime":0},"masterKeys":[]} \ No newline at end of file diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/locks/.gitignore b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/locks/.gitignore new file mode 100644 index 0000000000..5e7d2734cf --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/locks/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore diff --git a/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/temp/.gitignore b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/temp/.gitignore new file mode 100644 index 0000000000..5e7d2734cf --- /dev/null +++ b/packages/app-cli/tests/support/syncTargetSnapshots/3/normal/temp/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore diff --git a/packages/app-desktop/app.ts b/packages/app-desktop/app.ts index 2586142032..81a7deed82 100644 --- a/packages/app-desktop/app.ts +++ b/packages/app-desktop/app.ts @@ -26,9 +26,7 @@ import ExternalEditWatcher from '@joplin/lib/services/ExternalEditWatcher'; import produce from 'immer'; import iterateItems from './gui/ResizableLayout/utils/iterateItems'; import validateLayout from './gui/ResizableLayout/utils/validateLayout'; - const { FoldersScreenUtils } = require('@joplin/lib/folders-screen-utils.js'); -import MasterKey from '@joplin/lib/models/MasterKey'; import Folder from '@joplin/lib/models/Folder'; const fs = require('fs-extra'); import Tag from '@joplin/lib/models/Tag'; @@ -707,12 +705,12 @@ class Application extends BaseApplication { items: tags, }); - const masterKeys = await MasterKey.all(); + // const masterKeys = await MasterKey.all(); - this.dispatch({ - type: 'MASTERKEY_UPDATE_ALL', - items: masterKeys, - }); + // this.dispatch({ + // type: 'MASTERKEY_UPDATE_ALL', + // items: masterKeys, + // }); this.store().dispatch({ type: 'FOLDER_SELECT', diff --git a/packages/app-desktop/gui/EncryptionConfigScreen.tsx b/packages/app-desktop/gui/EncryptionConfigScreen.tsx index a09f189867..8684b6be8f 100644 --- a/packages/app-desktop/gui/EncryptionConfigScreen.tsx +++ b/packages/app-desktop/gui/EncryptionConfigScreen.tsx @@ -10,17 +10,18 @@ import shim from '@joplin/lib/shim'; import dialogs from './dialogs'; import bridge from '../services/bridge'; import shared from '@joplin/lib/components/shared/encryption-config-shared'; -import { MasterKeyEntity } from '../../lib/services/database/types'; +import { MasterKeyEntity } from '@joplin/lib/services/database/types'; +import { getEncryptionEnabled, SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils'; +import { toggleAndSetupEncryption } from '../../lib/services/e2ee/utils'; +import MasterKey from '../../lib/models/MasterKey'; -interface Props { - -} +interface Props {} class EncryptionConfigScreenComponent extends React.Component { constructor(props: Props) { super(props); - shared.constructor(this, props); + shared.initialize(this, props); } componentWillUnmount() { @@ -167,23 +168,21 @@ class EncryptionConfigScreenComponent extends React.Component { } const onToggleButtonClick = async () => { - const isEnabled = Setting.value('encryption.enabled'); + const isEnabled = getEncryptionEnabled(); + const masterKey = MasterKey.latest(); let answer = null; if (isEnabled) { answer = await dialogs.confirm(_('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?')); } else { - answer = await dialogs.prompt(_('Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.'), '', '', { type: 'password' }); + const msg = shared.enableEncryptionConfirmationMessages(masterKey); + answer = await dialogs.prompt(msg.join('\n\n'), '', '', { type: 'password' }); } if (!answer) return; try { - if (isEnabled) { - await EncryptionService.instance().disableEncryption(); - } else { - await EncryptionService.instance().generateMasterKeyAndEnableEncryption(answer); - } + await toggleAndSetupEncryption(EncryptionService.instance(), !isEnabled, masterKey, answer); } catch (error) { await dialogs.alert(error.message); } @@ -295,12 +294,14 @@ class EncryptionConfigScreenComponent extends React.Component { } const mapStateToProps = (state: State) => { + const syncInfo = new SyncInfo(state.settings['syncInfoCache']); + return { themeId: state.settings.theme, - masterKeys: state.masterKeys, + masterKeys: syncInfo.masterKeys, passwords: state.settings['encryption.passwordCache'], - encryptionEnabled: state.settings['encryption.enabled'], - activeMasterKeyId: state.settings['encryption.activeMasterKeyId'], + encryptionEnabled: syncInfo.e2ee, + activeMasterKeyId: syncInfo.activeMasterKeyId, shouldReencrypt: state.settings['encryption.shouldReencrypt'] >= Setting.SHOULD_REENCRYPT_YES, notLoadedMasterKeys: state.notLoadedMasterKeys, }; diff --git a/packages/app-desktop/gui/MainScreen/MainScreen.tsx b/packages/app-desktop/gui/MainScreen/MainScreen.tsx index bf28f81497..d82125dce9 100644 --- a/packages/app-desktop/gui/MainScreen/MainScreen.tsx +++ b/packages/app-desktop/gui/MainScreen/MainScreen.tsx @@ -35,6 +35,7 @@ import { ShareInvitation } from '@joplin/lib/services/share/reducer'; import ShareService from '@joplin/lib/services/share/ShareService'; import { reg } from '@joplin/lib/registry'; import removeKeylessItems from '../ResizableLayout/utils/removeKeylessItems'; +import { localSyncInfoFromState } from '@joplin/lib/services/synchronizer/syncInfoUtils'; const { connect } = require('react-redux'); const { PromptDialog } = require('../PromptDialog.min.js'); @@ -856,6 +857,8 @@ class MainScreenComponent extends React.Component { } const mapStateToProps = (state: AppState) => { + const syncInfo = localSyncInfoFromState(state); + return { themeId: state.settings.theme, settingEditorCodeView: state.settings['editor.codeView'], @@ -863,8 +866,8 @@ const mapStateToProps = (state: AppState) => { notes: state.notes, hasDisabledSyncItems: state.hasDisabledSyncItems, hasDisabledEncryptionItems: state.hasDisabledEncryptionItems, - showMissingMasterKeyMessage: state.notLoadedMasterKeys.length && state.masterKeys.length, - showNeedUpgradingMasterKeyMessage: !!EncryptionService.instance().masterKeysThatNeedUpgrading(state.masterKeys).length, + showMissingMasterKeyMessage: state.notLoadedMasterKeys.length && syncInfo.masterKeys.length, + showNeedUpgradingMasterKeyMessage: !!EncryptionService.instance().masterKeysThatNeedUpgrading(syncInfo.masterKeys).length, showShouldReencryptMessage: state.settings['encryption.shouldReencrypt'] >= Setting.SHOULD_REENCRYPT_YES, shouldUpgradeSyncTarget: state.settings['sync.upgradeState'] === Setting.SYNC_UPGRADE_STATE_SHOULD_DO, selectedFolderId: state.selectedFolderId, diff --git a/packages/app-desktop/gui/Navigator.jsx b/packages/app-desktop/gui/Navigator.jsx index 48e5d55909..ec395377c6 100644 --- a/packages/app-desktop/gui/Navigator.jsx +++ b/packages/app-desktop/gui/Navigator.jsx @@ -8,7 +8,7 @@ class NavigatorComponent extends Component { UNSAFE_componentWillReceiveProps(newProps) { if (newProps.route) { const screenInfo = this.props.screens[newProps.route.routeName]; - const devMarker = Setting.value('env') === 'dev' ? ' (DEV)' : ''; + const devMarker = Setting.value('env') === 'dev' ? ` (DEV - ${Setting.value('profileDir')})` : ''; const windowTitle = [`Joplin${devMarker}`]; if (screenInfo.title) { windowTitle.push(screenInfo.title()); diff --git a/packages/app-desktop/gui/ShareNoteDialog.tsx b/packages/app-desktop/gui/ShareNoteDialog.tsx index 63460cc2b7..4791a41b8f 100644 --- a/packages/app-desktop/gui/ShareNoteDialog.tsx +++ b/packages/app-desktop/gui/ShareNoteDialog.tsx @@ -3,7 +3,6 @@ import { useState, useEffect } from 'react'; import JoplinServerApi from '@joplin/lib/JoplinServerApi'; import { _, _n } from '@joplin/lib/locale'; import Note from '@joplin/lib/models/Note'; -import Setting from '@joplin/lib/models/Setting'; import DialogButtonRow from './DialogButtonRow'; import { themeStyle, buildStyle } from '@joplin/lib/theme'; import { reg } from '@joplin/lib/registry'; @@ -15,6 +14,7 @@ import { NoteEntity } from '@joplin/lib/services/database/types'; import Button from './Button/Button'; import { connect } from 'react-redux'; import { AppState } from '../app'; +import { getEncryptionEnabled } from '@joplin/lib/services/synchronizer/syncInfoUtils'; const { clipboard } = require('electron'); interface Props { @@ -210,7 +210,7 @@ export function ShareNoteDialog(props: Props) { }; function renderEncryptionWarningMessage() { - if (!Setting.value('encryption.enabled')) return null; + if (!getEncryptionEnabled()) return null; return
{_('Note: When a note is shared, it will no longer be encrypted on the server.')}
; } diff --git a/packages/app-desktop/runForTesting.sh b/packages/app-desktop/runForTesting.sh index ae7392eb88..2764b83ee2 100755 --- a/packages/app-desktop/runForTesting.sh +++ b/packages/app-desktop/runForTesting.sh @@ -39,11 +39,18 @@ do USER_EMAIL="user$USER_NUM@example.com" rm -rf "$PROFILE_DIR" - echo "config keychain.supported 0" >> "$CMD_FILE" - echo "config sync.target 10" >> "$CMD_FILE" - # echo "config sync.10.path http://api.joplincloud.local:22300" >> "$CMD_FILE" - echo "config sync.10.username $USER_EMAIL" >> "$CMD_FILE" - echo "config sync.10.password hunter1hunter2hunter3" >> "$CMD_FILE" + + rm -rf "$HOME/Temp/SyncTestE2EE copy" + rsync -a "$HOME/Temp/SyncTestE2EE/" "$HOME/Temp/SyncTestE2EE copy/" + + echo "config sync.target 2" >> "$CMD_FILE" + echo "config sync.2.path \"$HOME/Temp/SyncTestE2EE copy/\"" >> "$CMD_FILE" + + # echo "config keychain.supported 0" >> "$CMD_FILE" + # echo "config sync.target 10" >> "$CMD_FILE" + # # echo "config sync.10.path http://api.joplincloud.local:22300" >> "$CMD_FILE" + # echo "config sync.10.username $USER_EMAIL" >> "$CMD_FILE" + # echo "config sync.10.password hunter1hunter2hunter3" >> "$CMD_FILE" elif [[ $CMD == "e2ee" ]]; then diff --git a/packages/app-mobile/components/screen-header.js b/packages/app-mobile/components/screen-header.js index 4704f9b029..f20c8738f4 100644 --- a/packages/app-mobile/components/screen-header.js +++ b/packages/app-mobile/components/screen-header.js @@ -14,6 +14,7 @@ const { themeStyle } = require('./global-style.js'); const { Dropdown } = require('./Dropdown.js'); const { dialogs } = require('../utils/dialogs.js'); const DialogBox = require('react-native-dialogbox').default; +const { localSyncInfoFromState } = require('@joplin/lib/services/synchronizer/syncInfoUtils'); Icon.loadFont(); @@ -528,6 +529,8 @@ ScreenHeaderComponent.defaultProps = { }; const ScreenHeader = connect(state => { + const syncInfo = localSyncInfoFromState(state); + return { historyCanGoBack: state.historyCanGoBack, locale: state.settings.locale, @@ -535,7 +538,7 @@ const ScreenHeader = connect(state => { themeId: state.settings.theme, noteSelectionEnabled: state.noteSelectionEnabled, selectedNoteIds: state.selectedNoteIds, - showMissingMasterKeyMessage: state.notLoadedMasterKeys.length && state.masterKeys.length, + showMissingMasterKeyMessage: state.notLoadedMasterKeys.length && syncInfo.masterKeys.length, hasDisabledSyncItems: state.hasDisabledSyncItems, shouldUpgradeSyncTarget: state.settings['sync.upgradeState'] === Setting.SYNC_UPGRADE_STATE_SHOULD_DO, }; diff --git a/packages/app-mobile/components/screens/encryption-config.tsx b/packages/app-mobile/components/screens/encryption-config.tsx index 6813c15dc8..ee8c0a530c 100644 --- a/packages/app-mobile/components/screens/encryption-config.tsx +++ b/packages/app-mobile/components/screens/encryption-config.tsx @@ -10,12 +10,13 @@ import EncryptionService from '@joplin/lib/services/EncryptionService'; import { _ } from '@joplin/lib/locale'; import time from '@joplin/lib/time'; import shared from '@joplin/lib/components/shared/encryption-config-shared'; -import { MasterKeyEntity } from '../../../lib/services/database/types'; +import { MasterKeyEntity } from '@joplin/lib/services/database/types'; import { State } from '@joplin/lib/reducer'; +import { SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils'; +import { setupAndDisableEncryption, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils'; +import MasterKey from '../../../lib/models/MasterKey'; -interface Props { - -} +interface Props {} class EncryptionConfigScreenComponent extends BaseScreenComponent { static navigationOptions(): any { @@ -31,7 +32,7 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent { passwordPromptConfirmAnswer: '', }; - shared.constructor(this, props); + shared.initialize(this, props); this.styles_ = {}; } @@ -40,10 +41,6 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent { this.isMounted_ = false; } - initState(props: Props) { - return shared.initState(this, props); - } - async refreshStats() { return shared.refreshStats(this); } @@ -135,6 +132,7 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent { passwordPromptComponent() { const theme = themeStyle(this.props.themeId); + const masterKey = MasterKey.latest(); const onEnableClick = async () => { try { @@ -143,16 +141,23 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent { const password2 = this.state.passwordPromptConfirmAnswer; if (!password2) throw new Error(_('Confirm password cannot be empty')); if (password !== password2) throw new Error(_('Passwords do not match!')); - await EncryptionService.instance().generateMasterKeyAndEnableEncryption(password); + await toggleAndSetupEncryption(EncryptionService.instance(), true, masterKey, password); + // await generateMasterKeyAndEnableEncryption(EncryptionService.instance(), password); this.setState({ passwordPromptShow: false }); } catch (error) { await dialogs.error(this, error.message); } }; + const messages = shared.enableEncryptionConfirmationMessages(masterKey); + + const messageComps = messages.map(msg => { + return {msg}; + }); + return ( - {_('Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.')} + {messageComps} {_('Password:')} { if (!ok) return; try { - await EncryptionService.instance().disableEncryption(); + await setupAndDisableEncryption(EncryptionService.instance()); } catch (error) { await dialogs.error(this, error.message); } @@ -301,12 +306,14 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent { } const EncryptionConfigScreen = connect((state: State) => { + const syncInfo = new SyncInfo(state.settings['syncInfoCache']); + return { themeId: state.settings.theme, - masterKeys: state.masterKeys, + masterKeys: syncInfo.masterKeys, passwords: state.settings['encryption.passwordCache'], - encryptionEnabled: state.settings['encryption.enabled'], - activeMasterKeyId: state.settings['encryption.activeMasterKeyId'], + encryptionEnabled: syncInfo.e2ee, + activeMasterKeyId: syncInfo.activeMasterKeyId, notLoadedMasterKeys: state.notLoadedMasterKeys, }; })(EncryptionConfigScreenComponent); diff --git a/packages/app-mobile/root.tsx b/packages/app-mobile/root.tsx index 1e2eb8a9c6..b68cf415cf 100644 --- a/packages/app-mobile/root.tsx +++ b/packages/app-mobile/root.tsx @@ -102,6 +102,7 @@ import { clearSharedFilesCache } from './utils/ShareUtils'; import setIgnoreTlsErrors from './utils/TlsUtils'; import ShareService from '@joplin/lib/services/share/ShareService'; import setupNotifications from './utils/setupNotifications'; +import { loadMasterKeysFromSettings } from '@joplin/lib/services/e2ee/utils'; let storeDispatch = function(_action: any) {}; @@ -148,7 +149,7 @@ const generalMiddleware = (store: any) => (next: any) => async (action: any) => } if ((action.type == 'SETTING_UPDATE_ONE' && (action.key.indexOf('encryption.') === 0)) || (action.type == 'SETTING_UPDATE_ALL')) { - await EncryptionService.instance().loadMasterKeysFromSettings(); + await loadMasterKeysFromSettings(EncryptionService.instance()); void DecryptionWorker.instance().scheduleStart(); const loadedMasterKeyIds = EncryptionService.instance().loadedMasterKeyIds(); @@ -471,7 +472,7 @@ async function initialize(dispatch: Function) { if (Setting.value('env') == 'prod') { await db.open({ name: 'joplin.sqlite' }); } else { - await db.open({ name: 'joplin-100.sqlite' }); + await db.open({ name: 'joplin-101.sqlite' }); // await db.clearForTesting(); } @@ -535,7 +536,7 @@ async function initialize(dispatch: Function) { DecryptionWorker.instance().setLogger(mainLogger); DecryptionWorker.instance().setKvStore(KvStore.instance()); DecryptionWorker.instance().setEncryptionService(EncryptionService.instance()); - await EncryptionService.instance().loadMasterKeysFromSettings(); + await loadMasterKeysFromSettings(EncryptionService.instance()); DecryptionWorker.instance().on('resourceMetadataButNotBlobDecrypted', decryptionWorker_resourceMetadataButNotBlobDecrypted); // ---------------------------------------------------------------- @@ -555,12 +556,12 @@ async function initialize(dispatch: Function) { items: tags, }); - const masterKeys = await MasterKey.all(); + // const masterKeys = await MasterKey.all(); - dispatch({ - type: 'MASTERKEY_UPDATE_ALL', - items: masterKeys, - }); + // dispatch({ + // type: 'MASTERKEY_UPDATE_ALL', + // items: masterKeys, + // }); const folderId = Setting.value('activeFolderId'); let folder = await Folder.load(folderId); diff --git a/packages/lib/BaseApplication.ts b/packages/lib/BaseApplication.ts index cd8005fbdf..ac1f5e37cf 100644 --- a/packages/lib/BaseApplication.ts +++ b/packages/lib/BaseApplication.ts @@ -50,6 +50,8 @@ import handleSyncStartupOperation from './services/synchronizer/utils/handleSync import SyncTargetJoplinCloud from './SyncTargetJoplinCloud'; const { toSystemSlashes } = require('./path-utils'); const { setAutoFreeze } = require('immer'); +import { getEncryptionEnabled } from './services/synchronizer/syncInfoUtils'; +import { loadMasterKeysFromSettings } from './services/e2ee/utils'; const appLogger: LoggerWrapper = Logger.create('App'); @@ -428,9 +430,18 @@ export default class BaseApplication { syswidecas.addCAs(f); } }, - 'encryption.enabled': async () => { + + // Note: this used to run when "encryption.enabled" was changed, but + // now we run it anytime any property of the sync target info is + // changed. This is not optimal but: + // - The sync target info rarely changes. + // - All the calls below are cheap or do nothing if there's nothing + // to do. + 'syncInfoCache': async () => { if (this.hasGui()) { - await EncryptionService.instance().loadMasterKeysFromSettings(); + appLogger.info('"syncInfoCache" was changed - setting up encryption related code'); + + await loadMasterKeysFromSettings(EncryptionService.instance()); void DecryptionWorker.instance().scheduleStart(); const loadedMasterKeyIds = EncryptionService.instance().loadedMasterKeyIds(); @@ -444,6 +455,7 @@ export default class BaseApplication { void reg.scheduleSync(); } }, + 'sync.interval': async () => { if (this.hasGui()) reg.setupRecurrentSync(); }, @@ -451,8 +463,7 @@ export default class BaseApplication { sideEffects['timeFormat'] = sideEffects['dateFormat']; sideEffects['locale'] = sideEffects['dateFormat']; - sideEffects['encryption.activeMasterKeyId'] = sideEffects['encryption.enabled']; - sideEffects['encryption.passwordCache'] = sideEffects['encryption.enabled']; + sideEffects['encryption.passwordCache'] = sideEffects['syncInfoCache']; if (action) { const effect = sideEffects[action.key]; @@ -791,7 +802,7 @@ export default class BaseApplication { // and if encryption is enabled. This code runs only when shouldReencrypt = -1 // which can be set by a maintenance script for example. const folderCount = await Folder.count(); - const itShould = Setting.value('encryption.enabled') && !!folderCount ? Setting.SHOULD_REENCRYPT_YES : Setting.SHOULD_REENCRYPT_NO; + const itShould = getEncryptionEnabled() && !!folderCount ? Setting.SHOULD_REENCRYPT_YES : Setting.SHOULD_REENCRYPT_NO; Setting.setValue('encryption.shouldReencrypt', itShould); } @@ -818,7 +829,7 @@ export default class BaseApplication { DecryptionWorker.instance().setLogger(globalLogger); DecryptionWorker.instance().setEncryptionService(EncryptionService.instance()); DecryptionWorker.instance().setKvStore(KvStore.instance()); - await EncryptionService.instance().loadMasterKeysFromSettings(); + await loadMasterKeysFromSettings(EncryptionService.instance()); DecryptionWorker.instance().on('resourceMetadataButNotBlobDecrypted', this.decryptionWorker_resourceMetadataButNotBlobDecrypted); ResourceFetcher.instance().setFileApi(() => { diff --git a/packages/lib/Synchronizer.ts b/packages/lib/Synchronizer.ts index 799a1fb943..aaf803c70f 100644 --- a/packages/lib/Synchronizer.ts +++ b/packages/lib/Synchronizer.ts @@ -12,7 +12,7 @@ import Resource from './models/Resource'; import ItemChange from './models/ItemChange'; import ResourceLocalState from './models/ResourceLocalState'; import MasterKey from './models/MasterKey'; -import BaseModel from './BaseModel'; +import BaseModel, { ModelType } from './BaseModel'; import time from './time'; import ResourceService from './services/ResourceService'; import EncryptionService from './services/EncryptionService'; @@ -22,6 +22,8 @@ import TaskQueue from './TaskQueue'; import ItemUploader from './services/synchronizer/ItemUploader'; import { FileApi } from './file-api'; import JoplinDatabase from './JoplinDatabase'; +import { fetchSyncInfo, getActiveMasterKey, localSyncInfo, mergeSyncInfos, saveLocalSyncInfo, syncInfoEquals, uploadSyncInfo } from './services/synchronizer/syncInfoUtils'; +import { setupAndDisableEncryption, setupAndEnableEncryption } from './services/e2ee/utils'; const { sprintf } = require('sprintf-js'); const { Dirnames } = require('./services/synchronizer/utils/types'); @@ -71,7 +73,7 @@ export default class Synchronizer { private logger_: Logger = new Logger(); private state_: string = 'idle'; private cancelling_: boolean = false; - private maxResourceSize_: number = null; + public maxResourceSize_: number = null; private downloadQueue_: any = null; private clientId_: string; private lockHandler_: LockHandler; @@ -136,7 +138,7 @@ export default class Synchronizer { migrationHandler() { if (this.migrationHandler_) return this.migrationHandler_; - this.migrationHandler_ = new MigrationHandler(this.api(), this.lockHandler(), this.appType_, this.clientId_); + this.migrationHandler_ = new MigrationHandler(this.api(), this.db(), this.lockHandler(), this.appType_, this.clientId_); return this.migrationHandler_; } @@ -369,8 +371,8 @@ export default class Synchronizer { this.syncTargetIsLocked_ = false; this.cancelling_ = false; - const masterKeysBefore = await MasterKey.count(); - let hasAutoEnabledEncryption = false; + // const masterKeysBefore = await MasterKey.count(); + // let hasAutoEnabledEncryption = false; const synchronizationId = time.unixMs().toString(); @@ -418,13 +420,49 @@ export default class Synchronizer { this.api().setTempDirName(Dirnames.Temp); try { - const syncTargetInfo = await this.migrationHandler().checkCanSync(); + const remoteInfo = await fetchSyncInfo(this.api()); + logger.info('Sync target remote info:', remoteInfo); - logger.info('Sync target info:', syncTargetInfo); - - if (!syncTargetInfo.version) { + if (!remoteInfo.version) { logger.info('Sync target is new - setting it up...'); await this.migrationHandler().upgrade(Setting.value('syncVersion')); + } else { + logger.info('Sync target is already setup - checking it...'); + + await this.migrationHandler().checkCanSync(remoteInfo); + + const localInfo = await localSyncInfo(); + + logger.info('Sync target local info:', localInfo); + + // console.info('LOCAL', localInfo); + // console.info('REMOTE', remoteInfo); + + if (!syncInfoEquals(localInfo, remoteInfo)) { + const newInfo = mergeSyncInfos(localInfo, remoteInfo); + const previousE2EE = localInfo.e2ee; + logger.info('Sync target info differs between local and remote - merging infos: ', newInfo.toObject()); + + await this.lockHandler().acquireLock(LockType.Exclusive, this.appType_, this.clientId_); + await uploadSyncInfo(this.api(), newInfo); + await saveLocalSyncInfo(newInfo); + await this.lockHandler().releaseLock(LockType.Exclusive, this.appType_, this.clientId_); + + // console.info('NEW', newInfo); + + if (newInfo.e2ee !== previousE2EE) { + if (newInfo.e2ee) { + const mk = getActiveMasterKey(newInfo); + await setupAndEnableEncryption(this.encryptionService(), mk); + } else { + await setupAndDisableEncryption(this.encryptionService()); + } + } + } else { + // Set it to remote anyway so that timestamps are the same + // Note: that's probably not needed anymore? + // await uploadSyncInfo(this.api(), remoteInfo); + } } } catch (error) { if (error.code === 'outdatedSyncTarget') { @@ -543,6 +581,12 @@ export default class Synchronizer { } } + // We no longer upload Master Keys however we keep them + // in the database for extra safety. In a future + // version, once it's confirmed that the new E2EE system + // works well, we can delete them. + if (local.type_ === ModelType.MasterKey) action = null; + this.logSyncOperation(action, local, remote, reason); if (local.type_ == BaseModel.TYPE_RESOURCE && (action == 'createRemote' || action === 'updateRemote')) { @@ -911,18 +955,37 @@ export default class Synchronizer { await ResourceLocalState.save({ resource_id: content.id, fetch_status: Resource.FETCH_STATUS_IDLE }); } - await ItemClass.save(content, options); + if (content.type_ === ModelType.MasterKey) { + // Special case for master keys - if we download + // one, we only add it to the store if it's not + // already there. That can happen for example if + // the new E2EE migration was processed at a + // time a master key was still on the sync + // target. In that case, info.json would not + // have it. + // + // If info.json already has the key we shouldn't + // update because the most up to date keys + // should always be in info.json now. + const existingMasterKey = await MasterKey.load(content.id); + if (!existingMasterKey) { + logger.info(`Downloaded a master key that was not in info.json - adding it to the store. ID: ${content.id}`); + await MasterKey.save(content); + } + } else { + await ItemClass.save(content, options); + } if (creatingOrUpdatingResource) this.dispatch({ type: 'SYNC_CREATED_OR_UPDATED_RESOURCE', id: content.id }); - if (!hasAutoEnabledEncryption && content.type_ === BaseModel.TYPE_MASTER_KEY && !masterKeysBefore) { - hasAutoEnabledEncryption = true; - logger.info('One master key was downloaded and none was previously available: automatically enabling encryption'); - logger.info('Using master key: ', content.id); - await this.encryptionService().enableEncryption(content); - await this.encryptionService().loadMasterKeysFromSettings(); - logger.info('Encryption has been enabled with downloaded master key as active key. However, note that no password was initially supplied. It will need to be provided by user.'); - } + // if (!hasAutoEnabledEncryption && content.type_ === BaseModel.TYPE_MASTER_KEY && !masterKeysBefore) { + // hasAutoEnabledEncryption = true; + // logger.info('One master key was downloaded and none was previously available: automatically enabling encryption'); + // logger.info('Using master key: ', content.id); + // await this.encryptionService().enableEncryption(content); + // await this.encryptionService().loadMasterKeysFromSettings(); + // logger.info('Encryption has been enabled with downloaded master key as active key. However, note that no password was initially supplied. It will need to be provided by user.'); + // } if (content.encryption_applied) this.dispatch({ type: 'SYNC_GOT_ENCRYPTED_ITEM' }); } else if (action == 'deleteLocal') { diff --git a/packages/lib/components/shared/encryption-config-shared.ts b/packages/lib/components/shared/encryption-config-shared.ts index 2e02700e33..153ace38f0 100644 --- a/packages/lib/components/shared/encryption-config-shared.ts +++ b/packages/lib/components/shared/encryption-config-shared.ts @@ -6,132 +6,146 @@ import MasterKey from '../../models/MasterKey'; import { reg } from '../../registry.js'; import shim from '../../shim'; import { MasterKeyEntity } from '../../services/database/types'; +import time from '../../time'; -const shared: any = {}; +class Shared { -shared.constructor = function(comp: any, props: any) { - comp.state = { - passwordChecks: {}, - stats: { - encrypted: null, - total: null, - }, - passwords: Object.assign({}, props.passwords), - }; - comp.isMounted_ = false; + private refreshStatsIID_: any; - shared.refreshStatsIID_ = null; -}; + public initialize(comp: any, props: any) { + comp.state = { + passwordChecks: {}, + stats: { + encrypted: null, + total: null, + }, + passwords: Object.assign({}, props.passwords), + }; + comp.isMounted_ = false; -shared.refreshStats = async function(comp: any) { - const stats = await BaseItem.encryptedItemsStats(); - comp.setState({ - stats: stats, - }); -}; - -shared.reencryptData = async function() { - const ok = confirm(_('Please confirm that you would like to re-encrypt your complete database.')); - if (!ok) return; - - await BaseItem.forceSyncAll(); - void reg.waitForSyncFinishedThenSync(); - Setting.setValue('encryption.shouldReencrypt', Setting.SHOULD_REENCRYPT_NO); - alert(_('Your data is going to be re-encrypted and synced again.')); -}; - -shared.dontReencryptData = function() { - Setting.setValue('encryption.shouldReencrypt', Setting.SHOULD_REENCRYPT_NO); -}; - -shared.upgradeMasterKey = async function(comp: any, masterKey: MasterKeyEntity) { - const passwordCheck = comp.state.passwordChecks[masterKey.id]; - if (!passwordCheck) { - alert(_('Please enter your password in the master key list below before upgrading the key.')); - return; + this.refreshStatsIID_ = null; } - try { - const password = comp.state.passwords[masterKey.id]; - const newMasterKey = await EncryptionService.instance().upgradeMasterKey(masterKey, password); - await MasterKey.save(newMasterKey); + public async refreshStats(comp: any) { + const stats = await BaseItem.encryptedItemsStats(); + comp.setState({ + stats: stats, + }); + } + + public async reencryptData() { + const ok = confirm(_('Please confirm that you would like to re-encrypt your complete database.')); + if (!ok) return; + + await BaseItem.forceSyncAll(); void reg.waitForSyncFinishedThenSync(); - alert(_('The master key has been upgraded successfully!')); - } catch (error) { - alert(_('Could not upgrade master key: %s', error.message)); - } -}; - -shared.componentDidMount = async function(comp: any) { - shared.componentDidUpdate(comp); - - shared.refreshStats(comp); - - if (shared.refreshStatsIID_) { - shim.clearInterval(shared.refreshStatsIID_); - shared.refreshStatsIID_ = null; + Setting.setValue('encryption.shouldReencrypt', Setting.SHOULD_REENCRYPT_NO); + alert(_('Your data is going to be re-encrypted and synced again.')); } - shared.refreshStatsIID_ = shim.setInterval(() => { - if (!comp.isMounted_) { - shim.clearInterval(shared.refreshStatsIID_); - shared.refreshStatsIID_ = null; + public dontReencryptData() { + Setting.setValue('encryption.shouldReencrypt', Setting.SHOULD_REENCRYPT_NO); + } + + public async upgradeMasterKey(comp: any, masterKey: MasterKeyEntity) { + const passwordCheck = comp.state.passwordChecks[masterKey.id]; + if (!passwordCheck) { + alert(_('Please enter your password in the master key list below before upgrading the key.')); return; } - shared.refreshStats(comp); - }, 3000); -}; -shared.componentDidUpdate = async function(comp: any, prevProps: any = null) { - if (prevProps && comp.props.passwords !== prevProps.passwords) { - comp.setState({ passwords: Object.assign({}, comp.props.passwords) }); + try { + const password = comp.state.passwords[masterKey.id]; + const newMasterKey = await EncryptionService.instance().upgradeMasterKey(masterKey, password); + await MasterKey.save(newMasterKey); + void reg.waitForSyncFinishedThenSync(); + alert(_('The master key has been upgraded successfully!')); + } catch (error) { + alert(_('Could not upgrade master key: %s', error.message)); + } } - if (!prevProps || comp.props.masterKeys !== prevProps.masterKeys || comp.props.passwords !== prevProps.passwords) { + public componentDidMount(comp: any) { + this.componentDidUpdate(comp); + + void this.refreshStats(comp); + + if (this.refreshStatsIID_) { + shim.clearInterval(this.refreshStatsIID_); + this.refreshStatsIID_ = null; + } + + this.refreshStatsIID_ = shim.setInterval(() => { + if (!comp.isMounted_) { + shim.clearInterval(this.refreshStatsIID_); + this.refreshStatsIID_ = null; + return; + } + void this.refreshStats(comp); + }, 3000); + } + + public componentDidUpdate(comp: any, prevProps: any = null) { + if (prevProps && comp.props.passwords !== prevProps.passwords) { + comp.setState({ passwords: Object.assign({}, comp.props.passwords) }); + } + + if (!prevProps || comp.props.masterKeys !== prevProps.masterKeys || comp.props.passwords !== prevProps.passwords) { + comp.checkPasswords(); + } + } + + public componentWillUnmount() { + if (this.refreshStatsIID_) { + shim.clearInterval(this.refreshStatsIID_); + this.refreshStatsIID_ = null; + } + } + + public async checkPasswords(comp: any) { + const passwordChecks = Object.assign({}, comp.state.passwordChecks); + for (let i = 0; i < comp.props.masterKeys.length; i++) { + const mk = comp.props.masterKeys[i]; + const password = comp.state.passwords[mk.id]; + const ok = password ? await EncryptionService.instance().checkMasterKeyPassword(mk, password) : false; + passwordChecks[mk.id] = ok; + } + comp.setState({ passwordChecks: passwordChecks }); + } + + public decryptedStatText(comp: any) { + const stats = comp.state.stats; + const doneCount = stats.encrypted !== null ? stats.total - stats.encrypted : '-'; + const totalCount = stats.total !== null ? stats.total : '-'; + const result = _('Decrypted items: %s / %s', doneCount, totalCount); + return result; + } + + public onSavePasswordClick(comp: any, mk: MasterKeyEntity) { + const password = comp.state.passwords[mk.id]; + if (!password) { + Setting.deleteObjectValue('encryption.passwordCache', mk.id); + } else { + Setting.setObjectValue('encryption.passwordCache', mk.id, password); + } + comp.checkPasswords(); } -}; -shared.componentWillUnmount = function() { - if (shared.refreshStatsIID_) { - shim.clearInterval(shared.refreshStatsIID_); - shared.refreshStatsIID_ = null; - } -}; - -shared.checkPasswords = async function(comp: any) { - const passwordChecks = Object.assign({}, comp.state.passwordChecks); - for (let i = 0; i < comp.props.masterKeys.length; i++) { - const mk = comp.props.masterKeys[i]; - const password = comp.state.passwords[mk.id]; - const ok = password ? await EncryptionService.instance().checkMasterKeyPassword(mk, password) : false; - passwordChecks[mk.id] = ok; - } - comp.setState({ passwordChecks: passwordChecks }); -}; - -shared.decryptedStatText = function(comp: any) { - const stats = comp.state.stats; - const doneCount = stats.encrypted !== null ? stats.total - stats.encrypted : '-'; - const totalCount = stats.total !== null ? stats.total : '-'; - return _('Decrypted items: %s / %s', doneCount, totalCount); -}; - -shared.onSavePasswordClick = function(comp: any, mk: MasterKeyEntity) { - const password = comp.state.passwords[mk.id]; - if (!password) { - Setting.deleteObjectValue('encryption.passwordCache', mk.id); - } else { - Setting.setObjectValue('encryption.passwordCache', mk.id, password); + public onPasswordChange(comp: any, mk: MasterKeyEntity, password: string) { + const passwords = Object.assign({}, comp.state.passwords); + passwords[mk.id] = password; + comp.setState({ passwords: passwords }); } - comp.checkPasswords(); -}; + public enableEncryptionConfirmationMessages(masterKey: MasterKeyEntity) { + const msg = [_('Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.')]; + if (masterKey) msg.push(_('Encryption will be enabled using the master key created on %s', time.unixMsToLocalDateTime(masterKey.created_time))); + return msg; + } -shared.onPasswordChange = function(comp: any, mk: MasterKeyEntity, password: string) { - const passwords = Object.assign({}, comp.state.passwords); - passwords[mk.id] = password; - comp.setState({ passwords: passwords }); -}; +} + +const shared = new Shared(); export default shared; diff --git a/packages/lib/fsDriver.test.ts b/packages/lib/fsDriver.test.ts index 62396c9838..eab8b6a12a 100644 --- a/packages/lib/fsDriver.test.ts +++ b/packages/lib/fsDriver.test.ts @@ -1,6 +1,6 @@ import FsDriverNode from './fs-driver-node'; import shim from './shim'; -const { expectThrow } = require('./testing/test-utils.js'); +import { expectThrow } from './testing/test-utils'; // On Windows, path.resolve is going to convert a path such as // /tmp/file.txt to c:\tmp\file.txt @@ -14,16 +14,16 @@ function platformPath(path: string) { describe('fsDriver', function() { - it('should resolveRelativePathWithinDir', () => { + it('should resolveRelativePathWithinDir', async () => { const fsDriver = new FsDriverNode(); expect(fsDriver.resolveRelativePathWithinDir('/test/temp', './my/file.txt').toLowerCase()).toBe(platformPath('/test/temp/my/file.txt')); expect(fsDriver.resolveRelativePathWithinDir('/', './test').toLowerCase()).toBe(platformPath('/test')); expect(fsDriver.resolveRelativePathWithinDir('/test', 'myfile.txt').toLowerCase()).toBe(platformPath('/test/myfile.txt')); expect(fsDriver.resolveRelativePathWithinDir('/test/temp', './mydir/../test.txt').toLowerCase()).toBe(platformPath('/test/temp/test.txt')); - expectThrow(() => fsDriver.resolveRelativePathWithinDir('/test/temp', '../myfile.txt')); - expectThrow(() => fsDriver.resolveRelativePathWithinDir('/test/temp', './mydir/../../test.txt')); - expectThrow(() => fsDriver.resolveRelativePathWithinDir('/test/temp', '/var/local/no.txt')); + await expectThrow(() => fsDriver.resolveRelativePathWithinDir('/test/temp', '../myfile.txt')); + await expectThrow(() => fsDriver.resolveRelativePathWithinDir('/test/temp', './mydir/../../test.txt')); + await expectThrow(() => fsDriver.resolveRelativePathWithinDir('/test/temp', '/var/local/no.txt')); }); }); diff --git a/packages/lib/import-enex-md-gen.test.ts b/packages/lib/import-enex-md-gen.test.ts index ef7ef1ec22..013b82c125 100644 --- a/packages/lib/import-enex-md-gen.test.ts +++ b/packages/lib/import-enex-md-gen.test.ts @@ -4,7 +4,7 @@ import shim from './shim'; const fs = require('fs-extra'); const os = require('os'); const { filename } = require('./path-utils'); -const { setupDatabaseAndSynchronizer, switchClient, expectNotThrow, supportDir } = require('./testing/test-utils.js'); +import { setupDatabaseAndSynchronizer, switchClient, expectNotThrow, supportDir } from './testing/test-utils'; const { enexXmlToMd } = require('./import-enex-md-gen.js'); const { importEnex } = require('./import-enex'); import Note from './models/Note'; diff --git a/packages/lib/markdownUtils2.test.ts b/packages/lib/markdownUtils2.test.ts index e264a32f03..66549f126d 100644 --- a/packages/lib/markdownUtils2.test.ts +++ b/packages/lib/markdownUtils2.test.ts @@ -1,6 +1,6 @@ /* eslint-disable no-unused-vars */ -const markdownUtils = require('./markdownUtils').default; +import markdownUtils from './markdownUtils'; describe('markdownUtils', function() { @@ -44,7 +44,7 @@ describe('markdownUtils', function() { ]; for (let i = 0; i < testCases.length; i++) { - const md = testCases[i][0]; + const md = testCases[i][0] as string; const actual = markdownUtils.extractImageUrls(md); const expected = testCases[i][1]; expect(actual.join(' ')).toBe((expected as string[]).join(' ')); @@ -70,7 +70,7 @@ describe('markdownUtils', function() { ]; for (let i = 0; i < testCases.length; i++) { - const md = testCases[i][0]; + const md = testCases[i][0] as string; const actual = markdownUtils.extractFileUrls(md); const expected = testCases[i][1]; diff --git a/packages/lib/models/BaseItem.ts b/packages/lib/models/BaseItem.ts index 8c1e05ed99..bb3178d8fe 100644 --- a/packages/lib/models/BaseItem.ts +++ b/packages/lib/models/BaseItem.ts @@ -9,6 +9,7 @@ import Database from '../database'; import ItemChange from './ItemChange'; import ShareService from '../services/share/ShareService'; import itemCanBeEncrypted from './utils/itemCanBeEncrypted'; +import { getEncryptionEnabled } from '../services/synchronizer/syncInfoUtils'; const JoplinError = require('../JoplinError.js'); const { sprintf } = require('sprintf-js'); const moment = require('moment'); @@ -410,7 +411,7 @@ export default class BaseItem extends BaseModel { const serialized = await ItemClass.serialize(item, shownKeys); - if (!Setting.value('encryption.enabled') || !ItemClass.encryptionSupported() || !itemCanBeEncrypted(item)) { + if (!getEncryptionEnabled() || !ItemClass.encryptionSupported() || !itemCanBeEncrypted(item)) { // Normally not possible since itemsThatNeedSync should only return decrypted items if (item.encryption_applied) throw new JoplinError('Item is encrypted but encryption is currently disabled', 'cannotSyncEncrypted'); return serialized; @@ -598,7 +599,8 @@ export default class BaseItem extends BaseModel { } public static async itemsThatNeedSync(syncTarget: number, limit = 100): Promise { - const classNames = this.syncItemClassNames(); + // Although we keep the master keys in the database, we no longer sync them + const classNames = this.syncItemClassNames().filter(n => n !== 'MasterKey'); for (let i = 0; i < classNames.length; i++) { const className = classNames[i]; @@ -687,7 +689,7 @@ export default class BaseItem extends BaseModel { throw new Error('Unreachable'); } - static syncItemClassNames() { + static syncItemClassNames(): string[] { return BaseItem.syncItemDefinitions_.map((def: any) => { return def.className; }); diff --git a/packages/lib/models/MasterKey.test.ts b/packages/lib/models/MasterKey.test.ts new file mode 100644 index 0000000000..d245d45864 --- /dev/null +++ b/packages/lib/models/MasterKey.test.ts @@ -0,0 +1,34 @@ +import { encryptionService, msleep, setupDatabaseAndSynchronizer, switchClient } from '../testing/test-utils'; +import MasterKey from './MasterKey'; + +describe('models/MasterKey', function() { + + beforeEach(async (done) => { + await setupDatabaseAndSynchronizer(1); + await switchClient(1); + done(); + }); + + it('should return the latest master key', (async () => { + expect(await MasterKey.latest()).toBeFalsy(); + + let mk1 = await encryptionService().generateMasterKey('111111'); + mk1 = await MasterKey.save(mk1); + + expect((await MasterKey.latest()).id).toBe(mk1.id); + + await msleep(1); + + let mk2 = await encryptionService().generateMasterKey('111111'); + mk2 = await MasterKey.save(mk2); + + expect((await MasterKey.latest()).id).toBe(mk2.id); + + await msleep(1); + + mk1 = await MasterKey.save(mk1); + + expect((await MasterKey.latest()).id).toBe(mk1.id); + })); + +}); diff --git a/packages/lib/models/MasterKey.ts b/packages/lib/models/MasterKey.ts index cad44c8348..711100cc8a 100644 --- a/packages/lib/models/MasterKey.ts +++ b/packages/lib/models/MasterKey.ts @@ -1,6 +1,8 @@ import BaseModel from '../BaseModel'; import { MasterKeyEntity } from '../services/database/types'; +import { localSyncInfo, saveLocalSyncInfo } from '../services/synchronizer/syncInfoUtils'; import BaseItem from './BaseItem'; +import uuid from '../uuid'; export default class MasterKey extends BaseItem { static tableName() { @@ -15,21 +17,72 @@ export default class MasterKey extends BaseItem { return false; } - static latest() { - return this.modelSelectOne('SELECT * FROM master_keys WHERE created_time >= (SELECT max(created_time) FROM master_keys)'); + public static latest() { + let output: MasterKeyEntity = null; + const syncInfo = localSyncInfo(); + for (const mk of syncInfo.masterKeys) { + if (!output || output.updated_time < mk.updated_time) { + output = mk; + } + } + return output; + // return this.modelSelectOne('SELECT * FROM master_keys WHERE created_time >= (SELECT max(created_time) FROM master_keys)'); } static allWithoutEncryptionMethod(masterKeys: MasterKeyEntity[], method: number) { return masterKeys.filter(m => m.encryption_method !== method); } - static async save(o: MasterKeyEntity, options: any = null) { - return super.save(o, options).then(item => { - this.dispatch({ - type: 'MASTERKEY_UPDATE_ONE', - item: item, - }); - return item; + public static async all(): Promise { + return localSyncInfo().masterKeys; + } + + public static async allIds(): Promise { + return localSyncInfo().masterKeys.map(k => k.id); + } + + public static async count(): Promise { + return localSyncInfo().masterKeys.length; + } + + public static async load(id: string): Promise { + return localSyncInfo().masterKeys.find(mk => mk.id === id); + } + + public static async save(o: MasterKeyEntity): Promise { + const syncInfo = localSyncInfo(); + + const masterKey = { ...o }; + if (!masterKey.id) { + masterKey.id = uuid.create(); + masterKey.created_time = Date.now(); + } + + masterKey.updated_time = Date.now(); + + const idx = syncInfo.masterKeys.findIndex(mk => mk.id === masterKey.id); + + if (idx >= 0) { + syncInfo.masterKeys[idx] = masterKey; + } else { + syncInfo.masterKeys.push(masterKey); + } + + saveLocalSyncInfo(syncInfo); + + this.dispatch({ + type: 'MASTERKEY_UPDATE_ONE', + item: masterKey, }); + + return masterKey; + + // return super.save(o, options).then(item => { + // this.dispatch({ + // type: 'MASTERKEY_UPDATE_ONE', + // item: item, + // }); + // return item; + // }); } } diff --git a/packages/lib/models/Resource.ts b/packages/lib/models/Resource.ts index 408f4c6098..e1bce5638d 100644 --- a/packages/lib/models/Resource.ts +++ b/packages/lib/models/Resource.ts @@ -13,6 +13,7 @@ const { filename, safeFilename } = require('../path-utils'); const { FsDriverDummy } = require('../fs-driver-dummy.js'); import JoplinError from '../JoplinError'; import itemCanBeEncrypted from './utils/itemCanBeEncrypted'; +import { getEncryptionEnabled } from '../services/synchronizer/syncInfoUtils'; export default class Resource extends BaseItem { @@ -196,7 +197,7 @@ export default class Resource extends BaseItem { public static async fullPathForSyncUpload(resource: ResourceEntity) { const plainTextPath = this.fullPath(resource); - if (!Setting.value('encryption.enabled') || !itemCanBeEncrypted(resource as any)) { + if (!getEncryptionEnabled() || !itemCanBeEncrypted(resource as any)) { // Normally not possible since itemsThatNeedSync should only return decrypted items if (resource.encryption_blob_encrypted) throw new Error('Trying to access encrypted resource but encryption is currently disabled'); return { path: plainTextPath, resource: resource }; diff --git a/packages/lib/models/Setting.ts b/packages/lib/models/Setting.ts index 3b4ad33522..f47ba65930 100644 --- a/packages/lib/models/Setting.ts +++ b/packages/lib/models/Setting.ts @@ -194,7 +194,7 @@ class Setting extends BaseModel { cacheDir: '', pluginDir: '', flagOpenDevTools: false, - syncVersion: 2, + syncVersion: 3, startupDevPlugins: [], }; @@ -210,6 +210,7 @@ class Setting extends BaseModel { private static customSections_: SettingSections = {}; private static changedKeys_: string[] = []; private static fileHandler_: FileHandler = null; + private static settingFilename_: string = 'settings.json'; static tableName() { return 'settings'; @@ -233,7 +234,15 @@ class Setting extends BaseModel { } public static get settingFilePath(): string { - return `${this.value('profileDir')}/settings.json`; + return `${this.value('profileDir')}/${this.settingFilename_}`; + } + + public static get settingFilename(): string { + return this.settingFilename_; + } + + public static set settingFilename(v: string) { + this.settingFilename_ = v; } public static get fileHandler(): FileHandler { @@ -1251,6 +1260,12 @@ class Setting extends BaseModel { storage: SettingStorage.File, }, + 'syncInfoCache': { + value: '', + type: SettingItemType.String, + public: false, + }, + isSafeMode: { value: false, type: SettingItemType.Bool, @@ -1671,6 +1686,12 @@ class Setting extends BaseModel { return copyIfNeeded(md.value); } + // This function returns the default value if the setting key does not exist. + public static valueNoThrow(key: string, defaultValue: any) { + if (!this.keyExists(key)) return defaultValue; + return this.value(key); + } + static isEnum(key: string) { const md = this.settingMetadata(key); return md.isEnum === true; diff --git a/packages/lib/package.json b/packages/lib/package.json index 746c733889..99c5a73ea5 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -12,7 +12,7 @@ "tsc": "node node_modules/typescript/bin/tsc --project tsconfig.json", "watch": "node node_modules/typescript/bin/tsc --watch --project tsconfig.json", "generatePluginTypes": "rm -rf ./plugin_types && node node_modules/typescript/bin/tsc --declaration --declarationDir ./plugin_types --project tsconfig.json", - "test": "jest", + "test": "jest --verbose=false", "test-ci": "npm run test" }, "devDependencies": { diff --git a/packages/lib/reducer.ts b/packages/lib/reducer.ts index 31bf84e6ca..b4b5d7e105 100644 --- a/packages/lib/reducer.ts +++ b/packages/lib/reducer.ts @@ -983,9 +983,9 @@ const reducer = produce((draft: Draft = defaultState, action: any) => { handleItemDelete(draft, action); break; - case 'MASTERKEY_UPDATE_ALL': - draft.masterKeys = action.items; - break; + // case 'MASTERKEY_UPDATE_ALL': + // draft.masterKeys = action.items; + // break; case 'MASTERKEY_SET_NOT_LOADED': draft.notLoadedMasterKeys = action.ids; diff --git a/packages/lib/services/CommandService.test.ts b/packages/lib/services/CommandService.test.ts index 0c27227550..fc7818b471 100644 --- a/packages/lib/services/CommandService.test.ts +++ b/packages/lib/services/CommandService.test.ts @@ -3,8 +3,7 @@ import ToolbarButtonUtils from '../services/commands/ToolbarButtonUtils'; import CommandService, { CommandDeclaration, CommandRuntime } from '../services/CommandService'; import stateToWhenClauseContext from '../services/commands/stateToWhenClauseContext'; import KeymapService from '../services/KeymapService'; - -const { setupDatabaseAndSynchronizer, switchClient, expectThrow, expectNotThrow } = require('../testing/test-utils.js'); +import { setupDatabaseAndSynchronizer, switchClient, expectThrow, expectNotThrow } from '../testing/test-utils'; interface TestCommand { declaration: CommandDeclaration; diff --git a/packages/lib/services/EncryptionService.test.ts b/packages/lib/services/EncryptionService.test.ts index 90c222c80b..f395534f26 100644 --- a/packages/lib/services/EncryptionService.test.ts +++ b/packages/lib/services/EncryptionService.test.ts @@ -1,10 +1,11 @@ -import { fileContentEqual, setupDatabaseAndSynchronizer, supportDir, switchClient, objectsEqual, checkThrowAsync } from '../testing/test-utils'; +import { fileContentEqual, setupDatabaseAndSynchronizer, supportDir, switchClient, objectsEqual, checkThrowAsync, msleep } from '../testing/test-utils'; import Folder from '../models/Folder'; import Note from '../models/Note'; import Setting from '../models/Setting'; import BaseItem from '../models/BaseItem'; import MasterKey from '../models/MasterKey'; import EncryptionService from '../services/EncryptionService'; +import { setEncryptionEnabled } from '../services/synchronizer/syncInfoUtils'; let service: EncryptionService = null; @@ -15,7 +16,7 @@ describe('services_EncryptionService', function() { await switchClient(1); service = new EncryptionService(); BaseItem.encryptionService_ = service; - Setting.setValue('encryption.enabled', true); + setEncryptionEnabled(true); done(); }); @@ -65,14 +66,14 @@ describe('services_EncryptionService', function() { // Check that master key plain text is still the same const plainTextOld = await service.decryptMasterKey_(masterKey, '123456'); const plainTextNew = await service.decryptMasterKey_(upgradedMasterKey, '123456'); - expect(plainTextOld.content).toBe(plainTextNew.content); + expect(plainTextOld).toBe(plainTextNew); // Check that old content can be decrypted with new master key - await service.loadMasterKey_(masterKey, '123456', true); + await service.loadMasterKey(masterKey, '123456', true); const cipherText = await service.encryptString('some secret'); const plainTextFromOld = await service.decryptString(cipherText); - await service.loadMasterKey_(upgradedMasterKey, '123456', true); + await service.loadMasterKey(upgradedMasterKey, '123456', true); const plainTextFromNew = await service.decryptString(cipherText); expect(plainTextFromOld).toBe(plainTextFromNew); @@ -138,7 +139,7 @@ describe('services_EncryptionService', function() { let masterKey = await service.generateMasterKey('123456'); masterKey = await MasterKey.save(masterKey); - await service.loadMasterKey_(masterKey, '123456', true); + await service.loadMasterKey(masterKey, '123456', true); const cipherText = await service.encryptString('some secret'); const plainText = await service.decryptString(cipherText); @@ -159,7 +160,7 @@ describe('services_EncryptionService', function() { it('should decrypt various encryption methods', (async () => { let masterKey = await service.generateMasterKey('123456'); masterKey = await MasterKey.save(masterKey); - await service.loadMasterKey_(masterKey, '123456', true); + await service.loadMasterKey(masterKey, '123456', true); { const cipherText = await service.encryptString('some secret', { @@ -186,7 +187,7 @@ describe('services_EncryptionService', function() { let masterKey = await service.generateMasterKey('123456'); masterKey = await MasterKey.save(masterKey); - await service.loadMasterKey_(masterKey, '123456', true); + await service.loadMasterKey(masterKey, '123456', true); const cipherText = await service.encryptString('some secret'); @@ -202,7 +203,7 @@ describe('services_EncryptionService', function() { let masterKey = await service.generateMasterKey('123456'); masterKey = await MasterKey.save(masterKey); - await service.loadMasterKey_(masterKey, '123456', true); + await service.loadMasterKey(masterKey, '123456', true); let cipherText = await service.encryptString('some secret'); cipherText += 'ABCDEFGHIJ'; @@ -215,7 +216,7 @@ describe('services_EncryptionService', function() { it('should encrypt and decrypt notes and folders', (async () => { let masterKey = await service.generateMasterKey('123456'); masterKey = await MasterKey.save(masterKey); - await service.loadMasterKey_(masterKey, '123456', true); + await service.loadMasterKey(masterKey, '123456', true); const folder = await Folder.save({ title: 'folder' }); const note = await Note.save({ title: 'encrypted note', body: 'something', parent_id: folder.id }); @@ -246,7 +247,7 @@ describe('services_EncryptionService', function() { it('should encrypt and decrypt files', (async () => { let masterKey = await service.generateMasterKey('123456'); masterKey = await MasterKey.save(masterKey); - await service.loadMasterKey_(masterKey, '123456', true); + await service.loadMasterKey(masterKey, '123456', true); const sourcePath = `${supportDir}/photo.jpg`; const encryptedPath = `${Setting.value('tempDir')}/photo.crypted`; @@ -263,7 +264,7 @@ describe('services_EncryptionService', function() { let masterKey = await service.generateMasterKey('123456'); masterKey = await MasterKey.save(masterKey); - await service.loadMasterKey_(masterKey, '123456', true); + await service.loadMasterKey(masterKey, '123456', true); // First check that we can replicate the error with the old encryption method service.defaultEncryptionMethod_ = EncryptionService.METHOD_SJCL; @@ -276,4 +277,20 @@ describe('services_EncryptionService', function() { const plainText = await service.decryptString(cipherText); expect(plainText).toBe('🐶🐶🐶'.substr(0,5)); })); + + it('should check if a master key is loaded', (async () => { + let masterKey = await service.generateMasterKey('123456'); + masterKey = await MasterKey.save(masterKey); + + await service.loadMasterKey(masterKey, '123456', true); + + expect(service.isMasterKeyLoaded(masterKey)).toBe(true); + + await msleep(1); + + // If the master key is modified afterwards it should report that it is + // *not* loaded since it doesn't have this new version. + masterKey = await MasterKey.save(masterKey); + expect(service.isMasterKeyLoaded(masterKey)).toBe(false); + })); }); diff --git a/packages/lib/services/EncryptionService.ts b/packages/lib/services/EncryptionService.ts index 5301bb08cf..edaa3f524a 100644 --- a/packages/lib/services/EncryptionService.ts +++ b/packages/lib/services/EncryptionService.ts @@ -4,9 +4,9 @@ import shim from '../shim'; import Setting from '../models/Setting'; import MasterKey from '../models/MasterKey'; import BaseItem from '../models/BaseItem'; - -const { padLeft } = require('../string-utils.js'); import JoplinError from '../JoplinError'; +import { getActiveMasterKeyId, setActiveMasterKeyId } from './synchronizer/syncInfoUtils'; +const { padLeft } = require('../string-utils.js'); function hexPad(s: string, length: number) { return padLeft(s, length, '0'); @@ -18,6 +18,11 @@ export function isValidHeaderIdentifier(id: string, ignoreTooLongLength = false) return /JED\d\d/.test(id); } +interface DecryptedMasterKey { + updatedTime: number; + plainText: string; +} + export default class EncryptionService { public static instance_: EncryptionService = null; @@ -44,8 +49,7 @@ export default class EncryptionService { // So making the block 10 times smaller make it 100 times faster! So for now using 5KB. This can be // changed easily since the chunk size is incorporated into the encrypted data. private chunkSize_ = 5000; - private loadedMasterKeys_: Record = {}; - private activeMasterKeyId_: string = null; + private decryptedMasterKeys_: Record = {}; public defaultEncryptionMethod_ = EncryptionService.METHOD_SJCL_1A; // public because used in tests private defaultMasterKeyEncryptionMethod_ = EncryptionService.METHOD_SJCL_4; private logger_ = new Logger(); @@ -73,8 +77,7 @@ export default class EncryptionService { // So making the block 10 times smaller make it 100 times faster! So for now using 5KB. This can be // changed easily since the chunk size is incorporated into the encrypted data. this.chunkSize_ = 5000; - this.loadedMasterKeys_ = {}; - this.activeMasterKeyId_ = null; + this.decryptedMasterKeys_ = {}; this.defaultEncryptionMethod_ = EncryptionService.METHOD_SJCL_1A; this.defaultMasterKeyEncryptionMethod_ = EncryptionService.METHOD_SJCL_4; this.logger_ = new Logger(); @@ -102,74 +105,8 @@ export default class EncryptionService { return this.logger_; } - async generateMasterKeyAndEnableEncryption(password: string) { - let masterKey = await this.generateMasterKey(password); - masterKey = await MasterKey.save(masterKey); - await this.enableEncryption(masterKey, password); - await this.loadMasterKeysFromSettings(); - return masterKey; - } - - async enableEncryption(masterKey: MasterKeyEntity, password: string = null) { - Setting.setValue('encryption.enabled', true); - Setting.setValue('encryption.activeMasterKeyId', masterKey.id); - - if (password) { - const passwordCache = Setting.value('encryption.passwordCache'); - passwordCache[masterKey.id] = password; - Setting.setValue('encryption.passwordCache', passwordCache); - } - - // Mark only the non-encrypted ones for sync since, if there are encrypted ones, - // it means they come from the sync target and are already encrypted over there. - await BaseItem.markAllNonEncryptedForSync(); - } - - async disableEncryption() { - // Allow disabling encryption even if some items are still encrypted, because whether E2EE is enabled or disabled - // should not affect whether items will enventually be decrypted or not (DecryptionWorker will still work as - // long as there are encrypted items). Also even if decryption is disabled, it's possible that encrypted items - // will still be received via synchronisation. - - // const hasEncryptedItems = await BaseItem.hasEncryptedItems(); - // if (hasEncryptedItems) throw new Error(_('Encryption cannot currently be disabled because some items are still encrypted. Please wait for all the items to be decrypted and try again.')); - - Setting.setValue('encryption.enabled', false); - // The only way to make sure everything gets decrypted on the sync target is - // to re-sync everything. - await BaseItem.forceSyncAll(); - } - - async loadMasterKeysFromSettings() { - const masterKeys = await MasterKey.all(); - const passwords = Setting.value('encryption.passwordCache'); - const activeMasterKeyId = Setting.value('encryption.activeMasterKeyId'); - - this.logger().info(`Trying to load ${masterKeys.length} master keys...`); - - for (let i = 0; i < masterKeys.length; i++) { - const mk = masterKeys[i]; - const password = passwords[mk.id]; - if (this.isMasterKeyLoaded(mk.id)) continue; - if (!password) continue; - - try { - await this.loadMasterKey_(mk, password, activeMasterKeyId === mk.id); - } catch (error) { - this.logger().warn(`Cannot load master key ${mk.id}. Invalid password?`, error); - } - } - - this.logger().info(`Loaded master keys: ${this.loadedMasterKeysCount()}`); - } - loadedMasterKeysCount() { - let output = 0; - for (const n in this.loadedMasterKeys_) { - if (!this.loadedMasterKeys_[n]) continue; - output++; - } - return output; + return Object.keys(this.decryptedMasterKeys_).length; } chunkSize() { @@ -181,56 +118,50 @@ export default class EncryptionService { } setActiveMasterKeyId(id: string) { - this.activeMasterKeyId_ = id; + setActiveMasterKeyId(id); } activeMasterKeyId() { - if (!this.activeMasterKeyId_) { + const id = getActiveMasterKeyId(); + if (!id) { const error: any = new Error('No master key is defined as active. Check this: Either one or more master keys exist but no password was provided for any of them. Or no master key exist. Or master keys and password exist, but none was set as active.'); error.code = 'noActiveMasterKey'; throw error; } - return this.activeMasterKeyId_; + return id; } - isMasterKeyLoaded(id: string) { - return !!this.loadedMasterKeys_[id]; + public isMasterKeyLoaded(masterKey: MasterKeyEntity) { + const d = this.decryptedMasterKeys_[masterKey.id]; + if (!d) return false; + return d.updatedTime === masterKey.updated_time; } - async loadMasterKey_(model: MasterKeyEntity, password: string, makeActive = false) { + public async loadMasterKey(model: MasterKeyEntity, password: string, makeActive = false) { if (!model.id) throw new Error('Master key does not have an ID - save it first'); - this.loadedMasterKeys_[model.id] = await this.decryptMasterKey_(model, password); + this.decryptedMasterKeys_[model.id] = { + plainText: await this.decryptMasterKey_(model, password), + updatedTime: model.updated_time, + }; if (makeActive) this.setActiveMasterKeyId(model.id); } unloadMasterKey(model: MasterKeyEntity) { - delete this.loadedMasterKeys_[model.id]; + delete this.decryptedMasterKeys_[model.id]; } - // unloadAllMasterKeys() { - // for (const id in this.loadedMasterKeys_) { - // if (!this.loadedMasterKeys_.hasOwnProperty(id)) continue; - // this.unloadMasterKey(this.loadedMasterKeys_[id]); - // } - // } - loadedMasterKey(id: string) { - if (!this.loadedMasterKeys_[id]) { + if (!this.decryptedMasterKeys_[id]) { const error: any = new Error(`Master key is not loaded: ${id}`); error.code = 'masterKeyNotLoaded'; error.masterKeyId = id; throw error; } - return this.loadedMasterKeys_[id]; + return this.decryptedMasterKeys_[id]; } loadedMasterKeyIds() { - const output = []; - for (const id in this.loadedMasterKeys_) { - if (!this.loadedMasterKeys_.hasOwnProperty(id)) continue; - output.push(id); - } - return output; + return Object.keys(this.decryptedMasterKeys_); } fsDriver() { @@ -244,22 +175,6 @@ export default class EncryptionService { return sjcl.codec.hex.fromBits(bitArray); } - // async seedSjcl() { - // throw new Error('NOT TESTED'); - - // // Just putting this here in case it becomes needed - // // Normally seeding random bytes is not needed for our use since - // // we use shim.randomBytes directly to generate master keys. - - // const sjcl = shim.sjclModule; - // const randomBytes = await shim.randomBytes(1024 / 8); - // const hexBytes = randomBytes.map(a => { - // return a.toString(16); - // }); - // const hexSeed = sjcl.codec.hex.toBits(hexBytes.join('')); - // sjcl.random.addEntropy(hexSeed, 1024, 'shim.randomBytes'); - // } - async generateApiToken() { return await this.randomHexString(64); } @@ -318,12 +233,13 @@ export default class EncryptionService { return model; } - async decryptMasterKey_(model: MasterKeyEntity, password: string) { + public async decryptMasterKey_(model: MasterKeyEntity, password: string): Promise { const plainText = await this.decrypt(model.encryption_method, password, model.content); if (model.encryption_method === EncryptionService.METHOD_SJCL_2) { const checksum = this.sha256(plainText); if (checksum !== model.checksum) throw new Error('Could not decrypt master key (checksum failed)'); } + return plainText; } @@ -469,7 +385,7 @@ export default class EncryptionService { const method = options.encryptionMethod; const masterKeyId = this.activeMasterKeyId(); - const masterKeyPlainText = this.loadedMasterKey(masterKeyId); + const masterKeyPlainText = this.loadedMasterKey(masterKeyId).plainText; const header = { encryptionMethod: method, @@ -502,7 +418,7 @@ export default class EncryptionService { if (!options) options = {}; const header: any = await this.decodeHeaderSource_(source); - const masterKeyPlainText = this.loadedMasterKey(header.masterKeyId); + const masterKeyPlainText = this.loadedMasterKey(header.masterKeyId).plainText; let doneSize = 0; diff --git a/packages/lib/services/ResourceService.test.ts b/packages/lib/services/ResourceService.test.ts index 526bc4e85b..0e256eab51 100644 --- a/packages/lib/services/ResourceService.test.ts +++ b/packages/lib/services/ResourceService.test.ts @@ -2,14 +2,14 @@ import time from '../time'; import NoteResource from '../models/NoteResource'; import ResourceService from '../services/ResourceService'; import shim from '../shim'; - -const { resourceService, decryptionWorker, supportDir, encryptionService, loadEncryptionMasterKey, allSyncTargetItemsEncrypted, setupDatabaseAndSynchronizer, db, synchronizer, switchClient } = require('../testing/test-utils.js'); +import { resourceService, decryptionWorker, supportDir, encryptionService, loadEncryptionMasterKey, allSyncTargetItemsEncrypted, setupDatabaseAndSynchronizer, db, synchronizer, switchClient } from '../testing/test-utils'; import Folder from '../models/Folder'; import Note from '../models/Note'; import Resource from '../models/Resource'; import SearchEngine from '../services/searchengine/SearchEngine'; +import { loadMasterKeysFromSettings, setupAndEnableEncryption } from './e2ee/utils'; -describe('services_ResourceService', function() { +describe('services/ResourceService', function() { beforeEach(async (done) => { await setupDatabaseAndSynchronizer(1); @@ -139,8 +139,8 @@ describe('services_ResourceService', function() { // Eventually R1 is deleted because service thinks that it was at some point associated with a note, but no longer. const masterKey = await loadEncryptionMasterKey(); - await encryptionService().enableEncryption(masterKey, '123456'); - await encryptionService().loadMasterKeysFromSettings(); + await setupAndEnableEncryption(encryptionService(), masterKey, '123456'); + await loadMasterKeysFromSettings(encryptionService()); const folder1 = await Folder.save({ title: 'folder1' }); const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); await shim.attachFileToNote(note1, `${supportDir}/photo.jpg`); // R1 @@ -151,8 +151,8 @@ describe('services_ResourceService', function() { await switchClient(2); await synchronizer().start(); - await encryptionService().enableEncryption(masterKey, '123456'); - await encryptionService().loadMasterKeysFromSettings(); + await setupAndEnableEncryption(encryptionService(), masterKey, '123456'); + await loadMasterKeysFromSettings(encryptionService()); await decryptionWorker().start(); { const n1 = await Note.load(note1.id); diff --git a/packages/lib/services/SettingUtils.ts b/packages/lib/services/SettingUtils.ts index 4e5e11992d..022604b33e 100644 --- a/packages/lib/services/SettingUtils.ts +++ b/packages/lib/services/SettingUtils.ts @@ -3,6 +3,7 @@ import KeychainService from './keychain/KeychainService'; import Setting from '../models/Setting'; import uuid from '../uuid'; +import { migrateLocalSyncInfo } from './synchronizer/syncInfoUtils'; // This function takes care of initialising both the keychain service and settings. // @@ -18,6 +19,13 @@ export async function loadKeychainServiceAndSettings(KeychainServiceDriver: any) KeychainService.instance().initialize(new KeychainServiceDriver(Setting.value('appId'), clientId)); Setting.setKeychainService(KeychainService.instance()); await Setting.load(); + + // This is part of the migration to the new sync target info. It needs to be + // set as early as possible since it's used to tell if E2EE is enabled, it + // contains the master keys, etc. Once it has been set, it becomes a noop + // on future calls. + await migrateLocalSyncInfo(Setting.db()); + if (!clientIdSetting) Setting.setValue('clientId', clientId); await KeychainService.instance().detectIfKeychainSupported(); } diff --git a/packages/lib/services/e2ee/utils.ts b/packages/lib/services/e2ee/utils.ts new file mode 100644 index 0000000000..753b35b8f7 --- /dev/null +++ b/packages/lib/services/e2ee/utils.ts @@ -0,0 +1,92 @@ +import Logger from '../../Logger'; +import BaseItem from '../../models/BaseItem'; +import MasterKey from '../../models/MasterKey'; +import Setting from '../../models/Setting'; +import { MasterKeyEntity } from '../database/types'; +import EncryptionService from '../EncryptionService'; +import { getActiveMasterKeyId, setEncryptionEnabled } from '../synchronizer/syncInfoUtils'; + +const logger = Logger.create('e2ee/utils'); + +export async function setupAndEnableEncryption(service: EncryptionService, masterKey: MasterKeyEntity = null, password: string = null) { + if (!masterKey) { + // May happen for example if there are master keys in info.json but none + // of them is set as active. But in fact, unless there is a bug in the + // application, this shouldn't happen. + logger.warn('Setting up E2EE without a master key - user will need to either generate one or select one of the existing ones as active'); + } + + setEncryptionEnabled(true, masterKey ? masterKey.id : null); + + if (masterKey && password) { + const passwordCache = Setting.value('encryption.passwordCache'); + passwordCache[masterKey.id] = password; + Setting.setValue('encryption.passwordCache', passwordCache); + } + + // Mark only the non-encrypted ones for sync since, if there are encrypted ones, + // it means they come from the sync target and are already encrypted over there. + await BaseItem.markAllNonEncryptedForSync(); + + await loadMasterKeysFromSettings(service); +} + +export async function setupAndDisableEncryption(service: EncryptionService) { + // Allow disabling encryption even if some items are still encrypted, because whether E2EE is enabled or disabled + // should not affect whether items will enventually be decrypted or not (DecryptionWorker will still work as + // long as there are encrypted items). Also even if decryption is disabled, it's possible that encrypted items + // will still be received via synchronisation. + + setEncryptionEnabled(false); + + // The only way to make sure everything gets decrypted on the sync target is + // to re-sync everything. + await BaseItem.forceSyncAll(); + + await loadMasterKeysFromSettings(service); +} + +export async function toggleAndSetupEncryption(service: EncryptionService, enabled: boolean, masterKey: MasterKeyEntity, password: string) { + if (!enabled) { + await setupAndDisableEncryption(service); + } else { + if (masterKey) { + await setupAndEnableEncryption(service, masterKey, password); + } else { + await generateMasterKeyAndEnableEncryption(EncryptionService.instance(), password); + } + } + + await loadMasterKeysFromSettings(service); +} + +export async function generateMasterKeyAndEnableEncryption(service: EncryptionService, password: string) { + let masterKey = await service.generateMasterKey(password); + masterKey = await MasterKey.save(masterKey); + await setupAndEnableEncryption(service, masterKey, password); + await loadMasterKeysFromSettings(service); + return masterKey; +} + +export async function loadMasterKeysFromSettings(service: EncryptionService) { + const masterKeys = await MasterKey.all(); + const passwords = Setting.value('encryption.passwordCache'); + const activeMasterKeyId = getActiveMasterKeyId(); + + logger.info(`Trying to load ${masterKeys.length} master keys...`); + + for (let i = 0; i < masterKeys.length; i++) { + const mk = masterKeys[i]; + const password = passwords[mk.id]; + if (service.isMasterKeyLoaded(mk)) continue; + if (!password) continue; + + try { + await service.loadMasterKey(mk, password, activeMasterKeyId === mk.id); + } catch (error) { + logger.warn(`Cannot load master key ${mk.id}. Invalid password?`, error); + } + } + + logger.info(`Loaded master keys: ${service.loadedMasterKeysCount()}`); +} diff --git a/packages/lib/services/rest/Api.test.ts b/packages/lib/services/rest/Api.test.ts index 91c7476ba7..b213b111c3 100644 --- a/packages/lib/services/rest/Api.test.ts +++ b/packages/lib/services/rest/Api.test.ts @@ -1,8 +1,7 @@ import { PaginationOrderDir } from '../../models/utils/types'; import Api, { RequestMethod } from '../../services/rest/Api'; import shim from '../../shim'; - -const { setupDatabaseAndSynchronizer, switchClient, checkThrowAsync, db, msleep, supportDir } = require('../../testing/test-utils.js'); +import { setupDatabaseAndSynchronizer, switchClient, checkThrowAsync, db, msleep, supportDir } from '../../testing/test-utils'; import Folder from '../../models/Folder'; import Resource from '../../models/Resource'; import Note from '../../models/Note'; diff --git a/packages/lib/services/synchronizer/ItemUploader.test.ts b/packages/lib/services/synchronizer/ItemUploader.test.ts index 9cb541b099..437940c65e 100644 --- a/packages/lib/services/synchronizer/ItemUploader.test.ts +++ b/packages/lib/services/synchronizer/ItemUploader.test.ts @@ -41,7 +41,7 @@ function newFakeApiCall(callRecorder: ApiCall[], itemBodyCallback: Function = nu return apiCall; } -describe('synchronizer_ItemUplader', function() { +describe('synchronizer/ItemUploader', function() { beforeEach(async (done) => { await setupDatabaseAndSynchronizer(1); diff --git a/packages/lib/services/synchronizer/MigrationHandler.ts b/packages/lib/services/synchronizer/MigrationHandler.ts index b2207b6551..6ae366c46b 100644 --- a/packages/lib/services/synchronizer/MigrationHandler.ts +++ b/packages/lib/services/synchronizer/MigrationHandler.ts @@ -1,23 +1,30 @@ import LockHandler, { LockType } from './LockHandler'; import { Dirnames } from './utils/types'; import BaseService from '../BaseService'; +import migration1 from './migrations/1'; +import migration2 from './migrations/2'; +import migration3 from './migrations/3'; +import Setting from '../../models/Setting'; +import JoplinError from '../../JoplinError'; +import { FileApi } from '../../file-api'; +import JoplinDatabase from '../../JoplinDatabase'; +import { fetchSyncInfo, SyncInfo } from './syncInfoUtils'; +const { sprintf } = require('sprintf-js'); + +export type MigrationFunction = (api: FileApi, db: JoplinDatabase)=> Promise; // To add a new migration: // - Add the migration logic in ./migrations/VERSION_NUM.js // - Add the file to the array below. // - Set Setting.syncVersion to VERSION_NUM in models/Setting.js // - Add tests in synchronizer_migrationHandler -const migrations = [ +const migrations: MigrationFunction[] = [ null, - require('./migrations/1.js').default, - require('./migrations/2.js').default, + migration1, + migration2, + migration3, ]; -import Setting from '../../models/Setting'; -const { sprintf } = require('sprintf-js'); -import JoplinError from '../../JoplinError'; -import { FileApi } from '../../file-api'; - interface SyncTargetInfo { version: number; } @@ -28,10 +35,12 @@ export default class MigrationHandler extends BaseService { private lockHandler_: LockHandler = null; private clientType_: string; private clientId_: string; + private db_: JoplinDatabase; - constructor(api: FileApi, lockHandler: LockHandler, clientType: string, clientId: string) { + public constructor(api: FileApi, db: JoplinDatabase, lockHandler: LockHandler, clientType: string, clientId: string) { super(); this.api_ = api; + this.db_ = db; this.lockHandler_ = lockHandler; this.clientType_ = clientType; this.clientId_ = clientId; @@ -58,19 +67,17 @@ export default class MigrationHandler extends BaseService { return JSON.stringify(info); } - async checkCanSync(): Promise { + public async checkCanSync(remoteInfo: SyncInfo = null) { + remoteInfo = remoteInfo || await fetchSyncInfo(this.api_); const supportedSyncTargetVersion = Setting.value('syncVersion'); - const syncTargetInfo = await this.fetchSyncTargetInfo(); - if (syncTargetInfo.version) { - if (syncTargetInfo.version > supportedSyncTargetVersion) { - throw new JoplinError(sprintf('Sync version of the target (%d) is greater than the version supported by the client (%d). Please upgrade your client.', syncTargetInfo.version, supportedSyncTargetVersion), 'outdatedClient'); - } else if (syncTargetInfo.version < supportedSyncTargetVersion) { - throw new JoplinError(sprintf('Sync version of the target (%d) is lower than the version supported by the client (%d). Please upgrade the sync target.', syncTargetInfo.version, supportedSyncTargetVersion), 'outdatedSyncTarget'); + if (remoteInfo.version) { + if (remoteInfo.version > supportedSyncTargetVersion) { + throw new JoplinError(sprintf('Sync version of the target (%d) is greater than the version supported by the client (%d). Please upgrade your client.', remoteInfo.version, supportedSyncTargetVersion), 'outdatedClient'); + } else if (remoteInfo.version < supportedSyncTargetVersion) { + throw new JoplinError(sprintf('Sync version of the target (%d) is lower than the version supported by the client (%d). Please upgrade the sync target.', remoteInfo.version, supportedSyncTargetVersion), 'outdatedSyncTarget'); } } - - return syncTargetInfo; } async upgrade(targetVersion: number = 0) { @@ -120,13 +127,17 @@ export default class MigrationHandler extends BaseService { try { if (autoLockError) throw autoLockError; - await migration(this.api_); + await migration(this.api_, this.db_); if (autoLockError) throw autoLockError; - await this.api_.put('info.json', this.serializeSyncTargetInfo({ - ...syncTargetInfo, - version: newVersion, - })); + // For legacy support. New migrations should set the sync + // target info directly as needed. + if ([1, 2].includes(newVersion)) { + await this.api_.put('info.json', this.serializeSyncTargetInfo({ + ...syncTargetInfo, + version: newVersion, + })); + } this.logger().info(`MigrationHandler: Done migrating from version ${fromVersion} to version ${newVersion}`); } catch (error) { diff --git a/packages/lib/services/synchronizer/Synchronizer.conflicts.test.ts b/packages/lib/services/synchronizer/Synchronizer.conflicts.test.ts index 0a1324bc20..68621a632c 100644 --- a/packages/lib/services/synchronizer/Synchronizer.conflicts.test.ts +++ b/packages/lib/services/synchronizer/Synchronizer.conflicts.test.ts @@ -1,11 +1,10 @@ import time from '../../time'; -import Setting from '../../models/Setting'; import { allNotesFolders, localNotesFoldersSameAsRemote } from '../../testing/test-utils-synchronizer'; - -const { synchronizerStart, setupDatabaseAndSynchronizer, sleep, switchClient, syncTargetId, loadEncryptionMasterKey, decryptionWorker } = require('../../testing/test-utils.js'); +import { synchronizerStart, setupDatabaseAndSynchronizer, sleep, switchClient, syncTargetId, loadEncryptionMasterKey, decryptionWorker } from '../../testing/test-utils'; import Folder from '../../models/Folder'; import Note from '../../models/Note'; import BaseItem from '../../models/BaseItem'; +import { setEncryptionEnabled } from '../synchronizer/syncInfoUtils'; describe('Synchronizer.conflicts', function() { @@ -227,7 +226,7 @@ describe('Synchronizer.conflicts', function() { async function ignorableNoteConflictTest(withEncryption: boolean) { if (withEncryption) { - Setting.setValue('encryption.enabled', true); + setEncryptionEnabled(true); await loadEncryptionMasterKey(); } diff --git a/packages/lib/services/synchronizer/Synchronizer.e2ee.test.ts b/packages/lib/services/synchronizer/Synchronizer.e2ee.test.ts index a26993e9a1..e968ec8497 100644 --- a/packages/lib/services/synchronizer/Synchronizer.e2ee.test.ts +++ b/packages/lib/services/synchronizer/Synchronizer.e2ee.test.ts @@ -10,6 +10,8 @@ import MasterKey from '../../models/MasterKey'; import BaseItem from '../../models/BaseItem'; import { ResourceEntity } from '../database/types'; import Synchronizer from '../../Synchronizer'; +import { getEncryptionEnabled, setEncryptionEnabled } from '../synchronizer/syncInfoUtils'; +import { loadMasterKeysFromSettings, setupAndDisableEncryption, setupAndEnableEncryption } from '../e2ee/utils'; let insideBeforeEach = false; @@ -31,7 +33,7 @@ describe('Synchronizer.e2ee', function() { }); it('notes and folders should get encrypted when encryption is enabled', (async () => { - Setting.setValue('encryption.enabled', true); + setEncryptionEnabled(true); const masterKey = await loadEncryptionMasterKey(); const folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'un', body: 'to be encrypted', parent_id: folder1.id }); @@ -55,7 +57,7 @@ describe('Synchronizer.e2ee', function() { expect(masterKey_2.content).toBe(masterKey.content); expect(masterKey_2.checksum).toBe(masterKey.checksum); // Now load the master key we got from client 1 and try to decrypt - await encryptionService().loadMasterKey_(masterKey_2, '123456', true); + await encryptionService().loadMasterKey(masterKey_2, '123456', true); // Get the decrypted items back await Folder.decrypt(folder1_2); await Note.decrypt(note1_2); @@ -74,7 +76,7 @@ describe('Synchronizer.e2ee', function() { it('should enable encryption automatically when downloading new master key (and none was previously available)',(async () => { // Enable encryption on client 1 and sync an item - Setting.setValue('encryption.enabled', true); + setEncryptionEnabled(true); await loadEncryptionMasterKey(); let folder1 = await Folder.save({ title: 'folder1' }); await synchronizerStart(); @@ -82,9 +84,9 @@ describe('Synchronizer.e2ee', function() { await switchClient(2); // Synchronising should enable encryption since we're going to get a master key - expect(Setting.value('encryption.enabled')).toBe(false); + expect(getEncryptionEnabled()).toBe(false); await synchronizerStart(); - expect(Setting.value('encryption.enabled')).toBe(true); + expect(getEncryptionEnabled()).toBe(true); // Check that we got the master key from client 1 const masterKey = (await MasterKey.all())[0]; @@ -109,7 +111,7 @@ describe('Synchronizer.e2ee', function() { // Now client 2 set the master key password Setting.setObjectValue('encryption.passwordCache', masterKey.id, '123456'); - await encryptionService().loadMasterKeysFromSettings(); + await loadMasterKeysFromSettings(encryptionService()); // Now that master key should be loaded expect(encryptionService().loadedMasterKeyIds()[0]).toBe(masterKey.id); @@ -141,24 +143,22 @@ describe('Synchronizer.e2ee', function() { // Then enable encryption and sync again let masterKey = await encryptionService().generateMasterKey('123456'); masterKey = await MasterKey.save(masterKey); - await encryptionService().enableEncryption(masterKey, '123456'); - await encryptionService().loadMasterKeysFromSettings(); + await setupAndEnableEncryption(encryptionService(), masterKey, '123456'); + await loadMasterKeysFromSettings(encryptionService()); await synchronizerStart(); // Even though the folder has not been changed it should have been synced again so that // an encrypted version of it replaces the decrypted version. files = await fileApi().list('', { includeDirs: false, syncItemsOnly: true }); - expect(files.items.length).toBe(2); + expect(files.items.length).toBe(1); + // By checking that the folder title is not present, we can confirm that the item has indeed been encrypted - // One of the two items is the master key content = await fileApi().get(files.items[0].path); expect(content.indexOf('folder1') < 0).toBe(true); - content = await fileApi().get(files.items[1].path); - expect(content.indexOf('folder1') < 0).toBe(true); })); it('should upload decrypted items to sync target after encryption disabled', (async () => { - Setting.setValue('encryption.enabled', true); + setEncryptionEnabled(true); await loadEncryptionMasterKey(); await Folder.save({ title: 'folder1' }); @@ -167,7 +167,7 @@ describe('Synchronizer.e2ee', function() { let allEncrypted = await allSyncTargetItemsEncrypted(); expect(allEncrypted).toBe(true); - await encryptionService().disableEncryption(); + await setupAndDisableEncryption(encryptionService()); await synchronizerStart(); allEncrypted = await allSyncTargetItemsEncrypted(); @@ -179,7 +179,7 @@ describe('Synchronizer.e2ee', function() { // which means it's going to fail in unexpected way. So the loop below wait for beforeEach to be done. while (insideBeforeEach) await time.msleep(100); - Setting.setValue('encryption.enabled', true); + setEncryptionEnabled(true); const masterKey = await loadEncryptionMasterKey(); await Folder.save({ title: 'folder1' }); @@ -188,21 +188,21 @@ describe('Synchronizer.e2ee', function() { await switchClient(2); await synchronizerStart(); - expect(Setting.value('encryption.enabled')).toBe(true); + expect(getEncryptionEnabled()).toBe(true); // If we try to disable encryption now, it should throw an error because some items are // currently encrypted. They must be decrypted first so that they can be sent as // plain text to the sync target. - // let hasThrown = await checkThrowAsync(async () => await encryptionService().disableEncryption()); + // let hasThrown = await checkThrowAsync(async () => await setupAndDisableEncryption(encryptionService())); // expect(hasThrown).toBe(true); // Now supply the password, and decrypt the items Setting.setObjectValue('encryption.passwordCache', masterKey.id, '123456'); - await encryptionService().loadMasterKeysFromSettings(); + await loadMasterKeysFromSettings(encryptionService()); await decryptionWorker().start(); // Try to disable encryption again - const hasThrown = await checkThrowAsync(async () => await encryptionService().disableEncryption()); + const hasThrown = await checkThrowAsync(async () => await setupAndDisableEncryption(encryptionService())); expect(hasThrown).toBe(false); // If we sync now the target should receive the decrypted items @@ -212,7 +212,7 @@ describe('Synchronizer.e2ee', function() { })); it('should set the resource file size after decryption', (async () => { - Setting.setValue('encryption.enabled', true); + setEncryptionEnabled(true); const masterKey = await loadEncryptionMasterKey(); const folder1 = await Folder.save({ title: 'folder1' }); @@ -227,7 +227,7 @@ describe('Synchronizer.e2ee', function() { await synchronizerStart(); Setting.setObjectValue('encryption.passwordCache', masterKey.id, '123456'); - await encryptionService().loadMasterKeysFromSettings(); + await loadMasterKeysFromSettings(encryptionService()); const fetcher = newResourceFetcher(synchronizer()); fetcher.queueDownload_(resource1.id); @@ -249,8 +249,8 @@ describe('Synchronizer.e2ee', function() { expect(await allSyncTargetItemsEncrypted()).toBe(false); const masterKey = await loadEncryptionMasterKey(); - await encryptionService().enableEncryption(masterKey, '123456'); - await encryptionService().loadMasterKeysFromSettings(); + await setupAndEnableEncryption(encryptionService(), masterKey, '123456'); + await loadMasterKeysFromSettings(encryptionService()); await synchronizerStart(); @@ -264,20 +264,20 @@ describe('Synchronizer.e2ee', function() { const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); await shim.attachFileToNote(note1, `${supportDir}/photo.jpg`); const masterKey = await loadEncryptionMasterKey(); - await encryptionService().enableEncryption(masterKey, '123456'); - await encryptionService().loadMasterKeysFromSettings(); - await synchronizerStart(); + await setupAndEnableEncryption(encryptionService(), masterKey, '123456'); + await loadMasterKeysFromSettings(encryptionService()); + // await synchronizerStart(); - const resource1 = (await Resource.all())[0]; - expect(resource1.encryption_blob_encrypted).toBe(0); + // const resource1 = (await Resource.all())[0]; + // expect(resource1.encryption_blob_encrypted).toBe(0); })); it('should decrypt the resource metadata, but not try to decrypt the file, if it is not present', (async () => { const note1 = await Note.save({ title: 'note' }); await shim.attachFileToNote(note1, `${supportDir}/photo.jpg`); const masterKey = await loadEncryptionMasterKey(); - await encryptionService().enableEncryption(masterKey, '123456'); - await encryptionService().loadMasterKeysFromSettings(); + await setupAndEnableEncryption(encryptionService(), masterKey, '123456'); + await loadMasterKeysFromSettings(encryptionService()); await synchronizerStart(); expect(await allSyncTargetItemsEncrypted()).toBe(true); @@ -285,7 +285,7 @@ describe('Synchronizer.e2ee', function() { await synchronizerStart(); Setting.setObjectValue('encryption.passwordCache', masterKey.id, '123456'); - await encryptionService().loadMasterKeysFromSettings(); + await loadMasterKeysFromSettings(encryptionService()); await decryptionWorker().start(); let resource = (await Resource.all())[0]; @@ -311,8 +311,8 @@ describe('Synchronizer.e2ee', function() { const note = await Note.save({ title: 'ma note' }); const masterKey = await loadEncryptionMasterKey(); - await encryptionService().enableEncryption(masterKey, '123456'); - await encryptionService().loadMasterKeysFromSettings(); + await setupAndEnableEncryption(encryptionService(), masterKey, '123456'); + await loadMasterKeysFromSettings(encryptionService()); await synchronizerStart(); await switchClient(2); @@ -329,7 +329,7 @@ describe('Synchronizer.e2ee', function() { await Note.save({ id: note.id, encryption_cipher_text: 'doesntlookright' }); Setting.setObjectValue('encryption.passwordCache', masterKey.id, '123456'); - await encryptionService().loadMasterKeysFromSettings(); + await loadMasterKeysFromSettings(encryptionService()); hasThrown = await checkThrowAsync(async () => await decryptionWorker().start({ errorHandler: 'throw' })); expect(hasThrown).toBe(true); @@ -367,7 +367,7 @@ describe('Synchronizer.e2ee', function() { })); it('should not encrypt notes that are shared by link', (async () => { - Setting.setValue('encryption.enabled', true); + setEncryptionEnabled(true); await loadEncryptionMasterKey(); await createFolderTree('', [ @@ -459,7 +459,7 @@ describe('Synchronizer.e2ee', function() { return; } - Setting.setValue('encryption.enabled', true); + setEncryptionEnabled(true); await loadEncryptionMasterKey(); const folder1 = await createFolderTree('', [ diff --git a/packages/lib/services/synchronizer/Synchronizer.resources.test.ts b/packages/lib/services/synchronizer/Synchronizer.resources.test.ts index 5fece25467..33272a0866 100644 --- a/packages/lib/services/synchronizer/Synchronizer.resources.test.ts +++ b/packages/lib/services/synchronizer/Synchronizer.resources.test.ts @@ -3,14 +3,15 @@ import shim from '../../shim'; import Setting from '../../models/Setting'; import { NoteEntity } from '../../services/database/types'; import { remoteNotesFoldersResources, remoteResources } from '../../testing/test-utils-synchronizer'; - -const { synchronizerStart, tempFilePath, resourceFetcher, supportDir, setupDatabaseAndSynchronizer, synchronizer, fileApi, switchClient, syncTargetId, encryptionService, loadEncryptionMasterKey, fileContentEqual, checkThrowAsync } = require('../../testing/test-utils.js'); +import { synchronizerStart, tempFilePath, resourceFetcher, supportDir, setupDatabaseAndSynchronizer, synchronizer, fileApi, switchClient, syncTargetId, encryptionService, loadEncryptionMasterKey, fileContentEqual, checkThrowAsync } from '../../testing/test-utils'; import Folder from '../../models/Folder'; import Note from '../../models/Note'; import Resource from '../../models/Resource'; import ResourceFetcher from '../../services/ResourceFetcher'; import BaseItem from '../../models/BaseItem'; import { ModelType } from '../../BaseModel'; +import { setEncryptionEnabled } from '../synchronizer/syncInfoUtils'; +import { loadMasterKeysFromSettings } from '../e2ee/utils'; let insideBeforeEach = false; @@ -144,7 +145,7 @@ describe('Synchronizer.resources', function() { })); it('should encrypt resources', (async () => { - Setting.setValue('encryption.enabled', true); + setEncryptionEnabled(true); const masterKey = await loadEncryptionMasterKey(); const folder1 = await Folder.save({ title: 'folder1' }); @@ -158,7 +159,7 @@ describe('Synchronizer.resources', function() { await synchronizerStart(); Setting.setObjectValue('encryption.passwordCache', masterKey.id, '123456'); - await encryptionService().loadMasterKeysFromSettings(); + await loadMasterKeysFromSettings(encryptionService()); const fetcher = new ResourceFetcher(() => { return synchronizer().api(); }); fetcher.queueDownload_(resource1.id); diff --git a/packages/lib/services/synchronizer/Synchronizer.revisions.test.ts b/packages/lib/services/synchronizer/Synchronizer.revisions.test.ts index 31cd1a92fa..d4c3c8b135 100644 --- a/packages/lib/services/synchronizer/Synchronizer.revisions.test.ts +++ b/packages/lib/services/synchronizer/Synchronizer.revisions.test.ts @@ -1,9 +1,9 @@ import Setting from '../../models/Setting'; import BaseModel from '../../BaseModel'; - -const { synchronizerStart, revisionService, setupDatabaseAndSynchronizer, synchronizer, switchClient, encryptionService, loadEncryptionMasterKey, decryptionWorker } = require('../../testing/test-utils.js'); +import { synchronizerStart, revisionService, setupDatabaseAndSynchronizer, synchronizer, switchClient, encryptionService, loadEncryptionMasterKey, decryptionWorker } from '../../testing/test-utils'; import Note from '../../models/Note'; import Revision from '../../models/Revision'; +import { loadMasterKeysFromSettings, setupAndEnableEncryption } from '../e2ee/utils'; describe('Synchronizer.revisions', function() { @@ -165,8 +165,8 @@ describe('Synchronizer.revisions', function() { await Note.save({ title: 'ma note', updated_time: dateInPast, created_time: dateInPast }, { autoTimestamp: false }); const masterKey = await loadEncryptionMasterKey(); - await encryptionService().enableEncryption(masterKey, '123456'); - await encryptionService().loadMasterKeysFromSettings(); + await setupAndEnableEncryption(encryptionService(), masterKey, '123456'); + await loadMasterKeysFromSettings(encryptionService()); await synchronizerStart(); await switchClient(2); @@ -174,7 +174,7 @@ describe('Synchronizer.revisions', function() { await synchronizerStart(); Setting.setObjectValue('encryption.passwordCache', masterKey.id, '123456'); - await encryptionService().loadMasterKeysFromSettings(); + await loadMasterKeysFromSettings(encryptionService()); await decryptionWorker().start(); await revisionService().collectRevisions(); diff --git a/packages/lib/services/synchronizer/Synchronizer.tags.test.ts b/packages/lib/services/synchronizer/Synchronizer.tags.test.ts index 151c299f5d..238b1385f3 100644 --- a/packages/lib/services/synchronizer/Synchronizer.tags.test.ts +++ b/packages/lib/services/synchronizer/Synchronizer.tags.test.ts @@ -1,10 +1,9 @@ -import Setting from '../../models/Setting'; - -const { synchronizerStart, setupDatabaseAndSynchronizer, switchClient, encryptionService, loadEncryptionMasterKey } = require('../../testing/test-utils.js'); +import { synchronizerStart, setupDatabaseAndSynchronizer, switchClient, encryptionService, loadEncryptionMasterKey } from '../../testing/test-utils'; import Folder from '../../models/Folder'; import Note from '../../models/Note'; import Tag from '../../models/Tag'; import MasterKey from '../../models/MasterKey'; +import { setEncryptionEnabled } from '../synchronizer/syncInfoUtils'; describe('Synchronizer.tags', function() { @@ -18,7 +17,7 @@ describe('Synchronizer.tags', function() { async function shoudSyncTagTest(withEncryption: boolean) { let masterKey = null; if (withEncryption) { - Setting.setValue('encryption.enabled', true); + setEncryptionEnabled(true); masterKey = await loadEncryptionMasterKey(); } @@ -33,7 +32,7 @@ describe('Synchronizer.tags', function() { await synchronizerStart(); if (withEncryption) { const masterKey_2 = await MasterKey.load(masterKey.id); - await encryptionService().loadMasterKey_(masterKey_2, '123456', true); + await encryptionService().loadMasterKey(masterKey_2, '123456', true); const t = await Tag.load(tag.id); await Tag.decrypt(t); } diff --git a/packages/lib/services/synchronizer/gui/useSyncTargetUpgrade.ts b/packages/lib/services/synchronizer/gui/useSyncTargetUpgrade.ts index 0e3fe9c509..fad9436308 100644 --- a/packages/lib/services/synchronizer/gui/useSyncTargetUpgrade.ts +++ b/packages/lib/services/synchronizer/gui/useSyncTargetUpgrade.ts @@ -1,8 +1,8 @@ import shim from '../../../shim'; import MigrationHandler from '../MigrationHandler'; -const { useEffect, useState } = shim.react(); import Setting from '../../../models/Setting'; import { reg } from '../../../registry'; +const { useEffect, useState } = shim.react(); export interface SyncTargetUpgradeResult { done: boolean; @@ -26,6 +26,7 @@ export default function useSyncTargetUpgrade(): SyncTargetUpgradeResult { reg.logger().info('useSyncTargetUpgrade: Create migration handler...'); const migrationHandler = new MigrationHandler( synchronizer.api(), + reg.db(), synchronizer.lockHandler(), Setting.value('appType'), Setting.value('clientId') diff --git a/packages/lib/services/synchronizer/migrations/3.ts b/packages/lib/services/synchronizer/migrations/3.ts new file mode 100644 index 0000000000..74c4965cca --- /dev/null +++ b/packages/lib/services/synchronizer/migrations/3.ts @@ -0,0 +1,13 @@ +import { FileApi } from '../../../file-api'; +import JoplinDatabase from '../../../JoplinDatabase'; +import { localSyncInfo, saveLocalSyncInfo, uploadSyncInfo } from '../syncInfoUtils'; + +export default async function(api: FileApi, _db: JoplinDatabase): Promise { + // The local sync info cache is populated on application startup so for the + // migration we only need to upload that local cache. + + const syncInfo = localSyncInfo(); + syncInfo.version = 3; + await uploadSyncInfo(api, syncInfo); + saveLocalSyncInfo(syncInfo); +} diff --git a/packages/lib/services/synchronizer/syncInfoUtils.ts b/packages/lib/services/synchronizer/syncInfoUtils.ts new file mode 100644 index 0000000000..966bf3bd1e --- /dev/null +++ b/packages/lib/services/synchronizer/syncInfoUtils.ts @@ -0,0 +1,235 @@ +import { FileApi } from '../../file-api'; +import JoplinDatabase from '../../JoplinDatabase'; +import Setting from '../../models/Setting'; +import { State } from '../../reducer'; +import { MasterKeyEntity } from '../database/types'; + +export interface SyncInfoValueBoolean { + value: boolean; + updatedTime: number; +} + +export interface SyncInfoValueString { + value: string; + updatedTime: number; +} + +export async function migrateLocalSyncInfo(db: JoplinDatabase) { + if (Setting.value('syncInfoCache')) return; // Already initialized + + // TODO: if the sync info is changed, there should be steps to migrate from + // v3 to v4, v4 to v5, etc. + + const masterKeys = await db.selectAll('SELECT * FROM master_keys'); + + const masterKeyMap: Record = {}; + for (const mk of masterKeys) masterKeyMap[mk.id] = mk; + + const syncInfo = new SyncInfo(); + syncInfo.version = Setting.value('syncVersion'); + syncInfo.e2ee = Setting.valueNoThrow('encryption.enabled', false); + syncInfo.activeMasterKeyId = Setting.valueNoThrow('encryption.activeMasterKeyId', ''); + syncInfo.masterKeys = masterKeys; + + // We set the timestamp to 0 because we don't know when the source setting + // has been set. That way, if the parameter is changed later on in any + // client, the new value will have higher priority. This is to handle this + // case: + // + // - Client 1 upgrade local sync target info (with E2EE = false) + // - Client 1 set E2EE to true + // - Client 2 upgrade local sync target info (with E2EE = false) + // - => If we don't set the timestamp to 0, the local value of client 2 will + // have a higher timestamp and E2EE will get disabled, even though this is + // most likely not what the user wants. + syncInfo.setKeyTimestamp('e2ee', 0); + syncInfo.setKeyTimestamp('activeMasterKeyId', 0); + + await saveLocalSyncInfo(syncInfo); +} + +export async function uploadSyncInfo(api: FileApi, syncInfo: SyncInfo) { + await api.put('info.json', syncInfo.serialize()); +} + +export async function fetchSyncInfo(api: FileApi): Promise { + const syncTargetInfoText = await api.get('info.json'); + + // Returns version 0 if the sync target is empty + let output: any = { version: 0 }; + + if (syncTargetInfoText) { + output = JSON.parse(syncTargetInfoText); + if (!output.version) throw new Error('Missing "version" field in info.json'); + } else { + // If info.json is not present, this might be an old sync target, in + // which case we can at least get the version number from version.txt + const oldVersion = await api.get('.sync/version.txt'); + if (oldVersion) output = { version: 1 }; + } + + return new SyncInfo(JSON.stringify(output)); +} + +export function saveLocalSyncInfo(syncInfo: SyncInfo) { + Setting.setValue('syncInfoCache', syncInfo.serialize()); +} + +export function localSyncInfo(): SyncInfo { + return new SyncInfo(Setting.value('syncInfoCache')); +} + +export function localSyncInfoFromState(state: State): SyncInfo { + return new SyncInfo(state.settings['syncInfoCache']); +} + +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.version = s1.version > s2.version ? s1.version : s2.version; + + output.masterKeys = s1.masterKeys.slice(); + + for (const mk of s2.masterKeys) { + const idx = output.masterKeys.findIndex(m => m.id === mk.id); + if (idx < 0) { + output.masterKeys.push(mk); + } else { + const mk2 = output.masterKeys[idx]; + output.masterKeys[idx] = mk.updated_time > mk2.updated_time ? mk : mk2; + } + } + + return output; +} + +export function syncInfoEquals(s1: SyncInfo, s2: SyncInfo): boolean { + return s1.serialize() === s2.serialize(); +} + +export class SyncInfo { + + private version_: number = 0; + private e2ee_: SyncInfoValueBoolean; + private activeMasterKeyId_: SyncInfoValueString; + private masterKeys_: MasterKeyEntity[] = []; + + public constructor(serialized: string = null) { + this.e2ee_ = { value: false, updatedTime: 0 }; + this.activeMasterKeyId_ = { value: '', updatedTime: 0 }; + + if (serialized) this.load(serialized); + } + + public toObject(): any { + return { + version: this.version, + e2ee: this.e2ee_, + activeMasterKeyId: this.activeMasterKeyId_, + masterKeys: this.masterKeys, + }; + } + + public serialize(): string { + return JSON.stringify(this.toObject(), null, '\t'); + } + + public load(serialized: string) { + const s: any = JSON.parse(serialized); + this.version = 'version' in s ? s.version : 0; + this.e2ee_ = 'e2ee' in s ? s.e2ee : { value: false, updatedTime: 0 }; + this.activeMasterKeyId_ = 'activeMasterKeyId' in s ? s.activeMasterKeyId : { value: '', updatedTime: 0 }; + this.masterKeys_ = 'masterKeys' in s ? s.masterKeys : []; + } + + public setWithTimestamp(fromSyncInfo: SyncInfo, propName: string) { + if (!(propName in (this as any))) throw new Error(`Invalid prop name: ${propName}`); + + (this as any)[propName] = (fromSyncInfo as any)[propName]; + this.setKeyTimestamp(propName, fromSyncInfo.keyTimestamp(propName)); + } + + public get version(): number { + return this.version_; + } + + public set version(v: number) { + if (v === this.version_) return; + + this.version_ = v; + } + + public get e2ee(): boolean { + return this.e2ee_.value; + } + + public set e2ee(v: boolean) { + if (v === this.e2ee) return; + + this.e2ee_ = { value: v, updatedTime: Date.now() }; + } + + public get activeMasterKeyId(): string { + return this.activeMasterKeyId_.value; + } + + public set activeMasterKeyId(v: string) { + if (v === this.activeMasterKeyId) return; + + this.activeMasterKeyId_ = { value: v, updatedTime: Date.now() }; + } + + public get masterKeys(): MasterKeyEntity[] { + return this.masterKeys_; + } + + public set masterKeys(v: MasterKeyEntity[]) { + if (JSON.stringify(v) === JSON.stringify(this.masterKeys_)) return; + + this.masterKeys_ = v; + } + + public keyTimestamp(name: string): number { + if (!(`${name}_` in (this as any))) throw new Error(`Invalid name: ${name}`); + return (this as any)[`${name}_`].updatedTime; + } + + public setKeyTimestamp(name: string, timestamp: number) { + if (!(`${name}_` in (this as any))) throw new Error(`Invalid name: ${name}`); + (this as any)[`${name}_`].updatedTime = timestamp; + } + +} + +// --------------------------------------------------------- +// Shortcuts to simplify the refactoring +// --------------------------------------------------------- + +export function getEncryptionEnabled() { + return localSyncInfo().e2ee; +} + +export function setEncryptionEnabled(v: boolean, activeMasterKeyId: string = '') { + const s = localSyncInfo(); + s.e2ee = v; + if (activeMasterKeyId) s.activeMasterKeyId = activeMasterKeyId; + saveLocalSyncInfo(s); +} + +export function getActiveMasterKeyId() { + return localSyncInfo().activeMasterKeyId; +} + +export function setActiveMasterKeyId(id: string) { + const s = localSyncInfo(); + s.activeMasterKeyId = id; + saveLocalSyncInfo(s); +} + +export function getActiveMasterKey(s: SyncInfo = null): MasterKeyEntity | null { + s = s || localSyncInfo(); + if (!s.activeMasterKeyId) return null; + return s.masterKeys.find(mk => mk.id === s.activeMasterKeyId); +} diff --git a/packages/lib/services/synchronizer/synchronizer_LockHandler.test.ts b/packages/lib/services/synchronizer/synchronizer_LockHandler.test.ts index 57a1d55b57..63a24c6407 100644 --- a/packages/lib/services/synchronizer/synchronizer_LockHandler.test.ts +++ b/packages/lib/services/synchronizer/synchronizer_LockHandler.test.ts @@ -1,7 +1,5 @@ import LockHandler, { LockType, LockHandlerOptions, Lock } from '../../services/synchronizer/LockHandler'; - - -const { isNetworkSyncTarget, fileApi, setupDatabaseAndSynchronizer, synchronizer, switchClient, msleep, expectThrow, expectNotThrow } = require('../../testing/test-utils.js'); +import { isNetworkSyncTarget, fileApi, setupDatabaseAndSynchronizer, synchronizer, switchClient, msleep, expectThrow, expectNotThrow } from '../../testing/test-utils'; // For tests with memory of file system we can use low intervals to make the tests faster. // However if we use such low values with network sync targets, some calls might randomly fail with diff --git a/packages/lib/services/synchronizer/synchronizer_MigrationHandler.test.ts b/packages/lib/services/synchronizer/synchronizer_MigrationHandler.test.ts index 1940711a44..c729c42950 100644 --- a/packages/lib/services/synchronizer/synchronizer_MigrationHandler.test.ts +++ b/packages/lib/services/synchronizer/synchronizer_MigrationHandler.test.ts @@ -1,15 +1,19 @@ -import LockHandler from '../../services/synchronizer/LockHandler'; -import MigrationHandler from '../../services/synchronizer/MigrationHandler'; -import { Dirnames } from '../../services/synchronizer/utils/types'; -import { setSyncTargetName, fileApi, synchronizer, decryptionWorker, encryptionService, setupDatabaseAndSynchronizer, switchClient, expectThrow, expectNotThrow } from '../../testing/test-utils'; -import { deploySyncTargetSnapshot, testData, checkTestData } from '../../testing/syncTargetUtils'; -import Setting from '../../models/Setting'; -import MasterKey from '../../models/MasterKey'; - // To create a sync target snapshot for the current syncVersion: // - In test-utils, set syncTargetName_ to "filesystem" // - Then run: -// gulp buildTests -L && node tests-build/support/createSyncTargetSnapshot.js normal && node tests-build/support/createSyncTargetSnapshot.js e2ee +// node tests/support/createSyncTargetSnapshot.js normal && node tests/support/createSyncTargetSnapshot.js e2ee +// +// These tests work by a taking a sync target snapshot at a version n and upgrading it to n+1. + +import LockHandler from './LockHandler'; +import MigrationHandler from './MigrationHandler'; +import { Dirnames } from './utils/types'; +import { setSyncTargetName, fileApi, synchronizer, decryptionWorker, encryptionService, setupDatabaseAndSynchronizer, switchClient, expectThrow, expectNotThrow, db } from '../../testing/test-utils'; +import { deploySyncTargetSnapshot, testData, checkTestData } from '../../testing/syncTargetUtils'; +import Setting from '../../models/Setting'; +import MasterKey from '../../models/MasterKey'; +import { loadMasterKeysFromSettings } from '../e2ee/utils'; +import { fetchSyncInfo } from './syncInfoUtils'; const specTimeout = 60000 * 10; // Nextcloud tests can be slow @@ -24,7 +28,7 @@ function lockHandler(): LockHandler { function migrationHandler(clientId: string = 'abcd'): MigrationHandler { if (migrationHandler_) return migrationHandler_; - migrationHandler_ = new MigrationHandler(fileApi(), lockHandler(), 'desktop', clientId); + migrationHandler_ = new MigrationHandler(fileApi(), db(), lockHandler(), 'desktop', clientId); return migrationHandler_; } @@ -43,11 +47,103 @@ const migrationTests: MigrationTests = { const versionForOldClients = await fileApi().get('.sync/version.txt'); expect(versionForOldClients).toBe('2'); }, + + 3: async function() { + const items = (await fileApi().list('', { includeHidden: true })).items; + expect(items.filter((i: any) => i.path === '.resource' && i.isDir).length).toBe(1); + expect(items.filter((i: any) => i.path === 'locks' && i.isDir).length).toBe(1); + expect(items.filter((i: any) => i.path === 'temp' && i.isDir).length).toBe(1); + expect(items.filter((i: any) => i.path === 'info.json' && !i.isDir).length).toBe(1); + + const versionForOldClients = await fileApi().get('.sync/version.txt'); + expect(versionForOldClients).toBe('2'); + }, }; +const maxSyncVersion = Number(Object.keys(migrationTests).sort().pop()); + +async function testMigration(migrationVersion: number, maxSyncVersion: number) { + await deploySyncTargetSnapshot('normal', migrationVersion - 1); + + const info = await fetchSyncInfo(fileApi()); + expect(info.version).toBe(migrationVersion - 1); + + // Now, migrate to the new version + Setting.setConstant('syncVersion', migrationVersion); + await migrationHandler().upgrade(migrationVersion); + + // Verify that it has been upgraded + const newInfo = await fetchSyncInfo(fileApi()); + expect(newInfo.version).toBe(migrationVersion); + await migrationTests[migrationVersion](); + + // If we're not on the latest version, we exit here, because although the + // synchronizer can run the migration from one version to another, it cannot + // sync the data on an older version (since the code has been changed to + // work with the latest version). + if (migrationVersion !== maxSyncVersion) return; + + // Now sync with that upgraded target + await synchronizer().start(); + + // Check that the data has not been altered + await expectNotThrow(async () => await checkTestData(testData)); + + // Check what happens if we switch to a different client and sync + await switchClient(2); + Setting.setConstant('syncVersion', migrationVersion); + await synchronizer().start(); + await expectNotThrow(async () => await checkTestData(testData)); +} + +async function testMigrationE2EE(migrationVersion: number, maxSyncVersion: number) { + // First create some test data that will be used to validate + // that the migration didn't alter any data. + await deploySyncTargetSnapshot('e2ee', migrationVersion - 1); + + // Now, migrate to the new version + Setting.setConstant('syncVersion', migrationVersion); + await migrationHandler().upgrade(migrationVersion); + + // Verify that it has been upgraded + const newInfo = await fetchSyncInfo(fileApi()); + expect(newInfo.version).toBe(migrationVersion); + await migrationTests[migrationVersion](); + + if (migrationVersion !== maxSyncVersion) return; + + // Now sync with that upgraded target + await synchronizer().start(); + + // Decrypt the data + const masterKey = (await MasterKey.all())[0]; + Setting.setObjectValue('encryption.passwordCache', masterKey.id, '123456'); + await loadMasterKeysFromSettings(encryptionService()); + await decryptionWorker().start(); + + // Check that the data has not been altered + await expectNotThrow(async () => await checkTestData(testData)); + + // Check what happens if we switch to a different client and sync + await switchClient(2); + Setting.setConstant('syncVersion', migrationVersion); + await synchronizer().start(); + + // Should throw because data hasn't been decrypted yet + await expectThrow(async () => await checkTestData(testData)); + + // Enable E2EE and decrypt + Setting.setObjectValue('encryption.passwordCache', masterKey.id, '123456'); + await loadMasterKeysFromSettings(encryptionService()); + await decryptionWorker().start(); + + // Should not throw because data is decrypted + await expectNotThrow(async () => await checkTestData(testData)); +} + let previousSyncTargetName: string = ''; -describe('synchronizer_MigrationHandler', function() { +describe('MigrationHandler', function() { beforeEach(async (done: Function) => { // Note that, for undocumented reasons, the timeout argument passed @@ -95,78 +191,20 @@ describe('synchronizer_MigrationHandler', function() { await expectThrow(async () => await migrationHandler().checkCanSync(), 'outdatedClient'); }), specTimeout); - for (const migrationVersionString in migrationTests) { - const migrationVersion = Number(migrationVersionString); + it('should apply migration 2 normal', async () => { + await testMigration(2, maxSyncVersion); + }, specTimeout); - it(`should migrate (${migrationVersion})`, (async () => { - await deploySyncTargetSnapshot('normal', migrationVersion - 1); + it('should apply migration 2 E2EE', async () => { + await testMigrationE2EE(2, maxSyncVersion); + }, specTimeout); - const info = await migrationHandler().fetchSyncTargetInfo(); - expect(info.version).toBe(migrationVersion - 1); + it('should apply migration 3 normal', async () => { + await testMigration(3, maxSyncVersion); + }, specTimeout); - // Now, migrate to the new version - await migrationHandler().upgrade(migrationVersion); - - // Verify that it has been upgraded - const newInfo = await migrationHandler().fetchSyncTargetInfo(); - expect(newInfo.version).toBe(migrationVersion); - await migrationTests[migrationVersion](); - - // Now sync with that upgraded target - await synchronizer().start(); - - // Check that the data has not been altered - await expectNotThrow(async () => await checkTestData(testData)); - - // Check what happens if we switch to a different client and sync - await switchClient(2); - Setting.setConstant('syncVersion', migrationVersion); - await synchronizer().start(); - await expectNotThrow(async () => await checkTestData(testData)); - }), specTimeout); - - it(`should migrate (E2EE) (${migrationVersion})`, (async () => { - // First create some test data that will be used to validate - // that the migration didn't alter any data. - await deploySyncTargetSnapshot('e2ee', migrationVersion - 1); - - // Now, migrate to the new version - Setting.setConstant('syncVersion', migrationVersion); - await migrationHandler().upgrade(migrationVersion); - - // Verify that it has been upgraded - const newInfo = await migrationHandler().fetchSyncTargetInfo(); - expect(newInfo.version).toBe(migrationVersion); - await migrationTests[migrationVersion](); - - // Now sync with that upgraded target - await synchronizer().start(); - - // Decrypt the data - const masterKey = (await MasterKey.all())[0]; - Setting.setObjectValue('encryption.passwordCache', masterKey.id, '123456'); - await encryptionService().loadMasterKeysFromSettings(); - await decryptionWorker().start(); - - // Check that the data has not been altered - await expectNotThrow(async () => await checkTestData(testData)); - - // Check what happens if we switch to a different client and sync - await switchClient(2); - Setting.setConstant('syncVersion', migrationVersion); - await synchronizer().start(); - - // Should throw because data hasn't been decrypted yet - await expectThrow(async () => await checkTestData(testData)); - - // Enable E2EE and decrypt - Setting.setObjectValue('encryption.passwordCache', masterKey.id, '123456'); - await encryptionService().loadMasterKeysFromSettings(); - await decryptionWorker().start(); - - // Should not throw because data is decrypted - await expectNotThrow(async () => await checkTestData(testData)); - }), specTimeout); - } + it('should apply migration 3 E2EE', async () => { + await testMigrationE2EE(3, maxSyncVersion); + }, specTimeout); }); diff --git a/packages/lib/testing/syncTargetUtils.ts b/packages/lib/testing/syncTargetUtils.ts index abe6adabfb..88a4e4295f 100644 --- a/packages/lib/testing/syncTargetUtils.ts +++ b/packages/lib/testing/syncTargetUtils.ts @@ -1,4 +1,4 @@ -import { syncDir, synchronizer, supportDir, loadEncryptionMasterKey, setupDatabaseAndSynchronizer, switchClient } from '../testing/test-utils'; +import { syncDir, synchronizer, supportDir, loadEncryptionMasterKey, setupDatabaseAndSynchronizer, switchClient, synchronizerStart } from '../testing/test-utils'; import Setting from '../models/Setting'; import Folder from '../models/Folder'; import Note from '../models/Note'; @@ -7,10 +7,13 @@ import Resource from '../models/Resource'; import markdownUtils from '../markdownUtils'; import shim from '../shim'; import * as fs from 'fs-extra'; +import { setEncryptionEnabled } from '../services/synchronizer/syncInfoUtils'; +const { shimInit } = require('../shim-init-node'); +const sharp = require('sharp'); const snapshotBaseDir = `${supportDir}/syncTargetSnapshots`; -const testData = { +export const testData = { folder1: { subFolder1: {}, subFolder2: { @@ -36,8 +39,8 @@ const testData = { }, }; -async function createTestData(data: any) { - async function recurseStruct(s: any, parentId: string = '') { +export async function createTestData(data: any) { + async function recurseStruct(s: any, parentId = '') { for (const n in s) { if (n.toLowerCase().includes('folder')) { const folder = await Folder.save({ title: n, parent_id: parentId }); @@ -60,7 +63,7 @@ async function createTestData(data: any) { await recurseStruct(data); } -async function checkTestData(data: any) { +export async function checkTestData(data: any) { async function recurseCheck(s: any) { for (const n in s) { const obj = s[n]; @@ -98,13 +101,15 @@ async function checkTestData(data: any) { await recurseCheck(data); } -async function deploySyncTargetSnapshot(syncTargetType: string, syncVersion: number) { +export async function deploySyncTargetSnapshot(syncTargetType: string, syncVersion: number) { const sourceDir = `${snapshotBaseDir}/${syncVersion}/${syncTargetType}`; await fs.remove(syncDir); await fs.copy(sourceDir, syncDir); } -async function main(syncTargetType: string) { +export async function main(syncTargetType: string) { + shimInit(sharp); + const validSyncTargetTypes = ['normal', 'e2ee']; if (!validSyncTargetTypes.includes(syncTargetType)) throw new Error(`Sync target type must be: ${validSyncTargetTypes.join(', ')}`); @@ -113,10 +118,12 @@ async function main(syncTargetType: string) { await createTestData(testData); if (syncTargetType === 'e2ee') { - Setting.setValue('encryption.enabled', true); + setEncryptionEnabled(true); await loadEncryptionMasterKey(); } + await synchronizerStart(); + await synchronizer().start(); if (!Setting.value('syncVersion')) throw new Error('syncVersion is not set'); @@ -128,10 +135,3 @@ async function main(syncTargetType: string) { console.info(`Sync target snapshot created in: ${destDir}`); } - -export { - checkTestData, - main, - testData, - deploySyncTargetSnapshot, -}; diff --git a/packages/lib/testing/test-utils-synchronizer.ts b/packages/lib/testing/test-utils-synchronizer.ts index aa6913d347..2f58741944 100644 --- a/packages/lib/testing/test-utils-synchronizer.ts +++ b/packages/lib/testing/test-utils-synchronizer.ts @@ -1,5 +1,5 @@ import BaseModel from '../BaseModel'; -const { fileApi } = require('../testing/../testing/test-utils.js'); +import { fileApi } from '../testing/../testing/test-utils'; import Folder from '../models/Folder'; import Note from '../models/Note'; import BaseItem from '../models/BaseItem'; diff --git a/packages/lib/testing/test-utils.ts b/packages/lib/testing/test-utils.ts index d4f1bee1ed..b8d165c447 100644 --- a/packages/lib/testing/test-utils.ts +++ b/packages/lib/testing/test-utils.ts @@ -28,7 +28,7 @@ import NoteTag from '../models/NoteTag'; import Revision from '../models/Revision'; import MasterKey from '../models/MasterKey'; import BaseItem from '../models/BaseItem'; -const { FileApi } = require('../file-api.js'); +import { FileApi } from '../file-api'; const FileApiDriverMemory = require('../file-api-driver-memory').default; const { FileApiDriverLocal } = require('../file-api-driver-local.js'); const { FileApiDriverWebDav } = require('../file-api-driver-webdav.js'); @@ -54,6 +54,8 @@ import { credentialFile, readCredentialFile } from '../utils/credentialFiles'; import SyncTargetJoplinCloud from '../SyncTargetJoplinCloud'; import KeychainService from '../services/keychain/KeychainService'; import { loadKeychainServiceAndSettings } from '../services/SettingUtils'; +import { setActiveMasterKeyId, setEncryptionEnabled } from '../services/synchronizer/syncInfoUtils'; +import Synchronizer from '../Synchronizer'; const md5 = require('md5'); const S3 = require('aws-sdk/clients/s3'); const { Dirnames } = require('../services/synchronizer/utils/types'); @@ -65,14 +67,14 @@ const { Dirnames } = require('../services/synchronizer/utils/types'); // Jest, to make debugging easier, but it's not clear how to get this info). const suiteName_ = uuid.createNano(); -const databases_: any[] = []; -let synchronizers_: any[] = []; -const fileApis_: any = {}; -const encryptionServices_: any[] = []; -const revisionServices_: any[] = []; -const decryptionWorkers_: any[] = []; -const resourceServices_: any[] = []; -const resourceFetchers_: any[] = []; +const databases_: JoplinDatabase[] = []; +let synchronizers_: Synchronizer[] = []; +const fileApis_: Record = {}; +const encryptionServices_: EncryptionService[] = []; +const revisionServices_: RevisionService[] = []; +const decryptionWorkers_: DecryptionWorker[] = []; +const resourceServices_: ResourceService[] = []; +const resourceFetchers_: ResourceFetcher[] = []; const kvStores_: KvStore[] = []; let currentClient_ = 1; @@ -137,6 +139,7 @@ function setSyncTargetName(name: string) { } setSyncTargetName('memory'); +// setSyncTargetName('filesystem'); // setSyncTargetName('nextcloud'); // setSyncTargetName('dropbox'); // setSyncTargetName('onedrive'); @@ -152,7 +155,7 @@ const syncDir = `${oldTestDir}/sync/${suiteName_}`; // anyway. let defaultJestTimeout = 90 * 1000; if (isNetworkSyncTarget_) defaultJestTimeout = 60 * 1000 * 10; -jest.setTimeout(defaultJestTimeout); +if (typeof jest !== 'undefined') jest.setTimeout(defaultJestTimeout); const dbLogger = new Logger(); dbLogger.addTarget(TargetType.Console); @@ -262,6 +265,8 @@ async function switchClient(id: number, options: any = null) { BaseItem.revisionService_ = revisionServices_[id]; await Setting.reset(); + Setting.settingFilename = `settings-${id}.json`; + Setting.setConstant('resourceDirName', resourceDirName(id)); Setting.setConstant('resourceDir', resourceDir(id)); Setting.setConstant('pluginDir', pluginDir(id)); @@ -269,6 +274,10 @@ async function switchClient(id: number, options: any = null) { await loadKeychainServiceAndSettings(options.keychainEnabled ? KeychainServiceDriver : KeychainServiceDriverDummy); Setting.setValue('sync.wipeOutFailSafe', false); // To keep things simple, always disable fail-safe unless explicitely set in the test itself + + // More generally, this function should clear all data, and so that should + // include settings.json + await clearSettingFile(id); } async function clearDatabase(id: number = null) { @@ -336,9 +345,15 @@ async function setupDatabase(id: number = null, options: any = null) { await databases_[id].open({ name: filePath }); BaseModel.setDb(databases_[id]); + await clearSettingFile(id); await loadKeychainServiceAndSettings(options.keychainEnabled ? KeychainServiceDriver : KeychainServiceDriverDummy); } +async function clearSettingFile(id: number) { + Setting.settingFilename = `settings-${id}.json`; + await fs.remove(Setting.settingFilePath); +} + export async function createFolderTree(parentId: string, tree: any[], num: number = 0): Promise { let rootFolder: FolderEntity = null; @@ -496,7 +511,9 @@ async function loadEncryptionMasterKey(id: number = null, useExisting = false) { masterKey = masterKeys[0]; } - await service.loadMasterKey_(masterKey, '123456', true); + await service.loadMasterKey(masterKey, '123456', true); + + setActiveMasterKeyId(masterKey.id); return masterKey; } @@ -851,7 +868,7 @@ class TestApp extends BaseApplication { // For now, disable sync and encryption to avoid spurious intermittent failures // caused by them interupting processing and causing delays. Setting.setValue('sync.interval', 0); - Setting.setValue('encryption.enabled', false); + setEncryptionEnabled(true); this.initRedux(); Setting.dispatchUpdateAll();