[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 <djorkaeff.unb@gmail.com>
This commit is contained in:
Diego Mello 2020-10-30 15:31:04 -03:00 committed by GitHub
parent 32a0e9be15
commit fade17d0de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 677 additions and 99 deletions

View File

@ -82,6 +82,7 @@ export default class Button extends React.PureComponent {
{ color: textColor },
fontSize && { fontSize }
]}
accessibilityLabel={title}
>
{title}
</Text>

View File

@ -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',

View File

@ -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',

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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}
/>
<SettingsStack.Screen
name='SecurityPrivacyView'
component={SecurityPrivacyView}
options={SecurityPrivacyView.navigationOptions}
/>
<SettingsStack.Screen
name='E2EEncryptionSecurityView'
component={E2EEncryptionSecurityView}
options={E2EEncryptionSecurityView.navigationOptions}
/>
<SettingsStack.Screen
name='LanguageView'
component={LanguageView}

View File

@ -43,6 +43,8 @@ import NewMessageView from '../../views/NewMessageView';
import CreateChannelView from '../../views/CreateChannelView';
import UserPreferencesView from '../../views/UserPreferencesView';
import UserNotificationPrefView from '../../views/UserNotificationPreferencesView';
import SecurityPrivacyView from '../../views/SecurityPrivacyView';
import E2EEncryptionSecurityView from '../../views/E2EEncryptionSecurityView';
// InsideStackNavigator
import AttachmentView from '../../views/AttachmentView';
@ -283,6 +285,16 @@ const ModalStackNavigator = React.memo(({ navigation }) => {
component={UserNotificationPrefView}
options={UserNotificationPrefView.navigationOptions}
/>
<ModalStack.Screen
name='SecurityPrivacyView'
component={SecurityPrivacyView}
options={SecurityPrivacyView.navigationOptions}
/>
<ModalStack.Screen
name='E2EEncryptionSecurityView'
component={E2EEncryptionSecurityView}
options={E2EEncryptionSecurityView.navigationOptions}
/>
</ModalStack.Navigator>
</ModalContainer>
);

View File

@ -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'
};

View File

@ -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 (
<>
<List.Section>
<Text style={[styles.title, { color: themes[theme].titleColor }]}>{I18n.t('E2E_encryption_change_password_title')}</Text>
<Text style={[styles.description, { color: themes[theme].bodyText }]}>{I18n.t('E2E_encryption_change_password_description')}</Text>
<TextInput
inputRef={this.setNewPasswordRef}
placeholder={I18n.t('New_Password')}
returnKeyType='send'
secureTextEntry
onSubmitEditing={this.changePassword}
testID='e2e-encryption-security-view-password'
theme={theme}
onChangeText={this.onChangePasswordText}
/>
<Button
onPress={this.changePassword}
title={I18n.t('Save_Changes')}
theme={theme}
disabled={!newPassword.trim()}
style={styles.changePasswordButton}
testID='e2e-encryption-security-view-change-password'
/>
</List.Section>
<List.Separator />
</>
);
}
render() {
const { theme } = this.props;
return (
<SafeAreaView testID='e2e-encryption-security-view' style={{ backgroundColor: themes[theme].backgroundColor }}>
<StatusBar theme={theme} />
<List.Container>
<View style={styles.container}>
{this.renderChangePassword()}
<List.Section>
<Text style={[styles.title, { color: themes[theme].titleColor }]}>{I18n.t('E2E_encryption_reset_title')}</Text>
<Text style={[styles.description, { color: themes[theme].bodyText }]}>{I18n.t('E2E_encryption_reset_description')}</Text>
<Button
onPress={this.resetOwnKey}
title={I18n.t('E2E_encryption_reset_button')}
theme={theme}
type='secondary'
backgroundColor={themes[theme].chatComponentBackground}
testID='e2e-encryption-security-view-reset-key'
/>
</List.Section>
</View>
</List.Container>
</SafeAreaView>
);
}
}
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));

View File

@ -64,7 +64,7 @@ class E2EEnterYourPasswordView extends React.Component {
>
<StatusBar />
<ScrollView {...scrollPersistTaps} style={sharedStyles.container} contentContainerStyle={[sharedStyles.containerScrollView, styles.scrollView]}>
<SafeAreaView style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]}>
<SafeAreaView style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]} testID='e2e-enter-your-password-view'>
<TextInput
inputRef={(e) => { 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'
/>
<Text style={[styles.info, { color: themes[theme].bodyText }]}>{I18n.t('Enter_Your_Encryption_Password_desc1')}</Text>
<Text style={[styles.info, { color: themes[theme].bodyText }]}>{I18n.t('Enter_Your_Encryption_Password_desc2')}</Text>

View File

@ -126,7 +126,7 @@ class E2ESaveYourPasswordView extends React.Component {
const { theme } = this.props;
return (
<SafeAreaView style={{ backgroundColor: themes[theme].backgroundColor }}>
<SafeAreaView style={{ backgroundColor: themes[theme].backgroundColor }} testID='e2e-save-password-view'>
<StatusBar />
<ScrollView {...scrollPersistTaps} style={sharedStyles.container} contentContainerStyle={sharedStyles.containerScrollView}>
<View style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]}>
@ -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'
/>
<Button
onPress={this.onSaved}
title={I18n.t('I_Saved_My_E2E_Password')}
theme={theme}
testID='e2e-save-password-view-saved-password'
/>
</View>
</ScrollView>

View File

@ -417,7 +417,7 @@ class RoomActionsView extends React.Component {
<Avatar
text={avatar}
style={styles.avatar}
size={50}
size={50 * fontScale}
type={t}
rid={rid}
>

View File

@ -26,7 +26,13 @@ const Encryption = React.memo(({
}
return (
<BorderlessButton style={[styles.encryptionButton, { backgroundColor: themes[theme].actionTintColor }]} theme={theme} onPress={goEncryption}>
<BorderlessButton
style={[styles.encryptionButton, { backgroundColor: themes[theme].actionTintColor }]}
theme={theme}
onPress={goEncryption}
testID='listheader-encryption'
accessibilityLabel={text}
>
<CustomIcon name='encrypted' size={24} color={themes[theme].buttonText} style={styles.encryptionIcon} />
<Text style={[styles.encryptionText, { color: themes[theme].buttonText }]}>{text}</Text>
</BorderlessButton>

View File

@ -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 (
<Switch
value={allowCrashReport}
trackColor={SWITCH_TRACK_COLOR}
onValueChange={this.toggleCrashReport}
/>
);
}
renderAnalyticsEventsSwitch = () => {
const { allowAnalyticsEvents } = this.props;
return (
<Switch
value={allowAnalyticsEvents}
trackColor={SWITCH_TRACK_COLOR}
onValueChange={this.toggleAnalyticsEvents}
/>
);
}
render() {
const { e2eEnabled } = this.props;
return (
<SafeAreaView testID='security-privacy-view'>
<StatusBar />
<List.Container testID='security-privacy-view-list'>
<List.Section>
<List.Separator />
{e2eEnabled
? (
<>
<List.Item
title='E2E_Encryption'
showActionIndicator
onPress={() => this.navigateToScreen('E2EEncryptionSecurityView')}
testID='security-privacy-view-e2e-encryption'
/>
<List.Separator />
</>
)
: null
}
<List.Item
title='Screen_lock'
showActionIndicator
onPress={() => this.navigateToScreen('ScreenLockConfigView')}
testID='security-privacy-view-screen-lock'
/>
<List.Separator />
</List.Section>
{!isFDroidBuild ? (
<>
<List.Section>
<List.Separator />
<List.Item
title='Log_analytics_events'
testID='security-privacy-view-analytics-events'
right={() => this.renderAnalyticsEventsSwitch()}
/>
<List.Separator />
<List.Item
title='Send_crash_report'
testID='security-privacy-view-crash-report'
right={() => this.renderCrashReportSwitch()}
/>
<List.Separator />
<List.Info info='Crash_report_disclaimer' />
</List.Section>
</>
) : null}
</List.Container>
</SafeAreaView>
);
}
}
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);

View File

@ -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 ? (
<HeaderButton.CloseModal navigation={navigation} testID='settings-view-close' />
) : (
<HeaderButton.Drawer navigation={navigation} />
<HeaderButton.Drawer navigation={navigation} testID='settings-view-drawer' />
)),
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 (
<Switch
value={allowCrashReport}
trackColor={SWITCH_TRACK_COLOR}
onValueChange={this.toggleCrashReport}
/>
);
}
renderAnalyticsEventsSwitch = () => {
const { allowAnalyticsEvents } = this.props;
return (
<Switch
value={allowAnalyticsEvents}
trackColor={SWITCH_TRACK_COLOR}
onValueChange={this.toggleAnalyticsEvents}
/>
);
}
render() {
const { server, isMasterDetail, theme } = this.props;
return (
@ -299,9 +246,10 @@ class SettingsView extends React.Component {
/>
<List.Separator />
<List.Item
title='Screen_lock'
title='Security_and_privacy'
showActionIndicator
onPress={() => this.navigateToScreen('ScreenLockConfigView')}
onPress={() => this.navigateToScreen('SecurityPrivacyView')}
testID='settings-view-security-privacy'
/>
<List.Separator />
</List.Section>
@ -333,27 +281,6 @@ class SettingsView extends React.Component {
<List.Separator />
</List.Section>
{!isFDroidBuild ? (
<>
<List.Section>
<List.Separator />
<List.Item
title='Log_analytics_events'
testID='settings-view-analytics-events'
right={() => this.renderAnalyticsEventsSwitch()}
/>
<List.Separator />
<List.Item
title='Send_crash_report'
testID='settings-view-crash-report'
right={() => this.renderCrashReportSwitch()}
/>
<List.Separator />
<List.Info info='Crash_report_disclaimer' />
</List.Section>
</>
) : null}
<List.Section>
<List.Separator />
<List.Item
@ -382,16 +309,12 @@ class SettingsView extends React.Component {
const mapStateToProps = state => ({
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))
});

View File

@ -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);
})
});
});
});

View File

@ -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();
});