You've already forked joplin
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:
@ -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>
|
||||||
|
<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'],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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() {
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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) {
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user