mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-21 09:38:01 +02:00
Chore: Converted encryption config screens to React Hooks to share logic between desktop and mobile
This commit is contained in:
parent
a2c6461af8
commit
96ac12b460
@ -208,9 +208,9 @@ packages/app-desktop/gui/DialogTitle.js.map
|
|||||||
packages/app-desktop/gui/DropboxLoginScreen.d.ts
|
packages/app-desktop/gui/DropboxLoginScreen.d.ts
|
||||||
packages/app-desktop/gui/DropboxLoginScreen.js
|
packages/app-desktop/gui/DropboxLoginScreen.js
|
||||||
packages/app-desktop/gui/DropboxLoginScreen.js.map
|
packages/app-desktop/gui/DropboxLoginScreen.js.map
|
||||||
packages/app-desktop/gui/EncryptionConfigScreen.d.ts
|
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.d.ts
|
||||||
packages/app-desktop/gui/EncryptionConfigScreen.js
|
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js
|
||||||
packages/app-desktop/gui/EncryptionConfigScreen.js.map
|
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js.map
|
||||||
packages/app-desktop/gui/ErrorBoundary.d.ts
|
packages/app-desktop/gui/ErrorBoundary.d.ts
|
||||||
packages/app-desktop/gui/ErrorBoundary.js
|
packages/app-desktop/gui/ErrorBoundary.js
|
||||||
packages/app-desktop/gui/ErrorBoundary.js.map
|
packages/app-desktop/gui/ErrorBoundary.js.map
|
||||||
@ -931,9 +931,9 @@ packages/lib/commands/index.js.map
|
|||||||
packages/lib/commands/synchronize.d.ts
|
packages/lib/commands/synchronize.d.ts
|
||||||
packages/lib/commands/synchronize.js
|
packages/lib/commands/synchronize.js
|
||||||
packages/lib/commands/synchronize.js.map
|
packages/lib/commands/synchronize.js.map
|
||||||
packages/lib/components/shared/encryption-config-shared.d.ts
|
packages/lib/components/EncryptionConfigScreen/utils.d.ts
|
||||||
packages/lib/components/shared/encryption-config-shared.js
|
packages/lib/components/EncryptionConfigScreen/utils.js
|
||||||
packages/lib/components/shared/encryption-config-shared.js.map
|
packages/lib/components/EncryptionConfigScreen/utils.js.map
|
||||||
packages/lib/database.d.ts
|
packages/lib/database.d.ts
|
||||||
packages/lib/database.js
|
packages/lib/database.js
|
||||||
packages/lib/database.js.map
|
packages/lib/database.js.map
|
||||||
|
12
.gitignore
vendored
12
.gitignore
vendored
@ -193,9 +193,9 @@ packages/app-desktop/gui/DialogTitle.js.map
|
|||||||
packages/app-desktop/gui/DropboxLoginScreen.d.ts
|
packages/app-desktop/gui/DropboxLoginScreen.d.ts
|
||||||
packages/app-desktop/gui/DropboxLoginScreen.js
|
packages/app-desktop/gui/DropboxLoginScreen.js
|
||||||
packages/app-desktop/gui/DropboxLoginScreen.js.map
|
packages/app-desktop/gui/DropboxLoginScreen.js.map
|
||||||
packages/app-desktop/gui/EncryptionConfigScreen.d.ts
|
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.d.ts
|
||||||
packages/app-desktop/gui/EncryptionConfigScreen.js
|
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js
|
||||||
packages/app-desktop/gui/EncryptionConfigScreen.js.map
|
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js.map
|
||||||
packages/app-desktop/gui/ErrorBoundary.d.ts
|
packages/app-desktop/gui/ErrorBoundary.d.ts
|
||||||
packages/app-desktop/gui/ErrorBoundary.js
|
packages/app-desktop/gui/ErrorBoundary.js
|
||||||
packages/app-desktop/gui/ErrorBoundary.js.map
|
packages/app-desktop/gui/ErrorBoundary.js.map
|
||||||
@ -916,9 +916,9 @@ packages/lib/commands/index.js.map
|
|||||||
packages/lib/commands/synchronize.d.ts
|
packages/lib/commands/synchronize.d.ts
|
||||||
packages/lib/commands/synchronize.js
|
packages/lib/commands/synchronize.js
|
||||||
packages/lib/commands/synchronize.js.map
|
packages/lib/commands/synchronize.js.map
|
||||||
packages/lib/components/shared/encryption-config-shared.d.ts
|
packages/lib/components/EncryptionConfigScreen/utils.d.ts
|
||||||
packages/lib/components/shared/encryption-config-shared.js
|
packages/lib/components/EncryptionConfigScreen/utils.js
|
||||||
packages/lib/components/shared/encryption-config-shared.js.map
|
packages/lib/components/EncryptionConfigScreen/utils.js.map
|
||||||
packages/lib/database.d.ts
|
packages/lib/database.d.ts
|
||||||
packages/lib/database.js
|
packages/lib/database.js
|
||||||
packages/lib/database.js.map
|
packages/lib/database.js.map
|
||||||
|
@ -6,7 +6,7 @@ import { _ } from '@joplin/lib/locale';
|
|||||||
import bridge from '../../services/bridge';
|
import bridge from '../../services/bridge';
|
||||||
import Setting, { AppType, SyncStartupOperation } from '@joplin/lib/models/Setting';
|
import Setting, { AppType, SyncStartupOperation } from '@joplin/lib/models/Setting';
|
||||||
import control_PluginsStates from './controls/plugins/PluginsStates';
|
import control_PluginsStates from './controls/plugins/PluginsStates';
|
||||||
import EncryptionConfigScreen from '../EncryptionConfigScreen';
|
import EncryptionConfigScreen from '../EncryptionConfigScreen/EncryptionConfigScreen';
|
||||||
|
|
||||||
const { connect } = require('react-redux');
|
const { connect } = require('react-redux');
|
||||||
const { themeStyle } = require('@joplin/lib/theme');
|
const { themeStyle } = require('@joplin/lib/theme');
|
||||||
|
@ -1,394 +0,0 @@
|
|||||||
const React = require('react');
|
|
||||||
const { connect } = require('react-redux');
|
|
||||||
import Setting from '@joplin/lib/models/Setting';
|
|
||||||
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
|
|
||||||
import { themeStyle } from '@joplin/lib/theme';
|
|
||||||
import { _ } from '@joplin/lib/locale';
|
|
||||||
import time from '@joplin/lib/time';
|
|
||||||
import { State } from '@joplin/lib/reducer';
|
|
||||||
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/e2ee/types';
|
|
||||||
import { getEncryptionEnabled, masterKeyEnabled, SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
|
|
||||||
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 {}
|
|
||||||
|
|
||||||
class EncryptionConfigScreenComponent extends React.Component<Props> {
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
shared.initialize(this, props);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.isMounted_ = false;
|
|
||||||
shared.componentWillUnmount();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.isMounted_ = true;
|
|
||||||
shared.componentDidMount(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props) {
|
|
||||||
shared.componentDidUpdate(this, prevProps);
|
|
||||||
}
|
|
||||||
|
|
||||||
async checkPasswords() {
|
|
||||||
return shared.checkPasswords(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
border: '1px solid',
|
|
||||||
borderColor: theme.dividerColor,
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSaveClick = () => {
|
|
||||||
return shared.onSavePasswordClick(this, mk);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onPasswordChange = (event: any) => {
|
|
||||||
return shared.onPasswordChange(this, mk, event.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
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] : '';
|
|
||||||
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}>{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>
|
|
||||||
{renderPasswordInput(mk.id)}
|
|
||||||
<td style={theme.textStyle}>{passwordOk}</td>
|
|
||||||
<td style={theme.textStyle}>
|
|
||||||
<button style={theme.buttonStyle} onClick={() => onToggleEnabledClick()}>{masterKeyEnabled(mk) ? _('Disable') : _('Enable')}</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderNeedUpgradeSection() {
|
|
||||||
if (!shim.isElectron()) return null;
|
|
||||||
|
|
||||||
const needUpgradeMasterKeys = EncryptionService.instance().masterKeysThatNeedUpgrading(this.props.masterKeys);
|
|
||||||
if (!needUpgradeMasterKeys.length) return null;
|
|
||||||
|
|
||||||
const theme = themeStyle(this.props.themeId);
|
|
||||||
|
|
||||||
const rows = [];
|
|
||||||
const comp = this;
|
|
||||||
|
|
||||||
for (const mk of needUpgradeMasterKeys) {
|
|
||||||
rows.push(
|
|
||||||
<tr key={mk.id}>
|
|
||||||
<td style={theme.textStyle}>{mk.id}</td>
|
|
||||||
<td><button onClick={() => shared.upgradeMasterKey(comp, mk)} style={theme.buttonStyle}>Upgrade</button></td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1 style={theme.h1Style}>{_('Master keys that need upgrading')}</h1>
|
|
||||||
<p style={theme.textStyle}>{_('The following master keys use an out-dated encryption algorithm and it is recommended to upgrade them. The upgraded master key will still be able to decrypt and encrypt your data as usual.')}</p>
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th style={theme.textStyle}>{_('ID')}</th>
|
|
||||||
<th style={theme.textStyle}>{_('Upgrade')}</th>
|
|
||||||
</tr>
|
|
||||||
{rows}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderReencryptData() {
|
|
||||||
if (!shim.isElectron()) return null;
|
|
||||||
if (!this.props.shouldReencrypt) return null;
|
|
||||||
|
|
||||||
const theme = themeStyle(this.props.themeId);
|
|
||||||
const buttonLabel = _('Re-encrypt data');
|
|
||||||
|
|
||||||
const intro = this.props.shouldReencrypt ? _('The default encryption method has been changed to a more secure one and it is recommended that you apply it to your data.') : _('You may use the tool below to re-encrypt your data, for example if you know that some of your notes are encrypted with an obsolete encryption method.');
|
|
||||||
|
|
||||||
let t = `${intro}\n\n${_('In order to do so, your entire data set will have to be encrypted and synchronised, so it is best to run it overnight.\n\nTo start, please follow these instructions:\n\n1. Synchronise all your devices.\n2. Click "%s".\n3. Let it run to completion. While it runs, avoid changing any note on your other devices, to avoid conflicts.\n4. Once sync is done on this device, sync all your other devices and let it run to completion.\n\nImportant: you only need to run this ONCE on one device.', buttonLabel)}`;
|
|
||||||
|
|
||||||
t = t.replace(/\n\n/g, '</p><p>');
|
|
||||||
t = t.replace(/\n/g, '<br>');
|
|
||||||
t = `<p>${t}</p>`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1 style={theme.h1Style}>{_('Re-encryption')}</h1>
|
|
||||||
<p style={theme.textStyle} dangerouslySetInnerHTML={{ __html: t }}></p>
|
|
||||||
<span style={{ marginRight: 10 }}>
|
|
||||||
<button onClick={() => shared.reencryptData()} style={theme.buttonStyle}>{buttonLabel}</button>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{ !this.props.shouldReencrypt ? null : <button onClick={() => shared.dontReencryptData()} style={theme.buttonStyle}>{_('Ignore')}</button> }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderMasterPassword() {
|
|
||||||
if (!this.props.encryptionEnabled && !this.props.masterKeys.length) return null;
|
|
||||||
|
|
||||||
const theme = themeStyle(this.props.themeId);
|
|
||||||
|
|
||||||
const onMasterPasswordSave = async () => {
|
|
||||||
shared.onMasterPasswordSave(this);
|
|
||||||
|
|
||||||
if (!(await shared.masterPasswordIsValid(this, this.state.masterPasswordInput))) {
|
|
||||||
alert('Password is invalid. Please try again.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.state.passwordChecks['master']) {
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
|
||||||
<span style={theme.textStyle}>{_('Master password:')}</span>
|
|
||||||
<span style={{ ...theme.textStyle, fontWeight: 'bold' }}>✔ {_('Loaded')}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
|
||||||
<span style={theme.textStyle}>❌ {'The master password is not set or is invalid. Please type it below:'}</span>
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'row', marginTop: 10 }}>
|
|
||||||
<MasterPasswordInput placeholder={_('Enter your master password')} type="password" value={this.state.masterPasswordInput} onChange={(event: any) => shared.onMasterPasswordChange(this, event.target.value)} />{' '}
|
|
||||||
<Button ml="10px" level={ButtonLevel.Secondary} onClick={onMasterPasswordSave} title={_('Save')} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const theme = themeStyle(this.props.themeId);
|
|
||||||
const masterKeys: MasterKeyEntity[] = this.props.masterKeys;
|
|
||||||
|
|
||||||
const containerStyle = Object.assign({}, theme.containerStyle, {
|
|
||||||
padding: theme.configScreenPadding,
|
|
||||||
overflow: 'auto',
|
|
||||||
backgroundColor: theme.backgroundColor3,
|
|
||||||
});
|
|
||||||
|
|
||||||
const nonExistingMasterKeyIds = this.props.notLoadedMasterKeys.slice();
|
|
||||||
|
|
||||||
for (let i = 0; i < masterKeys.length; i++) {
|
|
||||||
const mk = masterKeys[i];
|
|
||||||
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
|
|
||||||
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const onToggleButtonClick = async () => {
|
|
||||||
const isEnabled = getEncryptionEnabled();
|
|
||||||
const masterKey = getDefaultMasterKey();
|
|
||||||
|
|
||||||
let answer = null;
|
|
||||||
if (isEnabled) {
|
|
||||||
answer = await dialogs.confirm(_('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?'));
|
|
||||||
} else {
|
|
||||||
const msg = shared.enableEncryptionConfirmationMessages(masterKey);
|
|
||||||
answer = await dialogs.prompt(msg.join('\n\n'), '', '', { type: 'password' });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!answer) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await toggleAndSetupEncryption(EncryptionService.instance(), !isEnabled, masterKey, answer);
|
|
||||||
} catch (error) {
|
|
||||||
await dialogs.alert(error.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const decryptedItemsInfo = <p style={theme.textStyle}>{shared.decryptedStatText(this)}</p>;
|
|
||||||
const toggleButton = (
|
|
||||||
<button
|
|
||||||
style={theme.buttonStyle}
|
|
||||||
onClick={() => {
|
|
||||||
void onToggleButtonClick();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{this.props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
|
|
||||||
const needUpgradeSection = this.renderNeedUpgradeSection();
|
|
||||||
const reencryptDataSection = this.renderReencryptData();
|
|
||||||
|
|
||||||
const enabledMasterKeySection = this.renderMasterKeySection(masterKeys.filter(mk => masterKeyEnabled(mk)), true);
|
|
||||||
const disabledMasterKeySection = this.renderMasterKeySection(masterKeys.filter(mk => !masterKeyEnabled(mk)), false);
|
|
||||||
|
|
||||||
let nonExistingMasterKeySection = null;
|
|
||||||
|
|
||||||
if (nonExistingMasterKeyIds.length) {
|
|
||||||
const rows = [];
|
|
||||||
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
|
|
||||||
const id = nonExistingMasterKeyIds[i];
|
|
||||||
rows.push(
|
|
||||||
<tr key={id}>
|
|
||||||
<td style={theme.textStyle}>{id}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
nonExistingMasterKeySection = (
|
|
||||||
<div>
|
|
||||||
<h1 style={theme.h1Style}>{_('Missing Master Keys')}</h1>
|
|
||||||
<p style={theme.textStyle}>{_('The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</p>
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th style={theme.textStyle}>{_('ID')}</th>
|
|
||||||
</tr>
|
|
||||||
{rows}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div style={containerStyle}>
|
|
||||||
{
|
|
||||||
<div className="alert alert-warning" style={{ backgroundColor: theme.warningBackgroundColor, paddingLeft: 10, paddingRight: 10, paddingTop: 2, paddingBottom: 2 }}>
|
|
||||||
<p style={theme.textStyle}>
|
|
||||||
<span>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</span>{' '}
|
|
||||||
<a
|
|
||||||
onClick={() => {
|
|
||||||
bridge().openExternal('https://joplinapp.org/e2ee/');
|
|
||||||
}}
|
|
||||||
href="#"
|
|
||||||
style={theme.urlStyle}
|
|
||||||
>
|
|
||||||
https://joplinapp.org/e2ee/
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<h1 style={theme.h1Style}>{_('Status')}</h1>
|
|
||||||
<p style={theme.textStyle}>
|
|
||||||
{_('Encryption is:')} <strong>{this.props.encryptionEnabled ? _('Enabled') : _('Disabled')}</strong>
|
|
||||||
</p>
|
|
||||||
{this.renderMasterPassword()}
|
|
||||||
{decryptedItemsInfo}
|
|
||||||
{toggleButton}
|
|
||||||
{needUpgradeSection}
|
|
||||||
{this.props.shouldReencrypt ? reencryptDataSection : null}
|
|
||||||
{enabledMasterKeySection}
|
|
||||||
{disabledMasterKeySection}
|
|
||||||
{nonExistingMasterKeySection}
|
|
||||||
{!this.props.shouldReencrypt ? reencryptDataSection : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = (state: State) => {
|
|
||||||
const syncInfo = new SyncInfo(state.settings['syncInfoCache']);
|
|
||||||
|
|
||||||
return {
|
|
||||||
themeId: state.settings.theme,
|
|
||||||
masterKeys: syncInfo.masterKeys,
|
|
||||||
passwords: state.settings['encryption.passwordCache'],
|
|
||||||
encryptionEnabled: syncInfo.e2ee,
|
|
||||||
activeMasterKeyId: syncInfo.activeMasterKeyId,
|
|
||||||
shouldReencrypt: state.settings['encryption.shouldReencrypt'] >= Setting.SHOULD_REENCRYPT_YES,
|
|
||||||
notLoadedMasterKeys: state.notLoadedMasterKeys,
|
|
||||||
masterPassword: state.settings['encryption.masterPassword'],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const EncryptionConfigScreen = connect(mapStateToProps)(EncryptionConfigScreenComponent);
|
|
||||||
|
|
||||||
export default EncryptionConfigScreen;
|
|
@ -0,0 +1,366 @@
|
|||||||
|
const React = require('react');
|
||||||
|
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
|
||||||
|
import { themeStyle } from '@joplin/lib/theme';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
import time from '@joplin/lib/time';
|
||||||
|
import shim from '@joplin/lib/shim';
|
||||||
|
import dialogs from '../dialogs';
|
||||||
|
import bridge from '../../services/bridge';
|
||||||
|
import { decryptedStatText, dontReencryptData, enableEncryptionConfirmationMessages, onSavePasswordClick, onToggleEnabledClick, reencryptData, upgradeMasterKey, useInputMasterPassword, useInputPasswords, usePasswordChecker, useStats, useToggleShowDisabledMasterKeys } from '@joplin/lib/components/EncryptionConfigScreen/utils';
|
||||||
|
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
|
||||||
|
import { getEncryptionEnabled, masterKeyEnabled, SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
|
||||||
|
import { getDefaultMasterKey, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
|
||||||
|
import StyledInput from '../style/StyledInput';
|
||||||
|
import Button, { ButtonLevel } from '../Button/Button';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { AppState } from '../../app.reducer';
|
||||||
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
|
|
||||||
|
const MasterPasswordInput = styled(StyledInput)`
|
||||||
|
min-width: 300px;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
themeId: any;
|
||||||
|
masterKeys: MasterKeyEntity[];
|
||||||
|
passwords: Record<string, string>;
|
||||||
|
notLoadedMasterKeys: string[];
|
||||||
|
encryptionEnabled: boolean;
|
||||||
|
shouldReencrypt: boolean;
|
||||||
|
activeMasterKeyId: string;
|
||||||
|
masterPassword: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EncryptionConfigScreen = (props: Props) => {
|
||||||
|
const { inputPasswords, onInputPasswordChange } = useInputPasswords(props.passwords);
|
||||||
|
|
||||||
|
const theme: any = useMemo(() => {
|
||||||
|
return themeStyle(props.themeId);
|
||||||
|
}, [props.themeId]);
|
||||||
|
|
||||||
|
const stats = useStats();
|
||||||
|
const { passwordChecks, masterPasswordKeys } = usePasswordChecker(props.masterKeys, props.activeMasterKeyId, props.masterPassword, props.passwords);
|
||||||
|
const { showDisabledMasterKeys, toggleShowDisabledMasterKeys } = useToggleShowDisabledMasterKeys();
|
||||||
|
|
||||||
|
const onUpgradeMasterKey = useCallback((mk: MasterKeyEntity) => {
|
||||||
|
void upgradeMasterKey(mk, passwordChecks, props.passwords);
|
||||||
|
}, [passwordChecks, props.passwords]);
|
||||||
|
|
||||||
|
const renderNeedUpgradeSection = () => {
|
||||||
|
if (!shim.isElectron()) return null;
|
||||||
|
|
||||||
|
const needUpgradeMasterKeys = EncryptionService.instance().masterKeysThatNeedUpgrading(props.masterKeys);
|
||||||
|
if (!needUpgradeMasterKeys.length) return null;
|
||||||
|
|
||||||
|
const theme = themeStyle(props.themeId);
|
||||||
|
|
||||||
|
const rows = [];
|
||||||
|
|
||||||
|
for (const mk of needUpgradeMasterKeys) {
|
||||||
|
rows.push(
|
||||||
|
<tr key={mk.id}>
|
||||||
|
<td style={theme.textStyle}>{mk.id}</td>
|
||||||
|
<td><button onClick={() => onUpgradeMasterKey(mk)} style={theme.buttonStyle}>Upgrade</button></td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 style={theme.h1Style}>{_('Master keys that need upgrading')}</h1>
|
||||||
|
<p style={theme.textStyle}>{_('The following master keys use an out-dated encryption algorithm and it is recommended to upgrade them. The upgraded master key will still be able to decrypt and encrypt your data as usual.')}</p>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th style={theme.textStyle}>{_('ID')}</th>
|
||||||
|
<th style={theme.textStyle}>{_('Upgrade')}</th>
|
||||||
|
</tr>
|
||||||
|
{rows}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderReencryptData = () => {
|
||||||
|
if (!shim.isElectron()) return null;
|
||||||
|
if (!props.shouldReencrypt) return null;
|
||||||
|
|
||||||
|
const theme = themeStyle(props.themeId);
|
||||||
|
const buttonLabel = _('Re-encrypt data');
|
||||||
|
|
||||||
|
const intro = props.shouldReencrypt ? _('The default encryption method has been changed to a more secure one and it is recommended that you apply it to your data.') : _('You may use the tool below to re-encrypt your data, for example if you know that some of your notes are encrypted with an obsolete encryption method.');
|
||||||
|
|
||||||
|
let t = `${intro}\n\n${_('In order to do so, your entire data set will have to be encrypted and synchronised, so it is best to run it overnight.\n\nTo start, please follow these instructions:\n\n1. Synchronise all your devices.\n2. Click "%s".\n3. Let it run to completion. While it runs, avoid changing any note on your other devices, to avoid conflicts.\n4. Once sync is done on this device, sync all your other devices and let it run to completion.\n\nImportant: you only need to run this ONCE on one device.', buttonLabel)}`;
|
||||||
|
|
||||||
|
t = t.replace(/\n\n/g, '</p><p>');
|
||||||
|
t = t.replace(/\n/g, '<br>');
|
||||||
|
t = `<p>${t}</p>`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 style={theme.h1Style}>{_('Re-encryption')}</h1>
|
||||||
|
<p style={theme.textStyle} dangerouslySetInnerHTML={{ __html: t }}></p>
|
||||||
|
<span style={{ marginRight: 10 }}>
|
||||||
|
<button onClick={() => void reencryptData()} style={theme.buttonStyle}>{buttonLabel}</button>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{ !props.shouldReencrypt ? null : <button onClick={() => dontReencryptData()} style={theme.buttonStyle}>{_('Ignore')}</button> }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderMasterKey = (mk: MasterKeyEntity) => {
|
||||||
|
const theme = themeStyle(props.themeId);
|
||||||
|
|
||||||
|
const passwordStyle = {
|
||||||
|
color: theme.color,
|
||||||
|
backgroundColor: theme.backgroundColor,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: theme.dividerColor,
|
||||||
|
};
|
||||||
|
|
||||||
|
const password = inputPasswords[mk.id] ? inputPasswords[mk.id] : '';
|
||||||
|
const isActive = props.activeMasterKeyId === mk.id;
|
||||||
|
const activeIcon = isActive ? '✔' : '';
|
||||||
|
const passwordOk = passwordChecks[mk.id] === true ? '✔' : '❌';
|
||||||
|
|
||||||
|
const renderPasswordInput = (masterKeyId: string) => {
|
||||||
|
if (masterPasswordKeys[masterKeyId] || !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 => onInputPasswordChange(mk, event.target.value)} />{' '}
|
||||||
|
<button style={theme.buttonStyle} onClick={() => onSavePasswordClick(mk, props.passwords)}>
|
||||||
|
{_('Save')}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={mk.id}>
|
||||||
|
<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>
|
||||||
|
{renderPasswordInput(mk.id)}
|
||||||
|
<td style={theme.textStyle}>{passwordOk}</td>
|
||||||
|
<td style={theme.textStyle}>
|
||||||
|
<button style={theme.buttonStyle} onClick={() => onToggleEnabledClick(mk)}>{masterKeyEnabled(mk) ? _('Disable') : _('Enable')}</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderMasterKeySection = (masterKeys: MasterKeyEntity[], isEnabledMasterKeys: boolean) => {
|
||||||
|
const theme = themeStyle(props.themeId);
|
||||||
|
const mkComps = [];
|
||||||
|
const showTable = isEnabledMasterKeys || showDisabledMasterKeys;
|
||||||
|
|
||||||
|
for (let i = 0; i < masterKeys.length; i++) {
|
||||||
|
const mk = masterKeys[i];
|
||||||
|
mkComps.push(renderMasterKey(mk));
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerComp = isEnabledMasterKeys ? <h1 style={theme.h1Style}>{_('Master Keys')}</h1> : <a onClick={() => toggleShowDisabledMasterKeys() } 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { inputMasterPassword, onMasterPasswordSave, onMasterPasswordChange } = useInputMasterPassword(props.masterKeys, props.activeMasterKeyId);
|
||||||
|
|
||||||
|
const renderMasterPassword = () => {
|
||||||
|
if (!props.encryptionEnabled && !props.masterKeys.length) return null;
|
||||||
|
|
||||||
|
const theme = themeStyle(props.themeId);
|
||||||
|
|
||||||
|
if (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={inputMasterPassword} onChange={(event: any) => onMasterPasswordChange(event.target.value)} />{' '}
|
||||||
|
<Button ml="10px" level={ButtonLevel.Secondary} onClick={onMasterPasswordSave} title={_('Save')} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const containerStyle = Object.assign({}, theme.containerStyle, {
|
||||||
|
padding: theme.configScreenPadding,
|
||||||
|
overflow: 'auto',
|
||||||
|
backgroundColor: theme.backgroundColor3,
|
||||||
|
});
|
||||||
|
|
||||||
|
const nonExistingMasterKeyIds = props.notLoadedMasterKeys.slice();
|
||||||
|
|
||||||
|
for (let i = 0; i < props.masterKeys.length; i++) {
|
||||||
|
const mk = props.masterKeys[i];
|
||||||
|
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
|
||||||
|
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const onToggleButtonClick = async () => {
|
||||||
|
const isEnabled = getEncryptionEnabled();
|
||||||
|
const masterKey = getDefaultMasterKey();
|
||||||
|
|
||||||
|
let answer = null;
|
||||||
|
if (isEnabled) {
|
||||||
|
answer = await dialogs.confirm(_('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?'));
|
||||||
|
} else {
|
||||||
|
const msg = enableEncryptionConfirmationMessages(masterKey);
|
||||||
|
answer = await dialogs.prompt(msg.join('\n\n'), '', '', { type: 'password' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!answer) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await toggleAndSetupEncryption(EncryptionService.instance(), !isEnabled, masterKey, answer);
|
||||||
|
} catch (error) {
|
||||||
|
await dialogs.alert(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const decryptedItemsInfo = <p style={theme.textStyle}>{decryptedStatText(stats)}</p>;
|
||||||
|
const toggleButton = (
|
||||||
|
<button
|
||||||
|
style={theme.buttonStyle}
|
||||||
|
onClick={() => {
|
||||||
|
void onToggleButtonClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
const needUpgradeSection = renderNeedUpgradeSection();
|
||||||
|
const reencryptDataSection = renderReencryptData();
|
||||||
|
|
||||||
|
const enabledMasterKeySection = renderMasterKeySection(props.masterKeys.filter(mk => masterKeyEnabled(mk)), true);
|
||||||
|
const disabledMasterKeySection = renderMasterKeySection(props.masterKeys.filter(mk => !masterKeyEnabled(mk)), false);
|
||||||
|
|
||||||
|
let nonExistingMasterKeySection = null;
|
||||||
|
|
||||||
|
if (nonExistingMasterKeyIds.length) {
|
||||||
|
const rows = [];
|
||||||
|
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
|
||||||
|
const id = nonExistingMasterKeyIds[i];
|
||||||
|
rows.push(
|
||||||
|
<tr key={id}>
|
||||||
|
<td style={theme.textStyle}>{id}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
nonExistingMasterKeySection = (
|
||||||
|
<div>
|
||||||
|
<h1 style={theme.h1Style}>{_('Missing Master Keys')}</h1>
|
||||||
|
<p style={theme.textStyle}>{_('The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</p>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th style={theme.textStyle}>{_('ID')}</th>
|
||||||
|
</tr>
|
||||||
|
{rows}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={containerStyle}>
|
||||||
|
{
|
||||||
|
<div className="alert alert-warning" style={{ backgroundColor: theme.warningBackgroundColor, paddingLeft: 10, paddingRight: 10, paddingTop: 2, paddingBottom: 2 }}>
|
||||||
|
<p style={theme.textStyle}>
|
||||||
|
<span>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</span>{' '}
|
||||||
|
<a
|
||||||
|
onClick={() => {
|
||||||
|
bridge().openExternal('https://joplinapp.org/e2ee/');
|
||||||
|
}}
|
||||||
|
href="#"
|
||||||
|
style={theme.urlStyle}
|
||||||
|
>
|
||||||
|
https://joplinapp.org/e2ee/
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<h1 style={theme.h1Style}>{_('Status')}</h1>
|
||||||
|
<p style={theme.textStyle}>
|
||||||
|
{_('Encryption is:')} <strong>{props.encryptionEnabled ? _('Enabled') : _('Disabled')}</strong>
|
||||||
|
</p>
|
||||||
|
{renderMasterPassword()}
|
||||||
|
{decryptedItemsInfo}
|
||||||
|
{toggleButton}
|
||||||
|
{needUpgradeSection}
|
||||||
|
{props.shouldReencrypt ? reencryptDataSection : null}
|
||||||
|
{enabledMasterKeySection}
|
||||||
|
{disabledMasterKeySection}
|
||||||
|
{nonExistingMasterKeySection}
|
||||||
|
{!props.shouldReencrypt ? reencryptDataSection : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state: AppState) => {
|
||||||
|
const syncInfo = new SyncInfo(state.settings['syncInfoCache']);
|
||||||
|
|
||||||
|
return {
|
||||||
|
themeId: state.settings.theme,
|
||||||
|
masterKeys: syncInfo.masterKeys,
|
||||||
|
passwords: state.settings['encryption.passwordCache'],
|
||||||
|
encryptionEnabled: syncInfo.e2ee,
|
||||||
|
activeMasterKeyId: syncInfo.activeMasterKeyId,
|
||||||
|
shouldReencrypt: state.settings['encryption.shouldReencrypt'] >= Setting.SHOULD_REENCRYPT_YES,
|
||||||
|
notLoadedMasterKeys: state.notLoadedMasterKeys,
|
||||||
|
masterPassword: state.settings['encryption.masterPassword'],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(EncryptionConfigScreen);
|
@ -2,68 +2,56 @@ const React = require('react');
|
|||||||
const { TextInput, TouchableOpacity, Linking, View, StyleSheet, Text, Button, ScrollView } = require('react-native');
|
const { TextInput, TouchableOpacity, Linking, View, StyleSheet, Text, Button, ScrollView } = require('react-native');
|
||||||
const { connect } = require('react-redux');
|
const { connect } = require('react-redux');
|
||||||
const { ScreenHeader } = require('../screen-header.js');
|
const { ScreenHeader } = require('../screen-header.js');
|
||||||
const { BaseScreenComponent } = require('../base-screen.js');
|
|
||||||
const { themeStyle } = require('../global-style.js');
|
const { themeStyle } = require('../global-style.js');
|
||||||
const DialogBox = require('react-native-dialogbox').default;
|
const DialogBox = require('react-native-dialogbox').default;
|
||||||
const { dialogs } = require('../../utils/dialogs.js');
|
const { dialogs } = require('../../utils/dialogs.js');
|
||||||
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
|
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import time from '@joplin/lib/time';
|
import time from '@joplin/lib/time';
|
||||||
import shared from '@joplin/lib/components/shared/encryption-config-shared';
|
import { decryptedStatText, enableEncryptionConfirmationMessages, onSavePasswordClick, useInputMasterPassword, useInputPasswords, usePasswordChecker, useStats } from '@joplin/lib/components/EncryptionConfigScreen/utils';
|
||||||
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
|
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
|
||||||
import { State } from '@joplin/lib/reducer';
|
import { State } from '@joplin/lib/reducer';
|
||||||
import { SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
|
import { SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
|
||||||
import { getDefaultMasterKey, setupAndDisableEncryption, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
|
import { getDefaultMasterKey, setupAndDisableEncryption, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
|
||||||
|
import { useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
interface Props {}
|
interface Props {
|
||||||
|
themeId: any;
|
||||||
|
masterKeys: MasterKeyEntity[];
|
||||||
|
passwords: Record<string, string>;
|
||||||
|
notLoadedMasterKeys: string[];
|
||||||
|
encryptionEnabled: boolean;
|
||||||
|
shouldReencrypt: boolean;
|
||||||
|
activeMasterKeyId: string;
|
||||||
|
masterPassword: string;
|
||||||
|
}
|
||||||
|
|
||||||
class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
|
const EncryptionConfigScreen = (props: Props) => {
|
||||||
static navigationOptions(): any {
|
const [passwordPromptShow, setPasswordPromptShow] = useState(false);
|
||||||
return { header: null };
|
const [passwordPromptAnswer, setPasswordPromptAnswer] = useState('');
|
||||||
}
|
const [passwordPromptConfirmAnswer, setPasswordPromptConfirmAnswer] = useState('');
|
||||||
|
const stats = useStats();
|
||||||
|
const { passwordChecks, masterPasswordKeys } = usePasswordChecker(props.masterKeys, props.activeMasterKeyId, props.masterPassword, props.passwords);
|
||||||
|
const { inputPasswords, onInputPasswordChange } = useInputPasswords(props.passwords);
|
||||||
|
const { inputMasterPassword, onMasterPasswordSave, onMasterPasswordChange } = useInputMasterPassword(props.masterKeys, props.activeMasterKeyId);
|
||||||
|
const dialogBoxRef = useRef(null);
|
||||||
|
|
||||||
constructor(props: Props) {
|
const mkComps = [];
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
const nonExistingMasterKeyIds = props.notLoadedMasterKeys.slice();
|
||||||
passwordPromptShow: false,
|
|
||||||
passwordPromptAnswer: '',
|
const theme: any = useMemo(() => {
|
||||||
passwordPromptConfirmAnswer: '',
|
return themeStyle(props.themeId);
|
||||||
|
}, [props.themeId]);
|
||||||
|
|
||||||
|
const rootStyle = useMemo(() => {
|
||||||
|
return {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: theme.backgroundColor,
|
||||||
};
|
};
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
shared.initialize(this, props);
|
const styles = useMemo(() => {
|
||||||
|
|
||||||
this.styles_ = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.isMounted_ = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshStats() {
|
|
||||||
return shared.refreshStats(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.isMounted_ = true;
|
|
||||||
shared.componentDidMount(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props) {
|
|
||||||
shared.componentDidUpdate(this, prevProps);
|
|
||||||
}
|
|
||||||
|
|
||||||
async checkPasswords() {
|
|
||||||
return shared.checkPasswords(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
styles() {
|
|
||||||
const themeId = this.props.themeId;
|
|
||||||
const theme = themeStyle(themeId);
|
|
||||||
|
|
||||||
if (this.styles_[themeId]) return this.styles_[themeId];
|
|
||||||
this.styles_ = {};
|
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
titleText: {
|
titleText: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@ -93,39 +81,32 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
this.styles_[themeId] = StyleSheet.create(styles);
|
return StyleSheet.create(styles);
|
||||||
return this.styles_[themeId];
|
}, [theme]);
|
||||||
}
|
|
||||||
|
|
||||||
renderMasterKey(_num: number, mk: MasterKeyEntity) {
|
const decryptedItemsInfo = props.encryptionEnabled ? <Text style={styles.normalText}>{decryptedStatText(stats)}</Text> : null;
|
||||||
const theme = themeStyle(this.props.themeId);
|
|
||||||
|
|
||||||
const onSaveClick = () => {
|
const renderMasterKey = (_num: number, mk: MasterKeyEntity) => {
|
||||||
return shared.onSavePasswordClick(this, mk);
|
const theme = themeStyle(props.themeId);
|
||||||
};
|
|
||||||
|
|
||||||
const onPasswordChange = (text: string) => {
|
const password = inputPasswords[mk.id] ? inputPasswords[mk.id] : '';
|
||||||
return shared.onPasswordChange(this, mk, text);
|
const passwordOk = passwordChecks[mk.id] === true ? '✔' : '❌';
|
||||||
};
|
|
||||||
|
|
||||||
const password = this.state.passwords[mk.id] ? this.state.passwords[mk.id] : '';
|
|
||||||
const passwordOk = this.state.passwordChecks[mk.id] === true ? '✔' : '❌';
|
|
||||||
|
|
||||||
const inputStyle: any = { flex: 1, marginRight: 10, color: theme.color };
|
const inputStyle: any = { flex: 1, marginRight: 10, color: theme.color };
|
||||||
inputStyle.borderBottomWidth = 1;
|
inputStyle.borderBottomWidth = 1;
|
||||||
inputStyle.borderBottomColor = theme.dividerColor;
|
inputStyle.borderBottomColor = theme.dividerColor;
|
||||||
|
|
||||||
const renderPasswordInput = (masterKeyId: string) => {
|
const renderPasswordInput = (masterKeyId: string) => {
|
||||||
if (this.state.masterPasswordKeys[masterKeyId] || !this.state.passwordChecks['master']) {
|
if (masterPasswordKeys[masterKeyId] || !passwordChecks['master']) {
|
||||||
return (
|
return (
|
||||||
<Text style={{ ...this.styles().normalText, color: theme.colorFaded, fontStyle: 'italic' }}>({_('Master password')})</Text>
|
<Text style={{ ...styles.normalText, color: theme.colorFaded, fontStyle: 'italic' }}>({_('Master password')})</Text>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, flexDirection: 'row', alignItems: 'center' }}>
|
<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>
|
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={password} onChangeText={(text: string) => onInputPasswordChange(mk, text)} style={inputStyle}></TextInput>
|
||||||
<Text style={{ fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{passwordOk}</Text>
|
<Text style={{ fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{passwordOk}</Text>
|
||||||
<Button title={_('Save')} onPress={() => onSaveClick()}></Button>
|
<Button title={_('Save')} onPress={() => onSavePasswordClick(mk, props.passwords)}></Button>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -133,65 +114,65 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
|
|||||||
|
|
||||||
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={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={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>
|
||||||
{renderPasswordInput(mk.id)}
|
{renderPasswordInput(mk.id)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
passwordPromptComponent() {
|
const renderPasswordPrompt = () => {
|
||||||
const theme = themeStyle(this.props.themeId);
|
const theme = themeStyle(props.themeId);
|
||||||
const masterKey = getDefaultMasterKey();
|
const masterKey = getDefaultMasterKey();
|
||||||
|
|
||||||
const onEnableClick = async () => {
|
const onEnableClick = async () => {
|
||||||
try {
|
try {
|
||||||
const password = this.state.passwordPromptAnswer;
|
const password = passwordPromptAnswer;
|
||||||
if (!password) throw new Error(_('Password cannot be empty'));
|
if (!password) throw new Error(_('Password cannot be empty'));
|
||||||
const password2 = this.state.passwordPromptConfirmAnswer;
|
const password2 = passwordPromptConfirmAnswer;
|
||||||
if (!password2) throw new Error(_('Confirm password cannot be empty'));
|
if (!password2) throw new Error(_('Confirm password cannot be empty'));
|
||||||
if (password !== password2) throw new Error(_('Passwords do not match!'));
|
if (password !== password2) throw new Error(_('Passwords do not match!'));
|
||||||
await toggleAndSetupEncryption(EncryptionService.instance(), true, masterKey, password);
|
await toggleAndSetupEncryption(EncryptionService.instance(), true, masterKey, password);
|
||||||
// await generateMasterKeyAndEnableEncryption(EncryptionService.instance(), password);
|
// await generateMasterKeyAndEnableEncryption(EncryptionService.instance(), password);
|
||||||
this.setState({ passwordPromptShow: false });
|
setPasswordPromptShow(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await dialogs.error(this, error.message);
|
alert(error.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const messages = shared.enableEncryptionConfirmationMessages(masterKey);
|
const messages = enableEncryptionConfirmationMessages(masterKey);
|
||||||
|
|
||||||
const messageComps = messages.map(msg => {
|
const messageComps = messages.map((msg: string) => {
|
||||||
return <Text key={msg} style={{ fontSize: theme.fontSize, color: theme.color, marginBottom: 10 }}>{msg}</Text>;
|
return <Text key={msg} style={{ fontSize: theme.fontSize, color: theme.color, marginBottom: 10 }}>{msg}</Text>;
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, borderColor: theme.dividerColor, borderWidth: 1, padding: 10, marginTop: 10, marginBottom: 10 }}>
|
<View style={{ flex: 1, borderColor: theme.dividerColor, borderWidth: 1, padding: 10, marginTop: 10, marginBottom: 10 }}>
|
||||||
<View>{messageComps}</View>
|
<View>{messageComps}</View>
|
||||||
<Text style={this.styles().normalText}>{_('Password:')}</Text>
|
<Text style={styles.normalText}>{_('Password:')}</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
selectionColor={theme.textSelectionColor}
|
selectionColor={theme.textSelectionColor}
|
||||||
keyboardAppearance={theme.keyboardAppearance}
|
keyboardAppearance={theme.keyboardAppearance}
|
||||||
style={this.styles().normalTextInput}
|
style={styles.normalTextInput}
|
||||||
secureTextEntry={true}
|
secureTextEntry={true}
|
||||||
value={this.state.passwordPromptAnswer}
|
value={passwordPromptAnswer}
|
||||||
onChangeText={(text: string) => {
|
onChangeText={(text: string) => {
|
||||||
this.setState({ passwordPromptAnswer: text });
|
setPasswordPromptAnswer(text);
|
||||||
}}
|
}}
|
||||||
></TextInput>
|
></TextInput>
|
||||||
|
|
||||||
<Text style={this.styles().normalText}>{_('Confirm password:')}</Text>
|
<Text style={styles.normalText}>{_('Confirm password:')}</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
selectionColor={theme.textSelectionColor}
|
selectionColor={theme.textSelectionColor}
|
||||||
keyboardAppearance={theme.keyboardAppearance}
|
keyboardAppearance={theme.keyboardAppearance}
|
||||||
style={this.styles().normalTextInput}
|
style={styles.normalTextInput}
|
||||||
secureTextEntry={true}
|
secureTextEntry={true}
|
||||||
value={this.state.passwordPromptConfirmAnswer}
|
value={passwordPromptConfirmAnswer}
|
||||||
onChangeText={(text: string) => {
|
onChangeText={(text: string) => {
|
||||||
this.setState({ passwordPromptConfirmAnswer: text });
|
setPasswordPromptConfirmAnswer(text);
|
||||||
}}
|
}}
|
||||||
></TextInput>
|
></TextInput>
|
||||||
<View style={{ flexDirection: 'row' }}>
|
<View style={{ flexDirection: 'row' }}>
|
||||||
@ -207,85 +188,66 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
|
|||||||
<Button
|
<Button
|
||||||
title={_('Cancel')}
|
title={_('Cancel')}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
this.setState({ passwordPromptShow: false });
|
setPasswordPromptShow(false);
|
||||||
}}
|
}}
|
||||||
></Button>
|
></Button>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
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 renderMasterPassword = () => {
|
||||||
|
if (!props.encryptionEnabled && !props.masterKeys.length) return null;
|
||||||
|
|
||||||
const inputStyle: any = { flex: 1, marginRight: 10, color: theme.color };
|
const inputStyle: any = { flex: 1, marginRight: 10, color: theme.color };
|
||||||
inputStyle.borderBottomWidth = 1;
|
inputStyle.borderBottomWidth = 1;
|
||||||
inputStyle.borderBottomColor = theme.dividerColor;
|
inputStyle.borderBottomColor = theme.dividerColor;
|
||||||
|
|
||||||
if (this.state.passwordChecks['master']) {
|
if (passwordChecks['master']) {
|
||||||
return (
|
return (
|
||||||
<View style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
<View style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||||
<Text style={{ ...this.styles().normalText, flex: 0, marginRight: 5 }}>{_('Master password:')}</Text>
|
<Text style={{ ...styles.normalText, flex: 0, marginRight: 5 }}>{_('Master password:')}</Text>
|
||||||
<Text style={{ ...this.styles().normalText, fontWeight: 'bold' }}>{_('Loaded')}</Text>
|
<Text style={{ ...styles.normalText, fontWeight: 'bold' }}>{_('Loaded')}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<View style={{ display: 'flex', flexDirection: 'column', marginTop: 10 }}>
|
<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>
|
<Text style={styles.normalText}>{'The master password is not set or is invalid. Please type it below:'}</Text>
|
||||||
<View style={{ display: 'flex', flexDirection: 'row', marginTop: 10 }}>
|
<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>
|
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={inputMasterPassword} onChangeText={(text: string) => onMasterPasswordChange(text)} style={inputStyle}></TextInput>
|
||||||
<Button onPress={onMasterPasswordSave} title={_('Save')} />
|
<Button onPress={onMasterPasswordSave} title={_('Save')} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
|
||||||
const theme = themeStyle(this.props.themeId);
|
|
||||||
const masterKeys = this.props.masterKeys;
|
|
||||||
const decryptedItemsInfo = this.props.encryptionEnabled ? <Text style={this.styles().normalText}>{shared.decryptedStatText(this)}</Text> : null;
|
|
||||||
|
|
||||||
const mkComps = [];
|
|
||||||
|
|
||||||
const nonExistingMasterKeyIds = this.props.notLoadedMasterKeys.slice();
|
for (let i = 0; i < props.masterKeys.length; i++) {
|
||||||
|
const mk = props.masterKeys[i];
|
||||||
for (let i = 0; i < masterKeys.length; i++) {
|
mkComps.push(renderMasterKey(i + 1, mk));
|
||||||
const mk = masterKeys[i];
|
|
||||||
mkComps.push(this.renderMasterKey(i + 1, mk));
|
|
||||||
|
|
||||||
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
|
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
|
||||||
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
|
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const onToggleButtonClick = async () => {
|
const onToggleButtonClick = async () => {
|
||||||
if (this.props.encryptionEnabled) {
|
if (props.encryptionEnabled) {
|
||||||
const ok = await dialogs.confirm(this, _('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?'));
|
const ok = await dialogs.confirmRef(dialogBoxRef.current, _('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?'));
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await setupAndDisableEncryption(EncryptionService.instance());
|
await setupAndDisableEncryption(EncryptionService.instance());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await dialogs.error(this, error.message);
|
alert(error.message);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
setPasswordPromptShow(true);
|
||||||
passwordPromptShow: true,
|
setPasswordPromptAnswer('');
|
||||||
passwordPromptAnswer: '',
|
setPasswordPromptConfirmAnswer('');
|
||||||
passwordPromptConfirmAnswer: '',
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -297,7 +259,7 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
|
|||||||
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
|
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
|
||||||
const id = nonExistingMasterKeyIds[i];
|
const id = nonExistingMasterKeyIds[i];
|
||||||
rows.push(
|
rows.push(
|
||||||
<Text style={this.styles().normalText} key={id}>
|
<Text style={styles.normalText} key={id}>
|
||||||
{id}
|
{id}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
@ -305,24 +267,24 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
|
|||||||
|
|
||||||
nonExistingMasterKeySection = (
|
nonExistingMasterKeySection = (
|
||||||
<View>
|
<View>
|
||||||
<Text style={this.styles().titleText}>{_('Missing Master Keys')}</Text>
|
<Text style={styles.titleText}>{_('Missing Master Keys')}</Text>
|
||||||
<Text style={this.styles().normalText}>{_('The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</Text>
|
<Text style={styles.normalText}>{_('The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</Text>
|
||||||
<View style={{ marginTop: 10 }}>{rows}</View>
|
<View style={{ marginTop: 10 }}>{rows}</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const passwordPromptComp = this.state.passwordPromptShow ? this.passwordPromptComponent() : null;
|
const passwordPromptComp = passwordPromptShow ? renderPasswordPrompt() : null;
|
||||||
const toggleButton = !this.state.passwordPromptShow ? (
|
const toggleButton = !passwordPromptShow ? (
|
||||||
<View style={{ marginTop: 10 }}>
|
<View style={{ marginTop: 10 }}>
|
||||||
<Button title={this.props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')} onPress={() => onToggleButtonClick()}></Button>
|
<Button title={props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')} onPress={() => onToggleButtonClick()}></Button>
|
||||||
</View>
|
</View>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={this.rootStyle(this.props.themeId).root}>
|
<View style={rootStyle}>
|
||||||
<ScreenHeader title={_('Encryption Config')} />
|
<ScreenHeader title={_('Encryption Config')} />
|
||||||
<ScrollView style={this.styles().container}>
|
<ScrollView style={styles.container}>
|
||||||
{
|
{
|
||||||
<View style={{ backgroundColor: theme.warningBackgroundColor, paddingTop: 5, paddingBottom: 5, paddingLeft: 10, paddingRight: 10 }}>
|
<View style={{ backgroundColor: theme.warningBackgroundColor, paddingTop: 5, paddingBottom: 5, paddingLeft: 10, paddingRight: 10 }}>
|
||||||
<Text>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</Text>
|
<Text>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</Text>
|
||||||
@ -336,27 +298,22 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
|
|||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
|
|
||||||
<Text style={this.styles().titleText}>{_('Status')}</Text>
|
<Text style={styles.titleText}>{_('Status')}</Text>
|
||||||
<Text style={this.styles().normalText}>{_('Encryption is: %s', this.props.encryptionEnabled ? _('Enabled') : _('Disabled'))}</Text>
|
<Text style={styles.normalText}>{_('Encryption is: %s', props.encryptionEnabled ? _('Enabled') : _('Disabled'))}</Text>
|
||||||
{decryptedItemsInfo}
|
{decryptedItemsInfo}
|
||||||
{this.renderMasterPassword()}
|
{renderMasterPassword()}
|
||||||
{toggleButton}
|
{toggleButton}
|
||||||
{passwordPromptComp}
|
{passwordPromptComp}
|
||||||
{mkComps}
|
{mkComps}
|
||||||
{nonExistingMasterKeySection}
|
{nonExistingMasterKeySection}
|
||||||
<View style={{ flex: 1, height: 20 }}></View>
|
<View style={{ flex: 1, height: 20 }}></View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<DialogBox
|
<DialogBox ref={dialogBoxRef}/>
|
||||||
ref={(dialogbox: any) => {
|
|
||||||
this.dialogbox = dialogbox;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const EncryptionConfigScreen = connect((state: State) => {
|
export default connect((state: State) => {
|
||||||
const syncInfo = new SyncInfo(state.settings['syncInfoCache']);
|
const syncInfo = new SyncInfo(state.settings['syncInfoCache']);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -368,6 +325,4 @@ const EncryptionConfigScreen = connect((state: State) => {
|
|||||||
notLoadedMasterKeys: state.notLoadedMasterKeys,
|
notLoadedMasterKeys: state.notLoadedMasterKeys,
|
||||||
masterPassword: state.settings['encryption.masterPassword'],
|
masterPassword: state.settings['encryption.masterPassword'],
|
||||||
};
|
};
|
||||||
})(EncryptionConfigScreenComponent);
|
})(EncryptionConfigScreen);
|
||||||
|
|
||||||
export default EncryptionConfigScreen;
|
|
||||||
|
@ -15,8 +15,8 @@
|
|||||||
"postinstall": "jetify && npm run build"
|
"postinstall": "jetify && npm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@joplin/lib": "^2.2.0",
|
"@joplin/lib": "~2.4",
|
||||||
"@joplin/renderer": "^2.2.0",
|
"@joplin/renderer": "~2.4",
|
||||||
"@react-native-community/clipboard": "^1.5.0",
|
"@react-native-community/clipboard": "^1.5.0",
|
||||||
"@react-native-community/datetimepicker": "^3.0.3",
|
"@react-native-community/datetimepicker": "^3.0.3",
|
||||||
"@react-native-community/geolocation": "^2.0.2",
|
"@react-native-community/geolocation": "^2.0.2",
|
||||||
@ -69,7 +69,7 @@
|
|||||||
"@codemirror/lang-markdown": "^0.18.4",
|
"@codemirror/lang-markdown": "^0.18.4",
|
||||||
"@codemirror/state": "^0.18.7",
|
"@codemirror/state": "^0.18.7",
|
||||||
"@codemirror/view": "^0.18.19",
|
"@codemirror/view": "^0.18.19",
|
||||||
"@joplin/tools": "^1.0.9",
|
"@joplin/tools": "~2.4",
|
||||||
"@rollup/plugin-node-resolve": "^13.0.0",
|
"@rollup/plugin-node-resolve": "^13.0.0",
|
||||||
"@rollup/plugin-typescript": "^8.2.1",
|
"@rollup/plugin-typescript": "^8.2.1",
|
||||||
"@types/node": "^14.14.6",
|
"@types/node": "^14.14.6",
|
||||||
|
@ -7,14 +7,13 @@ const { Keyboard } = require('react-native');
|
|||||||
|
|
||||||
const dialogs = {};
|
const dialogs = {};
|
||||||
|
|
||||||
dialogs.confirm = (parentComponent, message) => {
|
dialogs.confirmRef = (ref, message) => {
|
||||||
if (!parentComponent) throw new Error('parentComponent is required');
|
if (!ref) throw new Error('ref is required');
|
||||||
if (!('dialogbox' in parentComponent)) throw new Error('A "dialogbox" component must be defined on the parent component!');
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
Keyboard.dismiss();
|
Keyboard.dismiss();
|
||||||
|
|
||||||
parentComponent.dialogbox.confirm({
|
ref.confirm({
|
||||||
content: message,
|
content: message,
|
||||||
|
|
||||||
ok: {
|
ok: {
|
||||||
@ -32,6 +31,13 @@ dialogs.confirm = (parentComponent, message) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
dialogs.confirm = (parentComponent, message) => {
|
||||||
|
if (!parentComponent) throw new Error('parentComponent is required');
|
||||||
|
if (!('dialogbox' in parentComponent)) throw new Error('A "dialogbox" component must be defined on the parent component!');
|
||||||
|
|
||||||
|
return dialogs.confirmRef(parentComponent.dialogBox, message);
|
||||||
|
};
|
||||||
|
|
||||||
dialogs.pop = (parentComponent, message, buttons, options = null) => {
|
dialogs.pop = (parentComponent, message, buttons, options = null) => {
|
||||||
if (!parentComponent) throw new Error('parentComponent is required');
|
if (!parentComponent) throw new Error('parentComponent is required');
|
||||||
if (!('dialogbox' in parentComponent)) throw new Error('A "dialogbox" component must be defined on the parent component!');
|
if (!('dialogbox' in parentComponent)) throw new Error('A "dialogbox" component must be defined on the parent component!');
|
||||||
|
183
packages/lib/components/EncryptionConfigScreen/utils.ts
Normal file
183
packages/lib/components/EncryptionConfigScreen/utils.ts
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import shim from '../../shim';
|
||||||
|
import { _ } from '../../locale';
|
||||||
|
import BaseItem, { EncryptedItemsStats } from '../../models/BaseItem';
|
||||||
|
import useAsyncEffect, { AsyncEffectEvent } from '../../hooks/useAsyncEffect';
|
||||||
|
import { MasterKeyEntity } from '../../services/e2ee/types';
|
||||||
|
import time from '../../time';
|
||||||
|
import { findMasterKeyPassword } from '../../services/e2ee/utils';
|
||||||
|
import EncryptionService from '../../services/e2ee/EncryptionService';
|
||||||
|
import { masterKeyEnabled, setMasterKeyEnabled } from '../../services/synchronizer/syncInfoUtils';
|
||||||
|
import MasterKey from '../../models/MasterKey';
|
||||||
|
import { reg } from '../../registry';
|
||||||
|
import Setting from '../../models/Setting';
|
||||||
|
const { useCallback, useEffect, useState } = shim.react();
|
||||||
|
|
||||||
|
type PasswordChecks = Record<string, boolean>;
|
||||||
|
|
||||||
|
export const useStats = () => {
|
||||||
|
const [stats, setStats] = useState<EncryptedItemsStats>({ encrypted: null, total: null });
|
||||||
|
const [statsUpdateTime, setStatsUpdateTime] = useState<number>(0);
|
||||||
|
|
||||||
|
useAsyncEffect(async (event: AsyncEffectEvent) => {
|
||||||
|
const r = await BaseItem.encryptedItemsStats();
|
||||||
|
if (event.cancelled) return;
|
||||||
|
setStats(r);
|
||||||
|
}, [statsUpdateTime]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const iid = shim.setInterval(() => {
|
||||||
|
setStatsUpdateTime(Date.now());
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(iid);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decryptedStatText = (stats: EncryptedItemsStats) => {
|
||||||
|
const doneCount = stats.encrypted !== null ? stats.total - stats.encrypted : '-';
|
||||||
|
const totalCount = stats.total !== null ? stats.total : '-';
|
||||||
|
const result = _('Decrypted items: %s / %s', doneCount, totalCount);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const 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)));
|
||||||
|
return msg;
|
||||||
|
};
|
||||||
|
|
||||||
|
const masterPasswordIsValid = async (masterKeys: MasterKeyEntity[], activeMasterKeyId: string, masterPassword: string = null) => {
|
||||||
|
const activeMasterKey = masterKeys.find((mk: MasterKeyEntity) => mk.id === activeMasterKeyId);
|
||||||
|
masterPassword = masterPassword === null ? masterPassword : masterPassword;
|
||||||
|
if (activeMasterKey && masterPassword) {
|
||||||
|
return EncryptionService.instance().checkMasterKeyPassword(activeMasterKey, masterPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reencryptData = async () => {
|
||||||
|
const ok = confirm(_('Please confirm that you would like to re-encrypt your complete database.'));
|
||||||
|
if (!ok) return;
|
||||||
|
|
||||||
|
await BaseItem.forceSyncAll();
|
||||||
|
void reg.waitForSyncFinishedThenSync();
|
||||||
|
Setting.setValue('encryption.shouldReencrypt', Setting.SHOULD_REENCRYPT_NO);
|
||||||
|
alert(_('Your data is going to be re-encrypted and synced again.'));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dontReencryptData = () => {
|
||||||
|
Setting.setValue('encryption.shouldReencrypt', Setting.SHOULD_REENCRYPT_NO);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useToggleShowDisabledMasterKeys = () => {
|
||||||
|
const [showDisabledMasterKeys, setShowDisabledMasterKeys] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const toggleShowDisabledMasterKeys = () => {
|
||||||
|
setShowDisabledMasterKeys((current) => !current);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { showDisabledMasterKeys, toggleShowDisabledMasterKeys };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const onToggleEnabledClick = (mk: MasterKeyEntity) => {
|
||||||
|
setMasterKeyEnabled(mk.id, !masterKeyEnabled(mk));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const onSavePasswordClick = (mk: MasterKeyEntity, passwords: Record<string, string>) => {
|
||||||
|
const password = passwords[mk.id];
|
||||||
|
if (!password) {
|
||||||
|
Setting.deleteObjectValue('encryption.passwordCache', mk.id);
|
||||||
|
} else {
|
||||||
|
Setting.setObjectValue('encryption.passwordCache', mk.id, password);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const onMasterPasswordSave = (masterPasswordInput: string) => {
|
||||||
|
Setting.setValue('encryption.masterPassword', masterPasswordInput);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useInputMasterPassword = (masterKeys: MasterKeyEntity[], activeMasterKeyId: string) => {
|
||||||
|
const [inputMasterPassword, setInputMasterPassword] = useState<string>('');
|
||||||
|
|
||||||
|
const onMasterPasswordSave = useCallback(async () => {
|
||||||
|
Setting.setValue('encryption.masterPassword', inputMasterPassword);
|
||||||
|
|
||||||
|
if (!(await masterPasswordIsValid(masterKeys, activeMasterKeyId, inputMasterPassword))) {
|
||||||
|
alert('Password is invalid. Please try again.');
|
||||||
|
}
|
||||||
|
}, [inputMasterPassword]);
|
||||||
|
|
||||||
|
const onMasterPasswordChange = useCallback((password: string) => {
|
||||||
|
setInputMasterPassword(password);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { inputMasterPassword, onMasterPasswordSave, onMasterPasswordChange };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useInputPasswords = (propsPasswords: Record<string, string>) => {
|
||||||
|
const [inputPasswords, setInputPasswords] = useState<Record<string, string>>(propsPasswords);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInputPasswords(propsPasswords);
|
||||||
|
}, [propsPasswords]);
|
||||||
|
|
||||||
|
const onInputPasswordChange = useCallback((mk: MasterKeyEntity, password: string) => {
|
||||||
|
setInputPasswords(current => {
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
[mk.id]: password,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { inputPasswords, onInputPasswordChange };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePasswordChecker = (masterKeys: MasterKeyEntity[], activeMasterKeyId: string, masterPassword: string, passwords: Record<string, string>) => {
|
||||||
|
const [passwordChecks, setPasswordChecks] = useState<PasswordChecks>({});
|
||||||
|
const [masterPasswordKeys, setMasterPasswordKeys] = useState<PasswordChecks>({});
|
||||||
|
|
||||||
|
useAsyncEffect(async (event: AsyncEffectEvent) => {
|
||||||
|
const newPasswordChecks: PasswordChecks = {};
|
||||||
|
const newMasterPasswordKeys: PasswordChecks = {};
|
||||||
|
|
||||||
|
for (let i = 0; i < masterKeys.length; i++) {
|
||||||
|
const mk = masterKeys[i];
|
||||||
|
const password = await findMasterKeyPassword(EncryptionService.instance(), mk, passwords);
|
||||||
|
const ok = password ? await EncryptionService.instance().checkMasterKeyPassword(mk, password) : false;
|
||||||
|
newPasswordChecks[mk.id] = ok;
|
||||||
|
newMasterPasswordKeys[mk.id] = password === masterPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
newPasswordChecks['master'] = await masterPasswordIsValid(masterKeys, activeMasterKeyId, masterPassword);
|
||||||
|
|
||||||
|
if (event.cancelled) return;
|
||||||
|
|
||||||
|
setPasswordChecks(newPasswordChecks);
|
||||||
|
setMasterPasswordKeys(newMasterPasswordKeys);
|
||||||
|
}, [masterKeys, masterPassword]);
|
||||||
|
|
||||||
|
return { passwordChecks, masterPasswordKeys };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const upgradeMasterKey = async (masterKey: MasterKeyEntity, passwordChecks: PasswordChecks, passwords: Record<string, string>): Promise<string> => {
|
||||||
|
const passwordCheck = passwordChecks[masterKey.id];
|
||||||
|
if (!passwordCheck) {
|
||||||
|
return _('Please enter your password in the master key list below before upgrading the key.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const password = passwords[masterKey.id];
|
||||||
|
const newMasterKey = await EncryptionService.instance().upgradeMasterKey(masterKey, password);
|
||||||
|
await MasterKey.save(newMasterKey);
|
||||||
|
void reg.waitForSyncFinishedThenSync();
|
||||||
|
return _('The master key has been upgraded successfully!');
|
||||||
|
} catch (error) {
|
||||||
|
return _('Could not upgrade master key: %s', error.message);
|
||||||
|
}
|
||||||
|
};
|
@ -1,196 +0,0 @@
|
|||||||
import EncryptionService from '../../services/e2ee/EncryptionService';
|
|
||||||
import { _ } from '../../locale';
|
|
||||||
import BaseItem from '../../models/BaseItem';
|
|
||||||
import Setting from '../../models/Setting';
|
|
||||||
import MasterKey from '../../models/MasterKey';
|
|
||||||
import { reg } from '../../registry.js';
|
|
||||||
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 {
|
|
||||||
|
|
||||||
private refreshStatsIID_: any;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
this.refreshStatsIID_ = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async refreshStats(comp: any) {
|
|
||||||
const stats = await BaseItem.encryptedItemsStats();
|
|
||||||
comp.setState({
|
|
||||||
stats: stats,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
await BaseItem.forceSyncAll();
|
|
||||||
void reg.waitForSyncFinishedThenSync();
|
|
||||||
Setting.setValue('encryption.shouldReencrypt', Setting.SHOULD_REENCRYPT_NO);
|
|
||||||
alert(_('Your data is going to be re-encrypted and synced again.'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public dontReencryptData() {
|
|
||||||
Setting.setValue('encryption.shouldReencrypt', Setting.SHOULD_REENCRYPT_NO);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async upgradeMasterKey(comp: any, masterKey: MasterKeyEntity) {
|
|
||||||
const passwordCheck = comp.state.passwordChecks[masterKey.id];
|
|
||||||
if (!passwordCheck) {
|
|
||||||
alert(_('Please enter your password in the master key list below before upgrading the key.'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const password = comp.state.passwords[masterKey.id];
|
|
||||||
const newMasterKey = await EncryptionService.instance().upgradeMasterKey(masterKey, password);
|
|
||||||
await MasterKey.save(newMasterKey);
|
|
||||||
void reg.waitForSyncFinishedThenSync();
|
|
||||||
alert(_('The master key has been upgraded successfully!'));
|
|
||||||
} catch (error) {
|
|
||||||
alert(_('Could not upgrade master key: %s', error.message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentDidMount(comp: any) {
|
|
||||||
this.componentDidUpdate(comp);
|
|
||||||
|
|
||||||
void this.refreshStats(comp);
|
|
||||||
|
|
||||||
if (this.refreshStatsIID_) {
|
|
||||||
shim.clearInterval(this.refreshStatsIID_);
|
|
||||||
this.refreshStatsIID_ = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.refreshStatsIID_ = shim.setInterval(() => {
|
|
||||||
if (!comp.isMounted_) {
|
|
||||||
shim.clearInterval(this.refreshStatsIID_);
|
|
||||||
this.refreshStatsIID_ = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
void this.refreshStats(comp);
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentDidUpdate(comp: any, prevProps: any = null) {
|
|
||||||
if (prevProps && comp.props.passwords !== prevProps.passwords) {
|
|
||||||
comp.setState({ passwords: Object.assign({}, comp.props.passwords) });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!prevProps || comp.props.masterKeys !== prevProps.masterKeys || comp.props.passwords !== prevProps.passwords) {
|
|
||||||
comp.checkPasswords();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentWillUnmount() {
|
|
||||||
if (this.refreshStatsIID_) {
|
|
||||||
shim.clearInterval(this.refreshStatsIID_);
|
|
||||||
this.refreshStatsIID_ = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
const stats = comp.state.stats;
|
|
||||||
const doneCount = stats.encrypted !== null ? stats.total - stats.encrypted : '-';
|
|
||||||
const totalCount = stats.total !== null ? stats.total : '-';
|
|
||||||
const result = _('Decrypted items: %s / %s', doneCount, totalCount);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public onSavePasswordClick(comp: any, mk: MasterKeyEntity) {
|
|
||||||
const password = comp.state.passwords[mk.id];
|
|
||||||
if (!password) {
|
|
||||||
Setting.deleteObjectValue('encryption.passwordCache', mk.id);
|
|
||||||
} else {
|
|
||||||
Setting.setObjectValue('encryption.passwordCache', mk.id, password);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
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)));
|
|
||||||
return msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const shared = new Shared();
|
|
||||||
|
|
||||||
export default shared;
|
|
@ -25,6 +25,7 @@ function useEventListener(
|
|||||||
const eventListener = (event: Event) => {
|
const eventListener = (event: Event) => {
|
||||||
// eslint-disable-next-line no-extra-boolean-cast
|
// eslint-disable-next-line no-extra-boolean-cast
|
||||||
if (!!savedHandler?.current) {
|
if (!!savedHandler?.current) {
|
||||||
|
// @ts-ignore
|
||||||
savedHandler.current(event);
|
savedHandler.current(event);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -33,6 +33,11 @@ export interface ItemsThatNeedSyncResult {
|
|||||||
neverSyncedItemIds: string[];
|
neverSyncedItemIds: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EncryptedItemsStats {
|
||||||
|
encrypted: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
export default class BaseItem extends BaseModel {
|
export default class BaseItem extends BaseModel {
|
||||||
|
|
||||||
public static encryptionService_: any = null;
|
public static encryptionService_: any = null;
|
||||||
@ -513,7 +518,7 @@ export default class BaseItem extends BaseModel {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async encryptedItemsStats() {
|
public static async encryptedItemsStats(): Promise<EncryptedItemsStats> {
|
||||||
const classNames = this.encryptableItemClassNames();
|
const classNames = this.encryptableItemClassNames();
|
||||||
let encryptedCount = 0;
|
let encryptedCount = 0;
|
||||||
let totalCount = 0;
|
let totalCount = 0;
|
||||||
|
59
packages/lib/package-lock.json
generated
59
packages/lib/package-lock.json
generated
@ -67,6 +67,7 @@
|
|||||||
"@types/fs-extra": "^9.0.6",
|
"@types/fs-extra": "^9.0.6",
|
||||||
"@types/jest": "^26.0.15",
|
"@types/jest": "^26.0.15",
|
||||||
"@types/node": "^14.14.6",
|
"@types/node": "^14.14.6",
|
||||||
|
"@types/react": "^17.0.20",
|
||||||
"clean-html": "^1.5.0",
|
"clean-html": "^1.5.0",
|
||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
"sharp": "^0.26.2",
|
"sharp": "^0.26.2",
|
||||||
@ -1073,6 +1074,29 @@
|
|||||||
"integrity": "sha512-UEyp8LwZ4Dg30kVU2Q3amHHyTn1jEdhCIE59ANed76GaT1Vp76DD3ZWSAxgCrw6wJ0TqeoBpqmfUHiUDPs//HQ==",
|
"integrity": "sha512-UEyp8LwZ4Dg30kVU2Q3amHHyTn1jEdhCIE59ANed76GaT1Vp76DD3ZWSAxgCrw6wJ0TqeoBpqmfUHiUDPs//HQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/prop-types": {
|
||||||
|
"version": "15.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
|
||||||
|
"integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/@types/react": {
|
||||||
|
"version": "17.0.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.20.tgz",
|
||||||
|
"integrity": "sha512-wWZrPlihslrPpcKyCSlmIlruakxr57/buQN1RjlIeaaTWDLtJkTtRW429MoQJergvVKc4IWBpRhWw7YNh/7GVA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/prop-types": "*",
|
||||||
|
"@types/scheduler": "*",
|
||||||
|
"csstype": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/scheduler": {
|
||||||
|
"version": "0.16.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
|
||||||
|
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/stack-utils": {
|
"node_modules/@types/stack-utils": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz",
|
||||||
@ -2253,6 +2277,12 @@
|
|||||||
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
|
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/csstype": {
|
||||||
|
"version": "3.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz",
|
||||||
|
"integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/dashdash": {
|
"node_modules/dashdash": {
|
||||||
"version": "1.14.1",
|
"version": "1.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
||||||
@ -9758,6 +9788,29 @@
|
|||||||
"integrity": "sha512-UEyp8LwZ4Dg30kVU2Q3amHHyTn1jEdhCIE59ANed76GaT1Vp76DD3ZWSAxgCrw6wJ0TqeoBpqmfUHiUDPs//HQ==",
|
"integrity": "sha512-UEyp8LwZ4Dg30kVU2Q3amHHyTn1jEdhCIE59ANed76GaT1Vp76DD3ZWSAxgCrw6wJ0TqeoBpqmfUHiUDPs//HQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/prop-types": {
|
||||||
|
"version": "15.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
|
||||||
|
"integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@types/react": {
|
||||||
|
"version": "17.0.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.20.tgz",
|
||||||
|
"integrity": "sha512-wWZrPlihslrPpcKyCSlmIlruakxr57/buQN1RjlIeaaTWDLtJkTtRW429MoQJergvVKc4IWBpRhWw7YNh/7GVA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/prop-types": "*",
|
||||||
|
"@types/scheduler": "*",
|
||||||
|
"csstype": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/scheduler": {
|
||||||
|
"version": "0.16.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
|
||||||
|
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/stack-utils": {
|
"@types/stack-utils": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz",
|
||||||
@ -10725,6 +10778,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"csstype": {
|
||||||
|
"version": "3.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz",
|
||||||
|
"integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"dashdash": {
|
"dashdash": {
|
||||||
"version": "1.14.1",
|
"version": "1.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"@types/fs-extra": "^9.0.6",
|
"@types/fs-extra": "^9.0.6",
|
||||||
"@types/jest": "^26.0.15",
|
"@types/jest": "^26.0.15",
|
||||||
"@types/node": "^14.14.6",
|
"@types/node": "^14.14.6",
|
||||||
|
"@types/react": "^17.0.20",
|
||||||
"clean-html": "^1.5.0",
|
"clean-html": "^1.5.0",
|
||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
"sharp": "^0.26.2",
|
"sharp": "^0.26.2",
|
||||||
|
@ -103,7 +103,7 @@ export async function migrateMasterPassword() {
|
|||||||
// previously any master key could be encrypted with any password, so to support
|
// 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.
|
// 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.
|
// If not, try with the master key specific password, if any is defined.
|
||||||
export async function findMasterKeyPassword(service: EncryptionService, masterKey: MasterKeyEntity): Promise<string> {
|
export async function findMasterKeyPassword(service: EncryptionService, masterKey: MasterKeyEntity, passwordCache: Record<string, string> = null): Promise<string> {
|
||||||
const masterPassword = Setting.value('encryption.masterPassword');
|
const masterPassword = Setting.value('encryption.masterPassword');
|
||||||
if (masterPassword && await service.checkMasterKeyPassword(masterKey, masterPassword)) {
|
if (masterPassword && await service.checkMasterKeyPassword(masterKey, masterPassword)) {
|
||||||
logger.info('findMasterKeyPassword: Using master password');
|
logger.info('findMasterKeyPassword: Using master password');
|
||||||
@ -112,7 +112,7 @@ export async function findMasterKeyPassword(service: EncryptionService, masterKe
|
|||||||
|
|
||||||
logger.info('findMasterKeyPassword: No master password is defined - trying to get master key specific password');
|
logger.info('findMasterKeyPassword: No master password is defined - trying to get master key specific password');
|
||||||
|
|
||||||
const passwords = Setting.value('encryption.passwordCache');
|
const passwords = passwordCache ? passwordCache : Setting.value('encryption.passwordCache');
|
||||||
return passwords[masterKey.id];
|
return passwords[masterKey.id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,22 +1,26 @@
|
|||||||
|
import * as React from 'react';
|
||||||
import { NoteEntity, ResourceEntity } from './services/database/types';
|
import { NoteEntity, ResourceEntity } from './services/database/types';
|
||||||
|
|
||||||
let isTestingEnv_ = false;
|
let isTestingEnv_ = false;
|
||||||
|
|
||||||
// We need to ensure that there's only one instance of React being used by
|
// We need to ensure that there's only one instance of React being used by all
|
||||||
// all the packages. In particular, the lib might need React to define
|
// the packages. In particular, the lib might need React to define generic
|
||||||
// generic hooks, but it shouldn't have React in its dependencies as that
|
// hooks, but it shouldn't have React in its dependencies as that would cause
|
||||||
// would cause the following error:
|
// the following error:
|
||||||
//
|
//
|
||||||
// https://reactjs.org/warnings/invalid-hook-call-warning.html#duplicate-react
|
// https://reactjs.org/warnings/invalid-hook-call-warning.html#duplicate-react
|
||||||
//
|
//
|
||||||
// So instead, the **applications** include React as a dependency, then
|
// So instead, the **applications** include React as a dependency, then pass it
|
||||||
// pass it to any other packages using the shim. Essentially, only one
|
// to any other packages using the shim. Essentially, only one package should
|
||||||
// package should require React, and in our case that should be one of the
|
// require React, and in our case that should be one of the applications
|
||||||
// applications (app-desktop, app-mobile, etc.) since we are sure they
|
// (app-desktop, app-mobile, etc.) since we are sure they won't be dependency to
|
||||||
// won't be dependency to other packages (unlike the lib which can be
|
// other packages (unlike the lib which can be included anywhere).
|
||||||
// included anywhere).
|
//
|
||||||
|
// Regarding the type - althought we import React, we only use it as a type
|
||||||
let react_: any = null;
|
// using `typeof React`. This is just to get types in hooks.
|
||||||
|
//
|
||||||
|
// https://stackoverflow.com/a/42816077/561309
|
||||||
|
let react_: typeof React = null;
|
||||||
|
|
||||||
const shim = {
|
const shim = {
|
||||||
Geolocation: null as any,
|
Geolocation: null as any,
|
||||||
|
Loading…
Reference in New Issue
Block a user