一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
637 行
22 KiB
TypeScript
637 行
22 KiB
TypeScript
import * as React from '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, { MessageBoxType } from '@joplin/lib/shim';
|
|
import dialogs from '../dialogs';
|
|
import { decryptedStatText, determineKeyPassword, dontReencryptData, enableEncryptionConfirmationMessages, onSavePasswordClick, onToggleEnabledClick, reencryptData, upgradeMasterKey, useHasUnknownEncryptionMethodKeysMessage, useInputPasswords, useNeedMasterPassword, usePasswordChecker, useStats, useToggleShowDisabledMasterKeys } from '@joplin/lib/components/EncryptionConfigScreen/utils';
|
|
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
|
|
import { getEncryptionEnabled, localSyncInfoSelector, masterKeyEnabled } from '@joplin/lib/services/synchronizer/syncInfoUtils';
|
|
import { getDefaultMasterKey, getMasterPasswordStatusMessage, isKnownEncryptionMethod, masterPasswordIsValid, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
|
|
import Button, { ButtonLevel } from '../Button/Button';
|
|
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
import { connect } from 'react-redux';
|
|
import { AppState, AppStateDialogName } from '../../app.reducer';
|
|
import Setting from '@joplin/lib/models/Setting';
|
|
import CommandService from '@joplin/lib/services/CommandService';
|
|
import { PublicPrivateKeyPair } from '@joplin/lib/services/e2ee/ppk/ppk';
|
|
import ToggleAdvancedSettingsButton from '../ConfigScreen/controls/ToggleAdvancedSettingsButton';
|
|
import MacOSMissingPasswordHelpLink from '../ConfigScreen/controls/MissingPasswordHelpLink';
|
|
import { Dispatch } from 'redux';
|
|
import { shouldCancelPendingEnableAfterMasterPasswordDialog, shouldOpenMasterPasswordDialogForEnable, shouldResumeEnableAfterMasterPasswordDialog } from './enableFlow';
|
|
import Dialog from '@joplin/lib/components/Dialog';
|
|
import DialogButtonRow from '../DialogButtonRow';
|
|
import DialogTitle from '../DialogTitle';
|
|
import PasswordInput from '../PasswordInput/PasswordInput';
|
|
|
|
interface Props {
|
|
themeId: number;
|
|
dispatch: Dispatch;
|
|
masterKeys: MasterKeyEntity[];
|
|
passwords: Record<string, string>;
|
|
notLoadedMasterKeys: string[];
|
|
encryptionEnabled: boolean;
|
|
shouldReencrypt: boolean;
|
|
activeMasterKeyId: string;
|
|
masterPassword: string;
|
|
ppk: PublicPrivateKeyPair;
|
|
masterPasswordDialogOpen: boolean;
|
|
}
|
|
|
|
interface EncryptionDialogOptions{
|
|
className: string;
|
|
title: string;
|
|
content: React.ReactNode;
|
|
onClose: ()=> void;
|
|
onDialogButtonRowClick: (event: { buttonName: string })=> void;
|
|
okButtonDisabled?: boolean;
|
|
}
|
|
|
|
export const EncryptionConfigScreen = (props: Props) => {
|
|
const { inputPasswords, onInputPasswordChange } = useInputPasswords(props.passwords);
|
|
const [pendingEnableEncryption, setPendingEnableEncryption] = useState(false);
|
|
const [enableEncryptionPromptVisible, setEnableEncryptionPromptVisible] = useState(false);
|
|
const [enableEncryptionPassword, setEnableEncryptionPassword] = useState('');
|
|
const [enableEncryptionError, setEnableEncryptionError] = useState('');
|
|
const [disableEncryptionPromptVisible, setDisableEncryptionPromptVisible] = useState(false);
|
|
const disablePromptPromiseRef = useRef<(value: boolean)=> void>(null);
|
|
const promptPromiseRef = useRef<(password: string | null)=> void>(null);
|
|
|
|
// Cleanup on unmount to resolve pending promises if the user navigates away
|
|
useEffect(() => {
|
|
return () => {
|
|
if (disablePromptPromiseRef.current) disablePromptPromiseRef.current(false);
|
|
if (promptPromiseRef.current) promptPromiseRef.current(null);
|
|
};
|
|
}, []);
|
|
|
|
const wasMasterPasswordDialogOpen = useRef(props.masterPasswordDialogOpen);
|
|
|
|
const theme = useMemo(() => {
|
|
return themeStyle(props.themeId);
|
|
}, [props.themeId]);
|
|
|
|
const stats = useStats();
|
|
const { passwordChecks, masterPasswordKeys, masterPasswordStatus } = usePasswordChecker(props.masterKeys, props.activeMasterKeyId, props.masterPassword, props.passwords);
|
|
const { showDisabledMasterKeys, toggleShowDisabledMasterKeys } = useToggleShowDisabledMasterKeys();
|
|
const needMasterPassword = useNeedMasterPassword(passwordChecks, props.masterKeys);
|
|
const unknownEncryptionMethodMessage = useHasUnknownEncryptionMethodKeysMessage(props.masterKeys);
|
|
|
|
useEffect(() => {
|
|
const wasOpen = wasMasterPasswordDialogOpen.current;
|
|
wasMasterPasswordDialogOpen.current = props.masterPasswordDialogOpen;
|
|
|
|
if (shouldCancelPendingEnableAfterMasterPasswordDialog({
|
|
pendingEnableEncryption,
|
|
wasMasterPasswordDialogOpen: wasOpen,
|
|
masterPasswordDialogOpen: props.masterPasswordDialogOpen,
|
|
masterPassword: props.masterPassword,
|
|
})) {
|
|
setPendingEnableEncryption(false);
|
|
return;
|
|
}
|
|
|
|
if (!shouldResumeEnableAfterMasterPasswordDialog({
|
|
pendingEnableEncryption,
|
|
wasMasterPasswordDialogOpen: wasOpen,
|
|
masterPasswordDialogOpen: props.masterPasswordDialogOpen,
|
|
masterPassword: props.masterPassword,
|
|
})) return;
|
|
|
|
const masterKey = getDefaultMasterKey();
|
|
|
|
void (async () => {
|
|
try {
|
|
await toggleAndSetupEncryption(EncryptionService.instance(), true, masterKey, props.masterPassword);
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
await dialogs.alert(message);
|
|
} finally {
|
|
setPendingEnableEncryption(false);
|
|
}
|
|
})();
|
|
}, [pendingEnableEncryption, props.masterPasswordDialogOpen, props.masterPassword]);
|
|
|
|
const onUpgradeMasterKey = useCallback(async (mk: MasterKeyEntity) => {
|
|
const password = determineKeyPassword(mk.id, masterPasswordKeys, props.masterPassword, props.passwords);
|
|
const result = await upgradeMasterKey(mk, password);
|
|
await shim.showMessageBox(result, { type: MessageBoxType.Info });
|
|
}, [props.passwords, masterPasswordKeys, props.masterPassword]);
|
|
|
|
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>
|
|
<h2>{_('Keys that need upgrading')}</h2>
|
|
<p>{_('The following keys use an out-dated encryption algorithm and it is recommended to upgrade them. The upgraded 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 renderMasterKey = (mk: MasterKeyEntity) => {
|
|
const theme = themeStyle(props.themeId);
|
|
|
|
const passwordStyle = {
|
|
color: theme.color,
|
|
backgroundColor: theme.backgroundColor,
|
|
border: '1px solid',
|
|
borderColor: theme.dividerColor,
|
|
};
|
|
|
|
const missingPasswordCellStyle = {
|
|
...theme.textStyle,
|
|
border: '3px solid',
|
|
borderColor: theme.colorError,
|
|
};
|
|
|
|
const password = inputPasswords[mk.id] ? inputPasswords[mk.id] : '';
|
|
const isActive = props.activeMasterKeyId === mk.id;
|
|
const activeIcon = isActive ? '✔' : '';
|
|
const knownKeyType = isKnownEncryptionMethod(mk.encryption_method);
|
|
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={passwordChecks[masterKeyId] ? theme.textStyle : missingPasswordCellStyle}>
|
|
<input
|
|
type="password"
|
|
placeholder={_('Enter password')}
|
|
style={passwordStyle}
|
|
value={password}
|
|
onChange={event => onInputPasswordChange(mk, event.target.value)}
|
|
/>
|
|
{' '}
|
|
<button style={theme.buttonStyle} onClick={() => onSavePasswordClick(mk, { ...props.passwords, ...inputPasswords })}>
|
|
{_('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}
|
|
>{knownKeyType ? passwordOk : <span title={_('Unknown key format')}>❓</span>}</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 ? <h2>{_('Encryption keys')}</h2> : <a onClick={() => toggleShowDisabledMasterKeys() } style={{ ...theme.urlStyle, display: 'inline-block', marginBottom: 10 }} href="#">{showTable ? _('Hide disabled keys') : _('Show disabled keys')}</a>;
|
|
const infoComp: React.ReactNode = null; // isEnabledMasterKeys ? <p>{'Note: Only one 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 className="section">
|
|
{headerComp}
|
|
{tableComp}
|
|
{infoComp}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
const onToggleButtonClick = useCallback(async () => {
|
|
const isEnabled = getEncryptionEnabled();
|
|
const newEnabled = !isEnabled;
|
|
const masterKey = getDefaultMasterKey();
|
|
const hasMasterPassword = !!props.masterPassword;
|
|
let newPassword: string | null = '';
|
|
|
|
if (isEnabled) {
|
|
setDisableEncryptionPromptVisible(true);
|
|
const answer = await new Promise<boolean>((resolve) => {
|
|
disablePromptPromiseRef.current = resolve;
|
|
});
|
|
if (!answer) return;
|
|
} else {
|
|
if (shouldOpenMasterPasswordDialogForEnable({
|
|
hasMasterPassword,
|
|
masterPasswordDialogOpen: props.masterPasswordDialogOpen,
|
|
})) {
|
|
setPendingEnableEncryption(true);
|
|
props.dispatch({
|
|
type: 'DIALOG_OPEN',
|
|
name: AppStateDialogName.MasterPassword,
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Wait for the custom React Dialog to resolve
|
|
setEnableEncryptionPassword('');
|
|
setEnableEncryptionError('');
|
|
setEnableEncryptionPromptVisible(true);
|
|
newPassword = await new Promise<string | null>((resolve) => {
|
|
promptPromiseRef.current = resolve;
|
|
});
|
|
|
|
if (newPassword === null) return; // User cancelled
|
|
}
|
|
|
|
try {
|
|
await toggleAndSetupEncryption(EncryptionService.instance(), newEnabled, masterKey, newPassword);
|
|
} catch (error) {
|
|
await dialogs.alert(error.message);
|
|
}
|
|
}, [props.dispatch, props.masterPassword, props.masterPasswordDialogOpen]);
|
|
|
|
const renderEncryptionDialog = (options: EncryptionDialogOptions) => {
|
|
return (
|
|
<Dialog onCancel={options.onClose} className={options.className}>
|
|
<div className='dialog-root'>
|
|
<DialogTitle title={options.title}/>
|
|
<div className='dialog-content'>
|
|
{options.content}
|
|
</div>
|
|
<DialogButtonRow
|
|
themeId={props.themeId}
|
|
onClick={options.onDialogButtonRowClick}
|
|
okButtonDisabled={options.okButtonDisabled ?? false}
|
|
/>
|
|
</div>
|
|
</Dialog>
|
|
);
|
|
};
|
|
|
|
const renderEnableEncryptionDialog = () => {
|
|
if (!enableEncryptionPromptVisible) return null;
|
|
|
|
const masterKey = getDefaultMasterKey();
|
|
const hasMasterPassword = !!props.masterPassword;
|
|
|
|
const msg = enableEncryptionConfirmationMessages(masterKey, hasMasterPassword);
|
|
const messageComps = msg.map((m, index) => <p key={index} style={theme.textStyle}>{m}</p>);
|
|
|
|
const onClose = () => {
|
|
setEnableEncryptionPromptVisible(false);
|
|
if (promptPromiseRef.current) promptPromiseRef.current(null);
|
|
};
|
|
|
|
const onDialogButtonRowClick = async (event: { buttonName: string }) => {
|
|
if (event.buttonName === 'cancel') {
|
|
onClose();
|
|
return;
|
|
}
|
|
if (event.buttonName === 'ok') {
|
|
if (hasMasterPassword) {
|
|
if (!(await masterPasswordIsValid(enableEncryptionPassword))) {
|
|
setEnableEncryptionError(_('Invalid password. Please try again. If you have forgotten your password you will need to reset it.'));
|
|
return;
|
|
}
|
|
}
|
|
setEnableEncryptionError('');
|
|
setEnableEncryptionPromptVisible(false);
|
|
if (promptPromiseRef.current) promptPromiseRef.current(enableEncryptionPassword);
|
|
}
|
|
};
|
|
|
|
const onPasswordInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
setEnableEncryptionError('');
|
|
setEnableEncryptionPassword(event.target.value);
|
|
};
|
|
|
|
return renderEncryptionDialog({
|
|
className: 'enable-encryption-dialog',
|
|
title: _('Enable encryption'),
|
|
content: (
|
|
<>
|
|
<div style={{ marginBottom: 16 }}>
|
|
{messageComps}
|
|
</div>
|
|
<div style={{ marginBottom: 16 }}>
|
|
<label style={{ ...theme.textStyle, marginBottom: 5, display: 'block' }} htmlFor="enable-encryption-password">{_('Password:')}</label>
|
|
<PasswordInput
|
|
inputId="enable-encryption-password"
|
|
value={enableEncryptionPassword}
|
|
onChange={onPasswordInputChange}
|
|
/>
|
|
</div>
|
|
{enableEncryptionError && (
|
|
<div style={{ ...theme.textStyle, color: theme.colorError, marginTop: 10, marginBottom: 10 }}>
|
|
{enableEncryptionError}
|
|
</div>
|
|
)}
|
|
</>
|
|
),
|
|
onClose,
|
|
onDialogButtonRowClick,
|
|
okButtonDisabled: !enableEncryptionPassword,
|
|
});
|
|
};
|
|
|
|
const renderDisableEncryptionDialog = () => {
|
|
if (!disableEncryptionPromptVisible) return null;
|
|
|
|
const onClose = () => {
|
|
setDisableEncryptionPromptVisible(false);
|
|
if (disablePromptPromiseRef.current) disablePromptPromiseRef.current(false);
|
|
};
|
|
|
|
const onDialogButtonRowClick = (event: { buttonName: string }) => {
|
|
if (event.buttonName === 'cancel') {
|
|
onClose();
|
|
return;
|
|
}
|
|
|
|
if (event.buttonName === 'ok') {
|
|
setDisableEncryptionPromptVisible(false);
|
|
if (disablePromptPromiseRef.current) disablePromptPromiseRef.current(true);
|
|
}
|
|
};
|
|
|
|
return renderEncryptionDialog({
|
|
className: 'disable-encryption-dialog',
|
|
title: _('Disable encryption'),
|
|
content: (
|
|
<div style={{ marginBottom: 16 }}>
|
|
<p style={theme.textStyle}>
|
|
{_('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?')}
|
|
</p>
|
|
</div>
|
|
),
|
|
onClose,
|
|
onDialogButtonRowClick,
|
|
});
|
|
};
|
|
|
|
const renderEncryptionSection = () => {
|
|
const decryptedItemsInfo = <p>{decryptedStatText(stats)}</p>;
|
|
const toggleButton = (
|
|
<Button
|
|
onClick={onToggleButtonClick}
|
|
title={props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')}
|
|
level={ButtonLevel.Secondary}
|
|
/>
|
|
);
|
|
const needUpgradeSection = renderNeedUpgradeSection();
|
|
|
|
return (
|
|
<div className="section">
|
|
<div className="encryption-section">
|
|
<h2 className="-no-top-margin">{_('End-to-end encryption')}</h2>
|
|
<p>
|
|
{_('Encryption:')} <strong>{props.encryptionEnabled ? _('Enabled') : _('Disabled')}</strong>
|
|
</p>
|
|
<p>
|
|
{_('Public-private key pair:')} <strong>{props.ppk ? _('Generated') : _('Not generated')}</strong>
|
|
</p>
|
|
{decryptedItemsInfo}
|
|
{toggleButton}
|
|
{needUpgradeSection}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const renderMasterPasswordSection = () => {
|
|
const onManageMasterPassword = async () => {
|
|
void CommandService.instance().execute('openMasterPasswordDialog');
|
|
};
|
|
|
|
const buttonTitle = CommandService.instance().label('openMasterPasswordDialog');
|
|
|
|
const needPasswordMessage = !needMasterPassword ? null : (
|
|
<p className="warning">
|
|
{_('Your password is needed to decrypt some of your data.')}
|
|
<br/>
|
|
{_('Please click on "%s" to proceed, or set the passwords in the "%s" list below.', buttonTitle, _('Encryption keys'))}
|
|
<br/>
|
|
<MacOSMissingPasswordHelpLink
|
|
theme={theme}
|
|
text={_('%s: Missing password.', _('Help'))}
|
|
/>
|
|
</p>
|
|
);
|
|
|
|
const needUpgradeAppMessage = !!unknownEncryptionMethodMessage && (
|
|
<p className='warning'>
|
|
{unknownEncryptionMethodMessage}
|
|
</p>
|
|
);
|
|
|
|
return (
|
|
<div className="section">
|
|
<div className="manage-password-section">
|
|
<h2>{_('Master password')}</h2>
|
|
<p className="status"><span>{_('Master password:')}</span> <span className="bold">{getMasterPasswordStatusMessage(masterPasswordStatus)}</span></p>
|
|
{needPasswordMessage}
|
|
{needUpgradeAppMessage}
|
|
<Button className="managebutton" level={needMasterPassword ? ButtonLevel.Primary : ButtonLevel.Secondary} onClick={onManageMasterPassword} title={buttonTitle} />
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const onClearMasterPassword = useCallback(() => {
|
|
Setting.setValue('encryption.masterPassword', '');
|
|
}, []);
|
|
|
|
const renderDebugSection = () => {
|
|
if (Setting.value('env') !== 'dev') return null;
|
|
|
|
return (
|
|
<div style={{ paddingBottom: '20px' }}>
|
|
<Button level={ButtonLevel.Secondary} onClick={onClearMasterPassword} title="Clear master password" />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const renderNonExistingMasterKeysSection = () => {
|
|
let nonExistingMasterKeySection = null;
|
|
|
|
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);
|
|
}
|
|
|
|
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 className="section">
|
|
<h2>{_('Missing keys')}</h2>
|
|
<p>{_('The 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 nonExistingMasterKeySection;
|
|
};
|
|
|
|
const renderReencryptData = () => {
|
|
if (!shim.isElectron()) return null;
|
|
if (!props.encryptionEnabled) 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 (
|
|
<>
|
|
<h2>{_('Re-encryption')}</h2>
|
|
<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> }
|
|
</>
|
|
);
|
|
};
|
|
|
|
// If the user should re-encrypt, ensure that the section is visible initially.
|
|
const [showAdvanced, setShowAdvanced] = useState<boolean>(props.shouldReencrypt);
|
|
const toggleAdvanced = useCallback(() => {
|
|
setShowAdvanced(!showAdvanced);
|
|
}, [showAdvanced]);
|
|
|
|
const advancedSettingsId = useId();
|
|
const renderAdvancedSection = () => {
|
|
const reEncryptSection = renderReencryptData();
|
|
|
|
if (!reEncryptSection) return null;
|
|
|
|
|
|
return (
|
|
<div>
|
|
<ToggleAdvancedSettingsButton
|
|
onClick={toggleAdvanced}
|
|
advancedSettingsVisible={showAdvanced}
|
|
aria-controls={advancedSettingsId}
|
|
/>
|
|
<div id={advancedSettingsId}>
|
|
{ showAdvanced ? reEncryptSection : null }
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="config-screen-content">
|
|
{renderDebugSection()}
|
|
{renderEncryptionSection()}
|
|
{renderMasterPasswordSection()}
|
|
{renderMasterKeySection(props.masterKeys.filter(mk => masterKeyEnabled(mk)), true)}
|
|
{renderMasterKeySection(props.masterKeys.filter(mk => !masterKeyEnabled(mk)), false)}
|
|
{renderNonExistingMasterKeysSection()}
|
|
{renderAdvancedSection()}
|
|
{renderEnableEncryptionDialog()}
|
|
{renderDisableEncryptionDialog()}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const mapStateToProps = (state: AppState) => {
|
|
const syncInfo = localSyncInfoSelector(state);
|
|
|
|
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'],
|
|
ppk: syncInfo.ppk,
|
|
masterPasswordDialogOpen: !!state.dialogs.find(dialog => dialog.name === AppStateDialogName.MasterPassword),
|
|
};
|
|
};
|
|
|
|
export default connect(mapStateToProps)(EncryptionConfigScreen);
|