1
0
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:
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 { 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>&nbsp;&nbsp;
<span style={{ ...theme.textStyle, fontWeight: 'bold' }}> {_('Loaded')}</span>
</div>
);
} else {
return (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<span style={theme.textStyle}> {'The master password is not set or is invalid. Please type it below:'}</span>
<div style={{ display: 'flex', flexDirection: 'row', marginTop: 10 }}>
<MasterPasswordInput placeholder={_('Enter your master password')} type="password" value={this.state.masterPasswordInput} onChange={(event: any) => shared.onMasterPasswordChange(this, event.target.value)} />{' '}
<Button ml="10px" level={ButtonLevel.Secondary} onClick={onMasterPasswordSave} title={_('Save')} />
</div>
</div>
);
}
}
render() {
const theme = themeStyle(this.props.themeId);
const masterKeys: MasterKeyEntity[] = this.props.masterKeys;
@ -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'],
};
};

View File

@ -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);

View File

@ -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>

View File

@ -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

View File

@ -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() {

View File

@ -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);

View File

@ -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;

View File

@ -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) {

View File

@ -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,

View File

@ -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

View File

@ -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;

View File

@ -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');
}
});
});

View File

@ -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();
}