From ce89ee5bab491e0a62a130df28902aa0848066e5 Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Mon, 30 Aug 2021 14:15:35 +0100 Subject: [PATCH] All: Add support for single master password, to simplify handling of multiple encryption keys --- .../gui/EncryptionConfigScreen.tsx | 79 ++++++++++++++++--- .../components/screens/encryption-config.tsx | 59 +++++++++++++- packages/app-mobile/ios/Joplin/Info.plist | 5 ++ packages/app-mobile/ios/Podfile.lock | 2 +- packages/app-mobile/root.tsx | 18 ++++- packages/lib/BaseApplication.ts | 7 +- .../shared/encryption-config-shared.ts | 39 ++++++++- packages/lib/models/MasterKey.ts | 1 - packages/lib/models/Setting.ts | 1 + packages/lib/services/DecryptionWorker.ts | 8 +- .../lib/services/e2ee/EncryptionService.ts | 20 ++--- packages/lib/services/e2ee/utils.test.ts | 34 +++++++- packages/lib/services/e2ee/utils.ts | 70 ++++++++++++++-- 13 files changed, 295 insertions(+), 48 deletions(-) diff --git a/packages/app-desktop/gui/EncryptionConfigScreen.tsx b/packages/app-desktop/gui/EncryptionConfigScreen.tsx index 3da507a8a0c..1c7534f9f82 100644 --- a/packages/app-desktop/gui/EncryptionConfigScreen.tsx +++ b/packages/app-desktop/gui/EncryptionConfigScreen.tsx @@ -12,8 +12,16 @@ import bridge from '../services/bridge'; import shared from '@joplin/lib/components/shared/encryption-config-shared'; import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types'; import { getEncryptionEnabled, masterKeyEnabled, SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils'; -import { toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils'; +import { getDefaultMasterKey, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils'; import MasterKey from '@joplin/lib/models/MasterKey'; +import StyledInput from './style/StyledInput'; +import Button, { ButtonLevel } from './Button/Button'; +import styled from 'styled-components'; + +const MasterPasswordInput = styled(StyledInput)` + min-width: 300px; + align-items: center; +`; interface Props {} @@ -45,6 +53,10 @@ class EncryptionConfigScreenComponent extends React.Component { private renderMasterKey(mk: MasterKeyEntity, isDefault: boolean) { const theme = themeStyle(this.props.themeId); + const onToggleEnabledClick = () => { + return shared.onToggleEnabledClick(this, mk); + }; + const passwordStyle = { color: theme.color, backgroundColor: theme.backgroundColor, @@ -60,8 +72,23 @@ class EncryptionConfigScreenComponent extends React.Component { return shared.onPasswordChange(this, mk, event.target.value); }; - const onToggleEnabledClick = () => { - return shared.onToggleEnabledClick(this, mk); + const renderPasswordInput = (masterKeyId: string) => { + if (this.state.masterPasswordKeys[masterKeyId] || !this.state.passwordChecks['master']) { + return ( + + ({_('Master password')}) + + ); + } else { + return ( + + onPasswordChange(event)} />{' '} + + + ); + } }; const password = this.state.passwords[mk.id] ? this.state.passwords[mk.id] : ''; @@ -74,12 +101,7 @@ class EncryptionConfigScreenComponent extends React.Component { {activeIcon} {mk.id}
{_('Source: ')}{mk.source_application} {_('Created: ')}{time.formatMsToLocal(mk.created_time)}
{_('Updated: ')}{time.formatMsToLocal(mk.updated_time)} - - onPasswordChange(event)} />{' '} - - + {renderPasswordInput(mk.id)} {passwordOk} @@ -165,7 +187,7 @@ class EncryptionConfigScreenComponent extends React.Component { } const headerComp = isEnabledMasterKeys ?

{_('Master Keys')}

: shared.toggleShowDisabledMasterKeys(this) } style={{ ...theme.urlStyle, display: 'inline-block', marginBottom: 10 }} href="#">{showTable ? _('Hide disabled master keys') : _('Show disabled master keys')}; - const infoComp = isEnabledMasterKeys ?

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

: null; + const infoComp = isEnabledMasterKeys ?

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

: null; const tableComp = !showTable ? null : ( @@ -195,6 +217,39 @@ class EncryptionConfigScreenComponent extends React.Component { return null; } + private renderMasterPassword() { + if (!this.props.encryptionEnabled && !this.props.masterKeys.length) return null; + + const theme = themeStyle(this.props.themeId); + + const onMasterPasswordSave = async () => { + shared.onMasterPasswordSave(this); + + if (!(await shared.masterPasswordIsValid(this, this.state.masterPasswordInput))) { + alert('Password is invalid. Please try again.'); + } + }; + + if (this.state.passwordChecks['master']) { + return ( +
+ {_('Master password:')}   + ✔ {_('Loaded')} +
+ ); + } else { + return ( +
+ ❌ {'The master password is not set or is invalid. Please type it below:'} +
+ shared.onMasterPasswordChange(this, event.target.value)} />{' '} +
+
+ ); + } + } + render() { const theme = themeStyle(this.props.themeId); const masterKeys: MasterKeyEntity[] = this.props.masterKeys; @@ -215,7 +270,7 @@ class EncryptionConfigScreenComponent extends React.Component { const onToggleButtonClick = async () => { const isEnabled = getEncryptionEnabled(); - const masterKey = MasterKey.latest(); + const masterKey = getDefaultMasterKey(); let answer = null; if (isEnabled) { @@ -304,6 +359,7 @@ class EncryptionConfigScreenComponent extends React.Component {

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

+ {this.renderMasterPassword()} {decryptedItemsInfo} {toggleButton} {needUpgradeSection} @@ -329,6 +385,7 @@ const mapStateToProps = (state: State) => { activeMasterKeyId: syncInfo.activeMasterKeyId, shouldReencrypt: state.settings['encryption.shouldReencrypt'] >= Setting.SHOULD_REENCRYPT_YES, notLoadedMasterKeys: state.notLoadedMasterKeys, + masterPassword: state.settings['encryption.masterPassword'], }; }; diff --git a/packages/app-mobile/components/screens/encryption-config.tsx b/packages/app-mobile/components/screens/encryption-config.tsx index e4202667c7e..53594c57d95 100644 --- a/packages/app-mobile/components/screens/encryption-config.tsx +++ b/packages/app-mobile/components/screens/encryption-config.tsx @@ -116,15 +116,29 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent { inputStyle.borderBottomWidth = 1; inputStyle.borderBottomColor = theme.dividerColor; + const renderPasswordInput = (masterKeyId: string) => { + if (this.state.masterPasswordKeys[masterKeyId] || !this.state.passwordChecks['master']) { + return ( + ({_('Master password')}) + ); + } else { + return ( + + onPasswordChange(text)} style={inputStyle}> + {passwordOk} + + + ); + } + }; + return ( {_('Master Key %s', mk.id.substr(0, 6))} {_('Created: %s', time.formatMsToLocal(mk.created_time))} {_('Password:')} - onPasswordChange(text)} style={inputStyle}> - {passwordOk} - + {renderPasswordInput(mk.id)} ); @@ -203,6 +217,43 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent { ); } + private renderMasterPassword() { + if (!this.props.encryptionEnabled && !this.props.masterKeys.length) return null; + + const theme = themeStyle(this.props.themeId); + + const onMasterPasswordSave = async () => { + shared.onMasterPasswordSave(this); + + if (!(await shared.masterPasswordIsValid(this, this.state.masterPasswordInput))) { + alert('Password is invalid. Please try again.'); + } + }; + + const inputStyle: any = { flex: 1, marginRight: 10, color: theme.color }; + inputStyle.borderBottomWidth = 1; + inputStyle.borderBottomColor = theme.dividerColor; + + if (this.state.passwordChecks['master']) { + return ( + + {_('Master password:')} + {_('Loaded')} + + ); + } else { + return ( + + {'The master password is not set or is invalid. Please type it below:'} + + shared.onMasterPasswordChange(this, text)} style={inputStyle}> +