mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
All: Decryption worker and handling of missing master key passwords
This commit is contained in:
parent
df05d04dad
commit
5bc72e2b44
@ -1,6 +1,6 @@
|
|||||||
const Folder = require('lib/models/folder.js').Folder;
|
const Folder = require('lib/models/Folder.js');
|
||||||
const Tag = require('lib/models/tag.js').Tag;
|
const Tag = require('lib/models/Tag.js');
|
||||||
const BaseModel = require('lib/base-model.js').BaseModel;
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
const ListWidget = require('tkwidgets/ListWidget.js');
|
const ListWidget = require('tkwidgets/ListWidget.js');
|
||||||
const _ = require('lib/locale.js')._;
|
const _ = require('lib/locale.js')._;
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const Note = require('lib/models/note.js').Note;
|
const Note = require('lib/models/Note.js');
|
||||||
const ListWidget = require('tkwidgets/ListWidget.js');
|
const ListWidget = require('tkwidgets/ListWidget.js');
|
||||||
|
|
||||||
class NoteListWidget extends ListWidget {
|
class NoteListWidget extends ListWidget {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const Note = require('lib/models/note.js').Note;
|
const Note = require('lib/models/Note.js');
|
||||||
const TextWidget = require('tkwidgets/TextWidget.js');
|
const TextWidget = require('tkwidgets/TextWidget.js');
|
||||||
|
|
||||||
class NoteMetadataWidget extends TextWidget {
|
class NoteMetadataWidget extends TextWidget {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const Note = require('lib/models/note.js').Note;
|
const Note = require('lib/models/Note.js');
|
||||||
const TextWidget = require('tkwidgets/TextWidget.js');
|
const TextWidget = require('tkwidgets/TextWidget.js');
|
||||||
|
|
||||||
class NoteWidget extends TextWidget {
|
class NoteWidget extends TextWidget {
|
||||||
|
@ -288,8 +288,7 @@ class MainScreenComponent extends React.Component {
|
|||||||
const promptOptions = this.state.promptOptions;
|
const promptOptions = this.state.promptOptions;
|
||||||
const folders = this.props.folders;
|
const folders = this.props.folders;
|
||||||
const notes = this.props.notes;
|
const notes = this.props.notes;
|
||||||
const messageBoxVisible = this.props.hasDisabledSyncItems;
|
const messageBoxVisible = this.props.hasDisabledSyncItems || this.props.missingMasterKeys.length;
|
||||||
|
|
||||||
const styles = this.styles(this.props.theme, style.width, style.height, messageBoxVisible);
|
const styles = this.styles(this.props.theme, style.width, style.height, messageBoxVisible);
|
||||||
const theme = themeStyle(this.props.theme);
|
const theme = themeStyle(this.props.theme);
|
||||||
|
|
||||||
@ -343,13 +342,31 @@ class MainScreenComponent extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageComp = messageBoxVisible ? (
|
const onViewMasterKeysClick = () => {
|
||||||
|
this.props.dispatch({
|
||||||
|
type: 'NAV_GO',
|
||||||
|
routeName: 'MasterKeys',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let messageComp = null;
|
||||||
|
|
||||||
|
if (messageBoxVisible) {
|
||||||
|
let msg = null;
|
||||||
|
if (this.props.hasDisabledSyncItems) {
|
||||||
|
msg = <span>{_('Some items cannot be synchronised.')} <a href="#" onClick={() => { onViewDisabledItemsClick() }}>{_('View them now')}</a></span>
|
||||||
|
} else if (this.props.missingMasterKeys.length) {
|
||||||
|
msg = <span>{_('Some items cannot be decrypted.')} <a href="#" onClick={() => { onViewMasterKeysClick() }}>{_('Set the password')}</a></span>
|
||||||
|
}
|
||||||
|
|
||||||
|
messageComp = (
|
||||||
<div style={styles.messageBox}>
|
<div style={styles.messageBox}>
|
||||||
<span style={theme.textStyle}>
|
<span style={theme.textStyle}>
|
||||||
{_('Some items cannot be synchronised.')} <a href="#" onClick={() => { onViewDisabledItemsClick() }}>{_('View them now')}</a>
|
{msg}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={style}>
|
<div style={style}>
|
||||||
@ -383,6 +400,7 @@ const mapStateToProps = (state) => {
|
|||||||
folders: state.folders,
|
folders: state.folders,
|
||||||
notes: state.notes,
|
notes: state.notes,
|
||||||
hasDisabledSyncItems: state.hasDisabledSyncItems,
|
hasDisabledSyncItems: state.hasDisabledSyncItems,
|
||||||
|
missingMasterKeys: state.missingMasterKeys,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
64
ElectronClient/app/gui/MasterKeysScreen.jsx
Normal file
64
ElectronClient/app/gui/MasterKeysScreen.jsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
const React = require('react');
|
||||||
|
const { connect } = require('react-redux');
|
||||||
|
const MasterKeys = require('lib/models/MasterKey');
|
||||||
|
const { Header } = require('./Header.min.js');
|
||||||
|
const { themeStyle } = require('../theme.js');
|
||||||
|
const { _ } = require('lib/locale.js');
|
||||||
|
const { time } = require('lib/time-utils.js');
|
||||||
|
|
||||||
|
class MasterKeysScreenComponent extends React.Component {
|
||||||
|
|
||||||
|
renderMasterKey(mk) {
|
||||||
|
return (
|
||||||
|
<tr key={mk.id}>
|
||||||
|
<td>{mk.id}</td>
|
||||||
|
<td>{mk.source_application}</td>
|
||||||
|
<td>{time.formatMsToLocal(mk.created_time)}</td>
|
||||||
|
<td>{time.formatMsToLocal(mk.update_time)}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const style = this.props.style;
|
||||||
|
const theme = themeStyle(this.props.theme);
|
||||||
|
const masterKeys = this.props.masterKeys;
|
||||||
|
|
||||||
|
const headerStyle = {
|
||||||
|
width: style.width,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mkComps = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < masterKeys.length; i++) {
|
||||||
|
const mk = masterKeys[i];
|
||||||
|
mkComps.push(this.renderMasterKey(mk));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Header style={headerStyle} />
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>{_('ID')}</th><th>{_('Source')}</th><th>{_('Created')}</th><th>{_('Updated')}</th>
|
||||||
|
</tr>
|
||||||
|
{mkComps}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state) => {
|
||||||
|
return {
|
||||||
|
theme: state.settings.theme,
|
||||||
|
masterKeys: state.masterKeys,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const MasterKeysScreen = connect(mapStateToProps)(MasterKeysScreenComponent);
|
||||||
|
|
||||||
|
module.exports = { MasterKeysScreen };
|
@ -27,6 +27,7 @@ const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
|
|||||||
const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
|
const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
|
||||||
const SyncTargetOneDriveDev = require('lib/SyncTargetOneDriveDev.js');
|
const SyncTargetOneDriveDev = require('lib/SyncTargetOneDriveDev.js');
|
||||||
const EncryptionService = require('lib/services/EncryptionService');
|
const EncryptionService = require('lib/services/EncryptionService');
|
||||||
|
const DecryptionWorker = require('lib/services/DecryptionWorker');
|
||||||
|
|
||||||
SyncTargetRegistry.addClass(SyncTargetFilesystem);
|
SyncTargetRegistry.addClass(SyncTargetFilesystem);
|
||||||
SyncTargetRegistry.addClass(SyncTargetOneDrive);
|
SyncTargetRegistry.addClass(SyncTargetOneDrive);
|
||||||
@ -307,6 +308,7 @@ class BaseApplication {
|
|||||||
FoldersScreenUtils.dispatch = this.store().dispatch;
|
FoldersScreenUtils.dispatch = this.store().dispatch;
|
||||||
reg.dispatch = this.store().dispatch;
|
reg.dispatch = this.store().dispatch;
|
||||||
BaseSyncTarget.dispatch = this.store().dispatch;
|
BaseSyncTarget.dispatch = this.store().dispatch;
|
||||||
|
DecryptionWorker.instance().dispatch = this.store().dispatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
async readFlagsFromFile(flagPath) {
|
async readFlagsFromFile(flagPath) {
|
||||||
@ -394,7 +396,9 @@ class BaseApplication {
|
|||||||
}
|
}
|
||||||
|
|
||||||
BaseItem.encryptionService_ = EncryptionService.instance();
|
BaseItem.encryptionService_ = EncryptionService.instance();
|
||||||
|
DecryptionWorker.encryptionService_ = EncryptionService.instance();
|
||||||
await EncryptionService.instance().loadMasterKeysFromSettings();
|
await EncryptionService.instance().loadMasterKeysFromSettings();
|
||||||
|
DecryptionWorker.instance().start();
|
||||||
|
|
||||||
let currentFolderId = Setting.value('activeFolderId');
|
let currentFolderId = Setting.value('activeFolderId');
|
||||||
let currentFolder = null;
|
let currentFolder = null;
|
||||||
|
@ -271,12 +271,24 @@ class JoplinDatabase extends Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (targetVersion == 9) {
|
if (targetVersion == 9) {
|
||||||
queries.push('CREATE TABLE master_keys (id TEXT PRIMARY KEY, created_time INT NOT NULL, updated_time INT NOT NULL, encryption_method INT NOT NULL, checksum TEXT NOT NULL, content TEXT NOT NULL);');
|
const newTableSql = `
|
||||||
|
CREATE TABLE master_keys (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
created_time INT NOT NULL,
|
||||||
|
updated_time INT NOT NULL,
|
||||||
|
source_application TEXT NOT NULL,
|
||||||
|
encryption_method INT NOT NULL,
|
||||||
|
checksum TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL
|
||||||
|
);
|
||||||
|
`;
|
||||||
|
queries.push(this.sqlStringToLines(newTableSql)[0]);
|
||||||
const tableNames = ['notes', 'folders', 'tags', 'note_tags', 'resources'];
|
const tableNames = ['notes', 'folders', 'tags', 'note_tags', 'resources'];
|
||||||
for (let i = 0; i < tableNames.length; i++) {
|
for (let i = 0; i < tableNames.length; i++) {
|
||||||
const n = tableNames[i];
|
const n = tableNames[i];
|
||||||
queries.push('ALTER TABLE ' + n + ' ADD COLUMN encryption_cipher_text TEXT NOT NULL DEFAULT ""');
|
queries.push('ALTER TABLE ' + n + ' ADD COLUMN encryption_cipher_text TEXT NOT NULL DEFAULT ""');
|
||||||
queries.push('ALTER TABLE ' + n + ' ADD COLUMN encryption_applied INT NOT NULL DEFAULT 0');
|
queries.push('ALTER TABLE ' + n + ' ADD COLUMN encryption_applied INT NOT NULL DEFAULT 0');
|
||||||
|
queries.push('CREATE INDEX ' + n + '_encryption_applied ON ' + n + ' (encryption_applied)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -249,14 +249,17 @@ class BaseItem extends BaseModel {
|
|||||||
return temp.join("\n\n");
|
return temp.join("\n\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static encryptionService() {
|
||||||
|
if (!this.encryptionService_) throw new Error('BaseItem.encryptionService_ is not set!!');
|
||||||
|
return this.encryptionService_;
|
||||||
|
}
|
||||||
|
|
||||||
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()) return serialized;
|
||||||
|
|
||||||
if (!BaseItem.encryptionService_) throw new Error('BaseItem.encryptionService_ is not set!!');
|
const cipherText = await this.encryptionService().encryptString(serialized);
|
||||||
|
|
||||||
const cipherText = await BaseItem.encryptionService_.encryptString(serialized);
|
|
||||||
|
|
||||||
const reducedItem = Object.assign({}, item);
|
const reducedItem = Object.assign({}, item);
|
||||||
|
|
||||||
@ -284,7 +287,7 @@ class BaseItem extends BaseModel {
|
|||||||
if (!item.encryption_cipher_text) throw new Error('Item is not encrypted: ' + item.id);
|
if (!item.encryption_cipher_text) throw new Error('Item is not encrypted: ' + item.id);
|
||||||
|
|
||||||
const ItemClass = this.itemClass(item);
|
const ItemClass = this.itemClass(item);
|
||||||
const plainText = await BaseItem.encryptionService_.decryptString(item.encryption_cipher_text);
|
const plainText = await this.encryptionService().decryptString(item.encryption_cipher_text);
|
||||||
|
|
||||||
// Note: decryption does not count has a change, so don't update any timestamp
|
// Note: decryption does not count has a change, so don't update any timestamp
|
||||||
const plainItem = await ItemClass.unserialize(plainText);
|
const plainItem = await ItemClass.unserialize(plainText);
|
||||||
@ -339,6 +342,39 @@ class BaseItem extends BaseModel {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async itemsThatNeedDecryption(exclusions = [], limit = 100) {
|
||||||
|
const classNames = this.encryptableItemClassNames();
|
||||||
|
|
||||||
|
for (let i = 0; i < classNames.length; i++) {
|
||||||
|
const className = classNames[i];
|
||||||
|
const ItemClass = this.getClass(className);
|
||||||
|
|
||||||
|
const whereSql = ['encryption_applied = 1'];
|
||||||
|
if (exclusions.length) whereSql.push('id NOT IN ("' + exclusions.join('","') + '")');
|
||||||
|
|
||||||
|
const sql = sprintf(`
|
||||||
|
SELECT *
|
||||||
|
FROM %s
|
||||||
|
WHERE %s
|
||||||
|
LIMIT %d
|
||||||
|
`,
|
||||||
|
this.db().escapeField(ItemClass.tableName()),
|
||||||
|
whereSql.join(' AND '),
|
||||||
|
limit
|
||||||
|
);
|
||||||
|
|
||||||
|
const items = await ItemClass.modelSelectAll(sql);
|
||||||
|
|
||||||
|
if (i >= classNames.length - 1) {
|
||||||
|
return { hasMore: items.length >= limit, items: items };
|
||||||
|
} else {
|
||||||
|
if (items.length) return { hasMore: true, items: items };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Unreachable');
|
||||||
|
}
|
||||||
|
|
||||||
static async itemsThatNeedSync(syncTarget, limit = 100) {
|
static async itemsThatNeedSync(syncTarget, limit = 100) {
|
||||||
const classNames = this.syncItemClassNames();
|
const classNames = this.syncItemClassNames();
|
||||||
|
|
||||||
@ -419,6 +455,16 @@ class BaseItem extends BaseModel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static encryptableItemClassNames() {
|
||||||
|
const temp = this.syncItemClassNames();
|
||||||
|
let output = [];
|
||||||
|
for (let i = 0; i < temp.length; i++) {
|
||||||
|
if (temp[i] === 'MasterKey') continue;
|
||||||
|
output.push(temp[i]);
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
static syncItemTypes() {
|
static syncItemTypes() {
|
||||||
return BaseItem.syncItemDefinitions_.map((def) => {
|
return BaseItem.syncItemDefinitions_.map((def) => {
|
||||||
return def.type;
|
return def.type;
|
||||||
|
@ -9,6 +9,7 @@ const defaultState = {
|
|||||||
folders: [],
|
folders: [],
|
||||||
tags: [],
|
tags: [],
|
||||||
masterKeys: [],
|
masterKeys: [],
|
||||||
|
missingMasterKeys: [],
|
||||||
searches: [],
|
searches: [],
|
||||||
selectedNoteIds: [],
|
selectedNoteIds: [],
|
||||||
selectedFolderId: null,
|
selectedFolderId: null,
|
||||||
@ -369,6 +370,14 @@ const reducer = (state = defaultState, action) => {
|
|||||||
newState.masterKeys = action.items;
|
newState.masterKeys = action.items;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'MASTERKEY_ADD_MISSING':
|
||||||
|
|
||||||
|
if (state.missingMasterKeys.indexOf(action.id) < 0) {
|
||||||
|
newState = Object.assign({}, state);
|
||||||
|
newState.missingMasterKeys.push(action.id);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case 'SYNC_STARTED':
|
case 'SYNC_STARTED':
|
||||||
|
|
||||||
newState = Object.assign({}, state);
|
newState = Object.assign({}, state);
|
||||||
|
@ -1,13 +1,57 @@
|
|||||||
|
const BaseItem = require('lib/models/BaseItem');
|
||||||
|
|
||||||
class DecryptionWorker {
|
class DecryptionWorker {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.state_ = 'idle';
|
this.state_ = 'idle';
|
||||||
|
|
||||||
|
this.dispatch = (action) => {
|
||||||
|
console.warn('DecryptionWorker.dispatch is not defined');
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
static instance() {
|
||||||
|
if (this.instance_) return this.instance_;
|
||||||
|
this.instance_ = new DecryptionWorker();
|
||||||
|
return this.instance_;
|
||||||
|
}
|
||||||
|
|
||||||
|
static encryptionService() {
|
||||||
|
if (!this.encryptionService_) throw new Error('DecryptionWorker.encryptionService_ is not set!!');
|
||||||
|
return this.encryptionService_;
|
||||||
|
}
|
||||||
|
|
||||||
|
async start() {
|
||||||
if (this.state_ !== 'idle') return;
|
if (this.state_ !== 'idle') return;
|
||||||
|
|
||||||
this.state_ = 'started';
|
this.state_ = 'started';
|
||||||
|
|
||||||
|
let excludedIds = [];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.hasMore) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,7 @@ class EncryptionService {
|
|||||||
for (let i = 0; i < masterKeys.length; i++) {
|
for (let i = 0; i < masterKeys.length; i++) {
|
||||||
const mk = masterKeys[i];
|
const mk = masterKeys[i];
|
||||||
const password = passwords[mk.id];
|
const password = passwords[mk.id];
|
||||||
|
if (this.isMasterKeyLoaded(mk.id)) continue;
|
||||||
if (!password) continue;
|
if (!password) continue;
|
||||||
|
|
||||||
await this.loadMasterKey(mk, password, activeMasterKeyId === mk.id);
|
await this.loadMasterKey(mk, password, activeMasterKeyId === mk.id);
|
||||||
@ -56,6 +57,10 @@ class EncryptionService {
|
|||||||
return this.activeMasterKeyId_;
|
return this.activeMasterKeyId_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isMasterKeyLoaded(id) {
|
||||||
|
return !!this.loadedMasterKeys_[id];
|
||||||
|
}
|
||||||
|
|
||||||
async loadMasterKey(model, password, makeActive = false) {
|
async loadMasterKey(model, password, makeActive = false) {
|
||||||
if (!model.id) throw new Error('Master key does not have an ID - save it first');
|
if (!model.id) throw new Error('Master key does not have an ID - save it first');
|
||||||
this.loadedMasterKeys_[model.id] = await this.decryptMasterKey(model, password);
|
this.loadedMasterKeys_[model.id] = await this.decryptMasterKey(model, password);
|
||||||
@ -67,7 +72,12 @@ class EncryptionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadedMasterKey(id) {
|
loadedMasterKey(id) {
|
||||||
if (!this.loadedMasterKeys_[id]) throw new Error('Master key is not loaded: ' + id);
|
if (!this.loadedMasterKeys_[id]) {
|
||||||
|
const error = new Error('Master key is not loaded: ' + id);
|
||||||
|
error.code = 'missingMasterKey';
|
||||||
|
error.masterKeyId = id;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
return this.loadedMasterKeys_[id];
|
return this.loadedMasterKeys_[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,6 +115,7 @@ class EncryptionService {
|
|||||||
return {
|
return {
|
||||||
created_time: now,
|
created_time: now,
|
||||||
updated_time: now,
|
updated_time: now,
|
||||||
|
source_application: Setting.value('appId'),
|
||||||
encryption_method: encryptionMethod,
|
encryption_method: encryptionMethod,
|
||||||
checksum: checksum,
|
checksum: checksum,
|
||||||
content: cipherText,
|
content: cipherText,
|
||||||
|
Loading…
Reference in New Issue
Block a user