Skip to content

Commit

Permalink
All: Add support for single master password, to simplify handling of …
Browse files Browse the repository at this point in the history
…multiple encryption keys
  • Loading branch information
laurent22 committed Aug 30, 2021
1 parent 596f679 commit ce89ee5
Show file tree
Hide file tree
Showing 13 changed files with 295 additions and 48 deletions.
79 changes: 68 additions & 11 deletions packages/app-desktop/gui/EncryptionConfigScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}

Expand Down Expand Up @@ -45,6 +53,10 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
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,
Expand All @@ -60,8 +72,23 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
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 (
<td style={{ ...theme.textStyle, color: theme.colorFaded, fontStyle: 'italic' }}>
({_('Master password')})
</td>
);
} else {
return (
<td style={theme.textStyle}>
<input type="password" style={passwordStyle} value={password} onChange={event => onPasswordChange(event)} />{' '}
<button style={theme.buttonStyle} onClick={() => onSaveClick()}>
{_('Save')}
</button>
</td>
);
}
};

const password = this.state.passwords[mk.id] ? this.state.passwords[mk.id] : '';
Expand All @@ -74,12 +101,7 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
<td style={theme.textStyle}>{activeIcon}</td>
<td style={theme.textStyle}>{mk.id}<br/>{_('Source: ')}{mk.source_application}</td>
<td style={theme.textStyle}>{_('Created: ')}{time.formatMsToLocal(mk.created_time)}<br/>{_('Updated: ')}{time.formatMsToLocal(mk.updated_time)}</td>
<td style={theme.textStyle}>
<input type="password" style={passwordStyle} value={password} onChange={event => onPasswordChange(event)} />{' '}
<button style={theme.buttonStyle} onClick={() => onSaveClick()}>
{_('Save')}
</button>
</td>
{renderPasswordInput(mk.id)}
<td style={theme.textStyle}>{passwordOk}</td>
<td style={theme.textStyle}>
<button disabled={isActive || isDefault} style={theme.buttonStyle} onClick={() => onToggleEnabledClick()}>{masterKeyEnabled(mk) ? _('Disable') : _('Enable')}</button>
Expand Down Expand Up @@ -165,7 +187,7 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
}

const headerComp = isEnabledMasterKeys ? <h1 style={theme.h1Style}>{_('Master Keys')}</h1> : <a onClick={() => shared.toggleShowDisabledMasterKeys(this) } style={{ ...theme.urlStyle, display: 'inline-block', marginBottom: 10 }} href="#">{showTable ? _('Hide disabled master keys') : _('Show disabled master keys')}</a>;
const infoComp = isEnabledMasterKeys ? <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> : null;
const infoComp = isEnabledMasterKeys ? <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> : null;
const tableComp = !showTable ? null : (
<table>
<tbody>
Expand Down Expand Up @@ -195,6 +217,39 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
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 (
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<span style={theme.textStyle}>{_('Master password:')}</span>&nbsp;&nbsp;
<span style={{ ...theme.textStyle, fontWeight: 'bold' }}>{_('Loaded')}</span>
</div>
);
} else {
return (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<span style={theme.textStyle}>{'The master password is not set or is invalid. Please type it below:'}</span>
<div style={{ display: 'flex', flexDirection: 'row', marginTop: 10 }}>
<MasterPasswordInput placeholder={_('Enter your master password')} type="password" value={this.state.masterPasswordInput} onChange={(event: any) => shared.onMasterPasswordChange(this, event.target.value)} />{' '}
<Button ml="10px" level={ButtonLevel.Secondary} onClick={onMasterPasswordSave} title={_('Save')} />
</div>
</div>
);
}
}

render() {
const theme = themeStyle(this.props.themeId);
const masterKeys: MasterKeyEntity[] = this.props.masterKeys;
Expand All @@ -215,7 +270,7 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {

const onToggleButtonClick = async () => {
const isEnabled = getEncryptionEnabled();
const masterKey = MasterKey.latest();
const masterKey = getDefaultMasterKey();

let answer = null;
if (isEnabled) {
Expand Down Expand Up @@ -304,6 +359,7 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
<p style={theme.textStyle}>
{_('Encryption is:')} <strong>{this.props.encryptionEnabled ? _('Enabled') : _('Disabled')}</strong>
</p>
{this.renderMasterPassword()}
{decryptedItemsInfo}
{toggleButton}
{needUpgradeSection}
Expand All @@ -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'],
};
};

Expand Down
59 changes: 56 additions & 3 deletions packages/app-mobile/components/screens/encryption-config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,29 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
inputStyle.borderBottomWidth = 1;
inputStyle.borderBottomColor = theme.dividerColor;

const renderPasswordInput = (masterKeyId: string) => {
if (this.state.masterPasswordKeys[masterKeyId] || !this.state.passwordChecks['master']) {
return (
<Text style={{ ...this.styles().normalText, color: theme.colorFaded, fontStyle: 'italic' }}>({_('Master password')})</Text>
);
} else {
return (
<View style={{ flex: 1, flexDirection: 'row', alignItems: 'center' }}>
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={password} onChangeText={(text: string) => onPasswordChange(text)} style={inputStyle}></TextInput>
<Text style={{ fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{passwordOk}</Text>
<Button title={_('Save')} onPress={() => onSaveClick()}></Button>
</View>
);
}
};

return (
<View key={mk.id}>
<Text style={this.styles().titleText}>{_('Master Key %s', mk.id.substr(0, 6))}</Text>
<Text style={this.styles().normalText}>{_('Created: %s', time.formatMsToLocal(mk.created_time))}</Text>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ flex: 0, fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{_('Password:')}</Text>
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={password} onChangeText={(text: string) => onPasswordChange(text)} style={inputStyle}></TextInput>
<Text style={{ fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{passwordOk}</Text>
<Button title={_('Save')} onPress={() => onSaveClick()}></Button>
{renderPasswordInput(mk.id)}
</View>
</View>
);
Expand Down Expand Up @@ -203,6 +217,43 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
);
}

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 (
<View style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ ...this.styles().normalText, flex: 0, marginRight: 5 }}>{_('Master password:')}</Text>
<Text style={{ ...this.styles().normalText, fontWeight: 'bold' }}>{_('Loaded')}</Text>
</View>
);
} else {
return (
<View style={{ display: 'flex', flexDirection: 'column', marginTop: 10 }}>
<Text style={this.styles().normalText}>{'The master password is not set or is invalid. Please type it below:'}</Text>
<View style={{ display: 'flex', flexDirection: 'row', marginTop: 10 }}>
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={this.state.masterPasswordInput} onChangeText={(text: string) => shared.onMasterPasswordChange(this, text)} style={inputStyle}></TextInput>
<Button onPress={onMasterPasswordSave} title={_('Save')} />
</View>
</View>
);
}
}

render() {
const theme = themeStyle(this.props.themeId);
const masterKeys = this.props.masterKeys;
Expand Down Expand Up @@ -289,6 +340,7 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
<Text style={this.styles().titleText}>{_('Status')}</Text>
<Text style={this.styles().normalText}>{_('Encryption is: %s', this.props.encryptionEnabled ? _('Enabled') : _('Disabled'))}</Text>
{decryptedItemsInfo}
{this.renderMasterPassword()}
{toggleButton}
{passwordPromptComp}
{mkComps}
Expand All @@ -315,6 +367,7 @@ const EncryptionConfigScreen = connect((state: State) => {
encryptionEnabled: syncInfo.e2ee,
activeMasterKeyId: syncInfo.activeMasterKeyId,
notLoadedMasterKeys: state.notLoadedMasterKeys,
masterPassword: state.settings['encryption.masterPassword'],
};
})(EncryptionConfigScreenComponent);

Expand Down
5 changes: 5 additions & 0 deletions packages/app-mobile/ios/Joplin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
<key>api.joplincloud.local</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
<key>NSCameraUsageDescription</key>
Expand Down
2 changes: 1 addition & 1 deletion packages/app-mobile/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,7 @@ SPEC CHECKSUMS:
boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c
DoubleConversion: cf9b38bf0b2d048436d9a82ad2abe1404f11e7de
FBLazyVector: e686045572151edef46010a6f819ade377dfeb4b
FBReactNativeSpec: 6da2c8ff1ebe6b6cf4510fcca58c24c4d02b16fc
FBReactNativeSpec: d2f54de51f69366bd1f5c1fb9270698dce678f8d
glog: 73c2498ac6884b13ede40eda8228cb1eee9d9d62
JoplinCommonShareExtension: 270b4f8eb4e22828eeda433a04ed689fc1fd09b5
JoplinRNShareExtension: 7137e9787374e1b0797ecbef9103d1588d90e403
Expand Down
18 changes: 14 additions & 4 deletions packages/app-mobile/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import ResourceService from '@joplin/lib/services/ResourceService';
import KvStore from '@joplin/lib/services/KvStore';
import NoteScreen from './components/screens/Note';
import UpgradeSyncTargetScreen from './components/screens/UpgradeSyncTargetScreen';
import Setting from '@joplin/lib/models/Setting';
import Setting, { Env } from '@joplin/lib/models/Setting';
import RNFetchBlob from 'rn-fetch-blob';
import PoorManIntervals from '@joplin/lib/PoorManIntervals';
import reducer from '@joplin/lib/reducer';
Expand Down Expand Up @@ -103,7 +103,7 @@ import { clearSharedFilesCache } from './utils/ShareUtils';
import setIgnoreTlsErrors from './utils/TlsUtils';
import ShareService from '@joplin/lib/services/share/ShareService';
import setupNotifications from './utils/setupNotifications';
import { loadMasterKeysFromSettings } from '@joplin/lib/services/e2ee/utils';
import { loadMasterKeysFromSettings, migrateMasterPassword } from '@joplin/lib/services/e2ee/utils';
import SyncTargetNone from '../lib/SyncTargetNone';

let storeDispatch = function(_action: any) {};
Expand Down Expand Up @@ -474,7 +474,7 @@ async function initialize(dispatch: Function) {
if (Setting.value('env') == 'prod') {
await db.open({ name: 'joplin.sqlite' });
} else {
await db.open({ name: 'joplin-101.sqlite' });
await db.open({ name: 'joplin-104.sqlite' });

// await db.clearForTesting();
}
Expand All @@ -483,6 +483,14 @@ async function initialize(dispatch: Function) {
reg.logger().info('Loading settings...');

await loadKeychainServiceAndSettings(KeychainServiceDriverMobile);
await migrateMasterPassword();

if (Setting.value('env') === Env.Dev) {
// Setting.setValue('sync.10.path', 'https://api.joplincloud.com');
// Setting.setValue('sync.10.userContentPath', 'https://joplinusercontent.com');
Setting.setValue('sync.10.path', 'http://api.joplincloud.local:22300');
Setting.setValue('sync.10.userContentPath', 'http://joplinusercontent.local:22300');
}

if (!Setting.value('clientId')) Setting.setValue('clientId', uuid.create());

Expand Down Expand Up @@ -530,7 +538,6 @@ async function initialize(dispatch: Function) {
// ----------------------------------------------------------------

EncryptionService.fsDriver_ = fsDriver;
EncryptionService.instance().setLogger(mainLogger);
// eslint-disable-next-line require-atomic-updates
BaseItem.encryptionService_ = EncryptionService.instance();
BaseItem.shareService_ = ShareService.instance();
Expand Down Expand Up @@ -725,6 +732,9 @@ class AppComponent extends React.Component {
setupQuickActions(this.props.dispatch, this.props.selectedFolderId);

await setupNotifications(this.props.dispatch);

// Setting.setValue('encryption.masterPassword', 'WRONG');
// setTimeout(() => NavService.go('EncryptionConfig'), 2000);
}

componentWillUnmount() {
Expand Down
7 changes: 4 additions & 3 deletions packages/lib/BaseApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@ import SearchEngine from './services/searchengine/SearchEngine';
import RevisionService from './services/RevisionService';
import ResourceService from './services/ResourceService';
import DecryptionWorker from './services/DecryptionWorker';
const { loadKeychainServiceAndSettings } = require('./services/SettingUtils');
import { loadKeychainServiceAndSettings } from './services/SettingUtils';
import MigrationService from './services/MigrationService';
import ShareService from './services/share/ShareService';
import handleSyncStartupOperation from './services/synchronizer/utils/handleSyncStartupOperation';
import SyncTargetJoplinCloud from './SyncTargetJoplinCloud';
const { toSystemSlashes } = require('./path-utils');
const { setAutoFreeze } = require('immer');
import { getEncryptionEnabled } from './services/synchronizer/syncInfoUtils';
import { loadMasterKeysFromSettings } from './services/e2ee/utils';
import { loadMasterKeysFromSettings, migrateMasterPassword } from './services/e2ee/utils';
import SyncTargetNone from './SyncTargetNone';

const appLogger: LoggerWrapper = Logger.create('App');
Expand Down Expand Up @@ -465,6 +465,7 @@ export default class BaseApplication {
sideEffects['timeFormat'] = sideEffects['dateFormat'];
sideEffects['locale'] = sideEffects['dateFormat'];
sideEffects['encryption.passwordCache'] = sideEffects['syncInfoCache'];
sideEffects['encryption.masterPassword'] = sideEffects['syncInfoCache'];

if (action) {
const effect = sideEffects[action.key];
Expand Down Expand Up @@ -768,6 +769,7 @@ export default class BaseApplication {
BaseModel.setDb(this.database_);

await loadKeychainServiceAndSettings(options.keychainEnabled ? KeychainServiceDriver : KeychainServiceDriverDummy);
await migrateMasterPassword();
await handleSyncStartupOperation();

appLogger.info(`Client ID: ${Setting.value('clientId')}`);
Expand Down Expand Up @@ -825,7 +827,6 @@ export default class BaseApplication {

KvStore.instance().setDb(reg.db());

EncryptionService.instance().setLogger(globalLogger);
BaseItem.encryptionService_ = EncryptionService.instance();
BaseItem.shareService_ = ShareService.instance();
DecryptionWorker.instance().setLogger(globalLogger);
Expand Down
Loading

0 comments on commit ce89ee5

Please sign in to comment.