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.');
|
||||
}
|
||||
|
||||
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) {
|
||||
// init
|
||||
// change-password
|
||||
|
||||
const options = args.options;
|
||||
|
||||
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) {
|
||||
this.stdout(_('Operation cancelled'));
|
||||
return;
|
||||
|
@ -1,11 +1,13 @@
|
||||
const React = require('react');
|
||||
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 { Header } = require('./Header.min.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const dialogs = require('./dialogs');
|
||||
|
||||
class EncryptionConfigScreenComponent extends React.Component {
|
||||
|
||||
@ -15,7 +17,21 @@ class EncryptionConfigScreenComponent extends React.Component {
|
||||
masterKeys: [],
|
||||
passwords: {},
|
||||
passwordChecks: {},
|
||||
stats: {
|
||||
encrypted: null,
|
||||
total: null,
|
||||
},
|
||||
};
|
||||
this.isMounted_ = false;
|
||||
this.refreshStatsIID_ = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.isMounted_ = true;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.isMounted_ = false;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
@ -25,6 +41,28 @@ class EncryptionConfigScreenComponent extends React.Component {
|
||||
}, () => {
|
||||
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() {
|
||||
@ -39,6 +77,8 @@ class EncryptionConfigScreenComponent extends React.Component {
|
||||
}
|
||||
|
||||
renderMasterKey(mk) {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
const onSaveClick = () => {
|
||||
const password = this.state.passwords[mk.id];
|
||||
if (!password) {
|
||||
@ -46,14 +86,6 @@ class EncryptionConfigScreenComponent extends React.Component {
|
||||
} else {
|
||||
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();
|
||||
}
|
||||
@ -70,13 +102,13 @@ class EncryptionConfigScreenComponent extends React.Component {
|
||||
|
||||
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.updated_time)}</td>
|
||||
<td><input type="password" value={password} onChange={(event) => onPasswordChange(event)}/> <button onClick={() => onSaveClick()}>{_('Save')}</button></td>
|
||||
<td>{passwordOk}</td>
|
||||
<td style={theme.textStyle}>{active}</td>
|
||||
<td style={theme.textStyle}>{mk.id}</td>
|
||||
<td style={theme.textStyle}>{mk.source_application}</td>
|
||||
<td style={theme.textStyle}>{time.formatMsToLocal(mk.created_time)}</td>
|
||||
<td style={theme.textStyle}>{time.formatMsToLocal(mk.updated_time)}</td>
|
||||
<td style={theme.textStyle}><input type="password" value={password} onChange={(event) => onPasswordChange(event)}/> <button onClick={() => onSaveClick()}>{_('Save')}</button></td>
|
||||
<td style={theme.textStyle}>{passwordOk}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
@ -85,11 +117,18 @@ class EncryptionConfigScreenComponent extends React.Component {
|
||||
const style = this.props.style;
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const masterKeys = this.state.masterKeys;
|
||||
const containerPadding = 10;
|
||||
|
||||
const headerStyle = {
|
||||
width: style.width,
|
||||
};
|
||||
|
||||
const containerStyle = {
|
||||
padding: containerPadding,
|
||||
overflow: 'auto',
|
||||
height: style.height - theme.headerHeight - containerPadding * 2,
|
||||
};
|
||||
|
||||
const mkComps = [];
|
||||
|
||||
for (let i = 0; i < masterKeys.length; i++) {
|
||||
@ -97,24 +136,71 @@ class EncryptionConfigScreenComponent extends React.Component {
|
||||
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 (
|
||||
<div>
|
||||
<Header style={headerStyle} />
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<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 style={containerStyle}>
|
||||
<h1 style={theme.h1Style}>{_('Status')}</h1>
|
||||
<p style={theme.textStyle}>{_('Encryption is:')} <strong>{this.props.encryptionEnabled ? _('Enabled') : _('Disabled')}</strong></p>
|
||||
{decryptedItemsInfo}
|
||||
{toggleButton}
|
||||
{masterKeySection}
|
||||
</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="css/font-awesome.min.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>
|
||||
<body>
|
||||
<div id="react-root"></div>
|
||||
|
@ -71,6 +71,9 @@ globalStyle.textStyle2 = Object.assign({}, globalStyle.textStyle, {
|
||||
color: globalStyle.color2,
|
||||
});
|
||||
|
||||
globalStyle.h1Style = Object.assign({}, globalStyle.textStyle);
|
||||
globalStyle.h1Style.fontSize *= 1.5;
|
||||
|
||||
globalStyle.h2Style = Object.assign({}, globalStyle.textStyle);
|
||||
globalStyle.h2Style.fontSize *= 1.3;
|
||||
|
||||
|
@ -346,6 +346,37 @@ class BaseItem extends BaseModel {
|
||||
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() {
|
||||
const classNames = this.encryptableItemClassNames();
|
||||
|
||||
|
@ -280,16 +280,11 @@ class EncryptionService {
|
||||
|
||||
await destination.append(this.encodeHeader_(header));
|
||||
|
||||
let fromIndex = 0;
|
||||
|
||||
while (true) {
|
||||
const block = await source.read(this.chunkSize_);
|
||||
if (!block) break;
|
||||
|
||||
fromIndex += block.length;
|
||||
|
||||
const encrypted = await this.encrypt(method, masterKeyPlainText, block);
|
||||
|
||||
await destination.append(padLeft(encrypted.length.toString(16), 6, '0'));
|
||||
await destination.append(encrypted);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user