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 { decryptedStatText, determineKeyPassword, dontReencryptData, enableEncryptionConfirmationMessages, onSavePasswordClick, onToggleEnabledClick, reencryptData, upgradeMasterKey, useInputPasswords, useNeedMasterPassword, 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, getMasterPasswordStatusMessage, masterPasswordIsValid, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils'; import Button, { ButtonLevel } from '../Button/Button'; import { useCallback, useMemo, useState } from 'react'; import { connect } from 'react-redux'; import { AppState } 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'; import ToggleAdvancedSettingsButton from '../ConfigScreen/controls/ToggleAdvancedSettingsButton'; import MacOSMissingPasswordHelpLink from '../ConfigScreen/controls/MissingPasswordHelpLink'; interface Props { themeId: any; masterKeys: MasterKeyEntity[]; passwords: Record; notLoadedMasterKeys: string[]; encryptionEnabled: boolean; shouldReencrypt: boolean; activeMasterKeyId: string; masterPassword: string; ppk: PublicPrivateKeyPair; } 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, masterPasswordStatus } = usePasswordChecker(props.masterKeys, props.activeMasterKeyId, props.masterPassword, props.passwords); const { showDisabledMasterKeys, toggleShowDisabledMasterKeys } = useToggleShowDisabledMasterKeys(); const needMasterPassword = useNeedMasterPassword(passwordChecks, props.masterKeys); const onUpgradeMasterKey = useCallback(async (mk: MasterKeyEntity) => { const password = determineKeyPassword(mk.id, masterPasswordKeys, props.masterPassword, props.passwords); const result = await upgradeMasterKey(mk, password); alert(result); }, [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( {mk.id} , ); } return (

{_('Keys that need upgrading')}

{_('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.')}

{rows}
{_('ID')} {_('Upgrade')}
); }; 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 passwordOk = passwordChecks[mk.id] === true ? '✔' : '❌'; const renderPasswordInput = (masterKeyId: string) => { if (masterPasswordKeys[masterKeyId] || !passwordChecks['master']) { return ( ({_('Master password')}) ); } else { return ( onInputPasswordChange(mk, event.target.value)} /> {' '} ); } }; return ( {activeIcon} {mk.id}
{_('Source: ')}{mk.source_application} {_('Created: ')}{time.formatMsToLocal(mk.created_time)}
{_('Updated: ')}{time.formatMsToLocal(mk.updated_time)} {renderPasswordInput(mk.id)} {passwordOk} ); }; 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 ?

{_('Encryption keys')}

: toggleShowDisabledMasterKeys() } style={{ ...theme.urlStyle, display: 'inline-block', marginBottom: 10 }} href="#">{showTable ? _('Hide disabled keys') : _('Show disabled keys')}; const infoComp: any = null; // isEnabledMasterKeys ?

{'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.'}

: null; const tableComp = !showTable ? null : ( {mkComps}
{_('Active')} {_('ID')} {_('Date')} {_('Password')} {_('Valid')} {_('Actions')}
); if (mkComps.length) { return (
{headerComp} {tableComp} {infoComp}
); } return null; }; const onToggleButtonClick = useCallback(async () => { const isEnabled = getEncryptionEnabled(); const newEnabled = !isEnabled; const masterKey = getDefaultMasterKey(); const hasMasterPassword = !!props.masterPassword; let newPassword = ''; if (isEnabled) { const 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?')); if (!answer) return; } else { const msg = enableEncryptionConfirmationMessages(masterKey, hasMasterPassword); newPassword = await dialogs.prompt(msg.join('\n\n'), '', '', { type: 'password' }); } if (hasMasterPassword && newEnabled) { if (!(await masterPasswordIsValid(newPassword))) { alert('Invalid password. Please try again. If you have forgotten your password you will need to reset it.'); return; } } try { await toggleAndSetupEncryption(EncryptionService.instance(), newEnabled, masterKey, newPassword); } catch (error) { await dialogs.alert(error.message); } }, [props.masterPassword]); const renderEncryptionSection = () => { const decryptedItemsInfo =

{decryptedStatText(stats)}

; const toggleButton = ( { !props.shouldReencrypt ? null : } ); }; // If the user should re-encrypt, ensure that the section is visible initially. const [showAdvanced, setShowAdvanced] = useState(props.shouldReencrypt); const toggleAdvanced = useCallback(() => { setShowAdvanced(!showAdvanced); }, [showAdvanced]); const renderAdvancedSection = () => { const reEncryptSection = renderReencryptData(); if (!reEncryptSection) return null; return (
{ showAdvanced ? reEncryptSection : null }
); }; return (
{renderDebugSection()} {renderEncryptionSection()} {renderMasterPasswordSection()} {renderMasterKeySection(props.masterKeys.filter(mk => masterKeyEnabled(mk)), true)} {renderMasterKeySection(props.masterKeys.filter(mk => !masterKeyEnabled(mk)), false)} {renderNonExistingMasterKeysSection()} {renderAdvancedSection()}
); }; 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'], ppk: syncInfo.ppk, }; }; export default connect(mapStateToProps)(EncryptionConfigScreen);