diff --git a/CliClient/app/command-encrypt-config.js b/CliClient/app/command-encrypt-config.js index 75000dd34..48cd1989c 100644 --- a/CliClient/app/command-encrypt-config.js +++ b/CliClient/app/command-encrypt-config.js @@ -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 ', '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; diff --git a/ElectronClient/app/gui/EncryptionConfigScreen.jsx b/ElectronClient/app/gui/EncryptionConfigScreen.jsx index 1569a1ec9..0a206a366 100644 --- a/ElectronClient/app/gui/EncryptionConfigScreen.jsx +++ b/ElectronClient/app/gui/EncryptionConfigScreen.jsx @@ -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 ( - {active} - {mk.id} - {mk.source_application} - {time.formatMsToLocal(mk.created_time)} - {time.formatMsToLocal(mk.updated_time)} - onPasswordChange(event)}/> - {passwordOk} + {active} + {mk.id} + {mk.source_application} + {time.formatMsToLocal(mk.created_time)} + {time.formatMsToLocal(mk.updated_time)} + onPasswordChange(event)}/> + {passwordOk} ); } @@ -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 all 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 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 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 ?

{_('Decrypted items: %s / %s', stats.encrypted !== null ? (stats.total - stats.encrypted) : '-', stats.total !== null ? stats.total : '-')}

: null; + const toggleButton = + + let masterKeySection = null; + + if (mkComps.length) { + masterKeySection = ( +
+

{_('Master Keys')}

+ + + + + + + + + + + + {mkComps} + +
{_('Active')}{_('ID')}{_('Source')}{_('Created')}{_('Updated')}{_('Password')}{_('Password OK')}
+

{_('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.')}

+
+ ); + } + + // 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 (
- - - - - - - - - - - - {mkComps} - -
{_('Active')}{_('ID')}{_('Source')}{_('Created')}{_('Updated')}{_('Password')}{_('Password OK')}
- {_('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.')} +
+

{_('Status')}

+

{_('Encryption is:')} {this.props.encryptionEnabled ? _('Enabled') : _('Disabled')}

+ {decryptedItemsInfo} + {toggleButton} + {masterKeySection} +
); } diff --git a/ElectronClient/app/gui/dialogs.js b/ElectronClient/app/gui/dialogs.js new file mode 100644 index 000000000..e2853f1a0 --- /dev/null +++ b/ElectronClient/app/gui/dialogs.js @@ -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; \ No newline at end of file diff --git a/ElectronClient/app/index.html b/ElectronClient/app/index.html index 6bb72a198..db4f88e75 100644 --- a/ElectronClient/app/index.html +++ b/ElectronClient/app/index.html @@ -6,6 +6,18 @@ + +
diff --git a/ElectronClient/app/theme.js b/ElectronClient/app/theme.js index 968f35842..e2816520d 100644 --- a/ElectronClient/app/theme.js +++ b/ElectronClient/app/theme.js @@ -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; diff --git a/ReactNativeClient/lib/models/BaseItem.js b/ReactNativeClient/lib/models/BaseItem.js index 2a0da2dc6..9a13febc5 100644 --- a/ReactNativeClient/lib/models/BaseItem.js +++ b/ReactNativeClient/lib/models/BaseItem.js @@ -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(); diff --git a/ReactNativeClient/lib/services/EncryptionService.js b/ReactNativeClient/lib/services/EncryptionService.js index 815c487b3..46fb089d0 100644 --- a/ReactNativeClient/lib/services/EncryptionService.js +++ b/ReactNativeClient/lib/services/EncryptionService.js @@ -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); }