import * as React from 'react'; import { useCallback, useState } from 'react'; import { connect } from 'react-redux'; import { _ } from '@joplin/lib/locale'; import NoteLockKey from '@joplin/lib/services/noteLock/NoteLockKey'; import NoteLockSession from '@joplin/lib/services/noteLock/NoteLockSession'; import shim, { MessageBoxType } from '@joplin/lib/shim'; import { SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils'; import LabelledPasswordInput from '../../PasswordInput/LabelledPasswordInput'; import Button, { ButtonLevel } from '../../Button/Button'; import { AppState } from '../../../app.reducer'; import useNoteLockMode, { ActionMode } from './useNoteLockMode'; interface Props { hasNoteLockKey: boolean; needsNoteLockKeyUpgrade: boolean; } // WebCrypto reports a wrong password as an OperationError with an unhelpful generic message const errorMessage = (error: unknown) => { if (!(error instanceof Error)) return String(error); if (error.name === 'OperationError') return _('Invalid password'); return error.message; }; const NoteLockSettings: React.FC = props => { const hasKey = props.hasNoteLockKey; const { mode, onModeChange, currentPassword, setCurrentPassword, password, setPassword, passwordRepeat, setPasswordRepeat, error, setError, } = useNoteLockMode(hasKey); const [upgradePassword, setUpgradePassword] = useState(''); const [upgradeError, setUpgradeError] = useState(''); const [saving, setSaving] = useState(false); const passwordMismatch = !!passwordRepeat && password !== passwordRepeat; const canSave = !!password && !!passwordRepeat && !passwordMismatch && !saving && (mode !== ActionMode.Change || !!currentPassword); const canUpgrade = !!upgradePassword && !saving; const forgotPasswordDescription = _('Only do this if you\'ve forgotten your current password. A new password will be created, and any notes locked with the old one will become permanently unreadable.'); const forgotPasswordWarning = `${_('Warning:')} ${forgotPasswordDescription}`; const resetConfirmationMessage = `${forgotPasswordWarning}\n\n${_('Continue?')}`; const onCurrentPasswordChange = useCallback((event: React.ChangeEvent) => { setCurrentPassword(event.target.value); }, [setCurrentPassword]); const onPasswordChange = useCallback((event: React.ChangeEvent) => { setPassword(event.target.value); }, [setPassword]); const onPasswordRepeatChange = useCallback((event: React.ChangeEvent) => { setPasswordRepeat(event.target.value); }, [setPasswordRepeat]); const onUpgradePasswordChange = useCallback((event: React.ChangeEvent) => { setUpgradePassword(event.target.value); }, []); const submit = useCallback(async () => { if (!canSave) return; if (mode === ActionMode.Reset) { const response = await shim.showMessageBox(resetConfirmationMessage, { buttons: [_('Yes'), _('No')], type: MessageBoxType.Confirm, }); if (response !== 0) return; } setSaving(true); setError(''); try { const noteLockKey = NoteLockKey.instance(); if (mode === ActionMode.Create) { await noteLockKey.create(password); } else if (mode === ActionMode.Change) { await noteLockKey.changePassword(currentPassword, password); } else if (mode === ActionMode.Reset) { await NoteLockSession.instance().reset(password); } onModeChange(ActionMode.Collapsed); } catch (error) { setError(errorMessage(error)); } finally { setSaving(false); } }, [canSave, onModeChange, currentPassword, mode, password, setError, resetConfirmationMessage]); const submitUpgrade = useCallback(async () => { if (!canUpgrade) return; setSaving(true); setUpgradeError(''); try { await NoteLockKey.instance().upgrade(upgradePassword); setUpgradePassword(''); } catch (error) { setUpgradeError(errorMessage(error)); } finally { setSaving(false); } }, [canUpgrade, upgradePassword]); const onChangePasswordClick = useCallback(() => { onModeChange(ActionMode.Change); }, [onModeChange]); const onForgotPasswordClick = useCallback(() => { onModeChange(ActionMode.Reset); }, [onModeChange]); const onCancel = useCallback(() => { onModeChange(ActionMode.Collapsed); }, [onModeChange]); const resetPasswordTitle = `⚠️ ${_('Reset password')} ⚠️`; const actionButtonTitle = mode === ActionMode.Reset ? resetPasswordTitle : _('Save'); // The sync advice only applies to a first setup, where there may be nothing to lose yet. const passwordReminder = mode === ActionMode.Create ? _('Please make sure you remember your password. It cannot be recovered if lost, and any locked notes will become inaccessible. If you have already set up the note lock password on another device, there is no need to set it again, as the information required to access your locked notes is updated when you set up sync.') : _('Please make sure you remember your password. It cannot be recovered if lost, and any locked notes will become inaccessible.'); const getSectionTitle = () => { if (mode === ActionMode.Reset) return resetPasswordTitle; if (mode === ActionMode.Change) return _('Change note lock password'); if (mode === ActionMode.Collapsed) return _('Manage password'); return _('Password setup'); }; const renderManageButtons = () => { return (

{forgotPasswordDescription}

); }; const renderPasswordForm = () => { return (
{mode === ActionMode.Reset &&

{_('Warning:')} {forgotPasswordDescription}

} {mode === ActionMode.Change && ( )} {passwordMismatch &&

{_('Passwords do not match')}

} {error ?

{error}

: null}

{passwordReminder}

); }; const renderUpgradeSection = () => { if (!props.needsNoteLockKeyUpgrade) return null; return (

{_('Key upgrade')}

{_('For better security, your note lock protection can be upgraded to a newer method. Enter your password to upgrade it. Your notes will keep locking and unlocking as usual.')}

{upgradeError ?

{upgradeError}

: null}
); }; return (

{getSectionTitle()}

{mode === ActionMode.Collapsed ? renderManageButtons() : renderPasswordForm()}
{renderUpgradeSection()}
); }; const mapStateToProps = (state: AppState) => { return { hasNoteLockKey: !!new SyncInfo(state.settings['syncInfoCache']).noteLockKey, needsNoteLockKeyUpgrade: NoteLockKey.instance().needsUpgrade(), }; }; export default connect(mapStateToProps)(NoteLockSettings);