1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-06-30 23:44:55 +02:00

All: Add support for single master password, to simplify handling of multiple encryption keys

This commit is contained in:
Laurent Cozic
2021-08-30 14:15:35 +01:00
parent 596f679b1f
commit ce89ee5bab
13 changed files with 295 additions and 48 deletions

View File

@ -12,8 +12,16 @@ import bridge from '../services/bridge';
import shared from '@joplin/lib/components/shared/encryption-config-shared'; import shared from '@joplin/lib/components/shared/encryption-config-shared';
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types'; import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
import { getEncryptionEnabled, masterKeyEnabled, SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils'; 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 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 {} interface Props {}
@ -45,6 +53,10 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
private renderMasterKey(mk: MasterKeyEntity, isDefault: boolean) { private renderMasterKey(mk: MasterKeyEntity, isDefault: boolean) {
const theme = themeStyle(this.props.themeId); const theme = themeStyle(this.props.themeId);
const onToggleEnabledClick = () => {
return shared.onToggleEnabledClick(this, mk);
};
const passwordStyle = { const passwordStyle = {
color: theme.color, color: theme.color,
backgroundColor: theme.backgroundColor, backgroundColor: theme.backgroundColor,
@ -60,8 +72,23 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
return shared.onPasswordChange(this, mk, event.target.value); return shared.onPasswordChange(this, mk, event.target.value);
}; };
const onToggleEnabledClick = () => { const renderPasswordInput = (masterKeyId: string) => {
return shared.onToggleEnabledClick(this, mk); 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] : ''; const password = this.state.passwords[mk.id] ? this.state.passwords[mk.id] : '';
@ -74,12 +101,7 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
<td style={theme.textStyle}>{activeIcon}</td> <td style={theme.textStyle}>{activeIcon}</td>
<td style={theme.textStyle}>{mk.id}<br/>{_('Source: ')}{mk.source_application}</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}>{_('Created: ')}{time.formatMsToLocal(mk.created_time)}<br/>{_('Updated: ')}{time.formatMsToLocal(mk.updated_time)}</td>
<td style={theme.textStyle}> {renderPasswordInput(mk.id)}
<input type="password" style={passwordStyle} value={password} onChange={event => onPasswordChange(event)} />{' '}
<button style={theme.buttonStyle} onClick={() => onSaveClick()}>
{_('Save')}
</button>
</td>
<td style={theme.textStyle}>{passwordOk}</td> <td style={theme.textStyle}>{passwordOk}</td>
<td style={theme.textStyle}> <td style={theme.textStyle}>
<button disabled={isActive || isDefault} style={theme.buttonStyle} onClick={() => onToggleEnabledClick()}>{masterKeyEnabled(mk) ? _('Disable') : _('Enable')}</button> <button disabled={isActive || isDefault} style={theme.buttonStyle} onClick={() => onToggleEnabledClick()}>{masterKeyEnabled(mk) ? _('Disable') : _('Enable')}</button>
@ -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 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 : ( const tableComp = !showTable ? null : (
<table> <table>
<tbody> <tbody>
@ -195,6 +217,39 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
return null; 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() { render() {
const theme = themeStyle(this.props.themeId); const theme = themeStyle(this.props.themeId);
const masterKeys: MasterKeyEntity[] = this.props.masterKeys; const masterKeys: MasterKeyEntity[] = this.props.masterKeys;
@ -215,7 +270,7 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
const onToggleButtonClick = async () => { const onToggleButtonClick = async () => {
const isEnabled = getEncryptionEnabled(); const isEnabled = getEncryptionEnabled();
const masterKey = MasterKey.latest(); const masterKey = getDefaultMasterKey();
let answer = null; let answer = null;
if (isEnabled) { if (isEnabled) {
@ -304,6 +359,7 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
<p style={theme.textStyle}> <p style={theme.textStyle}>
{_('Encryption is:')} <strong>{this.props.encryptionEnabled ? _('Enabled') : _('Disabled')}</strong> {_('Encryption is:')} <strong>{this.props.encryptionEnabled ? _('Enabled') : _('Disabled')}</strong>
</p> </p>
{this.renderMasterPassword()}
{decryptedItemsInfo} {decryptedItemsInfo}
{toggleButton} {toggleButton}
{needUpgradeSection} {needUpgradeSection}
@ -329,6 +385,7 @@ const mapStateToProps = (state: State) => {
activeMasterKeyId: syncInfo.activeMasterKeyId, activeMasterKeyId: syncInfo.activeMasterKeyId,
shouldReencrypt: state.settings['encryption.shouldReencrypt'] >= Setting.SHOULD_REENCRYPT_YES, shouldReencrypt: state.settings['encryption.shouldReencrypt'] >= Setting.SHOULD_REENCRYPT_YES,
notLoadedMasterKeys: state.notLoadedMasterKeys, notLoadedMasterKeys: state.notLoadedMasterKeys,
masterPassword: state.settings['encryption.masterPassword'],
}; };
}; };

View File

@ -116,15 +116,29 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
inputStyle.borderBottomWidth = 1; inputStyle.borderBottomWidth = 1;
inputStyle.borderBottomColor = theme.dividerColor; 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 ( return (
<View key={mk.id}> <View key={mk.id}>
<Text style={this.styles().titleText}>{_('Master Key %s', mk.id.substr(0, 6))}</Text> <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> <Text style={this.styles().normalText}>{_('Created: %s', time.formatMsToLocal(mk.created_time))}</Text>
<View style={{ flexDirection: 'row', alignItems: 'center' }}> <View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ flex: 0, fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{_('Password:')}</Text> <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> {renderPasswordInput(mk.id)}
<Text style={{ fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{passwordOk}</Text>
<Button title={_('Save')} onPress={() => onSaveClick()}></Button>
</View> </View>
</View> </View>
); );
@ -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() { render() {
const theme = themeStyle(this.props.themeId); const theme = themeStyle(this.props.themeId);
const masterKeys = this.props.masterKeys; const masterKeys = this.props.masterKeys;
@ -289,6 +340,7 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
<Text style={this.styles().titleText}>{_('Status')}</Text> <Text style={this.styles().titleText}>{_('Status')}</Text>
<Text style={this.styles().normalText}>{_('Encryption is: %s', this.props.encryptionEnabled ? _('Enabled') : _('Disabled'))}</Text> <Text style={this.styles().normalText}>{_('Encryption is: %s', this.props.encryptionEnabled ? _('Enabled') : _('Disabled'))}</Text>
{decryptedItemsInfo} {decryptedItemsInfo}
{this.renderMasterPassword()}
{toggleButton} {toggleButton}
{passwordPromptComp} {passwordPromptComp}
{mkComps} {mkComps}
@ -315,6 +367,7 @@ const EncryptionConfigScreen = connect((state: State) => {
encryptionEnabled: syncInfo.e2ee, encryptionEnabled: syncInfo.e2ee,
activeMasterKeyId: syncInfo.activeMasterKeyId, activeMasterKeyId: syncInfo.activeMasterKeyId,
notLoadedMasterKeys: state.notLoadedMasterKeys, notLoadedMasterKeys: state.notLoadedMasterKeys,
masterPassword: state.settings['encryption.masterPassword'],
}; };
})(EncryptionConfigScreenComponent); })(EncryptionConfigScreenComponent);

View File

@ -46,6 +46,11 @@
<key>NSExceptionAllowsInsecureHTTPLoads</key> <key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/> <true/>
</dict> </dict>
<key>api.joplincloud.local</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict> </dict>
</dict> </dict>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>

View File

@ -488,7 +488,7 @@ SPEC CHECKSUMS:
boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c
DoubleConversion: cf9b38bf0b2d048436d9a82ad2abe1404f11e7de DoubleConversion: cf9b38bf0b2d048436d9a82ad2abe1404f11e7de
FBLazyVector: e686045572151edef46010a6f819ade377dfeb4b FBLazyVector: e686045572151edef46010a6f819ade377dfeb4b
FBReactNativeSpec: 6da2c8ff1ebe6b6cf4510fcca58c24c4d02b16fc FBReactNativeSpec: d2f54de51f69366bd1f5c1fb9270698dce678f8d
glog: 73c2498ac6884b13ede40eda8228cb1eee9d9d62 glog: 73c2498ac6884b13ede40eda8228cb1eee9d9d62
JoplinCommonShareExtension: 270b4f8eb4e22828eeda433a04ed689fc1fd09b5 JoplinCommonShareExtension: 270b4f8eb4e22828eeda433a04ed689fc1fd09b5
JoplinRNShareExtension: 7137e9787374e1b0797ecbef9103d1588d90e403 JoplinRNShareExtension: 7137e9787374e1b0797ecbef9103d1588d90e403

View File

@ -14,7 +14,7 @@ import ResourceService from '@joplin/lib/services/ResourceService';
import KvStore from '@joplin/lib/services/KvStore'; import KvStore from '@joplin/lib/services/KvStore';
import NoteScreen from './components/screens/Note'; import NoteScreen from './components/screens/Note';
import UpgradeSyncTargetScreen from './components/screens/UpgradeSyncTargetScreen'; 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 RNFetchBlob from 'rn-fetch-blob';
import PoorManIntervals from '@joplin/lib/PoorManIntervals'; import PoorManIntervals from '@joplin/lib/PoorManIntervals';
import reducer from '@joplin/lib/reducer'; import reducer from '@joplin/lib/reducer';
@ -103,7 +103,7 @@ import { clearSharedFilesCache } from './utils/ShareUtils';
import setIgnoreTlsErrors from './utils/TlsUtils'; import setIgnoreTlsErrors from './utils/TlsUtils';
import ShareService from '@joplin/lib/services/share/ShareService'; import ShareService from '@joplin/lib/services/share/ShareService';
import setupNotifications from './utils/setupNotifications'; 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'; import SyncTargetNone from '../lib/SyncTargetNone';
let storeDispatch = function(_action: any) {}; let storeDispatch = function(_action: any) {};
@ -474,7 +474,7 @@ async function initialize(dispatch: Function) {
if (Setting.value('env') == 'prod') { if (Setting.value('env') == 'prod') {
await db.open({ name: 'joplin.sqlite' }); await db.open({ name: 'joplin.sqlite' });
} else { } else {
await db.open({ name: 'joplin-101.sqlite' }); await db.open({ name: 'joplin-104.sqlite' });
// await db.clearForTesting(); // await db.clearForTesting();
} }
@ -483,6 +483,14 @@ async function initialize(dispatch: Function) {
reg.logger().info('Loading settings...'); reg.logger().info('Loading settings...');
await loadKeychainServiceAndSettings(KeychainServiceDriverMobile); 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()); if (!Setting.value('clientId')) Setting.setValue('clientId', uuid.create());
@ -530,7 +538,6 @@ async function initialize(dispatch: Function) {
// ---------------------------------------------------------------- // ----------------------------------------------------------------
EncryptionService.fsDriver_ = fsDriver; EncryptionService.fsDriver_ = fsDriver;
EncryptionService.instance().setLogger(mainLogger);
// eslint-disable-next-line require-atomic-updates // eslint-disable-next-line require-atomic-updates
BaseItem.encryptionService_ = EncryptionService.instance(); BaseItem.encryptionService_ = EncryptionService.instance();
BaseItem.shareService_ = ShareService.instance(); BaseItem.shareService_ = ShareService.instance();
@ -725,6 +732,9 @@ class AppComponent extends React.Component {
setupQuickActions(this.props.dispatch, this.props.selectedFolderId); setupQuickActions(this.props.dispatch, this.props.selectedFolderId);
await setupNotifications(this.props.dispatch); await setupNotifications(this.props.dispatch);
// Setting.setValue('encryption.masterPassword', 'WRONG');
// setTimeout(() => NavService.go('EncryptionConfig'), 2000);
} }
componentWillUnmount() { componentWillUnmount() {

View File

@ -43,7 +43,7 @@ import SearchEngine from './services/searchengine/SearchEngine';
import RevisionService from './services/RevisionService'; import RevisionService from './services/RevisionService';
import ResourceService from './services/ResourceService'; import ResourceService from './services/ResourceService';
import DecryptionWorker from './services/DecryptionWorker'; import DecryptionWorker from './services/DecryptionWorker';
const { loadKeychainServiceAndSettings } = require('./services/SettingUtils'); import { loadKeychainServiceAndSettings } from './services/SettingUtils';
import MigrationService from './services/MigrationService'; import MigrationService from './services/MigrationService';
import ShareService from './services/share/ShareService'; import ShareService from './services/share/ShareService';
import handleSyncStartupOperation from './services/synchronizer/utils/handleSyncStartupOperation'; import handleSyncStartupOperation from './services/synchronizer/utils/handleSyncStartupOperation';
@ -51,7 +51,7 @@ import SyncTargetJoplinCloud from './SyncTargetJoplinCloud';
const { toSystemSlashes } = require('./path-utils'); const { toSystemSlashes } = require('./path-utils');
const { setAutoFreeze } = require('immer'); const { setAutoFreeze } = require('immer');
import { getEncryptionEnabled } from './services/synchronizer/syncInfoUtils'; import { getEncryptionEnabled } from './services/synchronizer/syncInfoUtils';
import { loadMasterKeysFromSettings } from './services/e2ee/utils'; import { loadMasterKeysFromSettings, migrateMasterPassword } from './services/e2ee/utils';
import SyncTargetNone from './SyncTargetNone'; import SyncTargetNone from './SyncTargetNone';
const appLogger: LoggerWrapper = Logger.create('App'); const appLogger: LoggerWrapper = Logger.create('App');
@ -465,6 +465,7 @@ export default class BaseApplication {
sideEffects['timeFormat'] = sideEffects['dateFormat']; sideEffects['timeFormat'] = sideEffects['dateFormat'];
sideEffects['locale'] = sideEffects['dateFormat']; sideEffects['locale'] = sideEffects['dateFormat'];
sideEffects['encryption.passwordCache'] = sideEffects['syncInfoCache']; sideEffects['encryption.passwordCache'] = sideEffects['syncInfoCache'];
sideEffects['encryption.masterPassword'] = sideEffects['syncInfoCache'];
if (action) { if (action) {
const effect = sideEffects[action.key]; const effect = sideEffects[action.key];
@ -768,6 +769,7 @@ export default class BaseApplication {
BaseModel.setDb(this.database_); BaseModel.setDb(this.database_);
await loadKeychainServiceAndSettings(options.keychainEnabled ? KeychainServiceDriver : KeychainServiceDriverDummy); await loadKeychainServiceAndSettings(options.keychainEnabled ? KeychainServiceDriver : KeychainServiceDriverDummy);
await migrateMasterPassword();
await handleSyncStartupOperation(); await handleSyncStartupOperation();
appLogger.info(`Client ID: ${Setting.value('clientId')}`); appLogger.info(`Client ID: ${Setting.value('clientId')}`);
@ -825,7 +827,6 @@ export default class BaseApplication {
KvStore.instance().setDb(reg.db()); KvStore.instance().setDb(reg.db());
EncryptionService.instance().setLogger(globalLogger);
BaseItem.encryptionService_ = EncryptionService.instance(); BaseItem.encryptionService_ = EncryptionService.instance();
BaseItem.shareService_ = ShareService.instance(); BaseItem.shareService_ = ShareService.instance();
DecryptionWorker.instance().setLogger(globalLogger); DecryptionWorker.instance().setLogger(globalLogger);

View File

@ -8,6 +8,7 @@ import shim from '../../shim';
import { MasterKeyEntity } from '../../services/e2ee/types'; import { MasterKeyEntity } from '../../services/e2ee/types';
import time from '../../time'; import time from '../../time';
import { masterKeyEnabled, setMasterKeyEnabled } from '../../services/synchronizer/syncInfoUtils'; import { masterKeyEnabled, setMasterKeyEnabled } from '../../services/synchronizer/syncInfoUtils';
import { findMasterKeyPassword } from '../../services/e2ee/utils';
class Shared { class Shared {
@ -16,12 +17,16 @@ class Shared {
public initialize(comp: any, props: any) { public initialize(comp: any, props: any) {
comp.state = { comp.state = {
passwordChecks: {}, passwordChecks: {},
// Master keys that can be decrypted with the master password
// (normally all of them, but for legacy support we need this).
masterPasswordKeys: {},
stats: { stats: {
encrypted: null, encrypted: null,
total: null, total: null,
}, },
passwords: Object.assign({}, props.passwords), passwords: Object.assign({}, props.passwords),
showDisabledMasterKeys: false, showDisabledMasterKeys: false,
masterPasswordInput: '',
}; };
comp.isMounted_ = false; comp.isMounted_ = false;
@ -108,15 +113,37 @@ class Shared {
} }
} }
public async masterPasswordIsValid(comp: any, masterPassword: string = null) {
const activeMasterKey = comp.props.masterKeys.find((mk: MasterKeyEntity) => mk.id === comp.props.activeMasterKeyId);
masterPassword = masterPassword === null ? comp.props.masterPassword : masterPassword;
if (activeMasterKey && masterPassword) {
return EncryptionService.instance().checkMasterKeyPassword(activeMasterKey, masterPassword);
}
return false;
}
public async checkPasswords(comp: any) { public async checkPasswords(comp: any) {
const passwordChecks = Object.assign({}, comp.state.passwordChecks); const passwordChecks = Object.assign({}, comp.state.passwordChecks);
const masterPasswordKeys = Object.assign({}, comp.state.masterPasswordKeys);
for (let i = 0; i < comp.props.masterKeys.length; i++) { for (let i = 0; i < comp.props.masterKeys.length; i++) {
const mk = comp.props.masterKeys[i]; const mk = comp.props.masterKeys[i];
const password = comp.state.passwords[mk.id]; const password = await findMasterKeyPassword(EncryptionService.instance(), mk);
const ok = password ? await EncryptionService.instance().checkMasterKeyPassword(mk, password) : false; const ok = password ? await EncryptionService.instance().checkMasterKeyPassword(mk, password) : false;
passwordChecks[mk.id] = ok; passwordChecks[mk.id] = ok;
masterPasswordKeys[mk.id] = password === comp.props.masterPassword;
} }
comp.setState({ passwordChecks: passwordChecks });
passwordChecks['master'] = await this.masterPasswordIsValid(comp);
comp.setState({ passwordChecks, masterPasswordKeys });
}
public masterPasswordStatus(comp: any) {
// Don't translate for now because that's temporary - later it should
// always be set and the label should be replaced by a "Change master
// password" button.
return comp.props.masterPassword ? 'Master password is set' : 'Master password is not set';
} }
public decryptedStatText(comp: any) { public decryptedStatText(comp: any) {
@ -138,6 +165,14 @@ class Shared {
comp.checkPasswords(); comp.checkPasswords();
} }
public onMasterPasswordChange(comp: any, value: string) {
comp.setState({ masterPasswordInput: value });
}
public onMasterPasswordSave(comp: any) {
Setting.setValue('encryption.masterPassword', comp.state.masterPasswordInput);
}
public onPasswordChange(comp: any, mk: MasterKeyEntity, password: string) { public onPasswordChange(comp: any, mk: MasterKeyEntity, password: string) {
const passwords = Object.assign({}, comp.state.passwords); const passwords = Object.assign({}, comp.state.passwords);
passwords[mk.id] = password; passwords[mk.id] = password;

View File

@ -26,7 +26,6 @@ export default class MasterKey extends BaseItem {
} }
} }
return output; return output;
// return this.modelSelectOne('SELECT * FROM master_keys WHERE created_time >= (SELECT max(created_time) FROM master_keys)');
} }
static allWithoutEncryptionMethod(masterKeys: MasterKeyEntity[], method: number) { static allWithoutEncryptionMethod(masterKeys: MasterKeyEntity[], method: number) {

View File

@ -919,6 +919,7 @@ class Setting extends BaseModel {
'encryption.enabled': { value: false, type: SettingItemType.Bool, public: false }, 'encryption.enabled': { value: false, type: SettingItemType.Bool, public: false },
'encryption.activeMasterKeyId': { value: '', type: SettingItemType.String, public: false }, 'encryption.activeMasterKeyId': { value: '', type: SettingItemType.String, public: false },
'encryption.passwordCache': { value: {}, type: SettingItemType.Object, public: false, secure: true }, 'encryption.passwordCache': { value: {}, type: SettingItemType.Object, public: false, secure: true },
'encryption.masterPassword': { value: '', type: SettingItemType.String, public: false, secure: true },
'encryption.shouldReencrypt': { 'encryption.shouldReencrypt': {
value: -1, // will be set on app startup value: -1, // will be set on app startup
type: SettingItemType.Int, type: SettingItemType.Int,

View File

@ -6,6 +6,7 @@ import ResourceService from './ResourceService';
import Logger from '../Logger'; import Logger from '../Logger';
import shim from '../shim'; import shim from '../shim';
import KvStore from './KvStore'; import KvStore from './KvStore';
import EncryptionService from './e2ee/EncryptionService';
const EventEmitter = require('events'); const EventEmitter = require('events');
@ -28,7 +29,7 @@ export default class DecryptionWorker {
private kvStore_: KvStore = null; private kvStore_: KvStore = null;
private maxDecryptionAttempts_ = 2; private maxDecryptionAttempts_ = 2;
private startCalls_: boolean[] = []; private startCalls_: boolean[] = [];
private encryptionService_: any = null; private encryptionService_: EncryptionService = null;
constructor() { constructor() {
this.state_ = 'idle'; this.state_ = 'idle';
@ -134,6 +135,11 @@ export default class DecryptionWorker {
this.logger().info(msg); this.logger().info(msg);
const ids = await MasterKey.allIds(); const ids = await MasterKey.allIds();
// Note that the current implementation means that a warning will be
// displayed even if the user has no encrypted note. Just having
// encrypted master key is sufficient. Not great but good enough for
// now.
if (ids.length) { if (ids.length) {
if (options.masterKeyNotLoadedHandler === 'throw') { if (options.masterKeyNotLoadedHandler === 'throw') {
// By trying to load the master key here, we throw the "masterKeyNotLoaded" error // By trying to load the master key here, we throw the "masterKeyNotLoaded" error

View File

@ -8,6 +8,8 @@ import JoplinError from '../../JoplinError';
import { getActiveMasterKeyId, setActiveMasterKeyId } from '../synchronizer/syncInfoUtils'; import { getActiveMasterKeyId, setActiveMasterKeyId } from '../synchronizer/syncInfoUtils';
const { padLeft } = require('../../string-utils.js'); const { padLeft } = require('../../string-utils.js');
const logger = Logger.create('EncryptionService');
function hexPad(s: string, length: number) { function hexPad(s: string, length: number) {
return padLeft(s, length, '0'); return padLeft(s, length, '0');
} }
@ -52,7 +54,6 @@ export default class EncryptionService {
private decryptedMasterKeys_: Record<string, DecryptedMasterKey> = {}; private decryptedMasterKeys_: Record<string, DecryptedMasterKey> = {};
public defaultEncryptionMethod_ = EncryptionService.METHOD_SJCL_1A; // public because used in tests public defaultEncryptionMethod_ = EncryptionService.METHOD_SJCL_1A; // public because used in tests
private defaultMasterKeyEncryptionMethod_ = EncryptionService.METHOD_SJCL_4; private defaultMasterKeyEncryptionMethod_ = EncryptionService.METHOD_SJCL_4;
private logger_ = new Logger();
private headerTemplates_ = { private headerTemplates_ = {
// Template version 1 // Template version 1
@ -80,7 +81,6 @@ export default class EncryptionService {
this.decryptedMasterKeys_ = {}; this.decryptedMasterKeys_ = {};
this.defaultEncryptionMethod_ = EncryptionService.METHOD_SJCL_1A; this.defaultEncryptionMethod_ = EncryptionService.METHOD_SJCL_1A;
this.defaultMasterKeyEncryptionMethod_ = EncryptionService.METHOD_SJCL_4; this.defaultMasterKeyEncryptionMethod_ = EncryptionService.METHOD_SJCL_4;
this.logger_ = new Logger();
this.headerTemplates_ = { this.headerTemplates_ = {
// Template version 1 // Template version 1
@ -97,14 +97,6 @@ export default class EncryptionService {
return this.instance_; return this.instance_;
} }
setLogger(l: Logger) {
this.logger_ = l;
}
logger() {
return this.logger_;
}
loadedMasterKeysCount() { loadedMasterKeysCount() {
return Object.keys(this.decryptedMasterKeys_).length; return Object.keys(this.decryptedMasterKeys_).length;
} }
@ -139,10 +131,14 @@ export default class EncryptionService {
public async loadMasterKey(model: MasterKeyEntity, password: string, makeActive = false) { public async loadMasterKey(model: MasterKeyEntity, password: string, makeActive = false) {
if (!model.id) throw new Error('Master key does not have an ID - save it first'); if (!model.id) throw new Error('Master key does not have an ID - save it first');
logger.info(`Loading master key: ${model.id}. Make active:`, makeActive);
this.decryptedMasterKeys_[model.id] = { this.decryptedMasterKeys_[model.id] = {
plainText: await this.decryptMasterKey_(model, password), plainText: await this.decryptMasterKey_(model, password),
updatedTime: model.updated_time, updatedTime: model.updated_time,
}; };
if (makeActive) this.setActiveMasterKeyId(model.id); if (makeActive) this.setActiveMasterKeyId(model.id);
} }
@ -245,7 +241,7 @@ export default class EncryptionService {
return plainText; return plainText;
} }
async checkMasterKeyPassword(model: MasterKeyEntity, password: string) { public async checkMasterKeyPassword(model: MasterKeyEntity, password: string) {
try { try {
await this.decryptMasterKey_(model, password); await this.decryptMasterKey_(model, password);
} catch (error) { } catch (error) {
@ -255,7 +251,7 @@ export default class EncryptionService {
return true; return true;
} }
wrapSjclError(sjclError: any) { private wrapSjclError(sjclError: any) {
const error = new Error(sjclError.message); const error = new Error(sjclError.message);
error.stack = sjclError.stack; error.stack = sjclError.stack;
return error; return error;

View File

@ -1,7 +1,8 @@
import { afterAllCleanUp, setupDatabaseAndSynchronizer, switchClient, encryptionService, expectNotThrow } from '../../testing/test-utils'; import { afterAllCleanUp, setupDatabaseAndSynchronizer, switchClient, encryptionService, expectNotThrow } from '../../testing/test-utils';
import MasterKey from '../../models/MasterKey'; import MasterKey from '../../models/MasterKey';
import { showMissingMasterKeyMessage } from './utils'; import { migrateMasterPassword, showMissingMasterKeyMessage } from './utils';
import { localSyncInfo, setMasterKeyEnabled } from '../synchronizer/syncInfoUtils'; import { localSyncInfo, setActiveMasterKeyId, setMasterKeyEnabled } from '../synchronizer/syncInfoUtils';
import Setting from '../../models/Setting';
describe('e2ee/utils', function() { describe('e2ee/utils', function() {
@ -41,4 +42,33 @@ describe('e2ee/utils', function() {
expect(showMissingMasterKeyMessage(syncInfo, [mk1.id, mk2.id])).toBe(false); expect(showMissingMasterKeyMessage(syncInfo, [mk1.id, mk2.id])).toBe(false);
}); });
it('should do the master password migration', async () => {
const mk1 = await MasterKey.save(await encryptionService().generateMasterKey('111111'));
const mk2 = await MasterKey.save(await encryptionService().generateMasterKey('222222'));
Setting.setValue('encryption.passwordCache', {
[mk1.id]: '111111',
[mk2.id]: '222222',
});
await migrateMasterPassword();
{
expect(Setting.value('encryption.masterPassword')).toBe('');
const newCache = Setting.value('encryption.passwordCache');
expect(newCache[mk1.id]).toBe('111111');
expect(newCache[mk2.id]).toBe('222222');
}
setActiveMasterKeyId(mk1.id);
await migrateMasterPassword();
{
expect(Setting.value('encryption.masterPassword')).toBe('111111');
const newCache = Setting.value('encryption.passwordCache');
expect(newCache[mk1.id]).toBe(undefined);
expect(newCache[mk2.id]).toBe('222222');
}
});
}); });

View File

@ -4,11 +4,11 @@ import MasterKey from '../../models/MasterKey';
import Setting from '../../models/Setting'; import Setting from '../../models/Setting';
import { MasterKeyEntity } from './types'; import { MasterKeyEntity } from './types';
import EncryptionService from './EncryptionService'; import EncryptionService from './EncryptionService';
import { getActiveMasterKeyId, masterKeyEnabled, setEncryptionEnabled, SyncInfo } from '../synchronizer/syncInfoUtils'; import { getActiveMasterKey, getActiveMasterKeyId, masterKeyEnabled, setEncryptionEnabled, SyncInfo } from '../synchronizer/syncInfoUtils';
const logger = Logger.create('e2ee/utils'); const logger = Logger.create('e2ee/utils');
export async function setupAndEnableEncryption(service: EncryptionService, masterKey: MasterKeyEntity = null, password: string = null) { export async function setupAndEnableEncryption(service: EncryptionService, masterKey: MasterKeyEntity = null, masterPassword: string = null) {
if (!masterKey) { if (!masterKey) {
// May happen for example if there are master keys in info.json but none // May happen for example if there are master keys in info.json but none
// of them is set as active. But in fact, unless there is a bug in the // of them is set as active. But in fact, unless there is a bug in the
@ -18,10 +18,8 @@ export async function setupAndEnableEncryption(service: EncryptionService, maste
setEncryptionEnabled(true, masterKey ? masterKey.id : null); setEncryptionEnabled(true, masterKey ? masterKey.id : null);
if (masterKey && password) { if (masterPassword) {
const passwordCache = Setting.value('encryption.passwordCache'); Setting.setValue('encryption.masterPassword', masterPassword);
passwordCache[masterKey.id] = password;
Setting.setValue('encryption.passwordCache', passwordCache);
} }
// Mark only the non-encrypted ones for sync since, if there are encrypted ones, // Mark only the non-encrypted ones for sync since, if there are encrypted ones,
@ -47,6 +45,8 @@ export async function setupAndDisableEncryption(service: EncryptionService) {
} }
export async function toggleAndSetupEncryption(service: EncryptionService, enabled: boolean, masterKey: MasterKeyEntity, password: string) { export async function toggleAndSetupEncryption(service: EncryptionService, enabled: boolean, masterKey: MasterKeyEntity, password: string) {
logger.info('toggleAndSetupEncryption: enabled:', enabled, ' Master key', masterKey);
if (!enabled) { if (!enabled) {
await setupAndDisableEncryption(service); await setupAndDisableEncryption(service);
} else { } else {
@ -68,17 +68,65 @@ export async function generateMasterKeyAndEnableEncryption(service: EncryptionSe
return masterKey; return masterKey;
} }
// Migration function to initialise the master password. Normally it is set when
// enabling E2EE, but previously it wasn't. So here we check if the password is
// set. If it is not, we set it from the active master key. It needs to be
// called after the settings have been initialized.
export async function migrateMasterPassword() {
if (Setting.value('encryption.masterPassword')) return; // Already migrated
logger.info('Master password is not set - trying to get it from the active master key...');
const mk = getActiveMasterKey();
if (!mk) return;
const masterPassword = Setting.value('encryption.passwordCache')[mk.id];
if (masterPassword) {
Setting.setValue('encryption.masterPassword', masterPassword);
logger.info('Master password is now set.');
// Also clear the key passwords that match the master password to avoid
// any confusion.
const cache = Setting.value('encryption.passwordCache');
const newCache = { ...cache };
for (const [mkId, password] of Object.entries(cache)) {
if (password === masterPassword) {
delete newCache[mkId];
}
}
Setting.setValue('encryption.passwordCache', newCache);
await Setting.saveAll();
}
}
// All master keys normally should be decryped with the master password, however
// previously any master key could be encrypted with any password, so to support
// this legacy case, we first check if the MK decrypts with the master password.
// If not, try with the master key specific password, if any is defined.
export async function findMasterKeyPassword(service: EncryptionService, masterKey: MasterKeyEntity): Promise<string> {
const masterPassword = Setting.value('encryption.masterPassword');
if (masterPassword && await service.checkMasterKeyPassword(masterKey, masterPassword)) {
logger.info('findMasterKeyPassword: Using master password');
return masterPassword;
}
logger.info('findMasterKeyPassword: No master password is defined - trying to get master key specific password');
const passwords = Setting.value('encryption.passwordCache');
return passwords[masterKey.id];
}
export async function loadMasterKeysFromSettings(service: EncryptionService) { export async function loadMasterKeysFromSettings(service: EncryptionService) {
const masterKeys = await MasterKey.all(); const masterKeys = await MasterKey.all();
const passwords = Setting.value('encryption.passwordCache');
const activeMasterKeyId = getActiveMasterKeyId(); const activeMasterKeyId = getActiveMasterKeyId();
logger.info(`Trying to load ${masterKeys.length} master keys...`); logger.info(`Trying to load ${masterKeys.length} master keys...`);
for (let i = 0; i < masterKeys.length; i++) { for (let i = 0; i < masterKeys.length; i++) {
const mk = masterKeys[i]; const mk = masterKeys[i];
const password = passwords[mk.id];
if (service.isMasterKeyLoaded(mk)) continue; if (service.isMasterKeyLoaded(mk)) continue;
const password = await findMasterKeyPassword(service, mk);
if (!password) continue; if (!password) continue;
try { try {
@ -111,3 +159,9 @@ export function showMissingMasterKeyMessage(syncInfo: SyncInfo, notLoadedMasterK
return !!notLoadedMasterKeys.length; return !!notLoadedMasterKeys.length;
} }
export function getDefaultMasterKey(): MasterKeyEntity {
const mk = getActiveMasterKey();
if (mk) return mk;
return MasterKey.latest();
}