You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	All: Allow disabling encryption and added more test cases
This commit is contained in:
		| @@ -29,7 +29,7 @@ class Command extends BaseCommand { | |||||||
| 			const service = new EncryptionService(); | 			const service = new EncryptionService(); | ||||||
| 			let masterKey = await service.generateMasterKey(password); | 			let masterKey = await service.generateMasterKey(password); | ||||||
| 			masterKey = await MasterKey.save(masterKey); | 			masterKey = await MasterKey.save(masterKey); | ||||||
| 			await service.initializeEncryption(masterKey, password); | 			await service.enableEncryption(masterKey, password); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| require('app-module-path').addPath(__dirname); | require('app-module-path').addPath(__dirname); | ||||||
|  |  | ||||||
| const { time } = require('lib/time-utils.js'); | const { time } = require('lib/time-utils.js'); | ||||||
| const { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker } = require('test-utils.js'); | const { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, checkThrowAsync } = require('test-utils.js'); | ||||||
| const { shim } = require('lib/shim.js'); | const { shim } = require('lib/shim.js'); | ||||||
| const Folder = require('lib/models/Folder.js'); | const Folder = require('lib/models/Folder.js'); | ||||||
| const Note = require('lib/models/Note.js'); | const Note = require('lib/models/Note.js'); | ||||||
| @@ -26,6 +26,24 @@ async function allItems() { | |||||||
| 	return folders.concat(notes); | 	return folders.concat(notes); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | async function allSyncTargetItemsEncrypted() { | ||||||
|  | 	const list = await fileApi().list(); | ||||||
|  | 	const files = list.items; | ||||||
|  |  | ||||||
|  | 	let output = false; | ||||||
|  | 	for (let i = 0; i < files.length; i++) { | ||||||
|  | 		const file = files[i]; | ||||||
|  | 		const remoteContentString = await fileApi().get(file.path); | ||||||
|  | 		const remoteContent = await BaseItem.unserialize(remoteContentString); | ||||||
|  | 		const ItemClass = BaseItem.itemClass(remoteContent); | ||||||
|  |  | ||||||
|  | 		if (!ItemClass.encryptionSupported()) continue; | ||||||
|  | 		if (!!remoteContent.encryption_applied) output = true; | ||||||
|  | 	}	 | ||||||
|  |  | ||||||
|  | 	return output; | ||||||
|  | } | ||||||
|  |  | ||||||
| async function localItemsSameAsRemote(locals, expect) { | async function localItemsSameAsRemote(locals, expect) { | ||||||
| 	try { | 	try { | ||||||
| 		let files = await fileApi().list(); | 		let files = await fileApi().list(); | ||||||
| @@ -56,13 +74,19 @@ async function localItemsSameAsRemote(locals, expect) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | let insideBeforeEach = false; | ||||||
|  |  | ||||||
| describe('Synchronizer', function() { | describe('Synchronizer', function() { | ||||||
|  |  | ||||||
| 	beforeEach( async (done) => { | 	beforeEach(async (done) => { | ||||||
|  | 		insideBeforeEach = true; | ||||||
|  |  | ||||||
| 		await setupDatabaseAndSynchronizer(1); | 		await setupDatabaseAndSynchronizer(1); | ||||||
| 		await setupDatabaseAndSynchronizer(2); | 		await setupDatabaseAndSynchronizer(2); | ||||||
| 		await switchClient(1); | 		await switchClient(1); | ||||||
| 		done(); | 		done(); | ||||||
|  |  | ||||||
|  | 		insideBeforeEach = false; | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	it('should create remote items', async (done) => { | 	it('should create remote items', async (done) => { | ||||||
| @@ -605,7 +629,10 @@ describe('Synchronizer', function() { | |||||||
| 		await switchClient(2); | 		await switchClient(2); | ||||||
|  |  | ||||||
| 		await synchronizer().start(); | 		await synchronizer().start(); | ||||||
| 		if (withEncryption) await loadEncryptionMasterKey(null, true); | 		if (withEncryption) { | ||||||
|  | 			await loadEncryptionMasterKey(null, true); | ||||||
|  | 			await decryptionWorker().start(); | ||||||
|  | 		} | ||||||
| 		let note2 = await Note.load(note1.id); | 		let note2 = await Note.load(note1.id); | ||||||
| 		note2.todo_completed = time.unixMs()-1; | 		note2.todo_completed = time.unixMs()-1; | ||||||
| 		await Note.save(note2); | 		await Note.save(note2); | ||||||
| @@ -654,6 +681,12 @@ describe('Synchronizer', function() { | |||||||
| 		done(); | 		done(); | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
|  | 	it('should always handle conflict if local or remote are encrypted', async (done) => { | ||||||
|  | 		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', async (done) => { | ||||||
| 		let folder1 = await Folder.save({ title: "folder1" }); | 		let folder1 = await Folder.save({ title: "folder1" }); | ||||||
| 		let note1 = await Note.save({ title: "un", is_todo: 1, parent_id: folder1.id }); | 		let note1 = await Note.save({ title: "un", is_todo: 1, parent_id: folder1.id }); | ||||||
| @@ -746,12 +779,6 @@ describe('Synchronizer', function() { | |||||||
| 		done(); | 		done(); | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	it('should always handle conflict if local or remote are encrypted', async (done) => { |  | ||||||
| 		await ignorableNoteConflictTest(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)', async (done) => { | ||||||
| 		// Enable encryption on client 1 and sync an item | 		// Enable encryption on client 1 and sync an item | ||||||
| 		Setting.setValue('encryption.enabled', true); | 		Setting.setValue('encryption.enabled', true); | ||||||
| @@ -776,7 +803,7 @@ describe('Synchronizer', function() { | |||||||
| 		// If we sync now, nothing should be sent to target since we don't have a password. | 		// If we sync now, nothing should be sent to target since we don't have a password. | ||||||
| 		// Technically it's incorrect to set the property of an encrypted variable but it allows confirming | 		// Technically it's incorrect to set the property of an encrypted variable but it allows confirming | ||||||
| 		// that encryption doesn't work if user hasn't supplied a password. | 		// that encryption doesn't work if user hasn't supplied a password. | ||||||
| 		let folder1_2 = await Folder.save({ id: folder1.id, title: "change test" }); | 		await BaseItem.forceSync(folder1.id); | ||||||
| 		await synchronizer().start(); | 		await synchronizer().start(); | ||||||
|  |  | ||||||
| 		await switchClient(1); | 		await switchClient(1); | ||||||
| @@ -814,7 +841,6 @@ describe('Synchronizer', function() { | |||||||
|  |  | ||||||
| 	it('should encrypt existing notes too when enabling E2EE', async (done) => { | 	it('should encrypt existing notes too when enabling E2EE', async (done) => { | ||||||
| 		// First create a folder, without encryption enabled, and sync it | 		// First create a folder, without encryption enabled, and sync it | ||||||
| 		const service = encryptionService(); |  | ||||||
| 		let folder1 = await Folder.save({ title: "folder1" }); | 		let folder1 = await Folder.save({ title: "folder1" }); | ||||||
| 		await synchronizer().start(); | 		await synchronizer().start(); | ||||||
| 		let files = await fileApi().list() | 		let files = await fileApi().list() | ||||||
| @@ -822,10 +848,10 @@ describe('Synchronizer', function() { | |||||||
| 		expect(content.indexOf('folder1') >= 0).toBe(true) | 		expect(content.indexOf('folder1') >= 0).toBe(true) | ||||||
|  |  | ||||||
| 		// Then enable encryption and sync again | 		// Then enable encryption and sync again | ||||||
| 		let masterKey = await service.generateMasterKey('123456'); | 		let masterKey = await encryptionService().generateMasterKey('123456'); | ||||||
| 		masterKey = await MasterKey.save(masterKey); | 		masterKey = await MasterKey.save(masterKey); | ||||||
| 		await service.initializeEncryption(masterKey, '123456'); | 		await encryptionService().enableEncryption(masterKey, '123456'); | ||||||
| 		await service.loadMasterKeysFromSettings(); | 		await encryptionService().loadMasterKeysFromSettings(); | ||||||
| 		await synchronizer().start(); | 		await synchronizer().start(); | ||||||
| 		 | 		 | ||||||
| 		// Even though the folder has not been changed it should have been synced again so that | 		// Even though the folder has not been changed it should have been synced again so that | ||||||
| @@ -849,11 +875,14 @@ describe('Synchronizer', function() { | |||||||
| 		let resource1 = (await Resource.all())[0]; | 		let resource1 = (await Resource.all())[0]; | ||||||
| 		let resourcePath1 = Resource.fullPath(resource1); | 		let resourcePath1 = Resource.fullPath(resource1); | ||||||
| 		await synchronizer().start(); | 		await synchronizer().start(); | ||||||
|  | 		expect((await fileApi().list()).items.length).toBe(3); | ||||||
|  |  | ||||||
| 		await switchClient(2); | 		await switchClient(2); | ||||||
|  |  | ||||||
| 		await synchronizer().start(); | 		await synchronizer().start(); | ||||||
| 		let resource1_2 = (await Resource.all())[0]; | 		let allResources = await Resource.all(); | ||||||
|  | 		expect(allResources.length).toBe(1); | ||||||
|  | 		let resource1_2 = allResources[0]; | ||||||
| 		let resourcePath1_2 = Resource.fullPath(resource1_2); | 		let resourcePath1_2 = Resource.fullPath(resource1_2); | ||||||
|  |  | ||||||
| 		expect(resource1_2.id).toBe(resource1.id); | 		expect(resource1_2.id).toBe(resource1.id); | ||||||
| @@ -888,4 +917,62 @@ describe('Synchronizer', function() { | |||||||
| 		done(); | 		done(); | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
|  | 	it('should upload decrypted items to sync target after encryption disabled', async (done) => { | ||||||
|  | 		Setting.setValue('encryption.enabled', true); | ||||||
|  | 		const masterKey = await loadEncryptionMasterKey(); | ||||||
|  |  | ||||||
|  | 		let folder1 = await Folder.save({ title: "folder1" }); | ||||||
|  | 		await synchronizer().start(); | ||||||
|  |  | ||||||
|  | 		let allEncrypted = await allSyncTargetItemsEncrypted(); | ||||||
|  | 		expect(allEncrypted).toBe(true); | ||||||
|  |  | ||||||
|  | 		await encryptionService().disableEncryption(); | ||||||
|  |  | ||||||
|  | 		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) => { | ||||||
|  | 		// 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); | ||||||
|  |  | ||||||
|  | 		Setting.setValue('encryption.enabled', true); | ||||||
|  | 		const masterKey = await loadEncryptionMasterKey(); | ||||||
|  |  | ||||||
|  | 		let folder1 = await Folder.save({ title: "folder1" }); | ||||||
|  | 		await synchronizer().start(); | ||||||
|  |  | ||||||
|  | 		await switchClient(2); | ||||||
|  |  | ||||||
|  | 		await synchronizer().start(); | ||||||
|  | 		expect(Setting.value('encryption.enabled')).toBe(true); | ||||||
|  |  | ||||||
|  | 		// 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); | ||||||
|  |  | ||||||
|  | 		// Now supply the password, and decrypt the items | ||||||
|  | 		Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456'); | ||||||
|  | 		await encryptionService().loadMasterKeysFromSettings();	 | ||||||
|  | 		await decryptionWorker().start(); | ||||||
|  |  | ||||||
|  | 		// Try to disable encryption again | ||||||
|  | 		hasThrown = await checkThrowAsync(async () => await encryptionService().disableEncryption()); | ||||||
|  | 		expect(hasThrown).toBe(false); | ||||||
|  |  | ||||||
|  | 		// If we sync now the target should receive the decrypted items | ||||||
|  | 		await synchronizer().start(); | ||||||
|  | 		allEncrypted = await allSyncTargetItemsEncrypted(); | ||||||
|  | 		expect(allEncrypted).toBe(false); | ||||||
|  |  | ||||||
|  | 		done(); | ||||||
|  | 	}); | ||||||
|  |  | ||||||
| }); | }); | ||||||
| @@ -98,7 +98,7 @@ async function switchClient(id) { | |||||||
| 	return Setting.load(); | 	return Setting.load(); | ||||||
| } | } | ||||||
|  |  | ||||||
| function clearDatabase(id = null) { | async function clearDatabase(id = null) { | ||||||
| 	if (id === null) id = currentClient_; | 	if (id === null) id = currentClient_; | ||||||
|  |  | ||||||
| 	let queries = [ | 	let queries = [ | ||||||
| @@ -114,31 +114,53 @@ function clearDatabase(id = null) { | |||||||
| 		'DELETE FROM sync_items', | 		'DELETE FROM sync_items', | ||||||
| 	]; | 	]; | ||||||
|  |  | ||||||
| 	return databases_[id].transactionExecBatch(queries); | 	await databases_[id].transactionExecBatch(queries); | ||||||
| } | } | ||||||
|  |  | ||||||
| function setupDatabase(id = null) { | async function setupDatabase(id = null) { | ||||||
| 	if (id === null) id = currentClient_; | 	if (id === null) id = currentClient_; | ||||||
|  |  | ||||||
|  | 	Setting.cancelScheduleSave(); | ||||||
|  | 	Setting.cache_ = null; | ||||||
|  |  | ||||||
| 	if (databases_[id]) { | 	if (databases_[id]) { | ||||||
| 		return clearDatabase(id).then(() => { | 		await clearDatabase(id); | ||||||
| 			return Setting.load(); | 		await Setting.load(); | ||||||
| 		}); | 		return; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	const filePath = __dirname + '/data/test-' + id + '.sqlite'; | 	const filePath = __dirname + '/data/test-' + id + '.sqlite'; | ||||||
| 	// Setting.setConstant('resourceDir', RNFetchBlob.fs.dirs.DocumentDir); |  | ||||||
| 	return fs.unlink(filePath).catch(() => { | 	try { | ||||||
|  | 		await fs.unlink(filePath); | ||||||
|  | 	} catch (error) { | ||||||
| 		// Don't care if the file doesn't exist | 		// Don't care if the file doesn't exist | ||||||
| 	}).then(() => { | 	}; | ||||||
| 		databases_[id] = new JoplinDatabase(new DatabaseDriverNode()); |  | ||||||
| 		// databases_[id].setLogger(logger); | 	databases_[id] = new JoplinDatabase(new DatabaseDriverNode()); | ||||||
| 		// console.info(filePath); | 	await databases_[id].open({ name: filePath }); | ||||||
| 		return databases_[id].open({ name: filePath }).then(() => { |  | ||||||
| 			BaseModel.db_ = databases_[id]; | 	BaseModel.db_ = databases_[id]; | ||||||
| 			return setupDatabase(id); | 	await Setting.load(); | ||||||
| 		}); | 	//return setupDatabase(id); | ||||||
| 	}); |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 	// return databases_[id].open({ name: filePath }).then(() => { | ||||||
|  | 	// 	BaseModel.db_ = databases_[id]; | ||||||
|  | 	// 	return setupDatabase(id); | ||||||
|  | 	// }); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 	// return fs.unlink(filePath).catch(() => { | ||||||
|  | 	// 	// Don't care if the file doesn't exist | ||||||
|  | 	// }).then(() => { | ||||||
|  | 	// 	databases_[id] = new JoplinDatabase(new DatabaseDriverNode()); | ||||||
|  | 	// 	return databases_[id].open({ name: filePath }).then(() => { | ||||||
|  | 	// 		BaseModel.db_ = databases_[id]; | ||||||
|  | 	// 		return setupDatabase(id); | ||||||
|  | 	// 	}); | ||||||
|  | 	// }); | ||||||
| } | } | ||||||
|  |  | ||||||
| function resourceDir(id = null) { | function resourceDir(id = null) { | ||||||
| @@ -151,6 +173,9 @@ async function setupDatabaseAndSynchronizer(id = null) { | |||||||
|  |  | ||||||
| 	await setupDatabase(id); | 	await setupDatabase(id); | ||||||
|  |  | ||||||
|  | 	EncryptionService.instance_ = null; | ||||||
|  | 	DecryptionWorker.instance_ = null; | ||||||
|  |  | ||||||
| 	await fs.remove(resourceDir(id)); | 	await fs.remove(resourceDir(id)); | ||||||
| 	await fs.mkdirp(resourceDir(id), 0o755); | 	await fs.mkdirp(resourceDir(id), 0o755); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -96,8 +96,11 @@ class BaseModel { | |||||||
| 		return options; | 		return options; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	static count() { | 	static count(options = null) { | ||||||
| 		return this.db().selectOne('SELECT count(*) as total FROM `' + this.tableName() + '`').then((r) => { | 		if (!options) options = {}; | ||||||
|  | 		let sql = 'SELECT count(*) as total FROM `' + this.tableName() + '`'; | ||||||
|  | 		if (options.where) sql += ' WHERE ' + options.where; | ||||||
|  | 		return this.db().selectOne(sql).then((r) => { | ||||||
| 			return r ? r['total'] : 0; | 			return r ? r['total'] : 0; | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -104,29 +104,38 @@ class Database { | |||||||
| 		return this.tryCall('exec', sql, params); | 		return this.tryCall('exec', sql, params); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	transactionExecBatch(queries) { | 	async transactionExecBatch(queries) { | ||||||
| 		if (queries.length <= 0) return Promise.resolve(); | 		if (queries.length <= 0) return; | ||||||
|  |  | ||||||
| 		if (queries.length == 1) { | 		if (queries.length == 1) { | ||||||
| 			let q = this.wrapQuery(queries[0]); | 			let q = this.wrapQuery(queries[0]); | ||||||
| 			return this.exec(q.sql, q.params); | 			await this.exec(q.sql, q.params); | ||||||
|  | 			return; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// There can be only one transaction running at a time so queue | 		// There can be only one transaction running at a time so queue | ||||||
| 		// any new transaction here. | 		// any new transaction here. | ||||||
| 		if (this.inTransaction_) { | 		if (this.inTransaction_) { | ||||||
| 			return new Promise((resolve, reject) => { | 			while (true) { | ||||||
| 				let iid = setInterval(() => { | 				await time.msleep(100); | ||||||
| 					if (!this.inTransaction_) { | 				if (!this.inTransaction_) { | ||||||
| 						clearInterval(iid); | 					this.inTransaction_ = true; | ||||||
| 						this.transactionExecBatch(queries).then(() => { | 					break; | ||||||
| 							resolve(); | 				} | ||||||
| 						}).catch((error) => { | 			} | ||||||
| 							reject(error); |  | ||||||
| 						}); | 			// return new Promise((resolve, reject) => { | ||||||
| 					} | 			// 	let iid = setInterval(() => { | ||||||
| 				}, 100); | 			// 		if (!this.inTransaction_) { | ||||||
| 			}); | 			// 			clearInterval(iid); | ||||||
|  | 			// 			this.transactionExecBatch(queries).then(() => { | ||||||
|  | 			// 				resolve(); | ||||||
|  | 			// 			}).catch((error) => { | ||||||
|  | 			// 				reject(error); | ||||||
|  | 			// 			}); | ||||||
|  | 			// 		} | ||||||
|  | 			// 	}, 100); | ||||||
|  | 			// }); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		this.inTransaction_ = true; | 		this.inTransaction_ = true; | ||||||
| @@ -134,17 +143,62 @@ class Database { | |||||||
| 		queries.splice(0, 0, 'BEGIN TRANSACTION'); | 		queries.splice(0, 0, 'BEGIN TRANSACTION'); | ||||||
| 		queries.push('COMMIT'); // Note: ROLLBACK is currently not supported | 		queries.push('COMMIT'); // Note: ROLLBACK is currently not supported | ||||||
|  |  | ||||||
| 		let chain = []; |  | ||||||
| 		for (let i = 0; i < queries.length; i++) { | 		for (let i = 0; i < queries.length; i++) { | ||||||
| 			let query = this.wrapQuery(queries[i]); | 			let query = this.wrapQuery(queries[i]); | ||||||
| 			chain.push(() => { | 			await this.exec(query.sql, query.params); | ||||||
| 				return this.exec(query.sql, query.params); |  | ||||||
| 			}); |  | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		return promiseChain(chain).then(() => { | 		this.inTransaction_ = false; | ||||||
| 			this.inTransaction_ = false; |  | ||||||
| 		}); | 		// return promiseChain(chain).then(() => { | ||||||
|  | 		// 	this.inTransaction_ = false; | ||||||
|  | 		// }); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 		// if (queries.length <= 0) return Promise.resolve(); | ||||||
|  |  | ||||||
|  | 		// if (queries.length == 1) { | ||||||
|  | 		// 	let q = this.wrapQuery(queries[0]); | ||||||
|  | 		// 	return this.exec(q.sql, q.params); | ||||||
|  | 		// } | ||||||
|  |  | ||||||
|  | 		// // There can be only one transaction running at a time so queue | ||||||
|  | 		// // any new transaction here. | ||||||
|  | 		// if (this.inTransaction_) { | ||||||
|  | 		// 	return new Promise((resolve, reject) => { | ||||||
|  | 		// 		let iid = setInterval(() => { | ||||||
|  | 		// 			if (!this.inTransaction_) { | ||||||
|  | 		// 				clearInterval(iid); | ||||||
|  | 		// 				this.transactionExecBatch(queries).then(() => { | ||||||
|  | 		// 					resolve(); | ||||||
|  | 		// 				}).catch((error) => { | ||||||
|  | 		// 					reject(error); | ||||||
|  | 		// 				}); | ||||||
|  | 		// 			} | ||||||
|  | 		// 		}, 100); | ||||||
|  | 		// 	}); | ||||||
|  | 		// } | ||||||
|  |  | ||||||
|  | 		// this.inTransaction_ = true; | ||||||
|  |  | ||||||
|  | 		// queries.splice(0, 0, 'BEGIN TRANSACTION'); | ||||||
|  | 		// queries.push('COMMIT'); // Note: ROLLBACK is currently not supported | ||||||
|  |  | ||||||
|  | 		// let chain = []; | ||||||
|  | 		// for (let i = 0; i < queries.length; i++) { | ||||||
|  | 		// 	let query = this.wrapQuery(queries[i]); | ||||||
|  | 		// 	chain.push(() => { | ||||||
|  | 		// 		return this.exec(query.sql, query.params); | ||||||
|  | 		// 	}); | ||||||
|  | 		// } | ||||||
|  |  | ||||||
|  | 		// return promiseChain(chain).then(() => { | ||||||
|  | 		// 	this.inTransaction_ = false; | ||||||
|  | 		// }); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	static enumId(type, s) { | 	static enumId(type, s) { | ||||||
|   | |||||||
| @@ -258,7 +258,13 @@ class BaseItem extends BaseModel { | |||||||
| 	static async serializeForSync(item) { | 	static async serializeForSync(item) { | ||||||
| 		const ItemClass = this.itemClass(item); | 		const ItemClass = this.itemClass(item); | ||||||
| 		let serialized = await ItemClass.serialize(item); | 		let serialized = await ItemClass.serialize(item); | ||||||
| 		if (!Setting.value('encryption.enabled') || !ItemClass.encryptionSupported()) return serialized; | 		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'); | ||||||
|  | 			return serialized; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if (!!item.encryption_applied) { const e = new Error('Trying to encrypt item that is already encrypted'); e.code = 'cannotEncryptEncrypted'; throw e; } | ||||||
|  |  | ||||||
| 		const cipherText = await this.encryptionService().encryptString(serialized); | 		const cipherText = await this.encryptionService().encryptString(serialized); | ||||||
|  |  | ||||||
| @@ -343,6 +349,20 @@ class BaseItem extends BaseModel { | |||||||
| 		return output; | 		return output; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	static async hasEncryptedItems() { | ||||||
|  | 		const classNames = this.encryptableItemClassNames(); | ||||||
|  |  | ||||||
|  | 		for (let i = 0; i < classNames.length; i++) { | ||||||
|  | 			const className = classNames[i]; | ||||||
|  | 			const ItemClass = this.getClass(className); | ||||||
|  |  | ||||||
|  | 			const count = await ItemClass.count({ where: 'encryption_applied = 1' }); | ||||||
|  | 			if (count) return true; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return false; | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	static async itemsThatNeedDecryption(exclusions = [], limit = 100) { | 	static async itemsThatNeedDecryption(exclusions = [], limit = 100) { | ||||||
| 		const classNames = this.encryptableItemClassNames(); | 		const classNames = this.encryptableItemClassNames(); | ||||||
|  |  | ||||||
| @@ -568,6 +588,14 @@ class BaseItem extends BaseModel { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	static async forceSync(itemId) { | ||||||
|  | 		await this.db().exec('UPDATE sync_items SET force_sync = 1 WHERE item_id = ?', [itemId]); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	static async forceSyncAll() { | ||||||
|  | 		await this.db().exec('UPDATE sync_items SET force_sync = 1'); | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	static async save(o, options = null) { | 	static async save(o, options = null) { | ||||||
| 		if (!options) options = {}; | 		if (!options) options = {}; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -81,10 +81,14 @@ class Resource extends BaseItem { | |||||||
| 		const plainTextPath = this.fullPath(resource); | 		const plainTextPath = this.fullPath(resource); | ||||||
|  |  | ||||||
| 		if (!Setting.value('encryption.enabled')) { | 		if (!Setting.value('encryption.enabled')) { | ||||||
| 			if (resource.encryption_blob_encrypted) { | 			// Sanity check - normally not possible | ||||||
| 				resource.encryption_blob_encrypted = 0; | 			if (!!resource.encryption_blob_encrypted) throw new Error('Trying to access encrypted resource but encryption is currently disabled'); | ||||||
| 				await Resource.save(resource, { autoTimestamp: false }); |  | ||||||
| 			} | 			// // 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 }; | 			return { path: plainTextPath, resource: resource }; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ const { shim } = require('lib/shim.js'); | |||||||
| const Setting = require('lib/models/Setting.js'); | const Setting = require('lib/models/Setting.js'); | ||||||
| const MasterKey = require('lib/models/MasterKey'); | const MasterKey = require('lib/models/MasterKey'); | ||||||
| const BaseItem = require('lib/models/BaseItem'); | const BaseItem = require('lib/models/BaseItem'); | ||||||
|  | const { _ } = require('lib/locale.js'); | ||||||
|  |  | ||||||
| function hexPad(s, length) { | function hexPad(s, length) { | ||||||
| 	return padLeft(s, length, '0'); | 	return padLeft(s, length, '0'); | ||||||
| @@ -33,7 +34,7 @@ class EncryptionService { | |||||||
| 		return this.logger_; | 		return this.logger_; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	async initializeEncryption(masterKey, password = null) { | 	async enableEncryption(masterKey, password = null) { | ||||||
| 		Setting.setValue('encryption.enabled', true); | 		Setting.setValue('encryption.enabled', true); | ||||||
| 		Setting.setValue('encryption.activeMasterKeyId', masterKey.id); | 		Setting.setValue('encryption.activeMasterKeyId', masterKey.id); | ||||||
|  |  | ||||||
| @@ -43,9 +44,21 @@ class EncryptionService { | |||||||
| 			Setting.setValue('encryption.passwordCache', passwordCache);		 | 			Setting.setValue('encryption.passwordCache', passwordCache);		 | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		// Mark only the non-encrypted ones for sync since, if there are encrypted ones, | ||||||
|  | 		// it means they come from the sync target and are already encrypted over there. | ||||||
| 		await BaseItem.markAllNonEncryptedForSync(); | 		await BaseItem.markAllNonEncryptedForSync(); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	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.')); | ||||||
|  | 		 | ||||||
|  | 		Setting.setValue('encryption.enabled', false); | ||||||
|  | 		// The only way to make sure everything gets decrypted on the sync target is | ||||||
|  | 		// to re-sync everything. | ||||||
|  | 		await BaseItem.forceSyncAll(); | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	async loadMasterKeysFromSettings() { | 	async loadMasterKeysFromSettings() { | ||||||
| 		if (!Setting.value('encryption.enabled')) { | 		if (!Setting.value('encryption.enabled')) { | ||||||
| 			this.unloadAllMasterKeys(); | 			this.unloadAllMasterKeys(); | ||||||
|   | |||||||
| @@ -217,7 +217,6 @@ class Synchronizer { | |||||||
| 					if (donePaths.indexOf(path) > 0) throw new Error(sprintf('Processing a path that has already been done: %s. sync_time was not updated?', path)); | 					if (donePaths.indexOf(path) > 0) throw new Error(sprintf('Processing a path that has already been done: %s. sync_time was not updated?', path)); | ||||||
|  |  | ||||||
| 					let remote = await this.api().stat(path); | 					let remote = await this.api().stat(path); | ||||||
| 					//let content = await ItemClass.serializeForSync(local); |  | ||||||
| 					let action = null; | 					let action = null; | ||||||
| 					let updateSyncTimeOnly = true; | 					let updateSyncTimeOnly = true; | ||||||
| 					let reason = '';					 | 					let reason = '';					 | ||||||
| @@ -561,9 +560,9 @@ class Synchronizer { | |||||||
| 				await BaseItem.deleteOrphanSyncItems(); | 				await BaseItem.deleteOrphanSyncItems(); | ||||||
| 			} | 			} | ||||||
| 		} catch (error) { | 		} catch (error) { | ||||||
| 			if (error && error.code === 'noActiveMasterKey') { | 			if (error && ['cannotEncryptEncrypted', 'noActiveMasterKey'].indexOf(error.code) >= 0) { | ||||||
| 				// Don't log an error for this as this is a common | 				// Only log an info statement for this since this is a common condition that is reported | ||||||
| 				// condition that the UI should report anyway. | 				// in the application, and needs to be resolved by the user | ||||||
| 				this.logger().info(error.message); | 				this.logger().info(error.message); | ||||||
| 			} else { | 			} else { | ||||||
| 				this.logger().error(error); | 				this.logger().error(error); | ||||||
| @@ -583,7 +582,7 @@ class Synchronizer { | |||||||
| 			const mk = await MasterKey.latest(); | 			const mk = await MasterKey.latest(); | ||||||
| 			if (mk) { | 			if (mk) { | ||||||
| 				this.logger().info('Using master key: ', mk); | 				this.logger().info('Using master key: ', mk); | ||||||
| 				await this.encryptionService().initializeEncryption(mk); | 				await this.encryptionService().enableEncryption(mk); | ||||||
| 				await this.encryptionService().loadMasterKeysFromSettings(); | 				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.'); | 				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.'); | ||||||
| 			} | 			} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user