diff --git a/.eslintignore b/.eslintignore index af0a6c8bc..60d282904 100644 --- a/.eslintignore +++ b/.eslintignore @@ -78,6 +78,9 @@ packages/app-cli/app/command-e2ee.js.map packages/app-cli/app/command-settingschema.d.ts packages/app-cli/app/command-settingschema.js packages/app-cli/app/command-settingschema.js.map +packages/app-cli/app/command-sync.d.ts +packages/app-cli/app/command-sync.js +packages/app-cli/app/command-sync.js.map packages/app-cli/app/command-testing.d.ts packages/app-cli/app/command-testing.js packages/app-cli/app/command-testing.js.map diff --git a/.gitignore b/.gitignore index 5e4f054e9..388107a1d 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,9 @@ packages/app-cli/app/command-e2ee.js.map packages/app-cli/app/command-settingschema.d.ts packages/app-cli/app/command-settingschema.js packages/app-cli/app/command-settingschema.js.map +packages/app-cli/app/command-sync.d.ts +packages/app-cli/app/command-sync.js +packages/app-cli/app/command-sync.js.map packages/app-cli/app/command-testing.d.ts packages/app-cli/app/command-testing.js packages/app-cli/app/command-testing.js.map diff --git a/packages/app-cli/app/command-e2ee.ts b/packages/app-cli/app/command-e2ee.ts index 284cb6cba..9bbf4669b 100644 --- a/packages/app-cli/app/command-e2ee.ts +++ b/packages/app-cli/app/command-e2ee.ts @@ -6,8 +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'; +import { getEncryptionEnabled, localSyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils'; +import { generateMasterKeyAndEnableEncryption, loadMasterKeysFromSettings, masterPasswordIsValid, setupAndDisableEncryption } from '@joplin/lib/services/e2ee/utils'; const imageType = require('image-type'); const readChunk = require('read-chunk'); @@ -40,6 +40,13 @@ class Command extends BaseCommand { this.stdout(_('Operation cancelled')); return false; } + + const masterKey = localSyncInfo().masterKeys.find(mk => mk.id === masterKeyId); + if (!(await masterPasswordIsValid(password, masterKey))) { + this.stdout(_('Invalid password')); + return false; + } + Setting.setObjectValue('encryption.passwordCache', masterKeyId, password); await loadMasterKeysFromSettings(EncryptionService.instance()); return true; diff --git a/packages/app-cli/app/command-sync.js b/packages/app-cli/app/command-sync.ts similarity index 86% rename from packages/app-cli/app/command-sync.js rename to packages/app-cli/app/command-sync.ts index 88ef19ab9..b146f9e8e 100644 --- a/packages/app-cli/app/command-sync.js +++ b/packages/app-cli/app/command-sync.ts @@ -1,25 +1,24 @@ +import { _ } from '@joplin/lib/locale'; +import Setting from '@joplin/lib/models/Setting'; +import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry'; +import MigrationHandler from '@joplin/lib/services/synchronizer/MigrationHandler'; +import ResourceFetcher from '@joplin/lib/services/ResourceFetcher'; +import Synchronizer from '@joplin/lib/Synchronizer'; +import { masterKeysWithoutPassword } from '@joplin/lib/services/e2ee/utils'; const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); -const { _ } = require('@joplin/lib/locale'); const { OneDriveApiNodeUtils } = require('@joplin/lib/onedrive-api-node-utils.js'); -const Setting = require('@joplin/lib/models/Setting').default; -const ResourceFetcher = require('@joplin/lib/services/ResourceFetcher').default; -const Synchronizer = require('@joplin/lib/Synchronizer').default; const { reg } = require('@joplin/lib/registry.js'); const { cliUtils } = require('./cli-utils.js'); const md5 = require('md5'); const locker = require('proper-lockfile'); const fs = require('fs-extra'); -const SyncTargetRegistry = require('@joplin/lib/SyncTargetRegistry').default; -const MigrationHandler = require('@joplin/lib/services/synchronizer/MigrationHandler').default; class Command extends BaseCommand { - constructor() { - super(); - this.syncTargetId_ = null; - this.releaseLockFn_ = null; - this.oneDriveApiUtils_ = null; - } + + private syncTargetId_: number = null; + private releaseLockFn_: Function = null; + private oneDriveApiUtils_: any = null; usage() { return 'sync'; @@ -37,9 +36,9 @@ class Command extends BaseCommand { ]; } - static lockFile(filePath) { + static lockFile(filePath: string): Promise { return new Promise((resolve, reject) => { - locker.lock(filePath, { stale: 1000 * 60 * 5 }, (error, release) => { + locker.lock(filePath, { stale: 1000 * 60 * 5 }, (error: any, release: any) => { if (error) { reject(error); return; @@ -50,9 +49,9 @@ class Command extends BaseCommand { }); } - static isLocked(filePath) { + static isLocked(filePath: string) { return new Promise((resolve, reject) => { - locker.check(filePath, (error, isLocked) => { + locker.check(filePath, (error: any, isLocked: boolean) => { if (error) { reject(error); return; @@ -71,7 +70,7 @@ class Command extends BaseCommand { // OneDrive this.oneDriveApiUtils_ = new OneDriveApiNodeUtils(syncTarget.api()); const auth = await this.oneDriveApiUtils_.oauthDance({ - log: (...s) => { + log: (...s: any[]) => { return this.stdout(...s); }, }); @@ -118,7 +117,7 @@ class Command extends BaseCommand { return !!this.oneDriveApiUtils_; } - async action(args) { + async action(args: any) { this.releaseLockFn_ = null; // Lock is unique per profile/database @@ -166,12 +165,12 @@ class Command extends BaseCommand { const sync = await syncTarget.synchronizer(); - const options = { - onProgress: report => { + const options: any = { + onProgress: (report: any) => { const lines = Synchronizer.reportToLines(report); if (lines.length) cliUtils.redraw(lines.join(' ')); }, - onMessage: msg => { + onMessage: (msg: string) => { cliUtils.redrawDone(); this.stdout(msg); }, @@ -238,6 +237,9 @@ class Command extends BaseCommand { await ResourceFetcher.instance().waitForAllFinished(); } + const noPasswordMkIds = await masterKeysWithoutPassword(); + if (noPasswordMkIds.length) this.stdout(`/!\\ ${_('Your password is needed to decrypt some of your data. Type `:e2ee decrypt` to set it.')}`); + await app().refreshCurrentFolder(); } catch (error) { cleanUp(); diff --git a/packages/lib/services/e2ee/utils.ts b/packages/lib/services/e2ee/utils.ts index a3db517f8..28883f1c8 100644 --- a/packages/lib/services/e2ee/utils.ts +++ b/packages/lib/services/e2ee/utils.ts @@ -340,3 +340,17 @@ export async function masterPasswordIsValid(masterPassword: string, activeMaster // compare to whatever they've entered earlier. return Setting.value('encryption.masterPassword') === masterPassword; } + +export async function masterKeysWithoutPassword(): Promise { + const syncInfo = localSyncInfo(); + const passwordCache = Setting.value('encryption.passwordCache'); + + const output: string[] = []; + for (const mk of syncInfo.masterKeys) { + if (!masterKeyEnabled(mk)) continue; + const password = await findMasterKeyPassword(EncryptionService.instance(), mk, passwordCache); + if (!password) output.push(mk.id); + } + + return output; +}