diff --git a/app/containers/Button/index.js b/app/containers/Button/index.js
index 0f573f876..ccc847b39 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 57a8da136..bb097d612 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 ad585a0d0..b665e7e11 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 b79c74369..90cbe5310 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 90adc0868..92995852f 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 3e76f1b1c..068db3138 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 a5a664986..9081da680 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 89b77848e..52ef73838 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 000000000..8d246b5a0
--- /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')}
+
+
+
+
+
+ >
+ );
+ }
+
+ render() {
+ const { theme } = this.props;
+ return (
+
+
+
+
+ {this.renderChangePassword()}
+
+
+ {I18n.t('E2E_encryption_reset_title')}
+ {I18n.t('E2E_encryption_reset_description')}
+
+
+
+
+
+ );
+ }
+}
+
+const mapStateToProps = state => ({
+ server: state.server.server,
+ user: getUserSelector(state)
+});
+
+const mapDispatchToProps = dispatch => ({
+ logout: () => dispatch(logoutAction(true))
+});
+
+E2EEncryptionSecurityView.navigationOptions = () => ({
+ title: I18n.t('E2E_Encryption')
+});
+
+E2EEncryptionSecurityView.propTypes = {
+ theme: PropTypes.string,
+ user: PropTypes.shape({
+ roles: PropTypes.array,
+ id: PropTypes.string
+ }),
+ server: PropTypes.string,
+ logout: PropTypes.func
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(withTheme(E2EEncryptionSecurityView));
diff --git a/app/views/E2EEnterYourPasswordView.js b/app/views/E2EEnterYourPasswordView.js
index c753a6208..563b1d9a7 100644
--- a/app/views/E2EEnterYourPasswordView.js
+++ b/app/views/E2EEnterYourPasswordView.js
@@ -64,7 +64,7 @@ class E2EEnterYourPasswordView extends React.Component {
>
-
+
{ this.passwordInput = e; }}
placeholder={I18n.t('Password')}
@@ -82,6 +82,7 @@ class E2EEnterYourPasswordView extends React.Component {
title={I18n.t('Confirm')}
disabled={!password}
theme={theme}
+ testID='e2e-enter-your-password-view-confirm'
/>
{I18n.t('Enter_Your_Encryption_Password_desc1')}
{I18n.t('Enter_Your_Encryption_Password_desc2')}
diff --git a/app/views/E2ESaveYourPasswordView.js b/app/views/E2ESaveYourPasswordView.js
index 5a645baa3..9163047cd 100644
--- a/app/views/E2ESaveYourPasswordView.js
+++ b/app/views/E2ESaveYourPasswordView.js
@@ -126,7 +126,7 @@ class E2ESaveYourPasswordView extends React.Component {
const { theme } = this.props;
return (
-
+
@@ -150,11 +150,13 @@ class E2ESaveYourPasswordView extends React.Component {
title={I18n.t('How_It_Works')}
type='secondary'
theme={theme}
+ testID='e2e-save-password-view-how-it-works'
/>
diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js
index ec23485dc..29e454060 100644
--- a/app/views/RoomActionsView/index.js
+++ b/app/views/RoomActionsView/index.js
@@ -417,7 +417,7 @@ class RoomActionsView extends React.Component {
diff --git a/app/views/RoomsListView/ListHeader/Encryption.js b/app/views/RoomsListView/ListHeader/Encryption.js
index a7a0ee7c4..b666f487f 100644
--- a/app/views/RoomsListView/ListHeader/Encryption.js
+++ b/app/views/RoomsListView/ListHeader/Encryption.js
@@ -26,7 +26,13 @@ const Encryption = React.memo(({
}
return (
-
+
{text}
diff --git a/app/views/SecurityPrivacyView.js b/app/views/SecurityPrivacyView.js
new file mode 100644
index 000000000..937a02504
--- /dev/null
+++ b/app/views/SecurityPrivacyView.js
@@ -0,0 +1,158 @@
+import React from 'react';
+import { Switch } from 'react-native';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import AsyncStorage from '@react-native-community/async-storage';
+
+import { toggleCrashReport as toggleCrashReportAction, toggleAnalyticsEvents as toggleAnalyticsEventsAction } from '../actions/crashReport';
+import { SWITCH_TRACK_COLOR } from '../constants/colors';
+import StatusBar from '../containers/StatusBar';
+import * as List from '../containers/List';
+import I18n from '../i18n';
+import { CRASH_REPORT_KEY, ANALYTICS_EVENTS_KEY } from '../lib/rocketchat';
+import {
+ loggerConfig, analytics, logEvent, events
+} from '../utils/log';
+import SafeAreaView from '../containers/SafeAreaView';
+import { isFDroidBuild } from '../constants/environment';
+import { getUserSelector } from '../selectors/login';
+
+class SecurityPrivacyView extends React.Component {
+ static navigationOptions = () => ({
+ title: I18n.t('Security_and_privacy')
+ });
+
+ static propTypes = {
+ navigation: PropTypes.object,
+ allowCrashReport: PropTypes.bool,
+ allowAnalyticsEvents: PropTypes.bool,
+ e2eEnabled: PropTypes.bool,
+ toggleCrashReport: PropTypes.func,
+ toggleAnalyticsEvents: PropTypes.func,
+ user: PropTypes.shape({
+ roles: PropTypes.array,
+ id: PropTypes.string
+ })
+ }
+
+ toggleCrashReport = (value) => {
+ logEvent(events.SE_TOGGLE_CRASH_REPORT);
+ AsyncStorage.setItem(CRASH_REPORT_KEY, JSON.stringify(value));
+ const { toggleCrashReport } = this.props;
+ toggleCrashReport(value);
+ if (!isFDroidBuild) {
+ loggerConfig.autoNotify = value;
+ if (value) {
+ loggerConfig.clearBeforeSendCallbacks();
+ } else {
+ loggerConfig.registerBeforeSendCallback(() => false);
+ }
+ }
+ }
+
+ toggleAnalyticsEvents = (value) => {
+ logEvent(events.SE_TOGGLE_ANALYTICS_EVENTS);
+ const { toggleAnalyticsEvents } = this.props;
+ AsyncStorage.setItem(ANALYTICS_EVENTS_KEY, JSON.stringify(value));
+ toggleAnalyticsEvents(value);
+ analytics().setAnalyticsCollectionEnabled(value);
+ }
+
+ navigateToScreen = (screen) => {
+ logEvent(events[`SP_GO_${ screen.replace('View', '').toUpperCase() }`]);
+ const { navigation } = this.props;
+ navigation.navigate(screen);
+ }
+
+ renderCrashReportSwitch = () => {
+ const { allowCrashReport } = this.props;
+ return (
+
+ );
+ }
+
+ renderAnalyticsEventsSwitch = () => {
+ const { allowAnalyticsEvents } = this.props;
+ return (
+
+ );
+ }
+
+ render() {
+ const { e2eEnabled } = this.props;
+ return (
+
+
+
+
+
+ {e2eEnabled
+ ? (
+ <>
+ this.navigateToScreen('E2EEncryptionSecurityView')}
+ testID='security-privacy-view-e2e-encryption'
+ />
+
+ >
+ )
+ : null
+ }
+ this.navigateToScreen('ScreenLockConfigView')}
+ testID='security-privacy-view-screen-lock'
+ />
+
+
+
+ {!isFDroidBuild ? (
+ <>
+
+
+ this.renderAnalyticsEventsSwitch()}
+ />
+
+ this.renderCrashReportSwitch()}
+ />
+
+
+
+ >
+ ) : null}
+
+
+ );
+ }
+}
+
+const mapStateToProps = state => ({
+ user: getUserSelector(state),
+ allowCrashReport: state.crashReport.allowCrashReport,
+ allowAnalyticsEvents: state.crashReport.allowAnalyticsEvents,
+ e2eEnabled: state.settings.E2E_Enable
+});
+
+const mapDispatchToProps = dispatch => ({
+ toggleCrashReport: params => dispatch(toggleCrashReportAction(params)),
+ toggleAnalyticsEvents: params => dispatch(toggleAnalyticsEventsAction(params))
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(SecurityPrivacyView);
diff --git a/app/views/SettingsView/index.js b/app/views/SettingsView/index.js
index 32b11c485..bc11da7b7 100644
--- a/app/views/SettingsView/index.js
+++ b/app/views/SettingsView/index.js
@@ -1,30 +1,26 @@
import React from 'react';
import {
- Linking, Switch, Share, Clipboard
+ Linking, Share, Clipboard
} from 'react-native';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
-import AsyncStorage from '@react-native-community/async-storage';
import FastImage from '@rocket.chat/react-native-fast-image';
import CookieManager from '@react-native-community/cookies';
import { logout as logoutAction } from '../../actions/login';
import { selectServerRequest as selectServerRequestAction } from '../../actions/server';
-import { toggleCrashReport as toggleCrashReportAction, toggleAnalyticsEvents as toggleAnalyticsEventsAction } from '../../actions/crashReport';
-import { SWITCH_TRACK_COLOR, themes } from '../../constants/colors';
+import { themes } from '../../constants/colors';
import * as HeaderButton from '../../containers/HeaderButton';
import StatusBar from '../../containers/StatusBar';
import * as List from '../../containers/List';
import I18n from '../../i18n';
-import RocketChat, { CRASH_REPORT_KEY, ANALYTICS_EVENTS_KEY } from '../../lib/rocketchat';
+import RocketChat from '../../lib/rocketchat';
import {
getReadableVersion, getDeviceModel, isAndroid
} from '../../utils/deviceInfo';
import openLink from '../../utils/openLink';
import { showErrorAlert, showConfirmationAlert } from '../../utils/info';
-import {
- loggerConfig, analytics, logEvent, events
-} from '../../utils/log';
+import { logEvent, events } from '../../utils/log';
import {
PLAY_MARKET_LINK, FDROID_MARKET_LINK, APP_STORE_LINK, LICENSE_LINK
} from '../../constants/links';
@@ -44,7 +40,7 @@ class SettingsView extends React.Component {
headerLeft: () => (isMasterDetail ? (
) : (
-
+
)),
title: I18n.t('Settings')
});
@@ -52,10 +48,6 @@ class SettingsView extends React.Component {
static propTypes = {
navigation: PropTypes.object,
server: PropTypes.object,
- allowCrashReport: PropTypes.bool,
- allowAnalyticsEvents: PropTypes.bool,
- toggleCrashReport: PropTypes.func,
- toggleAnalyticsEvents: PropTypes.func,
theme: PropTypes.string,
isMasterDetail: PropTypes.bool,
logout: PropTypes.func.isRequired,
@@ -122,29 +114,6 @@ class SettingsView extends React.Component {
});
}
- toggleCrashReport = (value) => {
- logEvent(events.SE_TOGGLE_CRASH_REPORT);
- AsyncStorage.setItem(CRASH_REPORT_KEY, JSON.stringify(value));
- const { toggleCrashReport } = this.props;
- toggleCrashReport(value);
- if (!isFDroidBuild) {
- loggerConfig.autoNotify = value;
- if (value) {
- loggerConfig.clearBeforeSendCallbacks();
- } else {
- loggerConfig.registerBeforeSendCallback(() => false);
- }
- }
- }
-
- toggleAnalyticsEvents = (value) => {
- logEvent(events.SE_TOGGLE_ANALYTICS_EVENTS);
- const { toggleAnalyticsEvents } = this.props;
- AsyncStorage.setItem(ANALYTICS_EVENTS_KEY, JSON.stringify(value));
- toggleAnalyticsEvents(value);
- analytics().setAnalyticsCollectionEnabled(value);
- }
-
navigateToScreen = (screen) => {
logEvent(events[`SE_GO_${ screen.replace('View', '').toUpperCase() }`]);
const { navigation } = this.props;
@@ -202,28 +171,6 @@ class SettingsView extends React.Component {
openLink(LICENSE_LINK, theme);
}
- renderCrashReportSwitch = () => {
- const { allowCrashReport } = this.props;
- return (
-
- );
- }
-
- renderAnalyticsEventsSwitch = () => {
- const { allowAnalyticsEvents } = this.props;
- return (
-
- );
- }
-
render() {
const { server, isMasterDetail, theme } = this.props;
return (
@@ -299,9 +246,10 @@ class SettingsView extends React.Component {
/>
this.navigateToScreen('ScreenLockConfigView')}
+ onPress={() => this.navigateToScreen('SecurityPrivacyView')}
+ testID='settings-view-security-privacy'
/>
@@ -333,27 +281,6 @@ class SettingsView extends React.Component {
- {!isFDroidBuild ? (
- <>
-
-
- this.renderAnalyticsEventsSwitch()}
- />
-
- this.renderCrashReportSwitch()}
- />
-
-
-
- >
- ) : null}
-
({
server: state.server,
user: getUserSelector(state),
- allowCrashReport: state.crashReport.allowCrashReport,
- allowAnalyticsEvents: state.crashReport.allowAnalyticsEvents,
isMasterDetail: state.app.isMasterDetail
});
const mapDispatchToProps = dispatch => ({
logout: () => dispatch(logoutAction()),
selectServerRequest: params => dispatch(selectServerRequestAction(params)),
- toggleCrashReport: params => dispatch(toggleCrashReportAction(params)),
- toggleAnalyticsEvents: params => dispatch(toggleAnalyticsEventsAction(params)),
appStart: params => dispatch(appStartAction(params))
});
diff --git a/e2e/tests/assorted/01-e2eencryption.spec.js b/e2e/tests/assorted/01-e2eencryption.spec.js
new file mode 100644
index 000000000..a7a28af77
--- /dev/null
+++ b/e2e/tests/assorted/01-e2eencryption.spec.js
@@ -0,0 +1,198 @@
+const {
+ expect, element, by, waitFor
+} = require('detox');
+const { navigateToLogin, login, sleep, tapBack, mockMessage, searchRoom, logout } = require('../../helpers/app');
+
+const data = require('../../data');
+
+const testuser = data.users.regular
+const otheruser = data.users.alternate
+
+async function navigateToRoom(roomName) {
+ await searchRoom(`${ roomName }`);
+ await waitFor(element(by.id(`rooms-list-view-item-${ roomName }`))).toExist().withTimeout(60000);
+ await element(by.id(`rooms-list-view-item-${ roomName }`)).tap();
+ await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000);
+}
+
+async function waitForToast() {
+ await sleep(300);
+}
+
+async function navigateSecurityPrivacy() {
+ await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
+ await element(by.id('rooms-list-view-sidebar')).tap();
+ await waitFor(element(by.id('sidebar-view'))).toBeVisible().withTimeout(2000);
+ await waitFor(element(by.id('sidebar-settings'))).toBeVisible().withTimeout(2000);
+ await element(by.id('sidebar-settings')).tap();
+ await waitFor(element(by.id('settings-view'))).toBeVisible().withTimeout(2000);
+ await element(by.id('settings-view-security-privacy')).tap();
+ await waitFor(element(by.id('security-privacy-view'))).toBeVisible().withTimeout(2000);
+}
+
+describe('E2E Encryption', () => {
+ const room = `encrypted${ data.random }`;
+ const newPassword = 'abc';
+
+ before(async () => {
+ await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
+ await navigateToLogin();
+ await login(testuser.username, testuser.password);
+ });
+
+ describe('Banner', async() => {
+ describe('Render', async () => {
+ it('should have encryption badge', async () => {
+ await waitFor(element(by.id('listheader-encryption').withDescendant(by.label('Save Your Encryption Password')))).toBeVisible().withTimeout(10000);
+ });
+ });
+
+ describe('Usage', async () => {
+ it('should tap encryption badge and open save password modal', async() => {
+ await element(by.id('listheader-encryption')).tap();
+ await waitFor(element(by.id('e2e-save-password-view'))).toBeVisible().withTimeout(2000);
+ });
+
+ it('should tap "How it works" and navigate', async() => {
+ await element(by.id('e2e-save-password-view-how-it-works').and(by.label('How It Works'))).tap();
+ await waitFor(element(by.id('e2e-how-it-works-view'))).toBeVisible().withTimeout(2000);
+ await tapBack();
+ });
+
+ it('should tap "Save my password" and close modal', async() => {
+ await element(by.id('e2e-save-password-view-saved-password').and(by.label('I Saved My E2E Password'))).tap();
+ await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
+ });
+
+ it('should create encrypted room', async() => {
+ await element(by.id('rooms-list-view-create-channel')).tap();
+ await waitFor(element(by.id('new-message-view'))).toBeVisible().withTimeout(2000);
+ await element(by.id('new-message-view-create-channel')).tap();
+ await waitFor(element(by.id('select-users-view'))).toBeVisible().withTimeout(2000);
+ await element(by.id('select-users-view-search')).replaceText(otheruser.username);
+ await waitFor(element(by.id(`select-users-view-item-${ otheruser.username }`))).toBeVisible().withTimeout(60000);
+ await element(by.id(`select-users-view-item-${ otheruser.username }`)).tap();
+ await waitFor(element(by.id(`selected-user-${ otheruser.username }`))).toBeVisible().withTimeout(5000);
+ await element(by.id('selected-users-view-submit')).tap();
+ await waitFor(element(by.id('create-channel-view'))).toExist().withTimeout(5000);
+ await element(by.id('create-channel-name')).replaceText(room);
+ await element(by.id('create-channel-encrypted')).longPress();
+ await element(by.id('create-channel-submit')).tap();
+ await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(60000);
+ await waitFor(element(by.id(`room-view-title-${ room }`))).toBeVisible().withTimeout(60000);
+ });
+
+ it('should send message and be able to read it', async() => {
+ await mockMessage('message');
+ await tapBack();
+ });
+ });
+ })
+
+ describe('Security and Privacy', async() => {
+ it('should navigate to security privacy', async() => {
+ await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
+ await element(by.id('rooms-list-view-sidebar')).tap();
+ await waitFor(element(by.id('sidebar-view'))).toBeVisible().withTimeout(2000);
+ await waitFor(element(by.id('sidebar-settings'))).toBeVisible().withTimeout(2000);
+ await element(by.id('sidebar-settings')).tap();
+ await waitFor(element(by.id('settings-view'))).toBeVisible().withTimeout(2000);
+ await element(by.id('settings-view-security-privacy')).tap();
+ await waitFor(element(by.id('security-privacy-view'))).toBeVisible().withTimeout(2000);
+ });
+
+ it('render', async() => {
+ await expect(element(by.id('security-privacy-view-e2e-encryption'))).toExist();
+ await expect(element(by.id('security-privacy-view-screen-lock'))).toExist();
+ await expect(element(by.id('security-privacy-view-analytics-events'))).toExist();
+ await expect(element(by.id('security-privacy-view-crash-report'))).toExist();
+ });
+ });
+
+ describe('E2E Encryption Security', async() => {
+ it('should navigate to e2e encryption security', async() => {
+ await element(by.id('security-privacy-view-e2e-encryption')).tap();
+ await waitFor(element(by.id('e2e-encryption-security-view'))).toBeVisible().withTimeout(2000);
+ });
+
+ describe('Render', () => {
+ it('should have items', async() => {
+ await waitFor(element(by.id('e2e-encryption-security-view'))).toBeVisible().withTimeout(2000);
+ await expect(element(by.id('e2e-encryption-security-view-password'))).toExist();
+ await expect(element(by.id('e2e-encryption-security-view-change-password').and(by.label('Save Changes')))).toExist();
+ await expect(element(by.id('e2e-encryption-security-view-reset-key').and(by.label('Reset E2E Key')))).toExist();
+ });
+ })
+
+ describe('Change password', async() => {
+ it('should change password', async() => {
+ await element(by.id('e2e-encryption-security-view-password')).typeText(newPassword);
+ await element(by.id('e2e-encryption-security-view-change-password')).tap();
+ await waitFor(element(by.text('Are you sure?'))).toExist().withTimeout(2000);
+ await expect(element(by.text('Make sure you\'ve saved it carefully somewhere else.'))).toExist();
+ await element(by.label('Yes, change it').and(by.type('_UIAlertControllerActionView'))).tap();
+ await waitForToast();
+ });
+
+ it('should navigate to the room and messages should remain decrypted', async() => {
+ await waitFor(element(by.id('e2e-encryption-security-view'))).toBeVisible().withTimeout(2000);
+ await tapBack();
+ await waitFor(element(by.id('security-privacy-view'))).toBeVisible().withTimeout(2000);
+ await tapBack();
+ await waitFor(element(by.id('settings-view'))).toBeVisible().withTimeout(2000);
+ await element(by.id('settings-view-drawer')).tap();
+ await waitFor(element(by.id('sidebar-view'))).toBeVisible().withTimeout(2000);
+ await element(by.id('sidebar-chats')).tap();
+ await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
+ await navigateToRoom(room);
+ await waitFor(element(by.label(`${ data.random }message`)).atIndex(0)).toExist().withTimeout(2000);
+ });
+
+ it('should logout, login and messages should be encrypted', async() => {
+ await tapBack();
+ await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
+ await logout();
+ await navigateToLogin();
+ await login(testuser.username, testuser.password);
+ await navigateToRoom(room);
+ await waitFor(element(by.label(`${ data.random }message`)).atIndex(0)).toNotExist().withTimeout(2000);
+ await expect(element(by.label('Encrypted message')).atIndex(0)).toExist();
+ });
+
+ it('should enter new e2e password and messages should be decrypted', async() => {
+ await tapBack();
+ await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
+ await waitFor(element(by.id('listheader-encryption').withDescendant(by.label('Enter Your E2E Password')))).toBeVisible().withTimeout(2000);
+ await element(by.id('listheader-encryption').withDescendant(by.label('Enter Your E2E Password'))).tap();
+ await waitFor(element(by.id('e2e-enter-your-password-view'))).toBeVisible().withTimeout(2000);
+ await element(by.id('e2e-enter-your-password-view-password')).typeText(newPassword);
+ await element(by.id('e2e-enter-your-password-view-confirm')).tap();
+ await waitFor(element(by.id('listheader-encryption'))).toNotExist().withTimeout(10000);
+ await navigateToRoom(room);
+ await waitFor(element(by.label(`${ data.random }message`)).atIndex(0)).toExist().withTimeout(2000);
+ });
+ });
+
+ describe('Reset E2E key', async() => {
+ it('should reset e2e key', async() => {
+ await tapBack();
+ await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
+ await navigateSecurityPrivacy();
+ await element(by.id('security-privacy-view-e2e-encryption')).tap();
+ await waitFor(element(by.id('e2e-encryption-security-view'))).toBeVisible().withTimeout(2000);
+ await element(by.id('e2e-encryption-security-view-reset-key').and(by.label('Reset E2E Key'))).tap();
+ await waitFor(element(by.text('Are you sure?'))).toExist().withTimeout(2000);
+ await expect(element(by.text('You\'re going to be logged out.'))).toExist();
+ await element(by.label('Yes, reset it').and(by.type('UILabel'))).tap();
+ await sleep(2000)
+ await waitFor(element(by.id('workspace-view'))).toBeVisible().withTimeout(10000);
+ await waitFor(element(by.text('You\'ve been logged out by the server. Please log in again.'))).toExist().withTimeout(2000);
+ await element(by.label('OK').and(by.type('_UIAlertControllerActionView'))).tap();
+ await element(by.id('workspace-view-login')).tap();
+ await waitFor(element(by.id('login-view'))).toBeVisible().withTimeout(2000);
+ await login(testuser.username, testuser.password);
+ await waitFor(element(by.id('listheader-encryption').withDescendant(by.label('Save Your Encryption Password')))).toBeVisible().withTimeout(2000);
+ })
+ });
+ });
+});
\ No newline at end of file
diff --git a/e2e/tests/assorted/04-setting.spec.js b/e2e/tests/assorted/04-setting.spec.js
index 951ccc70d..ea9e64d0b 100644
--- a/e2e/tests/assorted/04-setting.spec.js
+++ b/e2e/tests/assorted/04-setting.spec.js
@@ -30,14 +30,26 @@ describe('Settings screen', () => {
await expect(element(by.id('settings-view-language'))).toExist();
});
- it('should have theme', async() => {
- await expect(element(by.id('settings-view-theme'))).toExist();
+ it('should have review app', async() => {
+ await expect(element(by.id('settings-view-review-app'))).toExist();
});
it('should have share app', async() => {
await expect(element(by.id('settings-view-share-app'))).toExist();
});
+ it('should have default browser', async() => {
+ await expect(element(by.id('settings-view-default-browser'))).toExist();
+ });
+
+ it('should have theme', async() => {
+ await expect(element(by.id('settings-view-theme'))).toExist();
+ });
+
+ it('should have security and privacy', async() => {
+ await expect(element(by.id('settings-view-security-privacy'))).toExist();
+ });
+
it('should have licence', async() => {
await expect(element(by.id('settings-view-license'))).toExist();
});
diff --git a/e2e/tests/assorted/01-changeserver.spec.js b/e2e/tests/assorted/07-changeserver.spec.js
similarity index 100%
rename from e2e/tests/assorted/01-changeserver.spec.js
rename to e2e/tests/assorted/07-changeserver.spec.js