import * as React from 'react'; import { TextInput, TouchableOpacity, Linking, View, StyleSheet, Text, Button, ScrollView } from 'react-native'; import { connect } from 'react-redux'; import ScreenHeader from '../ScreenHeader'; import { themeStyle } from '../global-style'; import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService'; import { _ } from '@joplin/lib/locale'; import time from '@joplin/lib/time'; import { decryptedStatText, enableEncryptionConfirmationMessages, onSavePasswordClick, useInputMasterPassword, useInputPasswords, usePasswordChecker, useStats } from '@joplin/lib/components/EncryptionConfigScreen/utils'; import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types'; import { State } from '@joplin/lib/reducer'; import { masterKeyEnabled, SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils'; import { getDefaultMasterKey, setupAndDisableEncryption, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils'; import { useMemo, useState } from 'react'; import { Divider, List } from 'react-native-paper'; import shim from '@joplin/lib/shim'; interface Props { themeId: number; masterKeys: MasterKeyEntity[]; passwords: Record; notLoadedMasterKeys: string[]; encryptionEnabled: boolean; activeMasterKeyId: string; masterPassword: string; } const EncryptionConfigScreen = (props: Props) => { const [passwordPromptShow, setPasswordPromptShow] = useState(false); 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 [showDisabledKeys, setShowDisabledKeys] = useState(false); const mkComps = []; const disabledMkComps = []; const nonExistingMasterKeyIds = props.notLoadedMasterKeys.slice(); const theme = useMemo(() => { return themeStyle(props.themeId); }, [props.themeId]); const rootStyle = useMemo(() => { return { flex: 1, backgroundColor: theme.backgroundColor, }; }, [theme]); const styles = useMemo(() => { return StyleSheet.create({ titleText: { flex: 1, fontWeight: 'bold', flexDirection: 'column', fontSize: theme.fontSize, paddingTop: 5, paddingBottom: 5, marginTop: theme.marginTop, marginBottom: 5, color: theme.color, }, normalText: { flex: 1, fontSize: theme.fontSize, color: theme.color, }, normalTextInput: { margin: 10, color: theme.color, borderWidth: 1, borderColor: theme.dividerColor, }, container: { flex: 1, padding: theme.margin, }, disabledContainer: { paddingLeft: theme.margin, paddingRight: theme.margin, }, }); }, [theme]); const decryptedItemsInfo = props.encryptionEnabled ? {decryptedStatText(stats)} : null; const renderMasterKey = (mk: MasterKeyEntity) => { const theme = themeStyle(props.themeId); const password = inputPasswords[mk.id] ? inputPasswords[mk.id] : ''; const passwordOk = passwordChecks[mk.id] === true; const passwordOkIcon = passwordOk ? '✔' : '❌'; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied const inputStyle: any = { flex: 1, marginRight: 10, color: theme.color }; inputStyle.borderBottomWidth = 1; inputStyle.borderBottomColor = theme.dividerColor; const renderPasswordInput = (masterKeyId: string) => { if (masterPasswordKeys[masterKeyId] || !passwordChecks['master']) { return ( ({_('Master password')}) ); } else { return ( onInputPasswordChange(mk, text)} style={inputStyle} /> {passwordOkIcon} ); } }; return ( {_('Master Key %s', mk.id.substr(0, 6))} {_('Created: %s', time.formatMsToLocal(mk.created_time))} {_('Password:')} {renderPasswordInput(mk.id)} ); }; const renderPasswordPrompt = () => { const theme = themeStyle(props.themeId); const masterKey = getDefaultMasterKey(); const hasMasterPassword = !!props.masterPassword; const onEnableClick = async () => { try { const password = passwordPromptAnswer; if (!password) throw new Error(_('Password cannot be empty')); const password2 = passwordPromptConfirmAnswer; if (!password2) throw new Error(_('Confirm password cannot be empty')); if (password !== password2) throw new Error(_('Passwords do not match!')); await toggleAndSetupEncryption(EncryptionService.instance(), true, masterKey, password); // await generateMasterKeyAndEnableEncryption(EncryptionService.instance(), password); setPasswordPromptShow(false); } catch (error) { alert(error.message); } }; const messages = enableEncryptionConfirmationMessages(masterKey, hasMasterPassword); const messageComps = messages.map((msg: string) => { return {msg}; }); const passwordLabelId = 'password-label'; const confirmPasswordLabelId = 'confirm-password'; return ( {messageComps} {_('Password:')} { setPasswordPromptAnswer(text); }} > {_('Confirm password:')} { setPasswordPromptConfirmAnswer(text); }} > ); }; const renderMasterPassword = () => { if (!props.encryptionEnabled && !props.masterKeys.length) return null; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied const inputStyle: any = { flex: 1, marginRight: 10, color: theme.color }; inputStyle.borderBottomWidth = 1; inputStyle.borderBottomColor = theme.dividerColor; if (passwordChecks['master']) { return ( {_('Master password:')} {_('Loaded')} ); } else { const labelId = 'master-password-label'; return ( {'The master password is not set or is invalid. Please type it below:'} onMasterPasswordChange(text)} style={inputStyle} /> ) : null; const disabledMasterKeyList = disabledMkComps.length ? setShowDisabledKeys(st => !st)} > {disabledMkComps} : null; return ( {_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')} { void Linking.openURL('https://joplinapp.org/help/apps/sync/e2ee'); }} accessibilityRole='link' > https://joplinapp.org/help/apps/sync/e2ee {_('Status')} {_('Encryption is: %s', props.encryptionEnabled ? _('Enabled') : _('Disabled'))} {decryptedItemsInfo} {renderMasterPassword()} {toggleButton} {passwordPromptComp} {mkComps} {nonExistingMasterKeySection} {disabledMasterKeyList} ); }; export default connect((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, notLoadedMasterKeys: state.notLoadedMasterKeys, masterPassword: state.settings['encryption.masterPassword'], }; })(EncryptionConfigScreen);