From fade17d0de483bd72b1bcd3fa9e5452bf1115f26 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Fri, 30 Oct 2020 15:31:04 -0300 Subject: [PATCH] [IMPROVEMENT] Add `Change Encryption Password` and `Reset E2E Key` (#2542) * init * Basic tests passing * Add SecurityPrivacyView * List.Item * section * Start removing theme as prop * Remove StatusBar theme prop * SafeAreaView theme prop * Minor fixes * List.Container * Add translateTitle and translateSubtitle props * Storybook * Show action indicator * Header * Info * Theme stories * FlatList * DisplayName * Fix settings * FlatList tweaks * ThemeView * Screen Lock Config * DefaultBrowserView * PickerView and User Prefs * Notification Prefs * StatusView * Auto Translate * InviteUsersEdit * Visitor * Minor fixes * Remove Separator * Remove iteminfo * Font scale * Legal * Jitsi and e2e * Block * search, star, etc * auto translate and notifications * RoomInfo * Refactor RoomActions * lint * Remove DisclosureIndicator * padding horizontal 12 * Detox * Tests * SecurityPrivacy * E2E encryption sec view * stash * Reset own key * Reset key * Change password * Hide content * Small refactor * Fix tests * Tests passing * Change test order * add pt-br * Address review comments * tests * Missing i18n ptbr Co-authored-by: Djorkaeff Alexandre --- app/containers/Button/index.js | 1 + app/i18n/locales/en.js | 14 ++ app/i18n/locales/pt-BR.js | 17 ++ app/lib/encryption/encryption.js | 16 ++ app/lib/rocketchat.js | 17 +- app/sagas/login.js | 4 + app/stacks/InsideStack.js | 12 ++ app/stacks/MasterDetailStack/index.js | 12 ++ app/utils/log/events.js | 16 +- app/views/E2EEncryptionSecurityView.js | 185 ++++++++++++++++ app/views/E2EEnterYourPasswordView.js | 3 +- app/views/E2ESaveYourPasswordView.js | 4 +- app/views/RoomActionsView/index.js | 2 +- .../RoomsListView/ListHeader/Encryption.js | 8 +- app/views/SecurityPrivacyView.js | 158 ++++++++++++++ app/views/SettingsView/index.js | 93 +------- e2e/tests/assorted/01-e2eencryption.spec.js | 198 ++++++++++++++++++ e2e/tests/assorted/04-setting.spec.js | 16 +- ...server.spec.js => 07-changeserver.spec.js} | 0 19 files changed, 677 insertions(+), 99 deletions(-) create mode 100644 app/views/E2EEncryptionSecurityView.js create mode 100644 app/views/SecurityPrivacyView.js create mode 100644 e2e/tests/assorted/01-e2eencryption.spec.js rename e2e/tests/assorted/{01-changeserver.spec.js => 07-changeserver.spec.js} (100%) diff --git a/app/containers/Button/index.js b/app/containers/Button/index.js index 0f573f87..ccc847b3 100644 --- a/app/containers/Button/index.js +++ b/app/containers/Button/index.js @@ -82,6 +82,7 @@ export default class Button extends React.PureComponent { { color: textColor }, fontSize && { fontSize } ]} + accessibilityLabel={title} > {title} diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js index 57a8da13..bb097d61 100644 --- a/app/i18n/locales/en.js +++ b/app/i18n/locales/en.js @@ -197,6 +197,7 @@ export default { Do_you_have_an_account: 'Do you have an account?', Do_you_have_a_certificate: 'Do you have a certificate?', Do_you_really_want_to_key_this_room_question_mark: 'Do you really want to {{key}} this room?', + E2E_Encryption: 'E2E Encryption', E2E_How_It_Works_info1: 'You can now create encrypted private groups and direct messages. You may also change existing private groups or DMs to encrypted.', E2E_How_It_Works_info2: 'This is *end to end encryption* so the key to encode/decode your messages and they will not be saved on the server. For that reason *you need to store this password somewhere safe* which you can access later if you may need.', E2E_How_It_Works_info3: 'If you proceed, it will be auto generated an E2E password.', @@ -462,6 +463,7 @@ export default { Search_global_users: 'Search for global users', Search_global_users_description: 'If you turn-on, you can search for any user from others companies or servers.', Seconds: '{{second}} seconds', + Security_and_privacy: 'Security and privacy', Select_Avatar: 'Select Avatar', Select_Server: 'Select Server', Select_Users: 'Select Users', @@ -659,6 +661,18 @@ export default { Logged_out_of_other_clients_successfully: 'Logged out of other clients successfully', Logout_failed: 'Logout failed!', Log_analytics_events: 'Log analytics events', + E2E_encryption_change_password_title: 'Change Encryption Password', + E2E_encryption_change_password_description: 'You can now create encrypted private groups and direct messages. You may also change existing private groups or DMs to encrypted. \nThis is end to end encryption so the key to encode/decode your messages will not be saved on the server. For that reason you need to store your password somewhere safe. You will be required to enter it on other devices you wish to use e2e encryption on.', + E2E_encryption_change_password_error: 'Error while changing E2E key password!', + E2E_encryption_change_password_success: 'E2E key password changed successfully!', + E2E_encryption_change_password_message: 'Make sure you\'ve saved it carefully somewhere else.', + E2E_encryption_change_password_confirmation: 'Yes, change it', + E2E_encryption_reset_title: 'Reset E2E Key', + E2E_encryption_reset_description: 'This option will remove your current E2E key and log you out. \nWhen you login again, Rocket.Chat will generate you a new key and restore your access to any encrypted room that has one or more members online. \nDue to the nature of the E2E encryption, Rocket.Chat will not be able to restore access to any encrypted room that has no member online.', + E2E_encryption_reset_button: 'Reset E2E Key', + E2E_encryption_reset_error: 'Error while resetting E2E key!', + E2E_encryption_reset_message: 'You\'re going to be logged out.', + E2E_encryption_reset_confirmation: 'Yes, reset it', Following: 'Following', Threads_displaying_all: 'Displaying All', Threads_displaying_following: 'Displaying Following', diff --git a/app/i18n/locales/pt-BR.js b/app/i18n/locales/pt-BR.js index ad585a0d..b665e7e1 100644 --- a/app/i18n/locales/pt-BR.js +++ b/app/i18n/locales/pt-BR.js @@ -131,6 +131,7 @@ export default { Channel_Name: 'Nome do Canal', Channels: 'Canais', Chats: 'Conversas', + Change_Language: 'Alterar idioma', Change_language_loading: 'Alterando idioma.', Call_already_ended: 'A chamada já terminou!', Clear_cache_loading: 'Limpando cache.', @@ -156,6 +157,7 @@ export default { Conversation: 'Conversação', connecting_server: 'conectando no servidor', Connecting: 'Conectando...', + Contact_us: 'Entre em contato', Continue_with: 'Entrar com', Contact_your_server_admin: 'Contate o administrador do servidor.', Copied_to_clipboard: 'Copiado para a área de transferência!', @@ -191,6 +193,7 @@ export default { Dont_Have_An_Account: 'Não tem uma conta?', Do_you_have_an_account: 'Você tem uma conta?', Do_you_really_want_to_key_this_room_question_mark: 'Você quer realmente {{key}} esta sala?', + E2E_Encryption: 'Encriptação ponta a ponta', E2E_How_It_Works_info1: 'Agora você pode criar grupos privados criptografados e mensagens diretas. Você também pode alterar grupos privados existentes ou DMs para criptografados.', E2E_How_It_Works_info2: 'Esta é a criptografia *ponta a ponta*, portanto, a chave para codificar/decodificar suas mensagens e elas não serão salvas no servidor. Por esse motivo *você precisa armazenar esta senha em algum lugar seguro* que você pode acessar mais tarde se precisar.', E2E_How_It_Works_info3: 'Se você continuar, será gerada automaticamente uma senha E2E.', @@ -432,6 +435,7 @@ export default { Search_by: 'Buscar por', Search_global_users: 'Busca por usuários globais', Search_global_users_description: 'Caso ativado, busca por usuários de outras empresas ou servidores.', + Security_and_privacy: 'Segurança e privacidade', Select_Avatar: 'Selecionar Avatar', Select_Server: 'Selecionar Servidor', Select_Users: 'Selecionar Usuários', @@ -441,6 +445,7 @@ export default { Select_a_User: 'Selecione um Usuário', Send: 'Enviar', Send_audio_message: 'Enviar mensagem de áudio', + Send_crash_report: 'Enviar relatório de erros', Send_message: 'Enviar mensagem', Send_me_the_code_again: 'Envie-me o código novamente', Send_to: 'Enviar para...', @@ -603,6 +608,18 @@ export default { Logged_out_of_other_clients_successfully: 'Desconectado de outros clientes com sucesso', Logout_failed: 'Falha ao desconectar!', Log_analytics_events: 'Logar eventos no analytics', + E2E_encryption_change_password_title: 'Alterar Senha de Criptografia', + E2E_encryption_change_password_description: 'Agora você pode criar grupos privados criptografados e mensagens diretas. Você também pode alterar os grupos privados ou DMs existentes para criptografados. Esta é uma criptografia de ponta a ponta, logo a chave para codificar / decodificar suas mensagens não será salva no servidor. Por esse motivo, você precisa armazenar sua senha em algum lugar seguro. Será solicitada a inserção de senha em outros dispositivos nos quais deseja usar a criptografia E2E.', + E2E_encryption_change_password_error: 'Erro ao alterar senha de criptografia!', + E2E_encryption_change_password_success: 'Senha de criptografia alterada com sucesso!', + E2E_encryption_change_password_message: 'Certifique-se de tê-la guardado em local seguro.', + E2E_encryption_change_password_confirmation: 'Sim, alterar', + E2E_encryption_reset_title: 'Redefinir Chave de Criptografia', + E2E_encryption_reset_description: 'Essa opção irá remover a chave de criptografia corrente e desconectá-lo. \nQuando você se conectar novamente, uma nova chave será gerada e restaurará acesso a qualquer canal com uma ou mais pessoas online. \nDevico à natureza da criptografia ponta a ponta, não será possível restaurar acesso a canais sem membros online.', + E2E_encryption_reset_button: 'Redefinir', + E2E_encryption_reset_error: 'Erro ao redefinir chave!', + E2E_encryption_reset_message: 'Você será desconectado.', + E2E_encryption_reset_confirmation: 'Sim, redefinir', Following: 'Seguindo', Threads_displaying_all: 'Mostrando Tudo', Threads_displaying_following: 'Mostrando Seguindo', diff --git a/app/lib/encryption/encryption.js b/app/lib/encryption/encryption.js index b79c7436..90cbe531 100644 --- a/app/lib/encryption/encryption.js +++ b/app/lib/encryption/encryption.js @@ -68,6 +68,10 @@ class Encryption { return this.readyPromise; } + get hasPrivateKey() { + return !!this.privateKey; + } + // Stop Encryption client stop = () => { this.userId = null; @@ -192,6 +196,18 @@ class Encryption { return password; } + changePassword = async(server, password) => { + // Cast key to the format server is expecting + const privateKey = await SimpleCrypto.RSA.exportKey(this.privateKey); + + // Encode the private key + const encodedPrivateKey = await this.encodePrivateKey(EJSON.stringify(privateKey), password, this.userId); + const publicKey = await UserPreferences.getStringAsync(`${ server }-${ E2E_PUBLIC_KEY }`); + + // Send the new keys to the server + await RocketChat.e2eSetUserPublicAndPrivateKeys(EJSON.stringify(publicKey), encodedPrivateKey); + } + // get a encryption room instance getRoomInstance = async(rid) => { // Prevent handshake again diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 90adc086..92995852 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -84,6 +84,12 @@ const RocketChat = { } } }, + unsubscribeRooms() { + if (this.roomsSub) { + this.roomsSub.stop(); + this.roomsSub = null; + } + }, canOpenRoom, createChannel({ name, users, type, readOnly, broadcast, encrypted @@ -194,10 +200,7 @@ const RocketChat = { this.notifyLoggedListener.then(this.stopListener); } - if (this.roomsSub) { - this.roomsSub.stop(); - this.roomsSub = null; - } + this.unsubscribeRooms(); EventEmitter.emit('INQUIRY_UNSUBSCRIBE'); @@ -407,6 +410,12 @@ const RocketChat = { // RC 0.70.0 return this.methodCallWrapper('stream-notify-room-users', `${ rid }/e2ekeyRequest`, rid, e2eKeyId); }, + e2eResetOwnKey() { + this.unsubscribeRooms(); + + // RC 0.72.0 + return this.methodCallWrapper('e2e.resetOwnE2EKey'); + }, updateJitsiTimeout(roomId) { // RC 0.74.0 diff --git a/app/sagas/login.js b/app/sagas/login.js index 3e76f1b1..068db313 100644 --- a/app/sagas/login.js +++ b/app/sagas/login.js @@ -31,6 +31,7 @@ import UserPreferences from '../lib/userPreferences'; import { inquiryRequest, inquiryReset } from '../ee/omnichannel/actions/inquiry'; import { isOmnichannelStatusAvailable } from '../ee/omnichannel/lib'; import { E2E_REFRESH_MESSAGES_KEY } from '../lib/encryption/constants'; +import Navigation from '../lib/Navigation'; const getServer = state => state.server.server; const loginWithPasswordCall = args => RocketChat.loginWithPassword(args); @@ -239,6 +240,9 @@ const handleLogout = function* handleLogout({ forcedByServer }) { if (forcedByServer) { yield put(appStart({ root: ROOT_OUTSIDE })); showErrorAlert(I18n.t('Logged_out_by_server'), I18n.t('Oops')); + yield delay(300); + Navigation.navigate('NewServerView'); + yield delay(300); EventEmitter.emit('NewServer', { server }); } else { const serversDB = database.servers; diff --git a/app/stacks/InsideStack.js b/app/stacks/InsideStack.js index a5a66498..9081da68 100644 --- a/app/stacks/InsideStack.js +++ b/app/stacks/InsideStack.js @@ -38,6 +38,8 @@ import UserNotificationPrefView from '../views/UserNotificationPreferencesView'; // Settings Stack import SettingsView from '../views/SettingsView'; +import SecurityPrivacyView from '../views/SecurityPrivacyView'; +import E2EEncryptionSecurityView from '../views/E2EEncryptionSecurityView'; import LanguageView from '../views/LanguageView'; import ThemeView from '../views/ThemeView'; import DefaultBrowserView from '../views/DefaultBrowserView'; @@ -225,6 +227,16 @@ const SettingsStackNavigator = () => { component={SettingsView} options={SettingsView.navigationOptions} /> + + { component={UserNotificationPrefView} options={UserNotificationPrefView.navigationOptions} /> + + ); diff --git a/app/utils/log/events.js b/app/utils/log/events.js index 89b77848..52ef7383 100644 --- a/app/utils/log/events.js +++ b/app/utils/log/events.js @@ -133,16 +133,20 @@ export default { SE_SHARE_THIS_APP: 'se_share_this_app', SE_GO_DEFAULTBROWSER: 'se_go_default_browser', SE_GO_THEME: 'se_go_theme', - SE_GO_SCREENLOCKCONFIG: 'se_go_screen_lock_cfg', SE_GO_PROFILE: 'se_go_profile', + SE_GO_SECURITYPRIVACY: 'se_go_securityprivacy', SE_READ_LICENSE: 'se_read_license', SE_COPY_APP_VERSION: 'se_copy_app_version', SE_COPY_SERVER_VERSION: 'se_copy_server_version', - SE_TOGGLE_CRASH_REPORT: 'se_toggle_crash_report', - SE_TOGGLE_ANALYTICS_EVENTS: 'se_toggle_analytics_events', SE_CLEAR_LOCAL_SERVER_CACHE: 'se_clear_local_server_cache', SE_LOG_OUT: 'se_log_out', + // SECURITY PRIVACY VIEW + SP_GO_E2EENCRYPTIONSECURITY: 'sp_go_e2e_encryption_security', + SP_GO_SCREENLOCKCONFIG: 'sp_go_screen_lock_cfg', + SP_TOGGLE_CRASH_REPORT: 'sp_toggle_crash_report', + SP_TOGGLE_ANALYTICS_EVENTS: 'sp_toggle_analytics_events', + // LANGUAGE VIEW LANG_SET_LANGUAGE: 'lang_set_language', LANG_SET_LANGUAGE_F: 'lang_set_language_f', @@ -303,5 +307,9 @@ export default { E2E_SAVE_PW_HOW_IT_WORKS: 'e2e_save_pw_how_it_works', // E2E ENTER YOUR PASSWORD VIEW - E2E_ENTER_PW_SUBMIT: 'e2e_enter_pw_submit' + E2E_ENTER_PW_SUBMIT: 'e2e_enter_pw_submit', + + // E2E ENCRYPTION SECURITY VIEW + E2E_SEC_CHANGE_PASSWORD: 'e2e_sec_change_password', + E2E_SEC_RESET_OWN_KEY: 'e2e_sec_reset_own_key' }; diff --git a/app/views/E2EEncryptionSecurityView.js b/app/views/E2EEncryptionSecurityView.js new file mode 100644 index 00000000..8d246b5a --- /dev/null +++ b/app/views/E2EEncryptionSecurityView.js @@ -0,0 +1,185 @@ +import React from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +import StatusBar from '../containers/StatusBar'; +import * as List from '../containers/List'; +import I18n from '../i18n'; +import log, { logEvent, events } from '../utils/log'; +import { withTheme } from '../theme'; +import SafeAreaView from '../containers/SafeAreaView'; +import TextInput from '../containers/TextInput'; +import Button from '../containers/Button'; +import { getUserSelector } from '../selectors/login'; +import { PADDING_HORIZONTAL } from '../containers/List/constants'; +import sharedStyles from './Styles'; +import { themes } from '../constants/colors'; +import { Encryption } from '../lib/encryption'; +import RocketChat from '../lib/rocketchat'; +import { logout as logoutAction } from '../actions/login'; +import { showConfirmationAlert, showErrorAlert } from '../utils/info'; +import EventEmitter from '../utils/events'; +import { LISTENER } from '../containers/Toast'; +import debounce from '../utils/debounce'; + +const styles = StyleSheet.create({ + container: { + paddingHorizontal: PADDING_HORIZONTAL + }, + title: { + fontSize: 16, + ...sharedStyles.textMedium + }, + description: { + fontSize: 14, + paddingVertical: 10, + ...sharedStyles.textRegular + }, + changePasswordButton: { + marginBottom: 4 + } +}); + +class E2EEncryptionSecurityView extends React.Component { + state = { newPassword: '' } + + newPasswordInputRef = React.createRef(); + + onChangePasswordText = debounce(text => this.setState({ newPassword: text }), 300) + + setNewPasswordRef = ref => this.newPasswordInputRef = ref; + + changePassword = () => { + const { newPassword } = this.state; + if (!newPassword.trim()) { + return; + } + showConfirmationAlert({ + title: I18n.t('Are_you_sure_question_mark'), + message: I18n.t('E2E_encryption_change_password_message'), + confirmationText: I18n.t('E2E_encryption_change_password_confirmation'), + onPress: async() => { + logEvent(events.E2E_SEC_CHANGE_PASSWORD); + try { + const { server } = this.props; + await Encryption.changePassword(server, newPassword); + EventEmitter.emit(LISTENER, { message: I18n.t('E2E_encryption_change_password_success') }); + this.newPasswordInputRef?.clear(); + this.newPasswordInputRef?.blur(); + } catch (e) { + log(e); + showErrorAlert(I18n.t('E2E_encryption_change_password_error')); + } + } + }); + } + + resetOwnKey = () => { + showConfirmationAlert({ + title: I18n.t('Are_you_sure_question_mark'), + message: I18n.t('E2E_encryption_reset_message'), + confirmationText: I18n.t('E2E_encryption_reset_confirmation'), + onPress: () => { + logEvent(events.E2E_SEC_RESET_OWN_KEY); + try { + RocketChat.e2eResetOwnKey(); + const { logout } = this.props; + logout(); + } catch (e) { + log(e); + showErrorAlert(I18n.t('E2E_encryption_reset_error')); + } + } + }); + } + + renderChangePassword = () => { + const { newPassword } = this.state; + const { theme } = this.props; + const { hasPrivateKey } = Encryption; + if (!hasPrivateKey) { + return null; + } + return ( + <> + + {I18n.t('E2E_encryption_change_password_title')} + {I18n.t('E2E_encryption_change_password_description')} + +