diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index 86d499022..fe625a50b 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -841,5 +841,11 @@ "Your_invite_link_will_never_expire": "Your invite link will never expire.", "Your_password_is": "Your password is", "Your_push_was_sent_to_s_devices": "Your push was sent to {{s}} devices", - "Your_workspace": "Your workspace" + "Your_workspace": "Your workspace", + "Enable": "Enable", + "Disable": "Disable", + "Disable_encryption_title": "Disable encryption", + "Enable_encryption_title": "Enable encryption", + "Disable_encryption_description": "Disabling E2EE compromises privacy. You can re-enable it later if needed. Proceed with caution.", + "Enable_encryption_description": "Ensure conversations are kept private" } \ No newline at end of file diff --git a/app/i18n/locales/pt-BR.json b/app/i18n/locales/pt-BR.json index da8acd33c..6b05308df 100644 --- a/app/i18n/locales/pt-BR.json +++ b/app/i18n/locales/pt-BR.json @@ -827,5 +827,11 @@ "Your_invite_link_will_never_expire": "Seu link de convite nunca irá vencer.", "Your_password_is": "Sua senha é", "Your_push_was_sent_to_s_devices": "A sua notificação foi enviada para {{s}} dispositivos", - "Your_workspace": "Sua workspace" + "Your_workspace": "Sua workspace", + "Enable": "Habilitar", + "Disable": "Desabilitar", + "Disable_encryption_title": "Desabilitar criptografia", + "Enable_encryption_title": "Habilitar criptografia", + "Disable_encryption_description": "Desabilitar a criptografia compromete sua privacidade. Você pode reabilitá-la depois se precisar. Continue com cautela.", + "Enable_encryption_description": "Garante a privacidade das suas conversas" } \ No newline at end of file diff --git a/app/lib/encryption/encryption.ts b/app/lib/encryption/encryption.ts index 06f82d490..c51aaa517 100644 --- a/app/lib/encryption/encryption.ts +++ b/app/lib/encryption/encryption.ts @@ -501,26 +501,6 @@ class Encryption { // Decrypt multiple subscriptions decryptSubscriptions = (subscriptions: ISubscription[]) => Promise.all(subscriptions.map(s => this.decryptSubscription(s))); - - // Missing room encryption key - isMissingRoomE2EEKey = ({ - encryptionEnabled, - roomEncrypted, - E2EKey - }: { - encryptionEnabled: boolean; - roomEncrypted: TSubscriptionModel['encrypted']; - E2EKey: TSubscriptionModel['E2EKey']; - }) => (encryptionEnabled && roomEncrypted && !E2EKey) ?? false; - - // Encrypted room, but user session is not encrypted - isE2EEDisabledEncryptedRoom = ({ - encryptionEnabled, - roomEncrypted - }: { - encryptionEnabled: boolean; - roomEncrypted: TSubscriptionModel['encrypted']; - }) => (!encryptionEnabled && roomEncrypted) ?? false; } const encryption = new Encryption(); diff --git a/app/lib/encryption/helpers/toggleRoomE2EE.ts b/app/lib/encryption/helpers/toggleRoomE2EE.ts new file mode 100644 index 000000000..1f63704dd --- /dev/null +++ b/app/lib/encryption/helpers/toggleRoomE2EE.ts @@ -0,0 +1,70 @@ +import { Alert } from 'react-native'; + +import { Services } from '../../services'; +import database from '../../database'; +import { getSubscriptionByRoomId } from '../../database/services/Subscription'; +import log from '../../methods/helpers/log'; +import I18n from '../../../i18n'; + +export const toggleRoomE2EE = async (rid: string): Promise => { + const room = await getSubscriptionByRoomId(rid); + if (!room) { + return; + } + + const isEncrypted = room.encrypted; + const title = I18n.t(isEncrypted ? 'Disable_encryption_title' : 'Enable_encryption_title'); + const message = I18n.t(isEncrypted ? 'Disable_encryption_description' : 'Enable_encryption_description'); + const confirmationText = I18n.t(isEncrypted ? 'Disable' : 'Enable'); + + Alert.alert( + title, + message, + [ + { + text: I18n.t('Cancel'), + style: 'cancel' + }, + { + text: confirmationText, + style: isEncrypted ? 'destructive' : 'default', + onPress: async () => { + try { + const db = database.active; + + // Toggle encrypted value + const encrypted = !room.encrypted; + + // Instantly feedback to the user + await db.write(async () => { + await room.update(r => { + r.encrypted = encrypted; + }); + }); + + try { + // Send new room setting value to server + const { result } = await Services.saveRoomSettings(rid, { encrypted }); + // If it was saved successfully + if (result) { + return; + } + } catch { + // do nothing + } + + // If something goes wrong we go back to the previous value + await db.write(async () => { + await room.update(r => { + r.encrypted = room.encrypted; + }); + }); + } catch (e) { + log(e); + } + } + } + ], + { cancelable: true } + ); +}; diff --git a/app/lib/encryption/utils.ts b/app/lib/encryption/utils.ts index 27268bb82..3df3ce48a 100644 --- a/app/lib/encryption/utils.ts +++ b/app/lib/encryption/utils.ts @@ -3,6 +3,7 @@ import SimpleCrypto from 'react-native-simple-crypto'; import { random } from '../methods/helpers'; import { fromByteArray, toByteArray } from './helpers/base64-js'; +import { TSubscriptionModel } from '../../definitions'; const BASE64URI = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; @@ -58,3 +59,41 @@ export const toString = (thing: string | ByteBuffer | Buffer | ArrayBuffer | Uin return new ByteBuffer.wrap(thing).toString('binary'); }; export const randomPassword = (): string => `${random(3)}-${random(3)}-${random(3)}`.toLowerCase(); + +// Missing room encryption key +export const isMissingRoomE2EEKey = ({ + encryptionEnabled, + roomEncrypted, + E2EKey +}: { + encryptionEnabled: boolean; + roomEncrypted: TSubscriptionModel['encrypted']; + E2EKey: TSubscriptionModel['E2EKey']; +}): boolean => (encryptionEnabled && roomEncrypted && !E2EKey) ?? false; + +// Encrypted room, but user session is not encrypted +export const isE2EEDisabledEncryptedRoom = ({ + encryptionEnabled, + roomEncrypted +}: { + encryptionEnabled: boolean; + roomEncrypted: TSubscriptionModel['encrypted']; +}): boolean => (!encryptionEnabled && roomEncrypted) ?? false; + +export const hasE2EEWarning = ({ + encryptionEnabled, + roomEncrypted, + E2EKey +}: { + encryptionEnabled: boolean; + roomEncrypted: TSubscriptionModel['encrypted']; + E2EKey: TSubscriptionModel['E2EKey']; +}): boolean => { + if (isMissingRoomE2EEKey({ encryptionEnabled, roomEncrypted, E2EKey })) { + return true; + } + if (isE2EEDisabledEncryptedRoom({ encryptionEnabled, roomEncrypted })) { + return true; + } + return false; +}; diff --git a/app/lib/methods/search.ts b/app/lib/methods/search.ts index b8b65b039..9e14264be 100644 --- a/app/lib/methods/search.ts +++ b/app/lib/methods/search.ts @@ -6,7 +6,7 @@ import { store as reduxStore } from '../store/auxStore'; import { spotlight } from '../services/restApi'; import { ISearch, ISearchLocal, IUserMessage, SubscriptionType, TSubscriptionModel } from '../../definitions'; import { isGroupChat, isReadOnly } from './helpers'; -import { Encryption } from '../encryption'; +import { isE2EEDisabledEncryptedRoom, isMissingRoomE2EEKey } from '../encryption/utils'; export type TSearch = ISearchLocal | IUserMessage | ISearch; @@ -54,10 +54,10 @@ export const localSearchSubscription = async ({ return null; } - if (Encryption.isMissingRoomE2EEKey({ encryptionEnabled, roomEncrypted: item.encrypted, E2EKey: item.E2EKey })) { + if (isMissingRoomE2EEKey({ encryptionEnabled, roomEncrypted: item.encrypted, E2EKey: item.E2EKey })) { return null; } - if (Encryption.isE2EEDisabledEncryptedRoom({ encryptionEnabled, roomEncrypted: item.encrypted })) { + if (isE2EEDisabledEncryptedRoom({ encryptionEnabled, roomEncrypted: item.encrypted })) { return null; } diff --git a/app/views/RoomActionsView/components/CallSection.tsx b/app/views/RoomActionsView/components/CallSection.tsx index be75067a5..3f7d8408e 100644 --- a/app/views/RoomActionsView/components/CallSection.tsx +++ b/app/views/RoomActionsView/components/CallSection.tsx @@ -3,7 +3,7 @@ import React from 'react'; import * as List from '../../../containers/List'; import { useVideoConf } from '../../../lib/hooks/useVideoConf'; -export default function CallSection({ rid }: { rid: string }): React.ReactElement | null { +export default function CallSection({ rid, disabled }: { rid: string; disabled: boolean }): React.ReactElement | null { const { callEnabled, showInitCallActionSheet, disabledTooltip } = useVideoConf(rid); if (callEnabled) return ( @@ -15,7 +15,7 @@ export default function CallSection({ rid }: { rid: string }): React.ReactElemen testID='room-actions-call' left={() => } showActionIndicator - disabled={disabledTooltip} + disabled={disabledTooltip || disabled} /> diff --git a/app/views/RoomActionsView/index.tsx b/app/views/RoomActionsView/index.tsx index 4257b6593..ad3d3c5fc 100644 --- a/app/views/RoomActionsView/index.tsx +++ b/app/views/RoomActionsView/index.tsx @@ -54,6 +54,8 @@ import { ILivechatTag } from '../../definitions/ILivechatTag'; import CallSection from './components/CallSection'; import { TNavigation } from '../../stacks/stackType'; import Switch from '../../containers/Switch'; +import * as EncryptionUtils from '../../lib/encryption/utils'; +import { toggleRoomE2EE } from '../../lib/encryption/helpers/toggleRoomE2EE'; type StackType = ChatsStackParamList & TNavigation; @@ -100,6 +102,7 @@ interface IRoomActionsViewState { canCreateTeam: boolean; canAddChannelToTeam: boolean; canConvertTeam: boolean; + hasE2EEWarning: boolean; loading: boolean; } @@ -151,13 +154,20 @@ class RoomActionsView extends React.Component { if (this.mounted) { - this.setState({ room: changes, membersCount: changes.usersCount }); + const hasE2EEWarning = EncryptionUtils.hasE2EEWarning({ + encryptionEnabled, + E2EKey: room.E2EKey, + roomEncrypted: room.encrypted + }); + this.setState({ room: changes, membersCount: changes.usersCount, hasE2EEWarning }); } else { // @ts-ignore this.state.room = changes; @@ -171,6 +181,7 @@ class RoomActionsView extends React.Component { const { room, canToggleEncryption, canEdit } = this.state; - const { encrypted } = room; + const { rid, encrypted } = room; const { serverVersion } = this.props; let hasPermission = false; if (compareServerVersion(serverVersion, 'lowerThan', '3.11.0')) { @@ -353,7 +370,7 @@ class RoomActionsView extends React.Component; + return toggleRoomE2EE(rid)} disabled={!hasPermission} />; }; closeLivechat = async () => { @@ -477,49 +494,6 @@ class RoomActionsView extends React.Component { - logEvent(events.RA_TOGGLE_ENCRYPTED); - const { room } = this.state; - const { rid } = room; - const db = database.active; - - // Toggle encrypted value - const encrypted = !room.encrypted; - try { - // Instantly feedback to the user - await db.write(async () => { - await room.update( - protectedFunction((r: TSubscriptionModel) => { - r.encrypted = encrypted; - }) - ); - }); - - try { - // Send new room setting value to server - const { result } = await Services.saveRoomSettings(rid, { encrypted }); - // If it was saved successfully - if (result) { - return; - } - } catch { - // do nothing - } - - // If something goes wrong we go back to the previous value - await db.write(async () => { - await room.update( - protectedFunction((r: TSubscriptionModel) => { - r.encrypted = room.encrypted; - }) - ); - }); - } catch (e) { - logEvent(events.RA_TOGGLE_ENCRYPTED_F); - log(e); - } - }; - handleShare = () => { logEvent(events.RA_SHARE); const { room } = this.state; @@ -938,7 +912,7 @@ class RoomActionsView extends React.Component { - const { canEdit, canCreateTeam, canAddChannelToTeam } = this.state; + const { canEdit, canCreateTeam, canAddChannelToTeam, hasE2EEWarning } = this.state; const canConvertToTeam = canEdit && canCreateTeam && !room.teamMain; const canMoveToTeam = canEdit && canAddChannelToTeam && !room.teamId; @@ -956,6 +930,7 @@ class RoomActionsView extends React.Component } showActionIndicator + disabled={hasE2EEWarning} /> @@ -973,6 +948,7 @@ class RoomActionsView extends React.Component } showActionIndicator + disabled={hasE2EEWarning} /> @@ -982,7 +958,7 @@ class RoomActionsView extends React.Component { - const { canEdit, canConvertTeam, loading } = this.state; + const { canEdit, canConvertTeam, loading, hasE2EEWarning } = this.state; const canConvertTeamToChannel = canEdit && canConvertTeam && !!room?.teamMain; return ( @@ -991,7 +967,7 @@ class RoomActionsView extends React.Component this.onPressTouchable({ event: this.convertTeamToChannel @@ -1086,7 +1062,7 @@ class RoomActionsView extends React.Component {this.renderRoomInfo()} - + {this.renderE2EEncryption()} @@ -1132,6 +1108,7 @@ class RoomActionsView extends React.Component } showActionIndicator + disabled={hasE2EEWarning} /> @@ -1158,6 +1135,7 @@ class RoomActionsView extends React.Component } showActionIndicator + disabled={hasE2EEWarning} /> @@ -1187,6 +1165,7 @@ class RoomActionsView extends React.Component } showActionIndicator + disabled={hasE2EEWarning} /> @@ -1205,6 +1184,7 @@ class RoomActionsView extends React.Component } showActionIndicator + disabled={hasE2EEWarning} /> @@ -1223,6 +1203,7 @@ class RoomActionsView extends React.Component } showActionIndicator + disabled={hasE2EEWarning} /> @@ -1240,6 +1221,7 @@ class RoomActionsView extends React.Component } showActionIndicator + disabled={hasE2EEWarning} /> @@ -1258,6 +1240,7 @@ class RoomActionsView extends React.Component } showActionIndicator + disabled={hasE2EEWarning} /> @@ -1276,6 +1259,7 @@ class RoomActionsView extends React.Component } showActionIndicator + disabled={hasE2EEWarning} /> @@ -1294,6 +1278,7 @@ class RoomActionsView extends React.Component } showActionIndicator + disabled={hasE2EEWarning} /> diff --git a/app/views/RoomView/RightButtons.tsx b/app/views/RoomView/RightButtons.tsx index 4acdc078d..75cd575e6 100644 --- a/app/views/RoomView/RightButtons.tsx +++ b/app/views/RoomView/RightButtons.tsx @@ -22,6 +22,7 @@ import { TNavigation } from '../../stacks/stackType'; import { ChatsStackParamList } from '../../stacks/types'; import { HeaderCallButton } from './components'; import { TColors, TSupportedThemes, withTheme } from '../../theme'; +import { toggleRoomE2EE } from '../../lib/encryption/helpers/toggleRoomE2EE'; interface IRightButtonsProps extends Pick { userId?: string; @@ -48,7 +49,7 @@ interface IRightButtonsProps extends Pick { colors?: TColors; issuesWithNotifications: boolean; notificationsDisabled?: boolean; - disabled: boolean; + hasE2EEWarning: boolean; } interface IRigthButtonsState { @@ -97,8 +98,16 @@ class RightButtonsContainer extends Component - + ); } @@ -390,33 +398,33 @@ class RightButtonsContainer extends Component ); } return ( + {hasE2EEWarning ? toggleRoomE2EE(rid)} /> : null} {issuesWithNotifications || notificationsDisabled ? ( ) : null} - {rid ? : null} + {rid ? : null} {threadsEnabled ? ( } - disabled={disabled} + disabled={hasE2EEWarning} /> ) : null} - + ); } diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index 39ddc6167..090790e8a 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -98,7 +98,7 @@ import AudioManager from '../../lib/methods/AudioManager'; import { IListContainerRef, TListRef } from './List/definitions'; import { getMessageById } from '../../lib/database/services/Message'; import { getThreadById } from '../../lib/database/services/Thread'; -import { Encryption } from '../../lib/encryption'; +import { hasE2EEWarning, isE2EEDisabledEncryptedRoom, isMissingRoomE2EEKey } from '../../lib/encryption/utils'; import { clearInAppFeedback, removeInAppFeedback } from '../../actions/inAppFeedback'; import UserPreferences from '../../lib/methods/userPreferences'; import { IRoomViewProps, IRoomViewState } from './definitions'; @@ -415,23 +415,9 @@ class RoomView extends React.Component { return hideSystemMessages ?? []; } - hasE2EEWarning = () => { - const { room } = this.state; - const { encryptionEnabled } = this.props; - if ('encrypted' in room) { - if (Encryption.isMissingRoomE2EEKey({ encryptionEnabled, roomEncrypted: room.encrypted, E2EKey: room.E2EKey })) { - return true; - } - if (Encryption.isE2EEDisabledEncryptedRoom({ encryptionEnabled, roomEncrypted: room.encrypted })) { - return true; - } - } - return false; - }; - setHeader = () => { const { room, unreadsCount, roomUserId, joined, canForwardGuest, canReturnQueue, canPlaceLivechatOnHold } = this.state; - const { navigation, isMasterDetail, theme, baseUrl, user, route } = this.props; + const { navigation, isMasterDetail, theme, baseUrl, user, route, encryptionEnabled } = this.props; const { rid, tmid } = this; if (!room.rid) { return; @@ -516,7 +502,7 @@ class RoomView extends React.Component { onPress={this.goRoomActionsView} testID={`room-view-title-${title}`} sourceType={sourceType} - disabled={this.hasE2EEWarning() || !!tmid} + disabled={!!tmid} /> ), headerRight: () => ( @@ -534,7 +520,9 @@ class RoomView extends React.Component { showActionSheet={this.showActionSheet} departmentId={departmentId} notificationsDisabled={iSubRoom?.disableNotifications} - disabled={this.hasE2EEWarning()} + hasE2EEWarning={ + 'encrypted' in room && hasE2EEWarning({ encryptionEnabled, E2EKey: room.E2EKey, roomEncrypted: room.encrypted }) + } /> ) }); @@ -1475,12 +1463,12 @@ class RoomView extends React.Component { if ('encrypted' in room) { // Missing room encryption key - if (Encryption.isMissingRoomE2EEKey({ encryptionEnabled, roomEncrypted: room.encrypted, E2EKey: room.E2EKey })) { + if (isMissingRoomE2EEKey({ encryptionEnabled, roomEncrypted: room.encrypted, E2EKey: room.E2EKey })) { return ; } // Encrypted room, but user session is not encrypted - if (Encryption.isE2EEDisabledEncryptedRoom({ encryptionEnabled, roomEncrypted: room.encrypted })) { + if (isE2EEDisabledEncryptedRoom({ encryptionEnabled, roomEncrypted: room.encrypted })) { return ; } } diff --git a/app/views/ShareListView/index.tsx b/app/views/ShareListView/index.tsx index 9b4072d64..7d4290ca4 100644 --- a/app/views/ShareListView/index.tsx +++ b/app/views/ShareListView/index.tsx @@ -21,13 +21,13 @@ import { animateNextTransition } from '../../lib/methods/helpers/layoutAnimation import { TSupportedThemes, withTheme } from '../../theme'; import SafeAreaView from '../../containers/SafeAreaView'; import { sanitizeLikeString } from '../../lib/database/utils'; -import { Encryption } from '../../lib/encryption'; import styles from './styles'; import ShareListHeader from './Header'; import { IApplicationState, TServerModel, TSubscriptionModel } from '../../definitions'; import { ShareInsideStackParamList } from '../../definitions/navigationTypes'; import { getRoomAvatar, isAndroid, isIOS, askAndroidMediaPermissions } from '../../lib/methods/helpers'; import { encryptionInit } from '../../actions/encryption'; +import { isE2EEDisabledEncryptedRoom, isMissingRoomE2EEKey } from '../../lib/encryption/utils'; interface IDataFromShare { value: string; @@ -246,10 +246,10 @@ class ShareListView extends React.Component { return data .map(item => { - if (Encryption.isMissingRoomE2EEKey({ encryptionEnabled, roomEncrypted: item.encrypted, E2EKey: item.E2EKey })) { + if (isMissingRoomE2EEKey({ encryptionEnabled, roomEncrypted: item.encrypted, E2EKey: item.E2EKey })) { return null; } - if (Encryption.isE2EEDisabledEncryptedRoom({ encryptionEnabled, roomEncrypted: item.encrypted })) { + if (isE2EEDisabledEncryptedRoom({ encryptionEnabled, roomEncrypted: item.encrypted })) { return null; }