mirror of
https://github.com/laurent22/joplin.git
synced 2024-11-24 08:12:24 +02:00
All: Add support for single master password, to simplify handling of multiple encryption keys
This commit is contained in:
parent
596f679b1f
commit
ce89ee5bab
@ -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<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,
|
||||
@ -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] : '';
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
<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;
|
||||
@ -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) {
|
||||
@ -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}
|
||||
@ -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'],
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
@ -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;
|
||||
@ -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}
|
||||
@ -315,6 +367,7 @@ const EncryptionConfigScreen = connect((state: State) => {
|
||||
encryptionEnabled: syncInfo.e2ee,
|
||||
activeMasterKeyId: syncInfo.activeMasterKeyId,
|
||||
notLoadedMasterKeys: state.notLoadedMasterKeys,
|
||||
masterPassword: state.settings['encryption.masterPassword'],
|
||||
};
|
||||
})(EncryptionConfigScreenComponent);
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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';
|
||||
@ -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) {};
|
||||
@ -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();
|
||||
}
|
||||
@ -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());
|
||||
|
||||
@ -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();
|
||||
@ -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() {
|
||||
|
@ -43,7 +43,7 @@ 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';
|
||||
@ -51,7 +51,7 @@ 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');
|
||||
@ -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];
|
||||
@ -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')}`);
|
||||
@ -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);
|
||||
|
@ -8,6 +8,7 @@ import shim from '../../shim';
|
||||
import { MasterKeyEntity } from '../../services/e2ee/types';
|
||||
import time from '../../time';
|
||||
import { masterKeyEnabled, setMasterKeyEnabled } from '../../services/synchronizer/syncInfoUtils';
|
||||
import { findMasterKeyPassword } from '../../services/e2ee/utils';
|
||||
|
||||
class Shared {
|
||||
|
||||
@ -16,12 +17,16 @@ class Shared {
|
||||
public initialize(comp: any, props: any) {
|
||||
comp.state = {
|
||||
passwordChecks: {},
|
||||
// Master keys that can be decrypted with the master password
|
||||
// (normally all of them, but for legacy support we need this).
|
||||
masterPasswordKeys: {},
|
||||
stats: {
|
||||
encrypted: null,
|
||||
total: null,
|
||||
},
|
||||
passwords: Object.assign({}, props.passwords),
|
||||
showDisabledMasterKeys: false,
|
||||
masterPasswordInput: '',
|
||||
};
|
||||
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) {
|
||||
const passwordChecks = Object.assign({}, comp.state.passwordChecks);
|
||||
const masterPasswordKeys = Object.assign({}, comp.state.masterPasswordKeys);
|
||||
for (let i = 0; i < comp.props.masterKeys.length; 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;
|
||||
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) {
|
||||
@ -138,6 +165,14 @@ class Shared {
|
||||
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) {
|
||||
const passwords = Object.assign({}, comp.state.passwords);
|
||||
passwords[mk.id] = password;
|
||||
|
@ -26,7 +26,6 @@ export default class MasterKey extends BaseItem {
|
||||
}
|
||||
}
|
||||
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) {
|
||||
|
@ -919,6 +919,7 @@ class Setting extends BaseModel {
|
||||
'encryption.enabled': { value: false, type: SettingItemType.Bool, public: false },
|
||||
'encryption.activeMasterKeyId': { value: '', type: SettingItemType.String, public: false },
|
||||
'encryption.passwordCache': { value: {}, type: SettingItemType.Object, public: false, secure: true },
|
||||
'encryption.masterPassword': { value: '', type: SettingItemType.String, public: false, secure: true },
|
||||
'encryption.shouldReencrypt': {
|
||||
value: -1, // will be set on app startup
|
||||
type: SettingItemType.Int,
|
||||
|
@ -6,6 +6,7 @@ import ResourceService from './ResourceService';
|
||||
import Logger from '../Logger';
|
||||
import shim from '../shim';
|
||||
import KvStore from './KvStore';
|
||||
import EncryptionService from './e2ee/EncryptionService';
|
||||
|
||||
const EventEmitter = require('events');
|
||||
|
||||
@ -28,7 +29,7 @@ export default class DecryptionWorker {
|
||||
private kvStore_: KvStore = null;
|
||||
private maxDecryptionAttempts_ = 2;
|
||||
private startCalls_: boolean[] = [];
|
||||
private encryptionService_: any = null;
|
||||
private encryptionService_: EncryptionService = null;
|
||||
|
||||
constructor() {
|
||||
this.state_ = 'idle';
|
||||
@ -134,6 +135,11 @@ export default class DecryptionWorker {
|
||||
this.logger().info(msg);
|
||||
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 (options.masterKeyNotLoadedHandler === 'throw') {
|
||||
// By trying to load the master key here, we throw the "masterKeyNotLoaded" error
|
||||
|
@ -8,6 +8,8 @@ import JoplinError from '../../JoplinError';
|
||||
import { getActiveMasterKeyId, setActiveMasterKeyId } from '../synchronizer/syncInfoUtils';
|
||||
const { padLeft } = require('../../string-utils.js');
|
||||
|
||||
const logger = Logger.create('EncryptionService');
|
||||
|
||||
function hexPad(s: string, length: number) {
|
||||
return padLeft(s, length, '0');
|
||||
}
|
||||
@ -52,7 +54,6 @@ export default class EncryptionService {
|
||||
private decryptedMasterKeys_: Record<string, DecryptedMasterKey> = {};
|
||||
public defaultEncryptionMethod_ = EncryptionService.METHOD_SJCL_1A; // public because used in tests
|
||||
private defaultMasterKeyEncryptionMethod_ = EncryptionService.METHOD_SJCL_4;
|
||||
private logger_ = new Logger();
|
||||
|
||||
private headerTemplates_ = {
|
||||
// Template version 1
|
||||
@ -80,7 +81,6 @@ export default class EncryptionService {
|
||||
this.decryptedMasterKeys_ = {};
|
||||
this.defaultEncryptionMethod_ = EncryptionService.METHOD_SJCL_1A;
|
||||
this.defaultMasterKeyEncryptionMethod_ = EncryptionService.METHOD_SJCL_4;
|
||||
this.logger_ = new Logger();
|
||||
|
||||
this.headerTemplates_ = {
|
||||
// Template version 1
|
||||
@ -97,14 +97,6 @@ export default class EncryptionService {
|
||||
return this.instance_;
|
||||
}
|
||||
|
||||
setLogger(l: Logger) {
|
||||
this.logger_ = l;
|
||||
}
|
||||
|
||||
logger() {
|
||||
return this.logger_;
|
||||
}
|
||||
|
||||
loadedMasterKeysCount() {
|
||||
return Object.keys(this.decryptedMasterKeys_).length;
|
||||
}
|
||||
@ -139,10 +131,14 @@ export default class EncryptionService {
|
||||
|
||||
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');
|
||||
|
||||
logger.info(`Loading master key: ${model.id}. Make active:`, makeActive);
|
||||
|
||||
this.decryptedMasterKeys_[model.id] = {
|
||||
plainText: await this.decryptMasterKey_(model, password),
|
||||
updatedTime: model.updated_time,
|
||||
};
|
||||
|
||||
if (makeActive) this.setActiveMasterKeyId(model.id);
|
||||
}
|
||||
|
||||
@ -245,7 +241,7 @@ export default class EncryptionService {
|
||||
return plainText;
|
||||
}
|
||||
|
||||
async checkMasterKeyPassword(model: MasterKeyEntity, password: string) {
|
||||
public async checkMasterKeyPassword(model: MasterKeyEntity, password: string) {
|
||||
try {
|
||||
await this.decryptMasterKey_(model, password);
|
||||
} catch (error) {
|
||||
@ -255,7 +251,7 @@ export default class EncryptionService {
|
||||
return true;
|
||||
}
|
||||
|
||||
wrapSjclError(sjclError: any) {
|
||||
private wrapSjclError(sjclError: any) {
|
||||
const error = new Error(sjclError.message);
|
||||
error.stack = sjclError.stack;
|
||||
return error;
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { afterAllCleanUp, setupDatabaseAndSynchronizer, switchClient, encryptionService, expectNotThrow } from '../../testing/test-utils';
|
||||
import MasterKey from '../../models/MasterKey';
|
||||
import { showMissingMasterKeyMessage } from './utils';
|
||||
import { localSyncInfo, setMasterKeyEnabled } from '../synchronizer/syncInfoUtils';
|
||||
import { migrateMasterPassword, showMissingMasterKeyMessage } from './utils';
|
||||
import { localSyncInfo, setActiveMasterKeyId, setMasterKeyEnabled } from '../synchronizer/syncInfoUtils';
|
||||
import Setting from '../../models/Setting';
|
||||
|
||||
describe('e2ee/utils', function() {
|
||||
|
||||
@ -41,4 +42,33 @@ describe('e2ee/utils', function() {
|
||||
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');
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -4,11 +4,11 @@ import MasterKey from '../../models/MasterKey';
|
||||
import Setting from '../../models/Setting';
|
||||
import { MasterKeyEntity } from './types';
|
||||
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');
|
||||
|
||||
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) {
|
||||
// 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
|
||||
@ -18,10 +18,8 @@ export async function setupAndEnableEncryption(service: EncryptionService, maste
|
||||
|
||||
setEncryptionEnabled(true, masterKey ? masterKey.id : null);
|
||||
|
||||
if (masterKey && password) {
|
||||
const passwordCache = Setting.value('encryption.passwordCache');
|
||||
passwordCache[masterKey.id] = password;
|
||||
Setting.setValue('encryption.passwordCache', passwordCache);
|
||||
if (masterPassword) {
|
||||
Setting.setValue('encryption.masterPassword', masterPassword);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
logger.info('toggleAndSetupEncryption: enabled:', enabled, ' Master key', masterKey);
|
||||
|
||||
if (!enabled) {
|
||||
await setupAndDisableEncryption(service);
|
||||
} else {
|
||||
@ -68,17 +68,65 @@ export async function generateMasterKeyAndEnableEncryption(service: EncryptionSe
|
||||
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) {
|
||||
const masterKeys = await MasterKey.all();
|
||||
const passwords = Setting.value('encryption.passwordCache');
|
||||
const activeMasterKeyId = getActiveMasterKeyId();
|
||||
|
||||
logger.info(`Trying to load ${masterKeys.length} master keys...`);
|
||||
|
||||
for (let i = 0; i < masterKeys.length; i++) {
|
||||
const mk = masterKeys[i];
|
||||
const password = passwords[mk.id];
|
||||
if (service.isMasterKeyLoaded(mk)) continue;
|
||||
|
||||
const password = await findMasterKeyPassword(service, mk);
|
||||
if (!password) continue;
|
||||
|
||||
try {
|
||||
@ -111,3 +159,9 @@ export function showMissingMasterKeyMessage(syncInfo: SyncInfo, notLoadedMasterK
|
||||
|
||||
return !!notLoadedMasterKeys.length;
|
||||
}
|
||||
|
||||
export function getDefaultMasterKey(): MasterKeyEntity {
|
||||
const mk = getActiveMasterKey();
|
||||
if (mk) return mk;
|
||||
return MasterKey.latest();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user