1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-21 09:38:01 +02:00

All: Add a way to disable a master key

This commit is contained in:
Laurent Cozic 2021-08-17 12:03:19 +01:00
parent ea1269d122
commit 7faa58e0f9
18 changed files with 234 additions and 60 deletions

View File

@ -1191,9 +1191,15 @@ packages/lib/services/database/types.js.map
packages/lib/services/debug/populateDatabase.d.ts
packages/lib/services/debug/populateDatabase.js
packages/lib/services/debug/populateDatabase.js.map
packages/lib/services/e2ee/types.d.ts
packages/lib/services/e2ee/types.js
packages/lib/services/e2ee/types.js.map
packages/lib/services/e2ee/utils.d.ts
packages/lib/services/e2ee/utils.js
packages/lib/services/e2ee/utils.js.map
packages/lib/services/e2ee/utils.test.d.ts
packages/lib/services/e2ee/utils.test.js
packages/lib/services/e2ee/utils.test.js.map
packages/lib/services/interop/InteropService.d.ts
packages/lib/services/interop/InteropService.js
packages/lib/services/interop/InteropService.js.map
@ -1527,6 +1533,9 @@ packages/lib/services/synchronizer/migrations/3.js.map
packages/lib/services/synchronizer/syncInfoUtils.d.ts
packages/lib/services/synchronizer/syncInfoUtils.js
packages/lib/services/synchronizer/syncInfoUtils.js.map
packages/lib/services/synchronizer/syncInfoUtils.test.d.ts
packages/lib/services/synchronizer/syncInfoUtils.test.js
packages/lib/services/synchronizer/syncInfoUtils.test.js.map
packages/lib/services/synchronizer/synchronizer_LockHandler.test.d.ts
packages/lib/services/synchronizer/synchronizer_LockHandler.test.js
packages/lib/services/synchronizer/synchronizer_LockHandler.test.js.map

9
.gitignore vendored
View File

@ -1176,9 +1176,15 @@ packages/lib/services/database/types.js.map
packages/lib/services/debug/populateDatabase.d.ts
packages/lib/services/debug/populateDatabase.js
packages/lib/services/debug/populateDatabase.js.map
packages/lib/services/e2ee/types.d.ts
packages/lib/services/e2ee/types.js
packages/lib/services/e2ee/types.js.map
packages/lib/services/e2ee/utils.d.ts
packages/lib/services/e2ee/utils.js
packages/lib/services/e2ee/utils.js.map
packages/lib/services/e2ee/utils.test.d.ts
packages/lib/services/e2ee/utils.test.js
packages/lib/services/e2ee/utils.test.js.map
packages/lib/services/interop/InteropService.d.ts
packages/lib/services/interop/InteropService.js
packages/lib/services/interop/InteropService.js.map
@ -1512,6 +1518,9 @@ packages/lib/services/synchronizer/migrations/3.js.map
packages/lib/services/synchronizer/syncInfoUtils.d.ts
packages/lib/services/synchronizer/syncInfoUtils.js
packages/lib/services/synchronizer/syncInfoUtils.js.map
packages/lib/services/synchronizer/syncInfoUtils.test.d.ts
packages/lib/services/synchronizer/syncInfoUtils.test.js
packages/lib/services/synchronizer/syncInfoUtils.test.js.map
packages/lib/services/synchronizer/synchronizer_LockHandler.test.d.ts
packages/lib/services/synchronizer/synchronizer_LockHandler.test.js
packages/lib/services/synchronizer/synchronizer_LockHandler.test.js.map

View File

@ -859,10 +859,10 @@ class Application extends BaseApplication {
// type: 'NAV_GO',
// routeName: 'Config',
// props: {
// defaultSection: 'plugins',
// defaultSection: 'encryption',
// },
// });
// }, 5000);
// }, 2000);
// setTimeout(() => {

View File

@ -10,8 +10,8 @@ import shim from '@joplin/lib/shim';
import dialogs from './dialogs';
import bridge from '../services/bridge';
import shared from '@joplin/lib/components/shared/encryption-config-shared';
import { MasterKeyEntity } from '@joplin/lib/services/database/types';
import { getEncryptionEnabled, SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
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 MasterKey from '@joplin/lib/models/MasterKey';
@ -42,7 +42,7 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
return shared.checkPasswords(this);
}
renderMasterKey(mk: MasterKeyEntity) {
private renderMasterKey(mk: MasterKeyEntity, isDefault: boolean) {
const theme = themeStyle(this.props.themeId);
const passwordStyle = {
@ -60,17 +60,20 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
return shared.onPasswordChange(this, mk, event.target.value);
};
const onToggleEnabledClick = () => {
return shared.onToggleEnabledClick(this, mk);
};
const password = this.state.passwords[mk.id] ? this.state.passwords[mk.id] : '';
const active = this.props.activeMasterKeyId === mk.id ? '✔' : '';
const isActive = this.props.activeMasterKeyId === mk.id;
const activeIcon = isActive ? '✔' : '';
const passwordOk = this.state.passwordChecks[mk.id] === true ? '✔' : '❌';
return (
<tr key={mk.id}>
<td style={theme.textStyle}>{active}</td>
<td style={theme.textStyle}>{mk.id}</td>
<td style={theme.textStyle}>{mk.source_application}</td>
<td style={theme.textStyle}>{time.formatMsToLocal(mk.created_time)}</td>
<td style={theme.textStyle}>{time.formatMsToLocal(mk.updated_time)}</td>
<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()}>
@ -78,6 +81,9 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
</button>
</td>
<td style={theme.textStyle}>{passwordOk}</td>
<td style={theme.textStyle}>
<button disabled={isActive || isDefault} style={theme.buttonStyle} onClick={() => onToggleEnabledClick()}>{masterKeyEnabled(mk) ? _('Disable') : _('Enable')}</button>
</td>
</tr>
);
}
@ -121,6 +127,7 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
renderReencryptData() {
if (!shim.isElectron()) return null;
if (!this.props.shouldReencrypt) return null;
const theme = themeStyle(this.props.themeId);
const buttonLabel = _('Re-encrypt data');
@ -146,9 +153,51 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
);
}
private renderMasterKeySection(masterKeys: MasterKeyEntity[], isEnabledMasterKeys: boolean) {
const theme = themeStyle(this.props.themeId);
const mkComps = [];
const showTable = isEnabledMasterKeys || this.state.showDisabledMasterKeys;
const latestMasterKey = MasterKey.latest();
for (let i = 0; i < masterKeys.length; i++) {
const mk = masterKeys[i];
mkComps.push(this.renderMasterKey(mk, isEnabledMasterKeys && latestMasterKey && mk.id === latestMasterKey.id));
}
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 tableComp = !showTable ? null : (
<table>
<tbody>
<tr>
<th style={theme.textStyle}>{_('Active')}</th>
<th style={theme.textStyle}>{_('ID')}</th>
<th style={theme.textStyle}>{_('Date')}</th>
<th style={theme.textStyle}>{_('Password')}</th>
<th style={theme.textStyle}>{_('Valid')}</th>
<th style={theme.textStyle}>{_('Actions')}</th>
</tr>
{mkComps}
</tbody>
</table>
);
if (mkComps.length) {
return (
<div>
{headerComp}
{tableComp}
{infoComp}
</div>
);
}
return null;
}
render() {
const theme = themeStyle(this.props.themeId);
const masterKeys = this.props.masterKeys;
const masterKeys: MasterKeyEntity[] = this.props.masterKeys;
const containerStyle = Object.assign({}, theme.containerStyle, {
padding: theme.configScreenPadding,
@ -156,13 +205,10 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
backgroundColor: theme.backgroundColor3,
});
const mkComps = [];
const nonExistingMasterKeyIds = this.props.notLoadedMasterKeys.slice();
for (let i = 0; i < masterKeys.length; i++) {
const mk = masterKeys[i];
mkComps.push(this.renderMasterKey(mk));
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
}
@ -203,30 +249,8 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
const needUpgradeSection = this.renderNeedUpgradeSection();
const reencryptDataSection = this.renderReencryptData();
let masterKeySection = null;
if (mkComps.length) {
masterKeySection = (
<div>
<h1 style={theme.h1Style}>{_('Master Keys')}</h1>
<table>
<tbody>
<tr>
<th style={theme.textStyle}>{_('Active')}</th>
<th style={theme.textStyle}>{_('ID')}</th>
<th style={theme.textStyle}>{_('Source')}</th>
<th style={theme.textStyle}>{_('Created')}</th>
<th style={theme.textStyle}>{_('Updated')}</th>
<th style={theme.textStyle}>{_('Password')}</th>
<th style={theme.textStyle}>{_('Password OK')}</th>
</tr>
{mkComps}
</tbody>
</table>
<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>
</div>
);
}
const enabledMasterKeySection = this.renderMasterKeySection(masterKeys.filter(mk => masterKeyEnabled(mk)), true);
const disabledMasterKeySection = this.renderMasterKeySection(masterKeys.filter(mk => !masterKeyEnabled(mk)), false);
let nonExistingMasterKeySection = null;
@ -284,7 +308,8 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
{toggleButton}
{needUpgradeSection}
{this.props.shouldReencrypt ? reencryptDataSection : null}
{masterKeySection}
{enabledMasterKeySection}
{disabledMasterKeySection}
{nonExistingMasterKeySection}
{!this.props.shouldReencrypt ? reencryptDataSection : null}
</div>

View File

@ -36,6 +36,7 @@ import ShareService from '@joplin/lib/services/share/ShareService';
import { reg } from '@joplin/lib/registry';
import removeKeylessItems from '../ResizableLayout/utils/removeKeylessItems';
import { localSyncInfoFromState } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import { showMissingMasterKeyMessage } from '@joplin/lib/services/e2ee/utils';
const { connect } = require('react-redux');
const { PromptDialog } = require('../PromptDialog.min.js');
@ -866,7 +867,7 @@ const mapStateToProps = (state: AppState) => {
notes: state.notes,
hasDisabledSyncItems: state.hasDisabledSyncItems,
hasDisabledEncryptionItems: state.hasDisabledEncryptionItems,
showMissingMasterKeyMessage: state.notLoadedMasterKeys.length && syncInfo.masterKeys.length,
showMissingMasterKeyMessage: showMissingMasterKeyMessage(syncInfo, state.notLoadedMasterKeys),
showNeedUpgradingMasterKeyMessage: !!EncryptionService.instance().masterKeysThatNeedUpgrading(syncInfo.masterKeys).length,
showShouldReencryptMessage: state.settings['encryption.shouldReencrypt'] >= Setting.SHOULD_REENCRYPT_YES,
shouldUpgradeSyncTarget: state.settings['sync.upgradeState'] === Setting.SYNC_UPGRADE_STATE_SHOULD_DO,

View File

@ -15,6 +15,7 @@ const { Dropdown } = require('./Dropdown.js');
const { dialogs } = require('../utils/dialogs.js');
const DialogBox = require('react-native-dialogbox').default;
const { localSyncInfoFromState } = require('@joplin/lib/services/synchronizer/syncInfoUtils');
const { showMissingMasterKeyMessage } = require('@joplin/lib/services/e2ee/utils');
Icon.loadFont();
@ -538,7 +539,7 @@ const ScreenHeader = connect(state => {
themeId: state.settings.theme,
noteSelectionEnabled: state.noteSelectionEnabled,
selectedNoteIds: state.selectedNoteIds,
showMissingMasterKeyMessage: state.notLoadedMasterKeys.length && syncInfo.masterKeys.length,
showMissingMasterKeyMessage: showMissingMasterKeyMessage(syncInfo, state.notLoadedMasterKeys),
hasDisabledSyncItems: state.hasDisabledSyncItems,
shouldUpgradeSyncTarget: state.settings['sync.upgradeState'] === Setting.SYNC_UPGRADE_STATE_SHOULD_DO,
};

View File

@ -10,7 +10,7 @@ import EncryptionService from '@joplin/lib/services/EncryptionService';
import { _ } from '@joplin/lib/locale';
import time from '@joplin/lib/time';
import shared from '@joplin/lib/components/shared/encryption-config-shared';
import { MasterKeyEntity } from '@joplin/lib/services/database/types';
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
import { State } from '@joplin/lib/reducer';
import { SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import { setupAndDisableEncryption, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';

View File

@ -5,8 +5,9 @@ import Setting from '../../models/Setting';
import MasterKey from '../../models/MasterKey';
import { reg } from '../../registry.js';
import shim from '../../shim';
import { MasterKeyEntity } from '../../services/database/types';
import { MasterKeyEntity } from '../../services/e2ee/types';
import time from '../../time';
import { masterKeyEnabled, setMasterKeyEnabled } from '../../services/synchronizer/syncInfoUtils';
class Shared {
@ -20,6 +21,7 @@ class Shared {
total: null,
},
passwords: Object.assign({}, props.passwords),
showDisabledMasterKeys: false,
};
comp.isMounted_ = false;
@ -33,6 +35,10 @@ class Shared {
});
}
public async toggleShowDisabledMasterKeys(comp: any) {
comp.setState({ showDisabledMasterKeys: !comp.state.showDisabledMasterKeys });
}
public async reencryptData() {
const ok = confirm(_('Please confirm that you would like to re-encrypt your complete database.'));
if (!ok) return;
@ -138,6 +144,10 @@ class Shared {
comp.setState({ passwords: passwords });
}
public onToggleEnabledClick(_comp: any, mk: MasterKeyEntity) {
setMasterKeyEnabled(mk.id, !masterKeyEnabled(mk));
}
public enableEncryptionConfirmationMessages(masterKey: MasterKeyEntity) {
const msg = [_('Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.')];
if (masterKey) msg.push(_('Encryption will be enabled using the master key created on %s', time.unixMsToLocalDateTime(masterKey.created_time)));

View File

@ -1,5 +1,5 @@
import BaseModel from '../BaseModel';
import { MasterKeyEntity } from '../services/database/types';
import { MasterKeyEntity } from '../services/e2ee/types';
import { localSyncInfo, saveLocalSyncInfo } from '../services/synchronizer/syncInfoUtils';
import BaseItem from './BaseItem';
import uuid from '../uuid';

View File

@ -55,7 +55,7 @@ export interface State {
folders: any[];
tags: any[];
masterKeys: any[];
notLoadedMasterKeys: any[];
notLoadedMasterKeys: string[];
searches: any[];
highlightedWords: string[];
selectedNoteIds: string[];

View File

@ -1,4 +1,4 @@
import { MasterKeyEntity } from './database/types';
import { MasterKeyEntity } from './e2ee/types';
import Logger from '../Logger';
import shim from '../shim';
import Setting from '../models/Setting';

View File

@ -14,6 +14,10 @@ export interface BaseItemEntity {
// AUTO-GENERATED BY packages/tools/generate-database-types.js
/*
@ -66,16 +70,6 @@ export interface KeyValueEntity {
"updated_time"?: number
"type_"?: number
}
export interface MasterKeyEntity {
"id"?: string | null
"created_time"?: number
"updated_time"?: number
"source_application"?: string
"encryption_method"?: number
"checksum"?: string
"content"?: string
"type_"?: number
}
export interface MigrationEntity {
"id"?: number | null
"number"?: number

View File

@ -0,0 +1,11 @@
export interface MasterKeyEntity {
id?: string | null;
created_time?: number;
updated_time?: number;
source_application?: string;
encryption_method?: number;
checksum?: string;
content?: string;
type_?: number;
enabled?: number;
}

View File

@ -0,0 +1,42 @@
import { afterAllCleanUp, setupDatabaseAndSynchronizer, switchClient, encryptionService } from '../../testing/test-utils';
import MasterKey from '../../models/MasterKey';
import { showMissingMasterKeyMessage } from './utils';
import { localSyncInfo, setMasterKeyEnabled } from '../synchronizer/syncInfoUtils';
describe('e2ee/utils', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
done();
});
afterAll(async () => {
await afterAllCleanUp();
});
it('should tell if the missing master key message should be shown', async () => {
const mk1 = await MasterKey.save(await encryptionService().generateMasterKey('111111'));
const mk2 = await MasterKey.save(await encryptionService().generateMasterKey('111111'));
expect(showMissingMasterKeyMessage(localSyncInfo(), [mk1.id])).toBe(true);
expect(showMissingMasterKeyMessage(localSyncInfo(), [mk1.id, mk2.id])).toBe(true);
expect(showMissingMasterKeyMessage(localSyncInfo(), [])).toBe(false);
setMasterKeyEnabled(mk1.id, false);
expect(showMissingMasterKeyMessage(localSyncInfo(), [mk1.id])).toBe(false);
expect(showMissingMasterKeyMessage(localSyncInfo(), [mk1.id, mk2.id])).toBe(true);
setMasterKeyEnabled(mk2.id, false);
expect(showMissingMasterKeyMessage(localSyncInfo(), [mk1.id, mk2.id])).toBe(false);
setMasterKeyEnabled(mk1.id, true);
setMasterKeyEnabled(mk2.id, true);
expect(showMissingMasterKeyMessage(localSyncInfo(), [mk1.id, mk2.id])).toBe(true);
const syncInfo = localSyncInfo();
syncInfo.masterKeys = [];
expect(showMissingMasterKeyMessage(syncInfo, [mk1.id, mk2.id])).toBe(false);
});
});

View File

@ -2,9 +2,9 @@ import Logger from '../../Logger';
import BaseItem from '../../models/BaseItem';
import MasterKey from '../../models/MasterKey';
import Setting from '../../models/Setting';
import { MasterKeyEntity } from '../database/types';
import { MasterKeyEntity } from './types';
import EncryptionService from '../EncryptionService';
import { getActiveMasterKeyId, setEncryptionEnabled } from '../synchronizer/syncInfoUtils';
import { getActiveMasterKeyId, masterKeyEnabled, setEncryptionEnabled, SyncInfo } from '../synchronizer/syncInfoUtils';
const logger = Logger.create('e2ee/utils');
@ -90,3 +90,16 @@ export async function loadMasterKeysFromSettings(service: EncryptionService) {
logger.info(`Loaded master keys: ${service.loadedMasterKeysCount()}`);
}
export function showMissingMasterKeyMessage(syncInfo: SyncInfo, notLoadedMasterKeys: string[]) {
if (!syncInfo.masterKeys.length) return false;
notLoadedMasterKeys = notLoadedMasterKeys.slice();
for (let i = notLoadedMasterKeys.length - 1; i >= 0; i--) {
const mk = syncInfo.masterKeys.find(mk => mk.id === notLoadedMasterKeys[i]);
if (!masterKeyEnabled(mk)) notLoadedMasterKeys.pop();
}
return !!notLoadedMasterKeys.length;
}

View File

@ -0,0 +1,37 @@
import { afterAllCleanUp, setupDatabaseAndSynchronizer, switchClient, encryptionService } from '../../testing/test-utils';
import MasterKey from '../../models/MasterKey';
import { masterKeyEnabled, setMasterKeyEnabled } from './syncInfoUtils';
describe('syncInfoUtils', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
done();
});
afterAll(async () => {
await afterAllCleanUp();
});
it('should enable or disable a master key', async () => {
const mk1 = await MasterKey.save(await encryptionService().generateMasterKey('111111'));
const mk2 = await MasterKey.save(await encryptionService().generateMasterKey('111111'));
setMasterKeyEnabled(mk2.id, false);
expect(masterKeyEnabled(await MasterKey.load(mk1.id))).toBe(true);
expect(masterKeyEnabled(await MasterKey.load(mk2.id))).toBe(false);
setMasterKeyEnabled(mk1.id, false);
expect(masterKeyEnabled(await MasterKey.load(mk1.id))).toBe(false);
expect(masterKeyEnabled(await MasterKey.load(mk2.id))).toBe(false);
setMasterKeyEnabled(mk1.id, true);
expect(masterKeyEnabled(await MasterKey.load(mk1.id))).toBe(true);
expect(masterKeyEnabled(await MasterKey.load(mk2.id))).toBe(false);
});
});

View File

@ -2,7 +2,7 @@ import { FileApi } from '../../file-api';
import JoplinDatabase from '../../JoplinDatabase';
import Setting from '../../models/Setting';
import { State } from '../../reducer';
import { MasterKeyEntity } from '../database/types';
import { MasterKeyEntity } from '../e2ee/types';
export interface SyncInfoValueBoolean {
value: boolean;
@ -233,3 +233,24 @@ export function getActiveMasterKey(s: SyncInfo = null): MasterKeyEntity | null {
if (!s.activeMasterKeyId) return null;
return s.masterKeys.find(mk => mk.id === s.activeMasterKeyId);
}
export function setMasterKeyEnabled(mkId: string, enabled: boolean = true) {
const s = localSyncInfo();
const idx = s.masterKeys.findIndex(mk => mk.id === mkId);
if (idx < 0) throw new Error(`No such master key: ${mkId}`);
if (mkId === getActiveMasterKeyId() && !enabled) throw new Error('The active master key cannot be disabled');
s.masterKeys[idx] = {
...s.masterKeys[idx],
enabled: enabled ? 1 : 0,
updated_time: Date.now(),
};
saveLocalSyncInfo(s);
}
export function masterKeyEnabled(mk: MasterKeyEntity): boolean {
if ('enabled' in mk) return !!mk.enabled;
return true;
}

View File

@ -22,6 +22,7 @@ async function main() {
'main.notes_fts_segdir',
'main.notes_fts_docsize',
'main.notes_fts_stat',
'main.master_keys',
],
};