mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Electron: E2EE config
This commit is contained in:
parent
d13c2cf8d7
commit
d1abf4971d
@ -15,12 +15,21 @@ class Command extends BaseCommand {
|
|||||||
return _('Manages encryption configuration.');
|
return _('Manages encryption configuration.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
options() {
|
||||||
|
return [
|
||||||
|
// This is here mostly for testing - shouldn't be used
|
||||||
|
['-p, --password <password>', 'Use this password as master password (For security reasons, it is not recommended to use this option).'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
async action(args) {
|
async action(args) {
|
||||||
// init
|
// init
|
||||||
// change-password
|
// change-password
|
||||||
|
|
||||||
|
const options = args.options;
|
||||||
|
|
||||||
if (args.command === 'init') {
|
if (args.command === 'init') {
|
||||||
const password = await this.prompt(_('Enter master password:'), { type: 'string', secure: true });
|
const password = options.password ? options.password.toString() : await this.prompt(_('Enter master password:'), { type: 'string', secure: true });
|
||||||
if (!password) {
|
if (!password) {
|
||||||
this.stdout(_('Operation cancelled'));
|
this.stdout(_('Operation cancelled'));
|
||||||
return;
|
return;
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const { connect } = require('react-redux');
|
const { connect } = require('react-redux');
|
||||||
const MasterKeys = require('lib/models/MasterKey');
|
const Setting = require('lib/models/Setting');
|
||||||
|
const BaseItem = require('lib/models/BaseItem');
|
||||||
const EncryptionService = require('lib/services/EncryptionService');
|
const EncryptionService = require('lib/services/EncryptionService');
|
||||||
const { Header } = require('./Header.min.js');
|
const { Header } = require('./Header.min.js');
|
||||||
const { themeStyle } = require('../theme.js');
|
const { themeStyle } = require('../theme.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { time } = require('lib/time-utils.js');
|
const { time } = require('lib/time-utils.js');
|
||||||
|
const dialogs = require('./dialogs');
|
||||||
|
|
||||||
class EncryptionConfigScreenComponent extends React.Component {
|
class EncryptionConfigScreenComponent extends React.Component {
|
||||||
|
|
||||||
@ -15,7 +17,21 @@ class EncryptionConfigScreenComponent extends React.Component {
|
|||||||
masterKeys: [],
|
masterKeys: [],
|
||||||
passwords: {},
|
passwords: {},
|
||||||
passwordChecks: {},
|
passwordChecks: {},
|
||||||
|
stats: {
|
||||||
|
encrypted: null,
|
||||||
|
total: null,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
this.isMounted_ = false;
|
||||||
|
this.refreshStatsIID_ = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.isMounted_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.isMounted_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
@ -25,6 +41,28 @@ class EncryptionConfigScreenComponent extends React.Component {
|
|||||||
}, () => {
|
}, () => {
|
||||||
this.checkPasswords();
|
this.checkPasswords();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.refreshStats();
|
||||||
|
|
||||||
|
if (this.refreshStatsIID_) {
|
||||||
|
clearInterval(this.refreshStatsIID_);
|
||||||
|
this.refreshStatsIID_ = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
this.refreshStatsIID_ = setInterval(() => {
|
||||||
|
if (!this.isMounted_) {
|
||||||
|
clearInterval(this.refreshStatsIID_);
|
||||||
|
this.refreshStatsIID_ = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.refreshStats();
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshStats() {
|
||||||
|
const stats = await BaseItem.encryptedItemsStats();
|
||||||
|
this.setState({ stats: stats });
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkPasswords() {
|
async checkPasswords() {
|
||||||
@ -39,6 +77,8 @@ class EncryptionConfigScreenComponent extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderMasterKey(mk) {
|
renderMasterKey(mk) {
|
||||||
|
const theme = themeStyle(this.props.theme);
|
||||||
|
|
||||||
const onSaveClick = () => {
|
const onSaveClick = () => {
|
||||||
const password = this.state.passwords[mk.id];
|
const password = this.state.passwords[mk.id];
|
||||||
if (!password) {
|
if (!password) {
|
||||||
@ -46,14 +86,6 @@ class EncryptionConfigScreenComponent extends React.Component {
|
|||||||
} else {
|
} else {
|
||||||
Setting.setObjectKey('encryption.passwordCache', mk.id, password);
|
Setting.setObjectKey('encryption.passwordCache', mk.id, password);
|
||||||
}
|
}
|
||||||
// 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();
|
this.checkPasswords();
|
||||||
}
|
}
|
||||||
@ -70,13 +102,13 @@ class EncryptionConfigScreenComponent extends React.Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={mk.id}>
|
<tr key={mk.id}>
|
||||||
<td>{active}</td>
|
<td style={theme.textStyle}>{active}</td>
|
||||||
<td>{mk.id}</td>
|
<td style={theme.textStyle}>{mk.id}</td>
|
||||||
<td>{mk.source_application}</td>
|
<td style={theme.textStyle}>{mk.source_application}</td>
|
||||||
<td>{time.formatMsToLocal(mk.created_time)}</td>
|
<td style={theme.textStyle}>{time.formatMsToLocal(mk.created_time)}</td>
|
||||||
<td>{time.formatMsToLocal(mk.updated_time)}</td>
|
<td style={theme.textStyle}>{time.formatMsToLocal(mk.updated_time)}</td>
|
||||||
<td><input type="password" value={password} onChange={(event) => onPasswordChange(event)}/> <button onClick={() => onSaveClick()}>{_('Save')}</button></td>
|
<td style={theme.textStyle}><input type="password" value={password} onChange={(event) => onPasswordChange(event)}/> <button onClick={() => onSaveClick()}>{_('Save')}</button></td>
|
||||||
<td>{passwordOk}</td>
|
<td style={theme.textStyle}>{passwordOk}</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -85,11 +117,18 @@ class EncryptionConfigScreenComponent extends React.Component {
|
|||||||
const style = this.props.style;
|
const style = this.props.style;
|
||||||
const theme = themeStyle(this.props.theme);
|
const theme = themeStyle(this.props.theme);
|
||||||
const masterKeys = this.state.masterKeys;
|
const masterKeys = this.state.masterKeys;
|
||||||
|
const containerPadding = 10;
|
||||||
|
|
||||||
const headerStyle = {
|
const headerStyle = {
|
||||||
width: style.width,
|
width: style.width,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const containerStyle = {
|
||||||
|
padding: containerPadding,
|
||||||
|
overflow: 'auto',
|
||||||
|
height: style.height - theme.headerHeight - containerPadding * 2,
|
||||||
|
};
|
||||||
|
|
||||||
const mkComps = [];
|
const mkComps = [];
|
||||||
|
|
||||||
for (let i = 0; i < masterKeys.length; i++) {
|
for (let i = 0; i < masterKeys.length; i++) {
|
||||||
@ -97,24 +136,71 @@ class EncryptionConfigScreenComponent extends React.Component {
|
|||||||
mkComps.push(this.renderMasterKey(mk));
|
mkComps.push(this.renderMasterKey(mk));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onToggleButtonClick = async () => {
|
||||||
|
const isEnabled = Setting.value('encryption.enabled');
|
||||||
|
|
||||||
|
let answer = null;
|
||||||
|
if (isEnabled) {
|
||||||
|
answer = await dialogs.confirm(_('Disabling encryption means <b>all</b> your notes and attachments are going to re-synchronized and sent unencrypted to the sync target. Do you wish to continue?'));
|
||||||
|
} else {
|
||||||
|
answer = await dialogs.prompt(_('Enabling encryption means <b>all</b> your notes and attachments are going to re-synchronized and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the <b>only</b> way to decrypt the data! To enable encryption, please enter your password below.'), '', '', { type: 'password' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!answer) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isEnabled) {
|
||||||
|
await EncryptionService.instance().disableEncryption();
|
||||||
|
} else {
|
||||||
|
await EncryptionService.instance().enableEncryption();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
await dialogs.alert(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = this.state.stats;
|
||||||
|
const decryptedItemsInfo = this.props.encryptionEnabled ? <p style={theme.textStyle}>{_('Decrypted items: %s / %s', stats.encrypted !== null ? (stats.total - stats.encrypted) : '-', stats.total !== null ? stats.total : '-')}</p> : null;
|
||||||
|
const toggleButton = <button onClick={() => { onToggleButtonClick() }}>{this.props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')}</button>
|
||||||
|
|
||||||
|
let masterKeySection = null;
|
||||||
|
|
||||||
|
if (mkComps.length) {
|
||||||
|
masterKeySection = (
|
||||||
|
<div>
|
||||||
|
<h1 style={theme.h1Style}>{_('Master Keys')}</h1>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th style={theme.textStyle}>{_('Active')}</th>
|
||||||
|
<th style={theme.textStyle}>{_('ID')}</th>
|
||||||
|
<th style={theme.textStyle}>{_('Source')}</th>
|
||||||
|
<th style={theme.textStyle}>{_('Created')}</th>
|
||||||
|
<th style={theme.textStyle}>{_('Updated')}</th>
|
||||||
|
<th style={theme.textStyle}>{_('Password')}</th>
|
||||||
|
<th style={theme.textStyle}>{_('Password OK')}</th>
|
||||||
|
</tr>
|
||||||
|
{mkComps}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p style={theme.textStyle}>{_('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.')}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disabling encryption means *all* your notes and attachments are going to re-synchronized and sent unencrypted to the sync target.
|
||||||
|
// Enabling End-To-End Encryption (E2EE) means *all* your notes and attachments are going to re-synchronized and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data. To enable E2EE, please enter your password below and click "Enable E2EE".
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Header style={headerStyle} />
|
<Header style={headerStyle} />
|
||||||
<table>
|
<div style={containerStyle}>
|
||||||
<tbody>
|
<h1 style={theme.h1Style}>{_('Status')}</h1>
|
||||||
<tr>
|
<p style={theme.textStyle}>{_('Encryption is:')} <strong>{this.props.encryptionEnabled ? _('Enabled') : _('Disabled')}</strong></p>
|
||||||
<th>{_('Active')}</th>
|
{decryptedItemsInfo}
|
||||||
<th>{_('ID')}</th>
|
{toggleButton}
|
||||||
<th>{_('Source')}</th>
|
{masterKeySection}
|
||||||
<th>{_('Created')}</th>
|
</div>
|
||||||
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
33
ElectronClient/app/gui/dialogs.js
Normal file
33
ElectronClient/app/gui/dialogs.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
const smalltalk = require('smalltalk');
|
||||||
|
|
||||||
|
class Dialogs {
|
||||||
|
|
||||||
|
async alert(message, title = '') {
|
||||||
|
await smalltalk.alert(title, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
async confirm(message, title = '') {
|
||||||
|
try {
|
||||||
|
await smalltalk.confirm(title, message);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async prompt(message, title = '', defaultValue = '', options = null) {
|
||||||
|
if (options === null) options = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const answer = await smalltalk.prompt(title, message, defaultValue, options);
|
||||||
|
return answer;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialogs = new Dialogs();
|
||||||
|
|
||||||
|
module.exports = dialogs;
|
@ -6,6 +6,18 @@
|
|||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
<link rel="stylesheet" href="css/font-awesome.min.css">
|
<link rel="stylesheet" href="css/font-awesome.min.css">
|
||||||
<link rel="stylesheet" href="node_modules/react-datetime/css/react-datetime.css">
|
<link rel="stylesheet" href="node_modules/react-datetime/css/react-datetime.css">
|
||||||
|
<link rel="stylesheet" href="node_modules/smalltalk/css/smalltalk.css">
|
||||||
|
<style>
|
||||||
|
.smalltalk {
|
||||||
|
background-color: rgba(0,0,0,.5);
|
||||||
|
}
|
||||||
|
.smalltalk input {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
.smalltalk .page {
|
||||||
|
max-width: 30em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="react-root"></div>
|
<div id="react-root"></div>
|
||||||
|
@ -71,6 +71,9 @@ globalStyle.textStyle2 = Object.assign({}, globalStyle.textStyle, {
|
|||||||
color: globalStyle.color2,
|
color: globalStyle.color2,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
globalStyle.h1Style = Object.assign({}, globalStyle.textStyle);
|
||||||
|
globalStyle.h1Style.fontSize *= 1.5;
|
||||||
|
|
||||||
globalStyle.h2Style = Object.assign({}, globalStyle.textStyle);
|
globalStyle.h2Style = Object.assign({}, globalStyle.textStyle);
|
||||||
globalStyle.h2Style.fontSize *= 1.3;
|
globalStyle.h2Style.fontSize *= 1.3;
|
||||||
|
|
||||||
|
@ -346,6 +346,37 @@ class BaseItem extends BaseModel {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async encryptedItemsStats() {
|
||||||
|
const classNames = this.encryptableItemClassNames();
|
||||||
|
let encryptedCount = 0;
|
||||||
|
let totalCount = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < classNames.length; i++) {
|
||||||
|
const ItemClass = this.getClass(classNames[i]);
|
||||||
|
encryptedCount += await ItemClass.count({ where: 'encryption_applied = 1' });
|
||||||
|
totalCount += await ItemClass.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
encrypted: encryptedCount,
|
||||||
|
total: totalCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static async encryptedItemsCount() {
|
||||||
|
const classNames = this.encryptableItemClassNames();
|
||||||
|
let output = 0;
|
||||||
|
|
||||||
|
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' });
|
||||||
|
output += count;
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
static async hasEncryptedItems() {
|
static async hasEncryptedItems() {
|
||||||
const classNames = this.encryptableItemClassNames();
|
const classNames = this.encryptableItemClassNames();
|
||||||
|
|
||||||
|
@ -280,16 +280,11 @@ class EncryptionService {
|
|||||||
|
|
||||||
await destination.append(this.encodeHeader_(header));
|
await destination.append(this.encodeHeader_(header));
|
||||||
|
|
||||||
let fromIndex = 0;
|
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const block = await source.read(this.chunkSize_);
|
const block = await source.read(this.chunkSize_);
|
||||||
if (!block) break;
|
if (!block) break;
|
||||||
|
|
||||||
fromIndex += block.length;
|
|
||||||
|
|
||||||
const encrypted = await this.encrypt(method, masterKeyPlainText, block);
|
const encrypted = await this.encrypt(method, masterKeyPlainText, block);
|
||||||
|
|
||||||
await destination.append(padLeft(encrypted.length.toString(16), 6, '0'));
|
await destination.append(padLeft(encrypted.length.toString(16), 6, '0'));
|
||||||
await destination.append(encrypted);
|
await destination.append(encrypted);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user