import * as React from 'react'; import { TextInput, TouchableOpacity, Linking, View, StyleSheet, Text, Button, ScrollView, TextStyle } 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, useHasUnknownEncryptionMethodKeysMessage, 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 { localSyncInfoSelector, masterKeyEnabled } from '@joplin/lib/services/synchronizer/syncInfoUtils'; import { getDefaultMasterKey, getMasterPasswordStatusMessage, isKnownEncryptionMethod, 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'; const allMasterPasswordKeysStoredInUnknownFormat = (keyIds: string[], allKeys: MasterKeyEntity[]) => { return keyIds.length > 0 && keyIds.every(id => { const masterKey = allKeys.find(key => key.id === id); return !!masterKey && !isKnownEncryptionMethod(masterKey.encryption_method); }); }; 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, masterPasswordStatus } = 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, }, warningText: { ...theme.normalText, color: theme.color, }, warningTextBold: { fontWeight: 'bold', }, warningBox: { marginBottom: theme.itemMarginBottom, backgroundColor: theme.warningBackgroundColor, paddingTop: 5, paddingBottom: 5, paddingLeft: 10, paddingRight: 10, }, masterPasswordLabel: { ...theme.normalText, flex: 0, flexBasis: 'auto', marginRight: 5, }, normalTextInput: { margin: 10, color: theme.color, borderWidth: 1, borderColor: theme.dividerColor, }, container: { flex: 1, flexBasis: 'auto', padding: theme.margin, }, disabledContainer: { paddingLeft: theme.margin, paddingRight: theme.margin, }, unknownKeyFormatMessage: { paddingTop: 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 ? '✔' : '❌'; const inputStyle: TextStyle = { flex: 1, marginRight: 10, color: theme.color, borderBottomWidth: 1, 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))} {!isKnownEncryptionMethod(mk.encryption_method) && ( {_('Status: Unsupported key format')} )} {_('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')); if (!props.masterKeys.length) { const password2 = passwordPromptConfirmAnswer; if (!password2) throw new Error(_('Confirm password cannot be empty')); if (password !== password2) throw new Error(_('Passwords do not match!')); } else if (!masterKey) { // If a different master password is entered in this scenario, it would be accepted, but would create a new key which won't be able to be decrypted throw new Error(_('Encryption was setup previously, but all keys have been disabled. To continue, please use the desktop app to enable a key.')); } 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); }} > {!props.masterKeys.length && ( <> {_('Confirm password:')} { setPasswordPromptConfirmAnswer(text); }} > )} ); }; const renderMasterPassword = () => { if (!props.encryptionEnabled && !props.masterKeys.length) return null; const inputStyle: TextStyle = { flex: 1, marginRight: 10, color: theme.color, borderBottomWidth: 1, borderBottomColor: theme.dividerColor, }; if ( passwordChecks['master'] || allMasterPasswordKeysStoredInUnknownFormat( Object.keys(masterPasswordKeys), props.masterKeys, ) ) { return ( {_('Master password:')} {getMasterPasswordStatusMessage(masterPasswordStatus)} ); } 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; const unknownKeyFormatText = useHasUnknownEncryptionMethodKeysMessage(props.masterKeys); const unknownKeyFormatMessage = !!unknownKeyFormatText && {_('Warning: ')}{unknownKeyFormatText} ; 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 {unknownKeyFormatMessage} {_('Status')} {_('Encryption is: %s', props.encryptionEnabled ? _('Enabled') : _('Disabled'))} {decryptedItemsInfo} {renderMasterPassword()} {toggleButton} {passwordPromptComp} {mkComps} {nonExistingMasterKeySection} {disabledMasterKeyList} ); }; export default connect((state: State) => { const syncInfo = localSyncInfoSelector(state); 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);