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

All: Testing and better handling of E2EE initialisation

This commit is contained in:
Laurent Cozic 2017-12-17 20:51:45 +01:00
parent f5d26e0d81
commit 4c0b472f67
7 changed files with 136 additions and 13 deletions

View File

@ -603,6 +603,7 @@ describe('Synchronizer', function() {
await switchClient(2);
await synchronizer().start();
if (withEncryption) await loadEncryptionMasterKey(null, true);
let note2 = await Note.load(note1.id);
note2.todo_completed = time.unixMs()-1;
await Note.save(note2);
@ -749,4 +750,88 @@ describe('Synchronizer', function() {
done();
});
it('should enable encryption automatically when downloading new master key (and none was previously available)', async (done) => {
// Enable encryption on client 1 and sync an item
Setting.setValue('encryption.enabled', true);
await loadEncryptionMasterKey();
let folder1 = await Folder.save({ title: "folder1" });
await synchronizer().start();
await switchClient(2);
// Synchronising should enable encryption since we're going to get a master key
expect(Setting.value('encryption.enabled')).toBe(false);
await synchronizer().start();
expect(Setting.value('encryption.enabled')).toBe(true);
// Check that we got the master key from client 1
const masterKey = (await MasterKey.all())[0];
expect(!!masterKey).toBe(true);
// Since client 2 hasn't supplied a password yet, no master key is currently loaded
expect(encryptionService().loadedMasterKeyIds().length).toBe(0);
// If we sync now, nothing should be sent to target since we don't have a password
let folder1_2 = await Folder.save({ id: folder1.id, title: "change test" });
await synchronizer().start();
await switchClient(1);
await synchronizer().start();
folder1 = await Folder.load(folder1.id);
expect(folder1.title).toBe('folder1'); // Still at old value
await switchClient(2);
// Now client 2 set the master key password
Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456');
await encryptionService().loadMasterKeysFromSettings();
// Now that master key should be loaded
expect(encryptionService().loadedMasterKeyIds()[0]).toBe(masterKey.id);
// If we sync now, this time client 1 should get the changes we did earlier
await synchronizer().start();
await switchClient(1);
// NOTE: there might be a race condition here but can't figure it out. Up to this point all the tests
// will pass, which means the master key is loaded. However, the below test find that the title is still
// the previous value. Possible reasons are:
// - Client 2 didn't send the updated item
// - Client 1 didn't receive it
// Maybe due to sync_time/updated_time having the same value on one or both of the clients when tests run fast?
await synchronizer().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) => {
// First create a folder, without encryption enabled, and sync it
const service = encryptionService();
let folder1 = await Folder.save({ title: "folder1" });
await synchronizer().start();
let files = await fileApi().list()
expect(files.items[0].content.indexOf('folder1') >= 0).toBe(true)
// Then enable encryption and sync again
let masterKey = await service.generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
await service.initializeEncryption(masterKey, '123456');
await service.loadMasterKeysFromSettings();
await synchronizer().start();
// 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()
expect(files.items.length).toBe(2);
// 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
expect(files.items[0].content.indexOf('folder1') < 0).toBe(true);
expect(files.items[1].content.indexOf('folder1') < 0).toBe(true);
done();
});
});

View File

@ -51,7 +51,7 @@ const sleepTime = syncTargetId_ == SyncTargetRegistry.nameToId('filesystem') ? 1
const logger = new Logger();
logger.addTarget('console');
logger.addTarget('file', { path: logDir + '/log.txt' });
logger.setLevel(Logger.LEVEL_WARN);
logger.setLevel(Logger.LEVEL_WARN); // Set to INFO to display sync process in console
BaseItem.loadClass('Note', Note);
BaseItem.loadClass('Folder', Folder);
@ -173,11 +173,19 @@ function encryptionService(id = null) {
return encryptionServices_[id];
}
async function loadEncryptionMasterKey(id = null) {
async function loadEncryptionMasterKey(id = null, useExisting = false) {
const service = encryptionService(id);
let masterKey = await service.generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
let masterKey = null;
if (!useExisting) { // Create it
masterKey = await service.generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
} else { // Use the one already available
materKey = await MasterKey.all();
if (!materKey.length) throw new Error('No mater key available');
masterKey = materKey[0];
}
await service.loadMasterKey(masterKey, '123456', true);

View File

@ -41,14 +41,19 @@ class MasterKeysScreenComponent extends React.Component {
renderMasterKey(mk) {
const onSaveClick = () => {
const password = this.state.passwords[mk.id];
const cache = Setting.value('encryption.passwordCache');
if (!cache) cache = {};
if (!password) {
delete cache[mk.id];
Setting.deleteObjectKey('encryption.passwordCache', mk.id);
} else {
cache[mk.id] = password;
Setting.setObjectKey('encryption.passwordCache', mk.id, password);
}
Setting.setValue('encryption.passwordCache', cache);
// const cache = Setting.value('encryption.passwordCache');
// if (!cache) cache = {};
// if (!password) {
// delete cache[mk.id];
// } else {
// cache[mk.id] = password;
// }
// Setting.setValue('encryption.passwordCache', cache);
this.checkPasswords();
}

View File

@ -221,6 +221,20 @@ class Setting extends BaseModel {
this.scheduleSave();
}
static setObjectKey(settingKey, objectKey, value) {
const o = this.value(settingKey);
if (typeof o !== 'object') o = {};
o[objectKey] = value;
this.setValue(settingKey, o);
}
static deleteObjectKey(settingKey, objectKey) {
const o = this.value(settingKey);
if (typeof o !== 'object') return;
delete o[objectKey];
this.setValue(settingKey, o);
}
static valueToString(key, value) {
const md = this.settingMetadata(key);
value = this.formatValue(key, value);

View File

@ -6,7 +6,7 @@ class DecryptionWorker {
this.state_ = 'idle';
this.dispatch = (action) => {
console.warn('DecryptionWorker.dispatch is not defined');
//console.warn('DecryptionWorker.dispatch is not defined');
};
this.scheduleId_ = null;

View File

@ -82,7 +82,11 @@ class EncryptionService {
}
activeMasterKeyId() {
if (!this.activeMasterKeyId_) throw new Error('No master key is defined as active');
if (!this.activeMasterKeyId_) {
const error = 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_;
}

View File

@ -570,8 +570,14 @@ class Synchronizer {
await BaseItem.deleteOrphanSyncItems();
}
} catch (error) {
this.logger().error(error);
this.progressReport_.errors.push(error);
if (error && error.code === 'noActiveMasterKey') {
// Don't log an error for this as this is a common
// condition that the UI should report anyway.
this.logger().info(error.message);
} else {
this.logger().error(error);
this.progressReport_.errors.push(error);
}
}
if (this.cancelling()) {
@ -588,6 +594,7 @@ class Synchronizer {
this.logger().info('Using master key: ', mk);
await this.encryptionService().initializeEncryption(mk);
await this.encryptionService().loadMasterKeysFromSettings();
this.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.');
}
}