mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
All: Improved encryption and synchronisation
This commit is contained in:
parent
500fbc5294
commit
71e877d369
@ -1,7 +1,7 @@
|
||||
require('app-module-path').addPath(__dirname);
|
||||
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, checkThrowAsync } = require('test-utils.js');
|
||||
const { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, checkThrowAsync, asyncTest } = require('test-utils.js');
|
||||
const { shim } = require('lib/shim.js');
|
||||
const fs = require('fs-extra');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
@ -104,7 +104,7 @@ describe('Synchronizer', function() {
|
||||
insideBeforeEach = false;
|
||||
});
|
||||
|
||||
it('should create remote items', async (done) => {
|
||||
it('should create remote items', asyncTest(async () => {
|
||||
let folder = await Folder.save({ title: "folder1" });
|
||||
await Note.save({ title: "un", parent_id: folder.id });
|
||||
|
||||
@ -113,11 +113,9 @@ describe('Synchronizer', function() {
|
||||
await synchronizer().start();
|
||||
|
||||
await localItemsSameAsRemote(all, expect);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should update remote item', async (done) => {
|
||||
it('should update remote item', asyncTest(async () => {
|
||||
let folder = await Folder.save({ title: "folder1" });
|
||||
let note = await Note.save({ title: "un", parent_id: folder.id });
|
||||
await synchronizer().start();
|
||||
@ -128,11 +126,9 @@ describe('Synchronizer', function() {
|
||||
await synchronizer().start();
|
||||
|
||||
await localItemsSameAsRemote(all, expect);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should create local items', async (done) => {
|
||||
it('should create local items', asyncTest(async () => {
|
||||
let folder = await Folder.save({ title: "folder1" });
|
||||
await Note.save({ title: "un", parent_id: folder.id });
|
||||
await synchronizer().start();
|
||||
@ -144,11 +140,9 @@ describe('Synchronizer', function() {
|
||||
let all = await allItems();
|
||||
|
||||
await localItemsSameAsRemote(all, expect);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should update local items', async (done) => {
|
||||
it('should update local items', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||
await synchronizer().start();
|
||||
@ -173,11 +167,9 @@ describe('Synchronizer', function() {
|
||||
let all = await allItems();
|
||||
|
||||
await localItemsSameAsRemote(all, expect);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should resolve note conflicts', async (done) => {
|
||||
it('should resolve note conflicts', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||
await synchronizer().start();
|
||||
@ -216,11 +208,9 @@ describe('Synchronizer', function() {
|
||||
if (!noteUpdatedFromRemote.hasOwnProperty(n)) continue;
|
||||
expect(noteUpdatedFromRemote[n]).toBe(note2[n], 'Property: ' + n);
|
||||
}
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should resolve folders conflicts', async (done) => {
|
||||
it('should resolve folders conflicts', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||
await synchronizer().start();
|
||||
@ -251,11 +241,9 @@ describe('Synchronizer', function() {
|
||||
|
||||
let folder1_final = await Folder.load(folder1.id);
|
||||
expect(folder1_final.title).toBe(folder1_modRemote.title);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should delete remote notes', async (done) => {
|
||||
it('should delete remote notes', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||
await synchronizer().start();
|
||||
@ -278,11 +266,9 @@ describe('Synchronizer', function() {
|
||||
|
||||
let deletedItems = await BaseItem.deletedItems(syncTargetId());
|
||||
expect(deletedItems.length).toBe(0);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should delete local notes', async (done) => {
|
||||
it('should delete local notes', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||
await synchronizer().start();
|
||||
@ -300,11 +286,9 @@ describe('Synchronizer', function() {
|
||||
expect(items.length).toBe(1);
|
||||
let deletedItems = await BaseItem.deletedItems(syncTargetId());
|
||||
expect(deletedItems.length).toBe(0);
|
||||
|
||||
done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should delete remote folder', async (done) => {
|
||||
it('should delete remote folder', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let folder2 = await Folder.save({ title: "folder2" });
|
||||
await synchronizer().start();
|
||||
@ -321,11 +305,9 @@ describe('Synchronizer', function() {
|
||||
|
||||
let all = await allItems();
|
||||
localItemsSameAsRemote(all, expect);
|
||||
|
||||
done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should delete local folder', async (done) => {
|
||||
it('should delete local folder', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let folder2 = await Folder.save({ title: "folder2" });
|
||||
await synchronizer().start();
|
||||
@ -346,11 +328,9 @@ describe('Synchronizer', function() {
|
||||
|
||||
let items = await allItems();
|
||||
localItemsSameAsRemote(items, expect);
|
||||
|
||||
done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should resolve conflict if remote folder has been deleted, but note has been added to folder locally', async (done) => {
|
||||
it('should resolve conflict if remote folder has been deleted, but note has been added to folder locally', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
await synchronizer().start();
|
||||
|
||||
@ -368,11 +348,9 @@ describe('Synchronizer', function() {
|
||||
expect(items.length).toBe(1);
|
||||
expect(items[0].title).toBe('note1');
|
||||
expect(items[0].is_conflict).toBe(1);
|
||||
|
||||
done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should resolve conflict if note has been deleted remotely and locally', async (done) => {
|
||||
it('should resolve conflict if note has been deleted remotely and locally', asyncTest(async () => {
|
||||
let folder = await Folder.save({ title: "folder" });
|
||||
let note = await Note.save({ title: "note", parent_id: folder.title });
|
||||
await synchronizer().start();
|
||||
@ -393,11 +371,9 @@ describe('Synchronizer', function() {
|
||||
expect(items[0].title).toBe('folder');
|
||||
|
||||
localItemsSameAsRemote(items, expect);
|
||||
|
||||
done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should cross delete all folders', async (done) => {
|
||||
it('should cross delete all folders', asyncTest(async () => {
|
||||
// If client1 and 2 have two folders, client 1 deletes item 1 and client
|
||||
// 2 deletes item 2, they should both end up with no items after sync.
|
||||
|
||||
@ -433,11 +409,9 @@ describe('Synchronizer', function() {
|
||||
|
||||
expect(items1.length).toBe(0);
|
||||
expect(items1.length).toBe(items2.length);
|
||||
|
||||
done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should handle conflict when remote note is deleted then local note is modified', async (done) => {
|
||||
it('should handle conflict when remote note is deleted then local note is modified', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||
await synchronizer().start();
|
||||
@ -467,11 +441,9 @@ describe('Synchronizer', function() {
|
||||
let unconflictedNotes = await Note.unconflictedNotes();
|
||||
|
||||
expect(unconflictedNotes.length).toBe(0);
|
||||
|
||||
done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should handle conflict when remote folder is deleted then local folder is renamed', async (done) => {
|
||||
it('should handle conflict when remote folder is deleted then local folder is renamed', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let folder2 = await Folder.save({ title: "folder2" });
|
||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||
@ -499,11 +471,9 @@ describe('Synchronizer', function() {
|
||||
let items = await allItems();
|
||||
|
||||
expect(items.length).toBe(1);
|
||||
|
||||
done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should allow duplicate folder titles', async (done) => {
|
||||
it('should allow duplicate folder titles', asyncTest(async () => {
|
||||
let localF1 = await Folder.save({ title: "folder" });
|
||||
|
||||
await switchClient(2);
|
||||
@ -535,9 +505,7 @@ describe('Synchronizer', function() {
|
||||
remoteF2 = await Folder.load(remoteF2.id);
|
||||
|
||||
expect(remoteF2.title == localF2.title).toBe(true);
|
||||
|
||||
done();
|
||||
});
|
||||
}));
|
||||
|
||||
async function shoudSyncTagTest(withEncryption) {
|
||||
let masterKey = null;
|
||||
@ -588,17 +556,13 @@ describe('Synchronizer', function() {
|
||||
expect(remoteNoteIds[0]).toBe(noteIds[0]);
|
||||
}
|
||||
|
||||
it('should sync tags', async (done) => {
|
||||
await shoudSyncTagTest(false);
|
||||
done();
|
||||
});
|
||||
it('should sync tags', asyncTest(async () => {
|
||||
await shoudSyncTagTest(false); }));
|
||||
|
||||
it('should sync encrypted tags', async (done) => {
|
||||
await shoudSyncTagTest(true);
|
||||
done();
|
||||
});
|
||||
it('should sync encrypted tags', asyncTest(async () => {
|
||||
await shoudSyncTagTest(true); }));
|
||||
|
||||
it('should not sync notes with conflicts', async (done) => {
|
||||
it('should not sync notes with conflicts', asyncTest(async () => {
|
||||
let f1 = await Folder.save({ title: "folder" });
|
||||
let n1 = await Note.save({ title: "mynote", parent_id: f1.id, is_conflict: 1 });
|
||||
await synchronizer().start();
|
||||
@ -610,11 +574,9 @@ describe('Synchronizer', function() {
|
||||
let folders = await Folder.all()
|
||||
expect(notes.length).toBe(0);
|
||||
expect(folders.length).toBe(1);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should not try to delete on remote conflicted notes that have been deleted', async (done) => {
|
||||
it('should not try to delete on remote conflicted notes that have been deleted', asyncTest(async () => {
|
||||
let f1 = await Folder.save({ title: "folder" });
|
||||
let n1 = await Note.save({ title: "mynote", parent_id: f1.id });
|
||||
await synchronizer().start();
|
||||
@ -627,9 +589,7 @@ describe('Synchronizer', function() {
|
||||
const deletedItems = await BaseItem.deletedItems(syncTargetId());
|
||||
|
||||
expect(deletedItems.length).toBe(0);
|
||||
|
||||
done();
|
||||
});
|
||||
}));
|
||||
|
||||
async function ignorableNoteConflictTest(withEncryption) {
|
||||
if (withEncryption) {
|
||||
@ -690,19 +650,15 @@ 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', asyncTest(async () => {
|
||||
await ignorableNoteConflictTest(false);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should always handle conflict if local or remote are encrypted', async (done) => {
|
||||
it('should always handle conflict if local or remote are encrypted', asyncTest(async () => {
|
||||
await ignorableNoteConflictTest(true);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('items should be downloaded again when user cancels in the middle of delta operation', async (done) => {
|
||||
it('items should be downloaded again when user cancels in the middle of delta operation', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", is_todo: 1, parent_id: folder1.id });
|
||||
await synchronizer().start();
|
||||
@ -718,11 +674,9 @@ describe('Synchronizer', function() {
|
||||
await synchronizer().start({ context: context });
|
||||
notes = await Note.all();
|
||||
expect(notes.length).toBe(1);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should skip items that cannot be synced', async (done) => {
|
||||
it('should skip items that cannot be synced', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", is_todo: 1, parent_id: folder1.id });
|
||||
const noteId = note1.id;
|
||||
@ -730,7 +684,7 @@ describe('Synchronizer', function() {
|
||||
let disabledItems = await BaseItem.syncDisabledItems(syncTargetId());
|
||||
expect(disabledItems.length).toBe(0);
|
||||
await Note.save({ id: noteId, title: "un mod", });
|
||||
synchronizer().debugFlags_ = ['cannotSync'];
|
||||
synchronizer().debugFlags_ = ['rejectedByTarget'];
|
||||
await synchronizer().start();
|
||||
synchronizer().debugFlags_ = [];
|
||||
await synchronizer().start(); // Another sync to check that this item is now excluded from sync
|
||||
@ -746,11 +700,9 @@ describe('Synchronizer', function() {
|
||||
|
||||
disabledItems = await BaseItem.syncDisabledItems(syncTargetId());
|
||||
expect(disabledItems.length).toBe(1);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('notes and folders should get encrypted when encryption is enabled', async (done) => {
|
||||
it('notes and folders should get encrypted when encryption is enabled', asyncTest(async () => {
|
||||
Setting.setValue('encryption.enabled', true);
|
||||
const masterKey = await loadEncryptionMasterKey();
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
@ -790,11 +742,9 @@ describe('Synchronizer', function() {
|
||||
expect(folder1_2.title).toBe(folder1.title);
|
||||
expect(folder1_2.updated_time).toBe(folder1.updated_time);
|
||||
expect(!folder1_2.encryption_cipher_text).toBe(true);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should enable encryption automatically when downloading new master key (and none was previously available)', async (done) => {
|
||||
it('should enable encryption automatically when downloading new master key (and none was previously available)',asyncTest(async () => {
|
||||
// Enable encryption on client 1 and sync an item
|
||||
Setting.setValue('encryption.enabled', true);
|
||||
await loadEncryptionMasterKey();
|
||||
@ -850,11 +800,9 @@ describe('Synchronizer', function() {
|
||||
await decryptionWorker().start();
|
||||
folder1 = await Folder.load(folder1.id);
|
||||
expect(folder1.title).toBe('change test'); // Got title from client 2
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should encrypt existing notes too when enabling E2EE', async (done) => {
|
||||
it('should encrypt existing notes too when enabling E2EE', asyncTest(async () => {
|
||||
// First create a folder, without encryption enabled, and sync it
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
await synchronizer().start();
|
||||
@ -879,11 +827,9 @@ describe('Synchronizer', function() {
|
||||
expect(content.indexOf('folder1') < 0).toBe(true);
|
||||
content = await fileApi().get(files.items[1].path);
|
||||
expect(content.indexOf('folder1') < 0).toBe(true);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should sync resources', async (done) => {
|
||||
it('should sync resources', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
|
||||
await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg');
|
||||
@ -902,11 +848,9 @@ describe('Synchronizer', function() {
|
||||
|
||||
expect(resource1_2.id).toBe(resource1.id);
|
||||
expect(fileContentEqual(resourcePath1, resourcePath1_2)).toBe(true);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should encryt resources', async (done) => {
|
||||
it('should encryt resources', asyncTest(async () => {
|
||||
Setting.setValue('encryption.enabled', true);
|
||||
const masterKey = await loadEncryptionMasterKey();
|
||||
|
||||
@ -928,11 +872,9 @@ describe('Synchronizer', function() {
|
||||
let resourcePath1_2 = Resource.fullPath(resource1_2);
|
||||
|
||||
expect(fileContentEqual(resourcePath1, resourcePath1_2)).toBe(true);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should upload decrypted items to sync target after encryption disabled', async (done) => {
|
||||
it('should upload decrypted items to sync target after encryption disabled', asyncTest(async () => {
|
||||
Setting.setValue('encryption.enabled', true);
|
||||
const masterKey = await loadEncryptionMasterKey();
|
||||
|
||||
@ -947,11 +889,9 @@ describe('Synchronizer', function() {
|
||||
await synchronizer().start();
|
||||
allEncrypted = await allSyncTargetItemsEncrypted();
|
||||
expect(allEncrypted).toBe(false);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should not upload any item if encryption was enabled, and items have not been decrypted, and then encryption disabled', async (done) => {
|
||||
it('should not upload any item if encryption was enabled, and items have not been decrypted, and then encryption disabled', asyncTest(async () => {
|
||||
// For some reason I can't explain, this test is sometimes executed before beforeEach is finished
|
||||
// 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);
|
||||
@ -970,8 +910,8 @@ describe('Synchronizer', function() {
|
||||
// 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());
|
||||
expect(hasThrown).toBe(true);
|
||||
//let hasThrown = await checkThrowAsync(async () => await encryptionService().disableEncryption());
|
||||
//expect(hasThrown).toBe(true);
|
||||
|
||||
// Now supply the password, and decrypt the items
|
||||
Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456');
|
||||
@ -986,11 +926,9 @@ describe('Synchronizer', function() {
|
||||
await synchronizer().start();
|
||||
allEncrypted = await allSyncTargetItemsEncrypted();
|
||||
expect(allEncrypted).toBe(false);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should encrypt remote resources after encryption has been enabled', async (done) => {
|
||||
it('should encrypt remote resources after encryption has been enabled', asyncTest(async () => {
|
||||
while (insideBeforeEach) await time.msleep(100);
|
||||
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
@ -1008,8 +946,6 @@ describe('Synchronizer', function() {
|
||||
await synchronizer().start();
|
||||
|
||||
expect(await allSyncTargetItemsEncrypted()).toBe(true);
|
||||
|
||||
done();
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
@ -297,4 +297,17 @@ function fileContentEqual(path1, path2) {
|
||||
return content1 === content2;
|
||||
}
|
||||
|
||||
module.exports = { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker };
|
||||
// Wrap an async test in a try/catch block so that done() is always called
|
||||
// and display a proper error message instead of "unhandled promise error"
|
||||
function asyncTest(callback) {
|
||||
return async function(done) {
|
||||
try {
|
||||
await callback();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, asyncTest };
|
@ -56,10 +56,10 @@ The apps handle displaying both decrypted and encrypted items, so that user is a
|
||||
|
||||
Enabling/disabling E2EE while two clients are in sync might have an unintuitive behaviour (although that behaviour might be correct), so below some scenarios are explained:
|
||||
|
||||
- If client 1 enables E2EE, all items will be synced to target and will appear encrypted on target. Although all items have been re-uploaded to the target, their timestamps did *not* change (because the item data itself has not changed, only its representation). Because of this, client 2 will not be re-download the items - it does not need to do so anyway since it has already the item data.
|
||||
- If client 1 enables E2EE, all items will be synced to target and will appear encrypted on target. Although all items have been re-uploaded to the target, their timestamps did *not* change (because the item data itself has not changed, only its representation). Because of this, client 2 will not re-download the items - it does not need to do so anyway since it has already the item data.
|
||||
|
||||
- When a client sync and download a master key for the first time, encryption will be automatically enabled (user will need to supply the master key password). In that case, all items that are not encrypted will be re-synced. Uploading only non-encrypted items is an optimisation since if an item is already encrypted locally it means it's encrytped on target too.
|
||||
|
||||
- If both clients are in sync with E2EE enabled: if client 1 disable E2EE, it's going to re-upload all the items unencrypted. Client 2 again will not re-download the items for the same reason as above (data did not change, only representation). Note that user *must* manually disable E2EE on all clients otherwise some will continue to upload encrypted items. Since synchronisation is stateless, clients do not know whether other clients use E2EE or not so this step has to be manual.
|
||||
|
||||
- Although messy, Joplin supports having some clients send encrypted and others unencrypted ones. The situation gets resolved once all the clients have the same E2EE settings.
|
||||
- Although messy, Joplin supports having some clients send encrypted items and others unencrypted ones. The situation gets resolved once all the clients have the same E2EE settings.
|
14
ReactNativeClient/lib/JoplinError.js
Normal file
14
ReactNativeClient/lib/JoplinError.js
Normal file
@ -0,0 +1,14 @@
|
||||
class JoplinError extends Error {
|
||||
|
||||
constructor(message, code = null) {
|
||||
super(message);
|
||||
this.code_ = code;
|
||||
}
|
||||
|
||||
get code() {
|
||||
return this.code_;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = JoplinError;
|
@ -109,7 +109,7 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent {
|
||||
|
||||
return (
|
||||
<View key={mk.id}>
|
||||
<Text style={this.styles().titleText}>{_('Master Key %d', num)}</Text>
|
||||
<Text style={this.styles().titleText}>{_('Master Key %s', mk.id.substr(0,6))}</Text>
|
||||
<Text style={this.styles().normalText}>{_('Created: %s', time.formatMsToLocal(mk.created_time))}</Text>
|
||||
<View style={{flexDirection: 'row', alignItems: 'center'}}>
|
||||
<Text style={{flex:0, fontSize: theme.fontSize, marginRight: 10}}>{_('Password:')}</Text>
|
||||
@ -193,6 +193,7 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent {
|
||||
{toggleButton}
|
||||
{passwordPromptComp}
|
||||
{mkComps}
|
||||
<View style={{flex:1, height: 20}}></View>
|
||||
</ScrollView>
|
||||
<DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/>
|
||||
</View>
|
||||
|
@ -137,7 +137,7 @@ class FileApiDriverOneDrive {
|
||||
}
|
||||
} catch (error) {
|
||||
if (error && error.code === 'BadRequest' && error.message === 'Maximum request length exceeded.') {
|
||||
error.code = 'cannotSync';
|
||||
error.code = 'rejectedByTarget';
|
||||
error.message = 'Resource exceeds OneDrive max file size (4MB)';
|
||||
}
|
||||
throw error;
|
||||
|
@ -1,6 +1,7 @@
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const { Database } = require('lib/database.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const JoplinError = require('lib/JoplinError.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
@ -261,8 +262,8 @@ class BaseItem extends BaseModel {
|
||||
const ItemClass = this.itemClass(item);
|
||||
let serialized = await ItemClass.serialize(item);
|
||||
if (!Setting.value('encryption.enabled') || !ItemClass.encryptionSupported()) {
|
||||
// Sanity check - normally not possible
|
||||
if (!!item.encryption_applied) throw new Error('Item is encrypted but encryption is currently disabled');
|
||||
// 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;
|
||||
}
|
||||
|
||||
@ -270,8 +271,6 @@ class BaseItem extends BaseModel {
|
||||
|
||||
const cipherText = await this.encryptionService().encryptString(serialized);
|
||||
|
||||
//const reducedItem = Object.assign({}, item);
|
||||
|
||||
// List of keys that won't be encrypted - mostly foreign keys required to link items
|
||||
// with each others and timestamp required for synchronisation.
|
||||
const keepKeys = ['id', 'note_id', 'tag_id', 'parent_id', 'updated_time', 'type_'];
|
||||
@ -440,7 +439,12 @@ class BaseItem extends BaseModel {
|
||||
// // CHANGED:
|
||||
// 'SELECT * FROM [ITEMS] items JOIN sync_items s ON s.item_id = items.id WHERE sync_target = ? AND'
|
||||
|
||||
let extraWhere = className == 'Note' ? 'AND is_conflict = 0' : '';
|
||||
let extraWhere = [];
|
||||
if (className == 'Note') extraWhere.push('is_conflict = 0');
|
||||
if (className == 'Resource') extraWhere.push('encryption_blob_encrypted = 0');
|
||||
if (ItemClass.encryptionSupported()) extraWhere.push('encryption_applied = 0');
|
||||
|
||||
extraWhere = extraWhere.length ? 'AND ' + extraWhere.join(' AND ') : '';
|
||||
|
||||
// First get all the items that have never been synced under this sync target
|
||||
|
||||
|
@ -71,8 +71,9 @@ class Resource extends BaseItem {
|
||||
}
|
||||
|
||||
await this.encryptionService().decryptFile(encryptedPath, plainTextPath);
|
||||
|
||||
decryptedItem.encryption_blob_encrypted = 0;
|
||||
return Resource.save(decryptedItem, { autoTimestamp: false });
|
||||
return super.save(decryptedItem, { autoTimestamp: false });
|
||||
}
|
||||
|
||||
|
||||
@ -83,14 +84,8 @@ class Resource extends BaseItem {
|
||||
const plainTextPath = this.fullPath(resource);
|
||||
|
||||
if (!Setting.value('encryption.enabled')) {
|
||||
// Sanity check - normally not possible
|
||||
// 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');
|
||||
|
||||
// // TODO: why is it set to 0 without decrypting first?
|
||||
// if (resource.encryption_blob_encrypted) {
|
||||
// resource.encryption_blob_encrypted = 0;
|
||||
// await Resource.save(resource, { autoTimestamp: false });
|
||||
// }
|
||||
return { path: plainTextPath, resource: resource };
|
||||
}
|
||||
|
||||
|
@ -67,8 +67,13 @@ class EncryptionService {
|
||||
}
|
||||
|
||||
async disableEncryption() {
|
||||
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.'));
|
||||
// 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
|
||||
@ -77,26 +82,35 @@ class EncryptionService {
|
||||
}
|
||||
|
||||
async loadMasterKeysFromSettings() {
|
||||
if (!Setting.value('encryption.enabled')) {
|
||||
this.unloadAllMasterKeys();
|
||||
} else {
|
||||
const masterKeys = await MasterKey.all();
|
||||
const passwords = Setting.value('encryption.passwordCache');
|
||||
const activeMasterKeyId = Setting.value('encryption.activeMasterKeyId');
|
||||
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 (this.isMasterKeyLoaded(mk.id)) continue;
|
||||
if (!password) continue;
|
||||
this.logger().info('Trying to load ' + masterKeys.length + ' master keys...');
|
||||
|
||||
try {
|
||||
await this.loadMasterKey(mk, password, activeMasterKeyId === mk.id);
|
||||
} catch (error) {
|
||||
this.logger().warn('Cannot load master key ' + mk.id + '. Invalid password?', error);
|
||||
}
|
||||
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 (let n in this.loadedMasterKeys_) {
|
||||
if (!this.loadedMasterKeys_[n]) continue;
|
||||
output++;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
chunkSize() {
|
||||
|
@ -281,7 +281,7 @@ class Synchronizer {
|
||||
const localResourceContentPath = result.path;
|
||||
await this.api().put(remoteContentPath, null, { path: localResourceContentPath, source: 'file' });
|
||||
} catch (error) {
|
||||
if (error && error.code === 'cannotSync') {
|
||||
if (error && error.code === 'rejectedByTarget') {
|
||||
await handleCannotSyncItem(syncTargetId, local, error.message);
|
||||
action = null;
|
||||
} else {
|
||||
@ -305,15 +305,15 @@ class Synchronizer {
|
||||
|
||||
let canSync = true;
|
||||
try {
|
||||
if (this.debugFlags_.indexOf('cannotSync') >= 0) {
|
||||
const error = new Error('Testing cannotSync');
|
||||
error.code = 'cannotSync';
|
||||
if (this.debugFlags_.indexOf('rejectedByTarget') >= 0) {
|
||||
const error = new Error('Testing rejectedByTarget');
|
||||
error.code = 'rejectedByTarget';
|
||||
throw error;
|
||||
}
|
||||
const content = await ItemClass.serializeForSync(local);
|
||||
await this.api().put(path, content);
|
||||
} catch (error) {
|
||||
if (error && error.code === 'cannotSync') {
|
||||
if (error && error.code === 'rejectedByTarget') {
|
||||
await handleCannotSyncItem(syncTargetId, local, error.message);
|
||||
canSync = false;
|
||||
} else {
|
||||
|
@ -56,6 +56,8 @@ const FsDriverRN = require('lib/fs-driver-rn.js').FsDriverRN;
|
||||
const DecryptionWorker = require('lib/services/DecryptionWorker');
|
||||
const EncryptionService = require('lib/services/EncryptionService');
|
||||
|
||||
let storeDispatch = function(action) {};
|
||||
|
||||
const generalMiddleware = store => next => async (action) => {
|
||||
if (action.type !== 'SIDE_MENU_OPEN_PERCENT') reg.logger().info('Reducer action', action.type);
|
||||
PoorManIntervals.update(); // This function needs to be called regularly so put it here
|
||||
@ -84,7 +86,18 @@ const generalMiddleware = store => next => async (action) => {
|
||||
|
||||
if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'locale' || action.type == 'SETTING_UPDATE_ALL') {
|
||||
setLocale(Setting.value('locale'));
|
||||
}
|
||||
}
|
||||
|
||||
if ((action.type == 'SETTING_UPDATE_ONE' && (action.key.indexOf('encryption.') === 0)) || (action.type == 'SETTING_UPDATE_ALL')) {
|
||||
await EncryptionService.instance().loadMasterKeysFromSettings();
|
||||
DecryptionWorker.instance().scheduleStart();
|
||||
const loadedMasterKeyIds = EncryptionService.instance().loadedMasterKeyIds();
|
||||
|
||||
storeDispatch({
|
||||
type: 'MASTERKEY_REMOVE_NOT_LOADED',
|
||||
ids: loadedMasterKeyIds,
|
||||
});
|
||||
}
|
||||
|
||||
if (action.type == 'NAV_GO' && action.routeName == 'Notes') {
|
||||
Setting.setValue('activeFolderId', newState.selectedFolderId);
|
||||
@ -265,12 +278,13 @@ const appReducer = (state = appDefaultState, action) => {
|
||||
}
|
||||
|
||||
let store = createStore(appReducer, applyMiddleware(generalMiddleware));
|
||||
storeDispatch = store.dispatch;
|
||||
|
||||
async function initialize(dispatch) {
|
||||
shimInit();
|
||||
|
||||
Setting.setConstant('env', __DEV__ ? 'dev' : 'prod');
|
||||
Setting.setConstant('appId', 'net.cozic.joplin');
|
||||
Setting.setConstant('appId', 'net.cozic.joplin-mobile');
|
||||
Setting.setConstant('appType', 'mobile');
|
||||
//Setting.setConstant('resourceDir', () => { return RNFetchBlob.fs.dirs.DocumentDir; });
|
||||
Setting.setConstant('resourceDir', RNFetchBlob.fs.dirs.DocumentDir);
|
||||
@ -367,6 +381,7 @@ async function initialize(dispatch) {
|
||||
EncryptionService.fsDriver_ = fsDriver;
|
||||
EncryptionService.instance().setLogger(mainLogger);
|
||||
BaseItem.encryptionService_ = EncryptionService.instance();
|
||||
DecryptionWorker.instance().dispatch = dispatch;
|
||||
DecryptionWorker.instance().setLogger(mainLogger);
|
||||
DecryptionWorker.instance().setEncryptionService(EncryptionService.instance());
|
||||
await EncryptionService.instance().loadMasterKeysFromSettings();
|
||||
|
Loading…
Reference in New Issue
Block a user