mirror of https://github.com/laurent22/joplin.git
All: Fixed various issues regarding encryption and decryptino of resources, and started doing GUI for Electron app
parent
7750b954fc
commit
6683da804b
|
@ -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 + ']');
|
||||
|
|
Loading…
Reference in New Issue