1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

All: Handle tag encryption and started CLI and Electron encryption front-end

This commit is contained in:
Laurent Cozic 2017-12-14 00:23:32 +00:00
parent 2ffa5419e2
commit 1008b1835b
14 changed files with 93 additions and 32 deletions

View File

@ -27,11 +27,16 @@ class Command extends BaseCommand {
} }
const service = new EncryptionService(); 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.enabled', true);
Setting.setValue('encryption.activeMasterKeyId', masterKey.id);
let passwordCache = Setting.value('encryption.passwordCache');
passwordCache[masterKey.id] = password;
Setting.setValue('encryption.passwordCache', passwordCache);
} }
} }

View File

@ -1,11 +1,16 @@
#!/bin/bash #!/bin/bash
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
BUILD_DIR="$ROOT_DIR/tests-build" BUILD_DIR="$ROOT_DIR/tests-build"
TEST_FILE="$1"
rsync -a --exclude "node_modules/" "$ROOT_DIR/tests/" "$BUILD_DIR/" rsync -a --exclude "node_modules/" "$ROOT_DIR/tests/" "$BUILD_DIR/"
rsync -a "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/" rsync -a "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/"
rsync -a "$ROOT_DIR/build/locales/" "$BUILD_DIR/locales/" rsync -a "$ROOT_DIR/build/locales/" "$BUILD_DIR/locales/"
mkdir -p "$BUILD_DIR/data" mkdir -p "$BUILD_DIR/data"
(cd "$ROOT_DIR" && npm test tests-build/synchronizer.js) if [[ $TEST_FILE == "" ]]; then
(cd "$ROOT_DIR" && npm test tests-build/encryption.js) (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

View File

@ -127,7 +127,7 @@ describe('Encryption', function() {
done(); 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'); let masterKey = await service.generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey); masterKey = await MasterKey.save(masterKey);
await service.loadMasterKey(masterKey, '123456', true); await service.loadMasterKey(masterKey, '123456', true);

View File

@ -498,7 +498,13 @@ describe('Synchronizer', function() {
done(); 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 f1 = await Folder.save({ title: "folder" });
let n1 = await Note.save({ title: "mynote" }); let n1 = await Note.save({ title: "mynote" });
let n2 = await Note.save({ title: "mynote2" }); let n2 = await Note.save({ title: "mynote2" });
@ -508,6 +514,12 @@ describe('Synchronizer', function() {
await switchClient(2); await switchClient(2);
await synchronizer().start(); 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); let remoteTag = await Tag.loadByTitle(tag.title);
expect(!!remoteTag).toBe(true); expect(!!remoteTag).toBe(true);
expect(remoteTag.id).toBe(tag.id); expect(remoteTag.id).toBe(tag.id);
@ -533,7 +545,15 @@ describe('Synchronizer', function() {
noteIds = await Tag.noteIds(tag.id); noteIds = await Tag.noteIds(tag.id);
expect(noteIds.length).toBe(1); expect(noteIds.length).toBe(1);
expect(remoteNoteIds[0]).toBe(noteIds[0]); 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(); done();
}); });
@ -570,7 +590,7 @@ describe('Synchronizer', function() {
done(); done();
}); });
async function ignorableConflictTest(withEncryption) { async function ignorableNoteConflictTest(withEncryption) {
if (withEncryption) { if (withEncryption) {
Setting.setValue('encryption.enabled', true); Setting.setValue('encryption.enabled', true);
await loadEncryptionMasterKey(); 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) => { 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(); done();
}); });
@ -724,12 +744,9 @@ describe('Synchronizer', function() {
}); });
it('should always handle conflict if local or remote are encrypted', async (done) => { it('should always handle conflict if local or remote are encrypted', async (done) => {
await ignorableConflictTest(true); await ignorableNoteConflictTest(true);
done(); done();
}); });
// TODO: test tags
// TODO: test resources
}); });

View File

@ -35,6 +35,8 @@ BaseItem.loadClass('MasterKey', MasterKey);
Setting.setConstant('appId', 'net.cozic.joplin-desktop'); Setting.setConstant('appId', 'net.cozic.joplin-desktop');
Setting.setConstant('appType', 'desktop'); Setting.setConstant('appType', 'desktop');
shimInit();
// Disable drag and drop of links inside application (which would // Disable drag and drop of links inside application (which would
// open it as if the whole app was a browser) // open it as if the whole app was a browser)
document.addEventListener('dragover', event => event.preventDefault()); document.addEventListener('dragover', event => event.preventDefault());
@ -48,8 +50,6 @@ document.addEventListener('auxclick', event => event.preventDefault());
// which would open a new browser window. // which would open a new browser window.
document.addEventListener('click', (event) => event.preventDefault()); document.addEventListener('click', (event) => event.preventDefault());
shimInit();
app().start(bridge().processArgv()).then(() => { app().start(bridge().processArgv()).then(() => {
require('./gui/Root.min.js'); require('./gui/Root.min.js');
}).catch((error) => { }).catch((error) => {

View File

@ -3,4 +3,4 @@ ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$ROOT_DIR" cd "$ROOT_DIR"
./build.sh || exit 1 ./build.sh || exit 1
cd "$ROOT_DIR/app" cd "$ROOT_DIR/app"
./node_modules/.bin/electron . --env dev --log-level warn --open-dev-tools "$@" ./node_modules/.bin/electron . --env dev --log-level debug --open-dev-tools "$@"

View File

@ -33,7 +33,7 @@ iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'
On macOS: On macOS:
brew install node joplin brew install joplin
On Linux or Windows (via [WSL](https://msdn.microsoft.com/en-us/commandline/wsl/faq?f=255&MSPPError=-2147217396)): On Linux or Windows (via [WSL](https://msdn.microsoft.com/en-us/commandline/wsl/faq?f=255&MSPPError=-2147217396)):

View File

@ -12,7 +12,7 @@ The notes can be [synchronised](#synchronisation) with various targets including
On macOS: On macOS:
brew install node joplin brew install joplin
On Linux or Windows (via [WSL](https://msdn.microsoft.com/en-us/commandline/wsl/faq?f=255&MSPPError=-2147217396)): On Linux or Windows (via [WSL](https://msdn.microsoft.com/en-us/commandline/wsl/faq?f=255&MSPPError=-2147217396)):

View File

@ -26,6 +26,7 @@ const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js'); const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js'); const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
const SyncTargetOneDriveDev = require('lib/SyncTargetOneDriveDev.js'); const SyncTargetOneDriveDev = require('lib/SyncTargetOneDriveDev.js');
const EncryptionService = require('lib/services/EncryptionService');
SyncTargetRegistry.addClass(SyncTargetFilesystem); SyncTargetRegistry.addClass(SyncTargetFilesystem);
SyncTargetRegistry.addClass(SyncTargetOneDrive); SyncTargetRegistry.addClass(SyncTargetOneDrive);
@ -392,6 +393,9 @@ class BaseApplication {
setLocale(Setting.value('locale')); setLocale(Setting.value('locale'));
} }
BaseItem.encryptionService_ = EncryptionService.instance();
await EncryptionService.instance().loadMasterKeysFromSettings();
let currentFolderId = Setting.value('activeFolderId'); let currentFolderId = Setting.value('activeFolderId');
let currentFolder = null; let currentFolder = null;
if (currentFolderId) currentFolder = await Folder.load(currentFolderId); if (currentFolderId) currentFolder = await Folder.load(currentFolderId);

View File

@ -259,9 +259,14 @@ class BaseItem extends BaseModel {
const cipherText = await BaseItem.encryptionService_.encryptString(serialized); const cipherText = await BaseItem.encryptionService_.encryptString(serialized);
const reducedItem = Object.assign({}, item); const reducedItem = Object.assign({}, item);
const keepKeys = ['id', 'title', 'parent_id', 'body', 'updated_time', 'type_'];
if ('title' in reducedItem) reducedItem.title = ''; // List of keys that won't be encrypted - mostly foreign keys required to link items
if ('body' in reducedItem) reducedItem.body = ''; // with each others and timestamp required for synchronisation.
const keepKeys = ['id', 'note_id', 'tag_id', 'parent_id', 'updated_time', 'type_'];
// const keepKeys = ['id', 'title', 'note_id', 'tag_id', 'parent_id', 'body', 'updated_time', 'type_'];
// if ('title' in reducedItem) reducedItem.title = '';
// if ('body' in reducedItem) reducedItem.body = '';
for (let n in reducedItem) { for (let n in reducedItem) {
if (!reducedItem.hasOwnProperty(n)) continue; if (!reducedItem.hasOwnProperty(n)) continue;

View File

@ -61,6 +61,8 @@ class Setting extends BaseModel {
'uncompletedTodosOnTop': { value: true, type: Setting.TYPE_BOOL, public: true, label: () => _('Show uncompleted todos on top of the lists') }, 'uncompletedTodosOnTop': { value: true, type: Setting.TYPE_BOOL, public: true, label: () => _('Show uncompleted todos on top of the lists') },
'trackLocation': { value: true, type: Setting.TYPE_BOOL, public: true, label: () => _('Save geo-location with notes') }, '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.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: () => { 'sync.interval': { value: 300, type: Setting.TYPE_INT, isEnum: true, public: true, label: () => _('Synchronisation interval'), options: () => {
return { return {
0: _('Disabled'), 0: _('Disabled'),

View File

@ -1,5 +1,7 @@
const { padLeft } = require('lib/string-utils.js'); const { padLeft } = require('lib/string-utils.js');
const { shim } = require('lib/shim.js'); const { shim } = require('lib/shim.js');
const { Setting } = require('lib/models/setting.js');
const MasterKey = require('lib/models/MasterKey');
function hexPad(s, length) { function hexPad(s, length) {
return padLeft(s, length, '0'); return padLeft(s, length, '0');
@ -16,6 +18,27 @@ class EncryptionService {
this.defaultEncryptionMethod_ = EncryptionService.METHOD_SJCL; 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() { chunkSize() {
return this.chunkSize_; return this.chunkSize_;
} }
@ -76,7 +99,7 @@ class EncryptionService {
const hexaBytes = bytes.map((a) => { return hexPad(a.toString(16), 2); }).join(''); const hexaBytes = bytes.map((a) => { return hexPad(a.toString(16), 2); }).join('');
const checksum = this.sha256(hexaBytes); const checksum = this.sha256(hexaBytes);
const encryptionMethod = EncryptionService.METHOD_SJCL_2; 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(); const now = Date.now();
return { return {
@ -89,13 +112,13 @@ class EncryptionService {
} }
async decryptMasterKey(model, password) { 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); const checksum = this.sha256(plainText);
if (checksum !== model.checksum) throw new Error('Could not decrypt master key (checksum failed)'); if (checksum !== model.checksum) throw new Error('Could not decrypt master key (checksum failed)');
return plainText; return plainText;
} }
async encrypt_(method, key, plainText) { async encrypt(method, key, plainText) {
const sjcl = shim.sjclModule; const sjcl = shim.sjclModule;
if (method === EncryptionService.METHOD_SJCL) { if (method === EncryptionService.METHOD_SJCL) {
@ -126,7 +149,7 @@ class EncryptionService {
throw new Error('Unknown encryption method: ' + method); throw new Error('Unknown encryption method: ' + method);
} }
async decrypt_(method, key, cipherText) { async decrypt(method, key, cipherText) {
const sjcl = shim.sjclModule; const sjcl = shim.sjclModule;
if (method === EncryptionService.METHOD_SJCL || method === EncryptionService.METHOD_SJCL_2) { if (method === EncryptionService.METHOD_SJCL || method === EncryptionService.METHOD_SJCL_2) {
@ -159,7 +182,7 @@ class EncryptionService {
fromIndex += block.length; 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(padLeft(encrypted.length.toString(16), 6, '0'));
cipherText.push(encrypted); cipherText.push(encrypted);
@ -184,7 +207,7 @@ class EncryptionService {
const block = cipherText.substr(index, length); const block = cipherText.substr(index, length);
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); output.push(plainText);
} }
@ -212,7 +235,7 @@ class EncryptionService {
const plainText = await fsDriver.readFileChunk(handle, this.chunkSize_, 'base64'); const plainText = await fsDriver.readFileChunk(handle, this.chunkSize_, 'base64');
if (!plainText) break; 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, padLeft(cipherText.length.toString(16), 6, '0'), 'ascii'); // Data - Length
await fsDriver.appendFile(destPath, cipherText, 'ascii'); // Data - Data await fsDriver.appendFile(destPath, cipherText, 'ascii'); // Data - Data
@ -251,7 +274,7 @@ class EncryptionService {
const cipherText = await fsDriver.readFileChunk(handle, length, 'ascii'); const cipherText = await fsDriver.readFileChunk(handle, length, 'ascii');
if (!cipherText) break; 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'); await fsDriver.appendFile(destPath, plainText, 'base64');
} }

View File

@ -251,7 +251,7 @@
</table> </table>
<h2 id="terminal-application">Terminal application</h2> <h2 id="terminal-application">Terminal application</h2>
<p>On macOS:</p> <p>On macOS:</p>
<pre><code>brew install node joplin <pre><code>brew install joplin
</code></pre><p>On Linux or Windows (via <a href="https://msdn.microsoft.com/en-us/commandline/wsl/faq?f=255&amp;MSPPError=-2147217396">WSL</a>):</p> </code></pre><p>On Linux or Windows (via <a href="https://msdn.microsoft.com/en-us/commandline/wsl/faq?f=255&amp;MSPPError=-2147217396">WSL</a>):</p>
<p><strong>Important:</strong> First, <a href="https://nodejs.org/en/download/package-manager/">install Node 8+</a>. Node 8 is LTS but not yet available everywhere so you might need to manually install it.</p> <p><strong>Important:</strong> First, <a href="https://nodejs.org/en/download/package-manager/">install Node 8+</a>. Node 8 is LTS but not yet available everywhere so you might need to manually install it.</p>
<pre><code>NPM_CONFIG_PREFIX=~/.joplin-bin npm install -g joplin <pre><code>NPM_CONFIG_PREFIX=~/.joplin-bin npm install -g joplin

View File

@ -205,7 +205,7 @@
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/ScreenshotTerminal.png" style="max-width: 60%"></p> <p><img src="https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/ScreenshotTerminal.png" style="max-width: 60%"></p>
<h1 id="installation">Installation</h1> <h1 id="installation">Installation</h1>
<p>On macOS:</p> <p>On macOS:</p>
<pre><code>brew install node joplin <pre><code>brew install joplin
</code></pre><p>On Linux or Windows (via <a href="https://msdn.microsoft.com/en-us/commandline/wsl/faq?f=255&amp;MSPPError=-2147217396">WSL</a>):</p> </code></pre><p>On Linux or Windows (via <a href="https://msdn.microsoft.com/en-us/commandline/wsl/faq?f=255&amp;MSPPError=-2147217396">WSL</a>):</p>
<p><strong>Important:</strong> First, <a href="https://nodejs.org/en/download/package-manager/">install Node 8+</a>. Node 8 is LTS but not yet available everywhere so you might need to manually install it.</p> <p><strong>Important:</strong> First, <a href="https://nodejs.org/en/download/package-manager/">install Node 8+</a>. Node 8 is LTS but not yet available everywhere so you might need to manually install it.</p>
<pre><code>NPM_CONFIG_PREFIX=~/.joplin-bin npm install -g joplin <pre><code>NPM_CONFIG_PREFIX=~/.joplin-bin npm install -g joplin