You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	All: Fixed various issues regarding encryption and decryptino of resources, and started doing GUI for Electron app
This commit is contained in:
		| @@ -14,6 +14,7 @@ const chalk = require('chalk'); | ||||
| const tk = require('terminal-kit'); | ||||
| const TermWrapper = require('tkwidgets/framework/TermWrapper.js'); | ||||
| const Renderer = require('tkwidgets/framework/Renderer.js'); | ||||
| const DecryptionWorker = require('lib/services/DecryptionWorker'); | ||||
|  | ||||
| const BaseWidget = require('tkwidgets/BaseWidget.js'); | ||||
| const ListWidget = require('tkwidgets/ListWidget.js'); | ||||
| @@ -65,6 +66,7 @@ class AppGui { | ||||
| 		// a regular command it's not necessary since the process | ||||
| 		// exits right away. | ||||
| 		reg.setupRecurrentSync(); | ||||
| 		DecryptionWorker.instance().scheduleStart(); | ||||
| 	} | ||||
|  | ||||
| 	store() { | ||||
|   | ||||
| @@ -267,8 +267,19 @@ class Application extends BaseApplication { | ||||
| 							routeName: 'Status', | ||||
| 						}); | ||||
| 					} | ||||
| 				}, { | ||||
| 					type: 'separator', | ||||
| 					screens: ['Main'], | ||||
| 				},{ | ||||
| 					label: _('Options'), | ||||
| 					label: _('Encryption options'), | ||||
| 					click: () => { | ||||
| 						this.dispatch({ | ||||
| 							type: 'NAV_GO', | ||||
| 							routeName: 'EncryptionConfig', | ||||
| 						}); | ||||
| 					} | ||||
| 				},{ | ||||
| 					label: _('General Options'), | ||||
| 					click: () => { | ||||
| 						this.dispatch({ | ||||
| 							type: 'NAV_GO', | ||||
| @@ -276,25 +287,6 @@ class Application extends BaseApplication { | ||||
| 						}); | ||||
| 					} | ||||
| 				}], | ||||
| 			}, { | ||||
| 				label: _('Encryption'), | ||||
| 				submenu: [{ | ||||
| 					label: _('Enable'), | ||||
| 					click: () => { | ||||
| 						// this.dispatch({ | ||||
| 						// 	type: 'NAV_GO', | ||||
| 						// 	routeName: 'MasterKeys', | ||||
| 						// }); | ||||
| 					} | ||||
| 				},{ | ||||
| 					label: _('Master Keys'), | ||||
| 					click: () => { | ||||
| 						this.dispatch({ | ||||
| 							type: 'NAV_GO', | ||||
| 							routeName: 'MasterKeys', | ||||
| 						}); | ||||
| 					} | ||||
| 				}], | ||||
| 			}, { | ||||
| 				label: _('Help'), | ||||
| 				submenu: [{ | ||||
| @@ -414,6 +406,8 @@ class Application extends BaseApplication { | ||||
| 				// Wait for the first sync before updating the notifications, since synchronisation | ||||
| 				// might change the notifications. | ||||
| 				AlarmService.updateAllNotifications(); | ||||
|  | ||||
| 				DecryptionWorker.instance().scheduleStart(); | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -7,7 +7,7 @@ const { themeStyle } = require('../theme.js'); | ||||
| const { _ } = require('lib/locale.js'); | ||||
| const { time } = require('lib/time-utils.js'); | ||||
| 
 | ||||
| class MasterKeysScreenComponent extends React.Component { | ||||
| class EncryptionConfigScreenComponent extends React.Component { | ||||
| 
 | ||||
| 	constructor() { | ||||
| 		super(); | ||||
| @@ -131,6 +131,6 @@ const mapStateToProps = (state) => { | ||||
| 	}; | ||||
| }; | ||||
| 
 | ||||
| const MasterKeysScreen = connect(mapStateToProps)(MasterKeysScreenComponent); | ||||
| const EncryptionConfigScreen = connect(mapStateToProps)(EncryptionConfigScreenComponent); | ||||
| 
 | ||||
| module.exports = { MasterKeysScreen }; | ||||
| module.exports = { EncryptionConfigScreen }; | ||||
| @@ -346,7 +346,7 @@ class MainScreenComponent extends React.Component { | ||||
| 		const onViewMasterKeysClick = () => { | ||||
| 			this.props.dispatch({ | ||||
| 				type: 'NAV_GO', | ||||
| 				routeName: 'MasterKeys', | ||||
| 				routeName: 'EncryptionConfig', | ||||
| 			}); | ||||
| 		} | ||||
|  | ||||
|   | ||||
| @@ -11,7 +11,7 @@ const { OneDriveLoginScreen } = require('./OneDriveLoginScreen.min.js'); | ||||
| const { StatusScreen } = require('./StatusScreen.min.js'); | ||||
| const { ImportScreen } = require('./ImportScreen.min.js'); | ||||
| const { ConfigScreen } = require('./ConfigScreen.min.js'); | ||||
| const { MasterKeysScreen } = require('./MasterKeysScreen.min.js'); | ||||
| const { EncryptionConfigScreen } = require('./EncryptionConfigScreen.min.js'); | ||||
| const { Navigator } = require('./Navigator.min.js'); | ||||
|  | ||||
| const { app } = require('../app'); | ||||
| @@ -78,7 +78,7 @@ class RootComponent extends React.Component { | ||||
| 			Import: { screen: ImportScreen, title: () => _('Import') }, | ||||
| 			Config: { screen: ConfigScreen, title: () => _('Options') }, | ||||
| 			Status: { screen: StatusScreen, title: () => _('Synchronisation Status') }, | ||||
| 			MasterKeys: { screen: MasterKeysScreen, title: () => _('Master Keys') }, | ||||
| 			EncryptionConfig: { screen: EncryptionConfigScreen, title: () => _('Encryption Options') }, | ||||
| 		}; | ||||
|  | ||||
| 		return ( | ||||
|   | ||||
| @@ -8,7 +8,14 @@ Encrypted data is encoded to ASCII because encryption/decryption functions in Re | ||||
|  | ||||
| Name               |  Size | ||||
| -------------------|------------------------- | ||||
| Identifier         |  3 chars ("JED") | ||||
| Version number     |  2 chars (Hexa string) | ||||
|  | ||||
| This is followed by the encryption metadata: | ||||
|  | ||||
| Name               |  Size | ||||
| -------------------|------------------------- | ||||
| Length             |  6 chars (Hexa string) | ||||
| Encryption method  |  2 chars (Hexa string) | ||||
| Master key ID      |  32 chars (Hexa string) | ||||
|  | ||||
|   | ||||
| @@ -268,14 +268,16 @@ class BaseApplication { | ||||
| 		} | ||||
|  | ||||
| 		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(); | ||||
| 			if (this.hasGui()) { | ||||
| 				await EncryptionService.instance().loadMasterKeysFromSettings(); | ||||
| 				DecryptionWorker.instance().scheduleStart(); | ||||
| 				const loadedMasterKeyIds = EncryptionService.instance().loadedMasterKeyIds(); | ||||
|  | ||||
| 			this.dispatch({ | ||||
| 				type: 'MASTERKEY_REMOVE_MISSING', | ||||
| 				ids: loadedMasterKeyIds, | ||||
| 			}); | ||||
| 				this.dispatch({ | ||||
| 					type: 'MASTERKEY_REMOVE_MISSING', | ||||
| 					ids: loadedMasterKeyIds, | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if (action.type == 'TAG_SELECT' || action.type === 'TAG_DELETE') { | ||||
| @@ -302,6 +304,10 @@ class BaseApplication { | ||||
| 			reg.setupRecurrentSync(); | ||||
| 		} | ||||
|  | ||||
| 		if (this.hasGui() && action.type === 'SYNC_GOT_ENCRYPTED_ITEM') { | ||||
| 			DecryptionWorker.instance().scheduleStart(); | ||||
| 		} | ||||
|  | ||||
| 	  	return result; | ||||
| 	} | ||||
|  | ||||
| @@ -411,7 +417,6 @@ class BaseApplication { | ||||
| 		DecryptionWorker.instance().setLogger(this.logger_); | ||||
| 		DecryptionWorker.instance().setEncryptionService(EncryptionService.instance()); | ||||
| 		await EncryptionService.instance().loadMasterKeysFromSettings(); | ||||
| 		DecryptionWorker.instance().scheduleStart(); | ||||
|  | ||||
| 		let currentFolderId = Setting.value('activeFolderId'); | ||||
| 		let currentFolder = null; | ||||
|   | ||||
| @@ -46,11 +46,10 @@ class FsDriverNode { | ||||
|  | ||||
| 	async readFileChunk(handle, length, encoding = 'base64') { | ||||
| 		let buffer = new Buffer(length); | ||||
| 		const bytesRead = await fs.read(handle, buffer, 0, length, null) | ||||
| 		if (!bytesRead) return null; | ||||
| 		const output = buffer.slice(0, bytesRead); | ||||
| 		if (encoding === 'base64') return output.toString('base64'); | ||||
| 		if (encoding === 'ascii') return output.toString('ascii'); | ||||
| 		const result = await fs.read(handle, buffer, 0, length, null) | ||||
| 		if (!result.bytesRead) return null; | ||||
| 		if (encoding === 'base64') return buffer.toString('base64'); | ||||
| 		if (encoding === 'ascii') return buffer.toString('ascii'); | ||||
| 		throw new Error('Unsupported encoding: ' + encoding); | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -268,20 +268,17 @@ class BaseItem extends BaseModel { | ||||
|  | ||||
| 		const cipherText = await this.encryptionService().encryptString(serialized); | ||||
|  | ||||
| 		const reducedItem = Object.assign({}, item); | ||||
| 		//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_']; | ||||
| 		const reducedItem = {}; | ||||
|  | ||||
| 		for (let n in reducedItem) { | ||||
| 			if (!reducedItem.hasOwnProperty(n)) continue; | ||||
|  | ||||
| 			if (keepKeys.indexOf(n) >= 0) { | ||||
| 				continue; | ||||
| 			} else { | ||||
| 				delete reducedItem[n]; | ||||
| 			} | ||||
| 		for (let i = 0; i < keepKeys.length; i++) { | ||||
| 			const n = keepKeys[i]; | ||||
| 			if (!item.hasOwnProperty(n)) continue; | ||||
| 			reducedItem[n] = item[n]; | ||||
| 		} | ||||
|  | ||||
| 		reducedItem.encryption_applied = 1; | ||||
| @@ -370,7 +367,7 @@ class BaseItem extends BaseModel { | ||||
| 			const className = classNames[i]; | ||||
| 			const ItemClass = this.getClass(className); | ||||
|  | ||||
| 			const whereSql = ['encryption_applied = 1']; | ||||
| 			const whereSql = className === 'Resource' ? ['(encryption_blob_encrypted = 1 OR encryption_applied = 1)'] : ['encryption_applied = 1']; | ||||
| 			if (exclusions.length) whereSql.push('id NOT IN ("' + exclusions.join('","') + '")'); | ||||
|  | ||||
| 			const sql = sprintf(` | ||||
|   | ||||
| @@ -53,7 +53,9 @@ class Resource extends BaseItem { | ||||
|  | ||||
| 	// For resources, we need to decrypt the item (metadata) and the resource binary blob. | ||||
| 	static async decrypt(item) { | ||||
| 		const decryptedItem = await super.decrypt(item); | ||||
| 		// The item might already be decrypted but not the blob (for instance if it crashes while | ||||
| 		// decrypting the blob or was otherwise interrupted). | ||||
| 		const decryptedItem = item.encryption_cipher_text ? await super.decrypt(item) : Object.assign({}, item); | ||||
| 		if (!decryptedItem.encryption_blob_encrypted) return decryptedItem; | ||||
|  | ||||
| 		const plainTextPath = this.fullPath(decryptedItem); | ||||
| @@ -69,7 +71,7 @@ class Resource extends BaseItem { | ||||
| 		} | ||||
|  | ||||
| 		await this.encryptionService().decryptFile(encryptedPath, plainTextPath); | ||||
| 		item.encryption_blob_encrypted = 0; | ||||
| 		decryptedItem.encryption_blob_encrypted = 0; | ||||
| 		return Resource.save(decryptedItem, { autoTimestamp: false }); | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -300,21 +300,16 @@ class EncryptionService { | ||||
| 		const mdSizeHex = await source.read(6); | ||||
| 		const md = await source.read(parseInt(mdSizeHex, 16)); | ||||
| 		const header = this.decodeHeader_(identifier + mdSizeHex + md); | ||||
|  | ||||
| 		const masterKeyPlainText = this.loadedMasterKey(header.masterKeyId); | ||||
|  | ||||
| 		let index = header.length; | ||||
|  | ||||
| 		while (true) { | ||||
| 			const lengthHex = await source.read(6); | ||||
| 			if (!lengthHex) break; | ||||
| 			if (lengthHex.length !== 6) throw new Error('Invalid block size: ' + lengthHex); | ||||
| 			const length = parseInt(lengthHex, 16); | ||||
| 			index += 6; | ||||
| 			if (!length) continue; // Weird but could be not completely invalid (block of size 0) so continue decrypting | ||||
|  | ||||
| 			const block = await source.read(length); | ||||
| 			index += length; | ||||
|  | ||||
| 			const plainText = await this.decrypt(header.encryptionMethod, masterKeyPlainText, block); | ||||
| 			await destination.append(plainText); | ||||
| @@ -349,25 +344,23 @@ class EncryptionService { | ||||
| 	} | ||||
|  | ||||
| 	async fileReader_(path, encoding) { | ||||
| 		const fsDriver = this.fsDriver(); | ||||
| 		const handle = await fsDriver.open(path, 'r'); | ||||
| 		const handle = await this.fsDriver().open(path, 'r'); | ||||
| 		const reader = { | ||||
| 			handle: handle, | ||||
| 			read: async function(size) { | ||||
| 				return fsDriver.readFileChunk(reader.handle, size, encoding); | ||||
| 			read: async (size) => { | ||||
| 				return this.fsDriver().readFileChunk(reader.handle, size, encoding); | ||||
| 			}, | ||||
| 			close: function() { | ||||
| 				fsDriver.close(reader.handle); | ||||
| 			close: () => { | ||||
| 				this.fsDriver().close(reader.handle); | ||||
| 			}, | ||||
| 		}; | ||||
| 		return reader; | ||||
| 	} | ||||
|  | ||||
| 	async fileWriter_(path, encoding) { | ||||
| 		const fsDriver = this.fsDriver(); | ||||
| 		return { | ||||
| 			append: async function(data) { | ||||
| 				return fsDriver.appendFile(path, data, encoding); | ||||
| 			append: async (data) => { | ||||
| 				return this.fsDriver().appendFile(path, data, encoding); | ||||
| 			}, | ||||
| 			close: function() {}, | ||||
| 		}; | ||||
| @@ -388,7 +381,6 @@ class EncryptionService { | ||||
| 	} | ||||
|  | ||||
| 	async encryptFile(srcPath, destPath) { | ||||
| 		const fsDriver = this.fsDriver();		 | ||||
| 		let source = await this.fileReader_(srcPath, 'base64'); | ||||
| 		let destination = await this.fileWriter_(destPath, 'ascii'); | ||||
|  | ||||
| @@ -400,11 +392,11 @@ class EncryptionService { | ||||
| 		} | ||||
|  | ||||
| 		try { | ||||
| 			await fsDriver.unlink(destPath); | ||||
| 			await this.fsDriver().unlink(destPath); | ||||
| 			await this.encryptAbstract_(source, destination); | ||||
| 		} catch (error) { | ||||
| 			cleanUp(); | ||||
| 			await fsDriver.unlink(destPath); | ||||
| 			await this.fsDriver().unlink(destPath); | ||||
| 			throw error; | ||||
| 		} | ||||
|  | ||||
| @@ -412,7 +404,6 @@ class EncryptionService { | ||||
| 	} | ||||
|  | ||||
| 	async decryptFile(srcPath, destPath) { | ||||
| 		const fsDriver = this.fsDriver();		 | ||||
| 		let source = await this.fileReader_(srcPath, 'ascii'); | ||||
| 		let destination = await this.fileWriter_(destPath, 'base64'); | ||||
|  | ||||
| @@ -424,11 +415,11 @@ class EncryptionService { | ||||
| 		} | ||||
|  | ||||
| 		try { | ||||
| 			await fsDriver.unlink(destPath); | ||||
| 			await this.fsDriver().unlink(destPath); | ||||
| 			await this.decryptAbstract_(source, destination); | ||||
| 		} catch (error) { | ||||
| 			cleanUp(); | ||||
| 			await fsDriver.unlink(destPath); | ||||
| 			await this.fsDriver().unlink(destPath); | ||||
| 			throw error; | ||||
| 		} | ||||
|  | ||||
| @@ -461,7 +452,7 @@ class EncryptionService { | ||||
| 		const reader = this.stringReader_(headerHexaBytes, true); | ||||
| 		const identifier = reader.read(3); | ||||
| 		const version = parseInt(reader.read(2), 16); | ||||
| 		if (identifier !== 'JED') throw new Error('Invalid header (missing identifier)'); | ||||
| 		if (identifier !== 'JED') throw new Error('Invalid header (missing identifier): ' + headerHexaBytes.substr(0,64)); | ||||
| 		const template = this.headerTemplate(version); | ||||
|  | ||||
| 		const size = parseInt(reader.read(6), 16); | ||||
|   | ||||
| @@ -181,6 +181,7 @@ class Synchronizer { | ||||
| 		this.cancelling_ = false; | ||||
|  | ||||
| 		const masterKeysBefore = await MasterKey.count(); | ||||
| 		let hasAutoEnabledEncryption = false; | ||||
|  | ||||
| 		// ------------------------------------------------------------------------ | ||||
| 		// First, find all the items that have been changed since the | ||||
| @@ -508,6 +509,17 @@ class Synchronizer { | ||||
|  | ||||
| 						await ItemClass.save(content, options); | ||||
|  | ||||
| 						if (!hasAutoEnabledEncryption && content.type_ === BaseModel.TYPE_MASTER_KEY && !masterKeysBefore) { | ||||
| 							hasAutoEnabledEncryption = true; | ||||
| 							this.logger().info('One master key was downloaded and none was previously available: automatically enabling encryption'); | ||||
| 							this.logger().info('Using master key: ', content); | ||||
| 							await this.encryptionService().enableEncryption(content); | ||||
| 							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.'); | ||||
| 						} | ||||
|  | ||||
| 						if (!!content.encryption_applied) this.dispatch({ type: 'SYNC_GOT_ENCRYPTED_ITEM' }); | ||||
|  | ||||
| 					} else if (action == 'deleteLocal') { | ||||
|  | ||||
| 						if (local.type_ == BaseModel.TYPE_FOLDER) { | ||||
| @@ -575,23 +587,6 @@ class Synchronizer { | ||||
| 			this.cancelling_ = false; | ||||
| 		} | ||||
|  | ||||
| 		const masterKeysAfter = await MasterKey.count(); | ||||
|  | ||||
| 		if (!masterKeysBefore && masterKeysAfter) { | ||||
| 			this.logger().info('One master key was downloaded and none was previously available: automatically enabling encryption'); | ||||
| 			const mk = await MasterKey.latest(); | ||||
| 			if (mk) { | ||||
| 				this.logger().info('Using master key: ', mk); | ||||
| 				await this.encryptionService().enableEncryption(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.'); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if (masterKeysAfter && this.autoStartDecryptionWorker_) { | ||||
| 			DecryptionWorker.instance().scheduleStart(); | ||||
| 		} | ||||
|  | ||||
| 		this.progressReport_.completedTime = time.unixMs(); | ||||
|  | ||||
| 		this.logSyncOperation('finished', null, null, 'Synchronisation finished [' + synchronizationId + ']'); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user