diff --git a/CliClient/app/command-encrypt-config.js b/CliClient/app/command-encrypt-config.js index 86eeff97d..fa5f97ddc 100644 --- a/CliClient/app/command-encrypt-config.js +++ b/CliClient/app/command-encrypt-config.js @@ -27,11 +27,16 @@ class Command extends BaseCommand { } const service = new EncryptionService(); - const masterKey = await service.generateMasterKey(password); - - await MasterKey.save(masterKey); + + let masterKey = await service.generateMasterKey(password); + masterKey = await MasterKey.save(masterKey); Setting.setValue('encryption.enabled', true); + Setting.setValue('encryption.activeMasterKeyId', masterKey.id); + + let passwordCache = Setting.value('encryption.passwordCache'); + passwordCache[masterKey.id] = password; + Setting.setValue('encryption.passwordCache', passwordCache); } } diff --git a/CliClient/run_test.sh b/CliClient/run_test.sh index 91641398f..85cf4ea29 100755 --- a/CliClient/run_test.sh +++ b/CliClient/run_test.sh @@ -1,11 +1,16 @@ #!/bin/bash ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" BUILD_DIR="$ROOT_DIR/tests-build" +TEST_FILE="$1" rsync -a --exclude "node_modules/" "$ROOT_DIR/tests/" "$BUILD_DIR/" rsync -a "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/" rsync -a "$ROOT_DIR/build/locales/" "$BUILD_DIR/locales/" mkdir -p "$BUILD_DIR/data" -(cd "$ROOT_DIR" && npm test tests-build/synchronizer.js) -(cd "$ROOT_DIR" && npm test tests-build/encryption.js) \ No newline at end of file +if [[ $TEST_FILE == "" ]]; then + (cd "$ROOT_DIR" && npm test tests-build/synchronizer.js) + (cd "$ROOT_DIR" && npm test tests-build/encryption.js) +else + (cd "$ROOT_DIR" && npm test tests-build/$TEST_FILE.js) +fi \ No newline at end of file diff --git a/CliClient/tests/encryption.js b/CliClient/tests/encryption.js index 63b6af1b0..7f941bb14 100644 --- a/CliClient/tests/encryption.js +++ b/CliClient/tests/encryption.js @@ -127,7 +127,7 @@ describe('Encryption', function() { done(); }); - it('should encrypt and decrypt serialised data', async (done) => { + it('should encrypt and decrypt notes and folders', async (done) => { let masterKey = await service.generateMasterKey('123456'); masterKey = await MasterKey.save(masterKey); await service.loadMasterKey(masterKey, '123456', true); diff --git a/CliClient/tests/synchronizer.js b/CliClient/tests/synchronizer.js index 4b124a603..b2ad57559 100644 --- a/CliClient/tests/synchronizer.js +++ b/CliClient/tests/synchronizer.js @@ -498,7 +498,13 @@ describe('Synchronizer', function() { done(); }); - it('should sync tags', async (done) => { + async function shoudSyncTagTest(withEncryption) { + let masterKey = null; + if (withEncryption) { + Setting.setValue('encryption.enabled', true); + masterKey = await loadEncryptionMasterKey(); + } + let f1 = await Folder.save({ title: "folder" }); let n1 = await Note.save({ title: "mynote" }); let n2 = await Note.save({ title: "mynote2" }); @@ -508,6 +514,12 @@ describe('Synchronizer', function() { await switchClient(2); await synchronizer().start(); + if (withEncryption) { + const masterKey_2 = await MasterKey.load(masterKey.id); + await encryptionService().loadMasterKey(masterKey_2, '123456', true); + let t = await Tag.load(tag.id); + await Tag.decrypt(t); + } let remoteTag = await Tag.loadByTitle(tag.title); expect(!!remoteTag).toBe(true); expect(remoteTag.id).toBe(tag.id); @@ -533,7 +545,15 @@ describe('Synchronizer', function() { noteIds = await Tag.noteIds(tag.id); expect(noteIds.length).toBe(1); expect(remoteNoteIds[0]).toBe(noteIds[0]); + } + it('should sync tags', async (done) => { + await shoudSyncTagTest(false); + done(); + }); + + it('should sync encrypted tags', async (done) => { + await shoudSyncTagTest(true); done(); }); @@ -570,7 +590,7 @@ describe('Synchronizer', function() { done(); }); - async function ignorableConflictTest(withEncryption) { + async function ignorableNoteConflictTest(withEncryption) { if (withEncryption) { Setting.setValue('encryption.enabled', true); await loadEncryptionMasterKey(); @@ -626,7 +646,7 @@ describe('Synchronizer', function() { } it('should not consider it is a conflict if neither the title nor body of the note have changed', async (done) => { - await ignorableConflictTest(false); + await ignorableNoteConflictTest(false); done(); }); @@ -724,12 +744,9 @@ describe('Synchronizer', function() { }); it('should always handle conflict if local or remote are encrypted', async (done) => { - await ignorableConflictTest(true); - + await ignorableNoteConflictTest(true); + done(); }); - // TODO: test tags - // TODO: test resources - }); \ No newline at end of file diff --git a/ElectronClient/app/main-html.js b/ElectronClient/app/main-html.js index f2045e6c3..cc27066e7 100644 --- a/ElectronClient/app/main-html.js +++ b/ElectronClient/app/main-html.js @@ -35,6 +35,8 @@ BaseItem.loadClass('MasterKey', MasterKey); Setting.setConstant('appId', 'net.cozic.joplin-desktop'); Setting.setConstant('appType', 'desktop'); +shimInit(); + // Disable drag and drop of links inside application (which would // open it as if the whole app was a browser) document.addEventListener('dragover', event => event.preventDefault()); @@ -48,8 +50,6 @@ document.addEventListener('auxclick', event => event.preventDefault()); // which would open a new browser window. document.addEventListener('click', (event) => event.preventDefault()); -shimInit(); - app().start(bridge().processArgv()).then(() => { require('./gui/Root.min.js'); }).catch((error) => { diff --git a/ElectronClient/run.sh b/ElectronClient/run.sh index eeebbf8dd..ca79bff01 100755 --- a/ElectronClient/run.sh +++ b/ElectronClient/run.sh @@ -3,4 +3,4 @@ ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" cd "$ROOT_DIR" ./build.sh || exit 1 cd "$ROOT_DIR/app" -./node_modules/.bin/electron . --env dev --log-level warn --open-dev-tools "$@" \ No newline at end of file +./node_modules/.bin/electron . --env dev --log-level debug --open-dev-tools "$@" \ No newline at end of file diff --git a/README.md b/README.md index 7e1180832..a3abaf93f 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ iOS | _('Show uncompleted todos on top of the lists') }, 'trackLocation': { value: true, type: Setting.TYPE_BOOL, public: true, label: () => _('Save geo-location with notes') }, 'encryption.enabled': { value: false, type: Setting.TYPE_BOOL, public: false }, + 'encryption.activeMasterKeyId': { value: '', type: Setting.TYPE_STRING, public: false }, + 'encryption.passwordCache': { value: {}, type: Setting.TYPE_OBJECT, public: false }, 'sync.interval': { value: 300, type: Setting.TYPE_INT, isEnum: true, public: true, label: () => _('Synchronisation interval'), options: () => { return { 0: _('Disabled'), diff --git a/ReactNativeClient/lib/services/EncryptionService.js b/ReactNativeClient/lib/services/EncryptionService.js index 57897f11f..94e868ac1 100644 --- a/ReactNativeClient/lib/services/EncryptionService.js +++ b/ReactNativeClient/lib/services/EncryptionService.js @@ -1,5 +1,7 @@ const { padLeft } = require('lib/string-utils.js'); const { shim } = require('lib/shim.js'); +const { Setting } = require('lib/models/setting.js'); +const MasterKey = require('lib/models/MasterKey'); function hexPad(s, length) { return padLeft(s, length, '0'); @@ -16,6 +18,27 @@ class EncryptionService { this.defaultEncryptionMethod_ = EncryptionService.METHOD_SJCL; } + static instance() { + if (this.instance_) return this.instance_; + this.instance_ = new EncryptionService(); + return this.instance_; + } + + async loadMasterKeysFromSettings() { + if (!Setting.value('encryption.enabled')) return; + const masterKeys = await MasterKey.all(); + const passwords = Setting.value('encryption.passwordCache'); + const activeMasterKeyId = Setting.value('encryption.activeMasterKeyId'); + + for (let i = 0; i < masterKeys.length; i++) { + const mk = masterKeys[i]; + const password = passwords[mk.id]; + if (!password) continue; + + await this.loadMasterKey(mk, password, activeMasterKeyId === mk.id); + } + } + chunkSize() { return this.chunkSize_; } @@ -76,7 +99,7 @@ class EncryptionService { const hexaBytes = bytes.map((a) => { return hexPad(a.toString(16), 2); }).join(''); const checksum = this.sha256(hexaBytes); const encryptionMethod = EncryptionService.METHOD_SJCL_2; - const cipherText = await this.encrypt_(encryptionMethod, password, hexaBytes); + const cipherText = await this.encrypt(encryptionMethod, password, hexaBytes); const now = Date.now(); return { @@ -89,13 +112,13 @@ class EncryptionService { } async decryptMasterKey(model, password) { - const plainText = await this.decrypt_(model.encryption_method, password, model.content); + const plainText = await this.decrypt(model.encryption_method, password, model.content); const checksum = this.sha256(plainText); if (checksum !== model.checksum) throw new Error('Could not decrypt master key (checksum failed)'); return plainText; } - async encrypt_(method, key, plainText) { + async encrypt(method, key, plainText) { const sjcl = shim.sjclModule; if (method === EncryptionService.METHOD_SJCL) { @@ -126,7 +149,7 @@ class EncryptionService { throw new Error('Unknown encryption method: ' + method); } - async decrypt_(method, key, cipherText) { + async decrypt(method, key, cipherText) { const sjcl = shim.sjclModule; if (method === EncryptionService.METHOD_SJCL || method === EncryptionService.METHOD_SJCL_2) { @@ -159,7 +182,7 @@ class EncryptionService { fromIndex += block.length; - const encrypted = await this.encrypt_(method, masterKeyPlainText, block); + const encrypted = await this.encrypt(method, masterKeyPlainText, block); cipherText.push(padLeft(encrypted.length.toString(16), 6, '0')); cipherText.push(encrypted); @@ -184,7 +207,7 @@ class EncryptionService { const block = cipherText.substr(index, length); index += length; - const plainText = await this.decrypt_(header.encryptionMethod, masterKeyPlainText, block); + const plainText = await this.decrypt(header.encryptionMethod, masterKeyPlainText, block); output.push(plainText); } @@ -212,7 +235,7 @@ class EncryptionService { const plainText = await fsDriver.readFileChunk(handle, this.chunkSize_, 'base64'); if (!plainText) break; - const cipherText = await this.encrypt_(method, key, plainText); + const cipherText = await this.encrypt(method, key, plainText); await fsDriver.appendFile(destPath, padLeft(cipherText.length.toString(16), 6, '0'), 'ascii'); // Data - Length await fsDriver.appendFile(destPath, cipherText, 'ascii'); // Data - Data @@ -251,7 +274,7 @@ class EncryptionService { const cipherText = await fsDriver.readFileChunk(handle, length, 'ascii'); if (!cipherText) break; - const plainText = await this.decrypt_(header.encryptionMethod, key, cipherText); + const plainText = await this.decrypt(header.encryptionMethod, key, cipherText); await fsDriver.appendFile(destPath, plainText, 'base64'); } diff --git a/docs/index.html b/docs/index.html index ee3c4b578..6416e8b6d 100644 --- a/docs/index.html +++ b/docs/index.html @@ -251,7 +251,7 @@

Terminal application

On macOS:

-
brew install node joplin
+
brew install joplin
 

On Linux or Windows (via WSL):

Important: First, install Node 8+. Node 8 is LTS but not yet available everywhere so you might need to manually install it.

NPM_CONFIG_PREFIX=~/.joplin-bin npm install -g joplin
diff --git a/docs/terminal/index.html b/docs/terminal/index.html
index c772c0d4a..a93325357 100644
--- a/docs/terminal/index.html
+++ b/docs/terminal/index.html
@@ -205,7 +205,7 @@
 

Installation

On macOS:

-
brew install node joplin
+
brew install joplin
 

On Linux or Windows (via WSL):

Important: First, install Node 8+. Node 8 is LTS but not yet available everywhere so you might need to manually install it.

NPM_CONFIG_PREFIX=~/.joplin-bin npm install -g joplin