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

All: getting encryption service and UI to work

This commit is contained in:
Laurent Cozic 2017-12-14 19:39:13 +00:00
parent d9c1e30e9b
commit 2c608bca3c
12 changed files with 328 additions and 68 deletions

View File

@ -27,16 +27,9 @@ class Command extends BaseCommand {
}
const service = new EncryptionService();
let masterKey = await service.generateMasterKey(password);
masterKey = await MasterKey.save(masterKey);
Setting.setValue('encryption.enabled', true);
Setting.setValue('encryption.activeMasterKeyId', masterKey.id);
let passwordCache = Setting.value('encryption.passwordCache');
passwordCache[masterKey.id] = password;
Setting.setValue('encryption.passwordCache', passwordCache);
await service.initializeEncryption(masterKey, password);
}
}

View File

@ -256,7 +256,7 @@ class Application extends BaseApplication {
name: 'search',
});
},
}]
}],
}, {
label: _('Tools'),
submenu: [{
@ -275,7 +275,26 @@ class Application extends BaseApplication {
routeName: 'Config',
});
}
}]
}],
}, {
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: [{

View File

@ -1,6 +1,7 @@
const React = require('react');
const { connect } = require('react-redux');
const MasterKeys = require('lib/models/MasterKey');
const EncryptionService = require('lib/services/EncryptionService');
const { Header } = require('./Header.min.js');
const { themeStyle } = require('../theme.js');
const { _ } = require('lib/locale.js');
@ -8,13 +9,69 @@ const { time } = require('lib/time-utils.js');
class MasterKeysScreenComponent extends React.Component {
constructor() {
super();
this.state = {
masterKeys: [],
passwords: {},
passwordChecks: {},
};
}
componentWillMount() {
this.setState({
masterKeys: this.props.masterKeys,
passwords: this.props.passwords ? this.props.passwords : {},
});
this.checkPasswords();
}
async checkPasswords() {
const passwordChecks = Object.assign({}, this.state.passwordChecks);
for (let i = 0; i < this.state.masterKeys.length; i++) {
const mk = this.state.masterKeys[i];
const password = this.state.passwords[mk.id];
const ok = password ? await EncryptionService.instance().checkMasterKeyPassword(mk, password) : false;
passwordChecks[mk.id] = ok;
}
this.setState({ passwordChecks: passwordChecks });
}
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];
} else {
cache[mk.id] = password;
}
Setting.setValue('encryption.passwordCache', cache);
this.checkPasswords();
}
const onPasswordChange = (event) => {
const passwords = this.state.passwords;
passwords[mk.id] = event.target.value;
this.setState({ passwords: passwords });
}
const password = this.state.passwords[mk.id] ? this.state.passwords[mk.id] : '';
const active = this.props.activeMasterKeyId === mk.id ? '✔' : '';
const passwordOk = this.state.passwordChecks[mk.id] === true ? '✔' : '❌';
return (
<tr key={mk.id}>
<td>{active}</td>
<td>{mk.id}</td>
<td>{mk.source_application}</td>
<td>{time.formatMsToLocal(mk.created_time)}</td>
<td>{time.formatMsToLocal(mk.update_time)}</td>
<td>{time.formatMsToLocal(mk.updated_time)}</td>
<td><input type="password" value={password} onChange={(event) => onPasswordChange(event)}/> <button onClick={() => onSaveClick()}>{_('Save')}</button></td>
<td>{passwordOk}</td>
</tr>
);
}
@ -22,7 +79,7 @@ class MasterKeysScreenComponent extends React.Component {
render() {
const style = this.props.style;
const theme = themeStyle(this.props.theme);
const masterKeys = this.props.masterKeys;
const masterKeys = this.state.masterKeys;
const headerStyle = {
width: style.width,
@ -41,11 +98,18 @@ class MasterKeysScreenComponent extends React.Component {
<table>
<tbody>
<tr>
<th>{_('ID')}</th><th>{_('Source')}</th><th>{_('Created')}</th><th>{_('Updated')}</th>
<th>{_('Active')}</th>
<th>{_('ID')}</th>
<th>{_('Source')}</th>
<th>{_('Created')}</th>
<th>{_('Updated')}</th>
<th>{_('Password')}</th>
<th>{_('Password OK')}</th>
</tr>
{mkComps}
</tbody>
</table>
{_('Note: Only one master key is going to be used for encryption (the one marked as "active"). Any of the keys might be used for decryption, depending on how the notes or notebooks were originally encrypted.')}
</div>
);
}
@ -56,6 +120,9 @@ const mapStateToProps = (state) => {
return {
theme: state.settings.theme,
masterKeys: state.masterKeys,
passwords: state.settings['encryption.passwordCache'],
encryptionEnabled: state.settings['encryption.enabled'],
activeMasterKeyId: state.settings['encryption.activeMasterKeyId'],
};
};

View File

@ -11,6 +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 { Navigator } = require('./Navigator.min.js');
const { app } = require('../app');
@ -77,6 +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') },
};
return (

View File

@ -267,6 +267,17 @@ class BaseApplication {
time.setTimeFormat(Setting.value('timeFormat'));
}
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();
this.dispatch({
type: 'MASTERKEY_REMOVE_MISSING',
ids: loadedMasterKeyIds,
});
}
if (action.type == 'TAG_SELECT' || action.type === 'TAG_DELETE') {
await this.refreshNotes(newState);
}
@ -395,10 +406,12 @@ class BaseApplication {
setLocale(Setting.value('locale'));
}
EncryptionService.instance().setLogger(this.logger_);
BaseItem.encryptionService_ = EncryptionService.instance();
DecryptionWorker.encryptionService_ = EncryptionService.instance();
DecryptionWorker.instance().setLogger(this.logger_);
DecryptionWorker.instance().setEncryptionService(EncryptionService.instance());
await EncryptionService.instance().loadMasterKeysFromSettings();
DecryptionWorker.instance().start();
DecryptionWorker.instance().scheduleStart();
let currentFolderId = Setting.value('activeFolderId');
let currentFolder = null;

View File

@ -1,4 +1,4 @@
const { reg } = require('lib/registry.js');
const EncryptionService = require('lib/services/EncryptionService.js');
class BaseSyncTarget {
@ -88,6 +88,7 @@ class BaseSyncTarget {
try {
this.synchronizer_ = await this.initSynchronizer();
this.synchronizer_.setLogger(this.logger());
this.synchronizer_.setEncryptionService(EncryptionService.instance());
this.synchronizer_.dispatch = BaseSyncTarget.dispatch;
this.initState_ = 'ready';
return this.synchronizer_;

View File

@ -15,12 +15,26 @@ class MasterKey extends BaseItem {
return false;
}
static latest() {
return this.modelSelectOne('SELECT * FROM master_keys WHERE created_time >= (SELECT max(created_time) FROM master_keys)');
}
static async serialize(item, type = null, shownKeys = null) {
let fieldNames = this.fieldNames();
fieldNames.push('type_');
return super.serialize(item, 'master_key', fieldNames);
}
static async save(o, options = null) {
return super.save(o, options).then((item) => {
this.dispatch({
type: 'MASTERKEY_UPDATE_ONE',
item: item,
});
return item;
});
}
}
module.exports = MasterKey;

View File

@ -264,6 +264,16 @@ class Setting extends BaseModel {
}
static value(key) {
// Need to copy arrays and objects since in setValue(), the old value and new one is compared
// with strict equality and the value is updated only if changed. However if the caller acquire
// and object and change a key, the objects will be detected as equal. By returning a copy
// we avoid this problem.
function copyIfNeeded(value) {
if (Array.isArray(value)) return value.slice();
if (typeof value === 'object') return Object.assign({}, value);
return value;
}
if (key in this.constants_) {
const v = this.constants_[key];
const output = typeof v === 'function' ? v() : v;
@ -275,12 +285,12 @@ class Setting extends BaseModel {
for (let i = 0; i < this.cache_.length; i++) {
if (this.cache_[i].key == key) {
return this.cache_[i].value;
return copyIfNeeded(this.cache_[i].value);
}
}
const md = this.settingMetadata(key);
return md.value;
return copyIfNeeded(md.value);
}
static isEnum(key) {

View File

@ -374,7 +374,24 @@ const reducer = (state = defaultState, action) => {
if (state.missingMasterKeys.indexOf(action.id) < 0) {
newState = Object.assign({}, state);
newState.missingMasterKeys.push(action.id);
const keys = newState.missingMasterKeys.slice();
keys.push(action.id);
newState.missingMasterKeys = keys;
}
break;
case 'MASTERKEY_REMOVE_MISSING':
const ids = action.id ? [action.id] : action.ids;
for (let i = 0; i < ids.length; i++) {
const id = ids[i];
const index = state.missingMasterKeys.indexOf(id);
if (index >= 0) {
newState = Object.assign({}, state);
const keys = newState.missingMasterKeys.slice();
keys.splice(index, 1);
newState.missingMasterKeys = keys;
}
}
break;

View File

@ -8,6 +8,16 @@ class DecryptionWorker {
this.dispatch = (action) => {
console.warn('DecryptionWorker.dispatch is not defined');
};
this.scheduleId_ = null;
}
setLogger(l) {
this.logger_ = l;
}
logger() {
return this.logger_;
}
static instance() {
@ -16,11 +26,24 @@ class DecryptionWorker {
return this.instance_;
}
static encryptionService() {
setEncryptionService(v) {
this.encryptionService_ = v;
}
encryptionService() {
if (!this.encryptionService_) throw new Error('DecryptionWorker.encryptionService_ is not set!!');
return this.encryptionService_;
}
async scheduleStart() {
if (this.scheduleId_) return;
this.scheduleId_ = setTimeout(() => {
this.scheduleId_ = null;
this.start();
}, 1000);
}
async start() {
if (this.state_ !== 'idle') return;
@ -28,30 +51,36 @@ class DecryptionWorker {
let excludedIds = [];
while (true) {
const result = await BaseItem.itemsThatNeedDecryption(excludedIds);
const items = result.items;
try {
while (true) {
const result = await BaseItem.itemsThatNeedDecryption(excludedIds);
const items = result.items;
for (let i = 0; i < items.length; i++) {
const item = items[i];
const ItemClass = BaseItem.itemClass(item);
try {
await ItemClass.decrypt(item);
} catch (error) {
if (error.code === 'missingMasterKey') {
excludedIds.push(item.id);
this.dispatch({
type: 'MASTERKEY_ADD_MISSING',
id: error.masterKeyId,
});
continue;
for (let i = 0; i < items.length; i++) {
const item = items[i];
const ItemClass = BaseItem.itemClass(item);
try {
await ItemClass.decrypt(item);
} catch (error) {
if (error.code === 'missingMasterKey') {
excludedIds.push(item.id);
this.dispatch({
type: 'MASTERKEY_ADD_MISSING',
id: error.masterKeyId,
});
continue;
}
throw error;
}
throw error;
}
}
if (!result.hasMore) break;
if (!result.hasMore) break;
}
} catch (error) {
this.logger().error('DecryptionWorker::start:', error);
}
this.state_ = 'idle';
}
}

View File

@ -24,19 +24,45 @@ class EncryptionService {
return this.instance_;
}
setLogger(l) {
this.logger_ = l;
}
logger() {
return this.logger_;
}
async initializeEncryption(masterKey, password = null) {
Setting.setValue('encryption.enabled', true);
Setting.setValue('encryption.activeMasterKeyId', masterKey.id);
if (password) {
let passwordCache = Setting.value('encryption.passwordCache');
passwordCache[masterKey.id] = password;
Setting.setValue('encryption.passwordCache', passwordCache);
}
}
async loadMasterKeysFromSettings() {
if (!Setting.value('encryption.enabled')) return;
const masterKeys = await MasterKey.all();
const passwords = Setting.value('encryption.passwordCache');
const activeMasterKeyId = Setting.value('encryption.activeMasterKeyId');
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');
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;
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;
await this.loadMasterKey(mk, password, activeMasterKeyId === mk.id);
try {
await this.loadMasterKey(mk, password, activeMasterKeyId === mk.id);
} catch (error) {
this.logger().warn('Cannot load master key ' + mk.id + '. Invalid password?', error);
}
}
}
}
@ -71,6 +97,13 @@ class EncryptionService {
delete this.loadedMasterKeys_[model.id];
}
unloadAllMasterKeys() {
for (let id in this.loadedMasterKeys_) {
if (!this.loadedMasterKeys_.hasOwnProperty(id)) continue;
this.unloadMasterKey(this.loadedMasterKeys_[id]);
}
}
loadedMasterKey(id) {
if (!this.loadedMasterKeys_[id]) {
const error = new Error('Master key is not loaded: ' + id);
@ -81,6 +114,15 @@ class EncryptionService {
return this.loadedMasterKeys_[id];
}
loadedMasterKeyIds() {
let output = [];
for (let id in this.loadedMasterKeys_) {
if (!this.loadedMasterKeys_.hasOwnProperty(id)) continue;
output.push(id);
}
return output;
}
fsDriver() {
if (!EncryptionService.fsDriver_) throw new Error('EncryptionService.fsDriver_ not set!');
return EncryptionService.fsDriver_;
@ -129,32 +171,52 @@ class EncryptionService {
return plainText;
}
async checkMasterKeyPassword(model, password) {
try {
await this.decryptMasterKey(model, password);
} catch (error) {
return false;
}
return true;
}
async encrypt(method, key, plainText) {
const sjcl = shim.sjclModule;
if (method === EncryptionService.METHOD_SJCL) {
// Good demo to understand each parameter: https://bitwiseshiftleft.github.io/sjcl/demo/
return sjcl.json.encrypt(key, plainText, {
v: 1, // version
iter: 1000, // Defaults to 10000 in sjcl but since we're running this on mobile devices, use a lower value. Maybe review this after some time. https://security.stackexchange.com/questions/3959/recommended-of-iterations-when-using-pkbdf2-sha256
ks: 128, // Key size - "128 bits should be secure enough"
ts: 64, // ???
mode: "ocb2", // The cipher mode is a standard for how to use AES and other algorithms to encrypt and authenticate your message. OCB2 mode is slightly faster and has more features, but CCM mode has wider support because it is not patented.
//"adata":"", // Associated Data - not needed?
cipher: "aes"
});
try {
// Good demo to understand each parameter: https://bitwiseshiftleft.github.io/sjcl/demo/
return sjcl.json.encrypt(key, plainText, {
v: 1, // version
iter: 1000, // Defaults to 10000 in sjcl but since we're running this on mobile devices, use a lower value. Maybe review this after some time. https://security.stackexchange.com/questions/3959/recommended-of-iterations-when-using-pkbdf2-sha256
ks: 128, // Key size - "128 bits should be secure enough"
ts: 64, // ???
mode: "ocb2", // The cipher mode is a standard for how to use AES and other algorithms to encrypt and authenticate your message. OCB2 mode is slightly faster and has more features, but CCM mode has wider support because it is not patented.
//"adata":"", // Associated Data - not needed?
cipher: "aes"
});
} catch (error) {
// SJCL returns a string as error which means stack trace is missing so convert to an error object here
throw new Error(error.message);
}
}
// Same as first one but slightly more secure (but slower) to encrypt master keys
if (method === EncryptionService.METHOD_SJCL_2) {
return sjcl.json.encrypt(key, plainText, {
v: 1,
iter: 10000,
ks: 256,
ts: 64,
mode: "ocb2",
cipher: "aes"
});
try {
return sjcl.json.encrypt(key, plainText, {
v: 1,
iter: 10000,
ks: 256,
ts: 64,
mode: "ocb2",
cipher: "aes"
});
} catch (error) {
// SJCL returns a string as error which means stack trace is missing so convert to an error object here
throw new Error(error.message);
}
}
throw new Error('Unknown encryption method: ' + method);
@ -164,7 +226,12 @@ class EncryptionService {
const sjcl = shim.sjclModule;
if (method === EncryptionService.METHOD_SJCL || method === EncryptionService.METHOD_SJCL_2) {
return sjcl.json.decrypt(key, cipherText);
try {
return sjcl.json.decrypt(key, cipherText);
} catch (error) {
// SJCL returns a string as error which means stack trace is missing so convert to an error object here
throw new Error(error.message);
}
}
throw new Error('Unknown decryption method: ' + method);

View File

@ -2,7 +2,9 @@ const BaseItem = require('lib/models/BaseItem.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const Resource = require('lib/models/Resource.js');
const MasterKey = require('lib/models/MasterKey.js');
const BaseModel = require('lib/BaseModel.js');
const DecryptionWorker = require('lib/services/DecryptionWorker');
const { sprintf } = require('sprintf-js');
const { time } = require('lib/time-utils.js');
const { Logger } = require('lib/logger.js');
@ -52,6 +54,14 @@ class Synchronizer {
return this.logger_;
}
setEncryptionService(v) {
this.encryptionService_ = v;
}
encryptionService(v) {
return this.encryptionService_;
}
static reportToLines(report) {
let lines = [];
if (report.createLocal) lines.push(_('Created local items: %d.', report.createLocal));
@ -169,6 +179,8 @@ class Synchronizer {
this.cancelling_ = false;
const masterKeysBefore = await MasterKey.count();
// ------------------------------------------------------------------------
// First, find all the items that have been changed since the
// last sync and apply the changes to remote.
@ -567,6 +579,22 @@ 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().initializeEncryption(mk);
await this.encryptionService().loadMasterKeysFromSettings();
}
}
if (masterKeysAfter) {
DecryptionWorker.instance().scheduleStart();
}
this.progressReport_.completedTime = time.unixMs();
this.logSyncOperation('finished', null, null, 'Synchronisation finished [' + synchronizationId + ']');