[NEW] E2E Encryption (#2394)
* Add E2EKey to Subscription Model * Install react-native-simple-crypto * Install bytebuffer * Add translations * CreateChannel Encrypted toggle * Request E2E_Enabled setting * Add some E2E API methods * POC E2E Encryption * Garbage remove * Remove keys cleaner * Android cast JWK -> PKCS1 * Initialize E2E when Login Success * Add some translations * Add e2e property to Message model * Send Encrypted messages * (iOS) PKCS1 -> JWK & e2e.setUserPublicAndPrivateKeys * (Android) PKCS1 -> JWK & e2e.setUserPublicAndPrivateKeys * Create an encrypted channel * Fix app crashing on RoomsList * Create room key * Set Room E2E Key (Android) * Edit room encrypted * Show encrypted icon on messages * logEvents * Decrypt pending subscriptions & messages * Handle user cancel e2e password entry * E2ESavePasswordView * Update Snapshot * Add encrypted props to message on Send * Thread messages encryption * E2E -> Encryption * Share Extension: Share encrypted text * (POC) Search messages on Encrypted room * Provide room key to new users * Request roomKey on stream-notify-room-users * Add e2eKeyId to Room Model * (WIP) E2E Encryption Screens * Remove encryption subscription file * Move E2E_Enable to Server Model * Encryption List Banner * Move Encryption init to Sagas * Show banner only when enabled * Use RocketChat/react-native-simple-crypto * Search on WM only when is an Encrypted channel * (WIP) Encryption Banner * Encryption banner * Patch -> Fork * Improve send encrypted message * Update simple-crypto * Not decrypt already decrypted messages * Add comments * Change eslint disable to inline * Improve code * Remove comment * Some fixes * (WIP) Encryption Screens * Improve sub find * Resend an encrypted message * Fix comment * Code improvements * Hide e2e buttons on features if it is not enabled * InApp notifications of a encrypted room * Encryption stop logic * Edit encrypted message * DB batch on decryptPending * Encryption ready client * Comments * Handle getRoomInstance errors * Multiple messages decrypt * Remove unnecessary try/catch * Fix decrypt all messages history * Just add a questionmark * Fix some subscriptions missing decrypt * Disable request key logic * Fix unicode emojis * Fix e2ekey request * roomId -> subscription * Decrypt subscription after merge * E2ERoom -> EncryptionRoom * Fix infinite loading * Handle import key errors * Handle request key errors * Move e2eRequestRoomKey to Rocket.Chat * WIP handshake when key should be requested * Add search messages explanation * Remove some TODO and update comments * Improvements * Dont show message hash to user * Handle key request & prevent multiple calls * Request E2EKey on decryptSubscription that doesn't exists on database yet * Insert decrypted subscription * Fix crash after login * Decrypt sub when receive the key * Decrypt pending messages of a room * Encrypted as a switch * Buffer to Base64 URI Safe * Add a relevant comment * Prevent import key without a privateKey * Prevent create a new instance when client is not ready * Update simple-crypto & remove replace trick * More comments * Remove useless comment * Remove useless try/catch * I18n all E2E screens * E2ESavePassword -> E2ESaveYourPassword * Prevent multiple views on message when is not encrypted * Fix encryption toggle not working sometimes * follow some suggestions * dont rotate icons * remove unnecessary condition * remove unreachable event * create channel comment * disable no-bitwise rule for entire file * loadKeys -> persistKeys * getMasterKey -> generateMasterKey * explicit difference between E2EKey & e2eKeyId * roomId -> rid * group columns * Remove server selector * missing log events * remove comment * use stored public key * update simple-crypto & remove base64-js patch * add some logs * remove unreachable condition * log errors * handle errors on provide key directly on subscription * Downgrade RocketChat/react-native-simple-crypto * improve get room instance * migration of older apps * check encrypted status before send a message * wait client ready * use our own base64-js * add more jest tests * explain return * remove unncessary stop * thrown error to caller * remove superfluous checks * use Encryption property * change ready state logic * ready -> establishing * encryption.room -> encryptionRoom * EncryptionRoom -> Room * add documentation * wait establishing before provide a room key * remove superfluous condition * improve error handling logic * fallback e2ekey set * remove no longer necessary check * remove e.g. * improve getRoomInstance * import from index * use batch * fix a comment * decrypt tmsg * dont show hash when message is encrypted * Fix detox * Apply suggestions from code review Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
parent
e9531298e7
commit
3c9017a62d
|
@ -3333,6 +3333,165 @@ exports[`Storyshots Message list message 1`] = `
|
||||||
>
|
>
|
||||||
Edited
|
Edited
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"fontSize": 20,
|
||||||
|
"fontWeight": "300",
|
||||||
|
"marginLeft": 10,
|
||||||
|
"marginVertical": 30,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"color": "#0d0e12",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"marginBottom": 0,
|
||||||
|
"marginTop": 30,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Encrypted
|
||||||
|
</Text>
|
||||||
|
<View>
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"flexDirection": "column",
|
||||||
|
"paddingHorizontal": 14,
|
||||||
|
"paddingVertical": 4,
|
||||||
|
"width": "100%",
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"flexDirection": "row",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"borderRadius": 4,
|
||||||
|
"height": 36,
|
||||||
|
"width": 36,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"marginTop": 4,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"flex": 1,
|
||||||
|
"marginLeft": 46,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"marginLeft": 10,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"alignItems": "center",
|
||||||
|
"flex": 1,
|
||||||
|
"flexDirection": "row",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fontFamily": "System",
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": "300",
|
||||||
|
"lineHeight": 22,
|
||||||
|
"paddingLeft": 10,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"color": "#9ca2a8",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
10:00 AM
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"flexDirection": "row",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"flex": 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
numberOfLines={0}
|
||||||
|
style={
|
||||||
|
Array [
|
||||||
|
undefined,
|
||||||
|
Object {
|
||||||
|
"color": "#2f343d",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
accessibilityLabel="This message has error and is encrypted"
|
||||||
|
numberOfLines={0}
|
||||||
|
style={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"fontFamily": "System",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontWeight": "400",
|
||||||
|
},
|
||||||
|
Array [
|
||||||
|
Object {},
|
||||||
|
Object {
|
||||||
|
"alignItems": "flex-start",
|
||||||
|
"flexDirection": "row",
|
||||||
|
"flexWrap": "wrap",
|
||||||
|
"justifyContent": "flex-start",
|
||||||
|
"marginBottom": 0,
|
||||||
|
"marginTop": 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
This message has error and is encrypted
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
<Text
|
<Text
|
||||||
style={
|
style={
|
||||||
Array [
|
Array [
|
||||||
|
|
|
@ -67,3 +67,4 @@ export const INVITE_LINKS = createRequestTypes('INVITE_LINKS', [
|
||||||
export const SETTINGS = createRequestTypes('SETTINGS', ['CLEAR', 'ADD']);
|
export const SETTINGS = createRequestTypes('SETTINGS', ['CLEAR', 'ADD']);
|
||||||
export const APP_STATE = createRequestTypes('APP_STATE', ['FOREGROUND', 'BACKGROUND']);
|
export const APP_STATE = createRequestTypes('APP_STATE', ['FOREGROUND', 'BACKGROUND']);
|
||||||
export const ENTERPRISE_MODULES = createRequestTypes('ENTERPRISE_MODULES', ['CLEAR', 'SET']);
|
export const ENTERPRISE_MODULES = createRequestTypes('ENTERPRISE_MODULES', ['CLEAR', 'SET']);
|
||||||
|
export const ENCRYPTION = createRequestTypes('ENCRYPTION', ['INIT', 'STOP', 'DECODE_KEY', 'SET_BANNER']);
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import * as types from './actionsTypes';
|
||||||
|
|
||||||
|
export function encryptionInit() {
|
||||||
|
return {
|
||||||
|
type: types.ENCRYPTION.INIT
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encryptionStop() {
|
||||||
|
return {
|
||||||
|
type: types.ENCRYPTION.STOP
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encryptionSetBanner(banner) {
|
||||||
|
return {
|
||||||
|
type: types.ENCRYPTION.SET_BANNER,
|
||||||
|
banner
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encryptionDecodeKey(password) {
|
||||||
|
return {
|
||||||
|
type: types.ENCRYPTION.DECODE_KEY,
|
||||||
|
password
|
||||||
|
};
|
||||||
|
}
|
|
@ -68,6 +68,9 @@ export default {
|
||||||
DirectMesssage_maxUsers: {
|
DirectMesssage_maxUsers: {
|
||||||
type: 'valueAsNumber'
|
type: 'valueAsNumber'
|
||||||
},
|
},
|
||||||
|
E2E_Enable: {
|
||||||
|
type: 'valueAsBoolean'
|
||||||
|
},
|
||||||
Accounts_Directory_DefaultView: {
|
Accounts_Directory_DefaultView: {
|
||||||
type: 'valueAsString'
|
type: 'valueAsString'
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,6 +9,8 @@ import Markdown from '../markdown';
|
||||||
import { getInfoMessage } from './utils';
|
import { getInfoMessage } from './utils';
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
import MessageContext from './Context';
|
import MessageContext from './Context';
|
||||||
|
import Encrypted from './Encrypted';
|
||||||
|
import { E2E_MESSAGE_TYPE } from '../../lib/encryption/constants';
|
||||||
|
|
||||||
const Content = React.memo((props) => {
|
const Content = React.memo((props) => {
|
||||||
if (props.isInfo) {
|
if (props.isInfo) {
|
||||||
|
@ -22,10 +24,13 @@ const Content = React.memo((props) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isPreview = props.tmid && !props.isThreadRoom;
|
||||||
let content = null;
|
let content = null;
|
||||||
|
|
||||||
if (props.tmid && !props.msg) {
|
if (props.tmid && !props.msg) {
|
||||||
content = <Text style={[styles.text, { color: themes[props.theme].bodyText }]}>{I18n.t('Sent_an_attachment')}</Text>;
|
content = <Text style={[styles.text, { color: themes[props.theme].bodyText }]}>{I18n.t('Sent_an_attachment')}</Text>;
|
||||||
|
} else if (props.isEncrypted) {
|
||||||
|
content = <Text style={[styles.textInfo, { color: themes[props.theme].auxiliaryText }]}>{I18n.t('Encrypted_message')}</Text>;
|
||||||
} else {
|
} else {
|
||||||
const { baseUrl, user } = useContext(MessageContext);
|
const { baseUrl, user } = useContext(MessageContext);
|
||||||
content = (
|
content = (
|
||||||
|
@ -35,8 +40,8 @@ const Content = React.memo((props) => {
|
||||||
getCustomEmoji={props.getCustomEmoji}
|
getCustomEmoji={props.getCustomEmoji}
|
||||||
username={user.username}
|
username={user.username}
|
||||||
isEdited={props.isEdited}
|
isEdited={props.isEdited}
|
||||||
numberOfLines={(props.tmid && !props.isThreadRoom) ? 1 : 0}
|
numberOfLines={isPreview ? 1 : 0}
|
||||||
preview={props.tmid && !props.isThreadRoom}
|
preview={isPreview}
|
||||||
channels={props.channels}
|
channels={props.channels}
|
||||||
mentions={props.mentions}
|
mentions={props.mentions}
|
||||||
navToRoomInfo={props.navToRoomInfo}
|
navToRoomInfo={props.navToRoomInfo}
|
||||||
|
@ -47,6 +52,21 @@ const Content = React.memo((props) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If this is a encrypted message and is not a preview
|
||||||
|
if (props.type === E2E_MESSAGE_TYPE && !isPreview) {
|
||||||
|
content = (
|
||||||
|
<View style={styles.flex}>
|
||||||
|
<View style={styles.contentContainer}>
|
||||||
|
{content}
|
||||||
|
</View>
|
||||||
|
<Encrypted
|
||||||
|
type={props.type}
|
||||||
|
theme={props.theme}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={props.isTemp && styles.temp}>
|
<View style={props.isTemp && styles.temp}>
|
||||||
{content}
|
{content}
|
||||||
|
@ -59,9 +79,15 @@ const Content = React.memo((props) => {
|
||||||
if (prevProps.msg !== nextProps.msg) {
|
if (prevProps.msg !== nextProps.msg) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (prevProps.type !== nextProps.type) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (prevProps.theme !== nextProps.theme) {
|
if (prevProps.theme !== nextProps.theme) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (prevProps.isEncrypted !== nextProps.isEncrypted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!equal(prevProps.mentions, nextProps.mentions)) {
|
if (!equal(prevProps.mentions, nextProps.mentions)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -79,11 +105,13 @@ Content.propTypes = {
|
||||||
msg: PropTypes.string,
|
msg: PropTypes.string,
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
isEdited: PropTypes.bool,
|
isEdited: PropTypes.bool,
|
||||||
|
isEncrypted: PropTypes.bool,
|
||||||
getCustomEmoji: PropTypes.func,
|
getCustomEmoji: PropTypes.func,
|
||||||
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||||
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||||
navToRoomInfo: PropTypes.func,
|
navToRoomInfo: PropTypes.func,
|
||||||
useRealName: PropTypes.bool
|
useRealName: PropTypes.bool,
|
||||||
|
type: PropTypes.string
|
||||||
};
|
};
|
||||||
Content.displayName = 'MessageContent';
|
Content.displayName = 'MessageContent';
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import React, { useContext } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import Touchable from './Touchable';
|
||||||
|
import { E2E_MESSAGE_TYPE } from '../../lib/encryption/constants';
|
||||||
|
import { CustomIcon } from '../../lib/Icons';
|
||||||
|
import { themes } from '../../constants/colors';
|
||||||
|
import { BUTTON_HIT_SLOP } from './utils';
|
||||||
|
import MessageContext from './Context';
|
||||||
|
import styles from './styles';
|
||||||
|
|
||||||
|
const Encrypted = React.memo(({ type, theme }) => {
|
||||||
|
if (type !== E2E_MESSAGE_TYPE) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { onEncryptedPress } = useContext(MessageContext);
|
||||||
|
return (
|
||||||
|
<Touchable onPress={onEncryptedPress} style={styles.encrypted} hitSlop={BUTTON_HIT_SLOP}>
|
||||||
|
<CustomIcon name='encrypted' size={16} color={themes[theme].auxiliaryText} />
|
||||||
|
</Touchable>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Encrypted.propTypes = {
|
||||||
|
type: PropTypes.string,
|
||||||
|
theme: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Encrypted;
|
|
@ -8,9 +8,10 @@ import { CustomIcon } from '../../lib/Icons';
|
||||||
import DisclosureIndicator from '../DisclosureIndicator';
|
import DisclosureIndicator from '../DisclosureIndicator';
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
|
import I18n from '../../i18n';
|
||||||
|
|
||||||
const RepliedThread = React.memo(({
|
const RepliedThread = React.memo(({
|
||||||
tmid, tmsg, isHeader, fetchThreadName, id, theme
|
tmid, tmsg, isHeader, fetchThreadName, id, isEncrypted, theme
|
||||||
}) => {
|
}) => {
|
||||||
if (!tmid || !isHeader) {
|
if (!tmid || !isHeader) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -24,6 +25,10 @@ const RepliedThread = React.memo(({
|
||||||
let msg = shortnameToUnicode(tmsg);
|
let msg = shortnameToUnicode(tmsg);
|
||||||
msg = removeMarkdown(msg);
|
msg = removeMarkdown(msg);
|
||||||
|
|
||||||
|
if (isEncrypted) {
|
||||||
|
msg = I18n.t('Encrypted_message');
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.repliedThread} testID={`message-thread-replied-on-${ msg }`}>
|
<View style={styles.repliedThread} testID={`message-thread-replied-on-${ msg }`}>
|
||||||
<CustomIcon name='threads' size={20} style={styles.repliedThreadIcon} color={themes[theme].tintColor} />
|
<CustomIcon name='threads' size={20} style={styles.repliedThreadIcon} color={themes[theme].tintColor} />
|
||||||
|
@ -38,6 +43,9 @@ const RepliedThread = React.memo(({
|
||||||
if (prevProps.tmsg !== nextProps.tmsg) {
|
if (prevProps.tmsg !== nextProps.tmsg) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (prevProps.isEncrypted !== nextProps.isEncrypted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (prevProps.isHeader !== nextProps.isHeader) {
|
if (prevProps.isHeader !== nextProps.isHeader) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -53,7 +61,8 @@ RepliedThread.propTypes = {
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
isHeader: PropTypes.bool,
|
isHeader: PropTypes.bool,
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
fetchThreadName: PropTypes.func
|
fetchThreadName: PropTypes.func,
|
||||||
|
isEncrypted: PropTypes.bool
|
||||||
};
|
};
|
||||||
RepliedThread.displayName = 'MessageRepliedThread';
|
RepliedThread.displayName = 'MessageRepliedThread';
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import debounce from '../../utils/debounce';
|
||||||
import { SYSTEM_MESSAGES, getMessageTranslation } from './utils';
|
import { SYSTEM_MESSAGES, getMessageTranslation } from './utils';
|
||||||
import messagesStatus from '../../constants/messagesStatus';
|
import messagesStatus from '../../constants/messagesStatus';
|
||||||
import { withTheme } from '../../theme';
|
import { withTheme } from '../../theme';
|
||||||
|
import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/encryption/constants';
|
||||||
|
|
||||||
class MessageContainer extends React.Component {
|
class MessageContainer extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -35,6 +36,7 @@ class MessageContainer extends React.Component {
|
||||||
getCustomEmoji: PropTypes.func,
|
getCustomEmoji: PropTypes.func,
|
||||||
onLongPress: PropTypes.func,
|
onLongPress: PropTypes.func,
|
||||||
onReactionPress: PropTypes.func,
|
onReactionPress: PropTypes.func,
|
||||||
|
onEncryptedPress: PropTypes.func,
|
||||||
onDiscussionPress: PropTypes.func,
|
onDiscussionPress: PropTypes.func,
|
||||||
onThreadPress: PropTypes.func,
|
onThreadPress: PropTypes.func,
|
||||||
errorActionsShow: PropTypes.func,
|
errorActionsShow: PropTypes.func,
|
||||||
|
@ -53,6 +55,7 @@ class MessageContainer extends React.Component {
|
||||||
getCustomEmoji: () => {},
|
getCustomEmoji: () => {},
|
||||||
onLongPress: () => {},
|
onLongPress: () => {},
|
||||||
onReactionPress: () => {},
|
onReactionPress: () => {},
|
||||||
|
onEncryptedPress: () => {},
|
||||||
onDiscussionPress: () => {},
|
onDiscussionPress: () => {},
|
||||||
onThreadPress: () => {},
|
onThreadPress: () => {},
|
||||||
errorActionsShow: () => {},
|
errorActionsShow: () => {},
|
||||||
|
@ -104,7 +107,7 @@ class MessageContainer extends React.Component {
|
||||||
|
|
||||||
onLongPress = () => {
|
onLongPress = () => {
|
||||||
const { archived, onLongPress, item } = this.props;
|
const { archived, onLongPress, item } = this.props;
|
||||||
if (this.isInfo || this.hasError || archived) {
|
if (this.isInfo || this.hasError || this.isEncrypted || archived) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (onLongPress) {
|
if (onLongPress) {
|
||||||
|
@ -133,6 +136,13 @@ class MessageContainer extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onEncryptedPress = () => {
|
||||||
|
const { onEncryptedPress } = this.props;
|
||||||
|
if (onEncryptedPress) {
|
||||||
|
onEncryptedPress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onDiscussionPress = () => {
|
onDiscussionPress = () => {
|
||||||
const { onDiscussionPress, item } = this.props;
|
const { onDiscussionPress, item } = this.props;
|
||||||
if (onDiscussionPress) {
|
if (onDiscussionPress) {
|
||||||
|
@ -196,6 +206,12 @@ class MessageContainer extends React.Component {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isEncrypted() {
|
||||||
|
const { item } = this.props;
|
||||||
|
const { t, e2e } = item;
|
||||||
|
return t === E2E_MESSAGE_TYPE && e2e !== E2E_STATUS.DONE;
|
||||||
|
}
|
||||||
|
|
||||||
get isInfo() {
|
get isInfo() {
|
||||||
const { item } = this.props;
|
const { item } = this.props;
|
||||||
return SYSTEM_MESSAGES.includes(item.t);
|
return SYSTEM_MESSAGES.includes(item.t);
|
||||||
|
@ -251,6 +267,7 @@ class MessageContainer extends React.Component {
|
||||||
onErrorPress: this.onErrorPress,
|
onErrorPress: this.onErrorPress,
|
||||||
replyBroadcast: this.replyBroadcast,
|
replyBroadcast: this.replyBroadcast,
|
||||||
onReactionPress: this.onReactionPress,
|
onReactionPress: this.onReactionPress,
|
||||||
|
onEncryptedPress: this.onEncryptedPress,
|
||||||
onDiscussionPress: this.onDiscussionPress,
|
onDiscussionPress: this.onDiscussionPress,
|
||||||
onReactionLongPress: this.onReactionLongPress
|
onReactionLongPress: this.onReactionLongPress
|
||||||
}}
|
}}
|
||||||
|
@ -295,6 +312,7 @@ class MessageContainer extends React.Component {
|
||||||
isThreadRoom={isThreadRoom}
|
isThreadRoom={isThreadRoom}
|
||||||
isInfo={this.isInfo}
|
isInfo={this.isInfo}
|
||||||
isTemp={this.isTemp}
|
isTemp={this.isTemp}
|
||||||
|
isEncrypted={this.isEncrypted}
|
||||||
hasError={this.hasError}
|
hasError={this.hasError}
|
||||||
showAttachment={showAttachment}
|
showAttachment={showAttachment}
|
||||||
getCustomEmoji={getCustomEmoji}
|
getCustomEmoji={getCustomEmoji}
|
||||||
|
|
|
@ -13,6 +13,9 @@ export default StyleSheet.create({
|
||||||
paddingHorizontal: 14,
|
paddingHorizontal: 14,
|
||||||
flexDirection: 'column'
|
flexDirection: 'column'
|
||||||
},
|
},
|
||||||
|
contentContainer: {
|
||||||
|
flex: 1
|
||||||
|
},
|
||||||
messageContent: {
|
messageContent: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
marginLeft: 46
|
marginLeft: 46
|
||||||
|
@ -163,5 +166,8 @@ export default StyleSheet.create({
|
||||||
},
|
},
|
||||||
readReceipt: {
|
readReceipt: {
|
||||||
lineHeight: 20
|
lineHeight: 20
|
||||||
|
},
|
||||||
|
encrypted: {
|
||||||
|
justifyContent: 'center'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -198,11 +198,17 @@ export default {
|
||||||
Do_you_have_an_account: 'Do you have an account?',
|
Do_you_have_an_account: 'Do you have an account?',
|
||||||
Do_you_have_a_certificate: 'Do you have a certificate?',
|
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?',
|
Do_you_really_want_to_key_this_room_question_mark: 'Do you really want to {{key}} this room?',
|
||||||
|
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.',
|
||||||
|
E2E_How_It_Works_info4: 'You can also setup a new password for your encryption key any time from any browser you have entered the existing E2E password.',
|
||||||
edit: 'edit',
|
edit: 'edit',
|
||||||
edited: 'edited',
|
edited: 'edited',
|
||||||
Edit: 'Edit',
|
Edit: 'Edit',
|
||||||
Edit_Status: 'Edit Status',
|
Edit_Status: 'Edit Status',
|
||||||
Edit_Invite: 'Edit Invite',
|
Edit_Invite: 'Edit Invite',
|
||||||
|
End_to_end_encrypted_room: 'End to end encrypted room',
|
||||||
|
end_to_end_encryption: 'end to end encryption',
|
||||||
Email_Notification_Mode_All: 'Every Mention/DM',
|
Email_Notification_Mode_All: 'Every Mention/DM',
|
||||||
Email_Notification_Mode_Disabled: 'Disabled',
|
Email_Notification_Mode_Disabled: 'Disabled',
|
||||||
Email_or_password_field_is_empty: 'Email or password field is empty',
|
Email_or_password_field_is_empty: 'Email or password field is empty',
|
||||||
|
@ -212,6 +218,13 @@ export default {
|
||||||
Empty_title: 'Empty title',
|
Empty_title: 'Empty title',
|
||||||
Enable_Auto_Translate: 'Enable Auto-Translate',
|
Enable_Auto_Translate: 'Enable Auto-Translate',
|
||||||
Enable_notifications: 'Enable notifications',
|
Enable_notifications: 'Enable notifications',
|
||||||
|
Encrypted: 'Encrypted',
|
||||||
|
Encrypted_message: 'Encrypted message',
|
||||||
|
Enter_Your_E2E_Password: 'Enter Your E2E Password',
|
||||||
|
Enter_Your_Encryption_Password_desc1: 'This will allow you to access your encrypted private groups and direct messages.',
|
||||||
|
Enter_Your_Encryption_Password_desc2: 'You need to enter the password to encode/decode messages every place you use the chat.',
|
||||||
|
Encryption_error_title: 'Your encryption password seems wrong',
|
||||||
|
Encryption_error_desc: 'Wasn\'t possible to decode your encryption key to be imported.',
|
||||||
Everyone_can_access_this_channel: 'Everyone can access this channel',
|
Everyone_can_access_this_channel: 'Everyone can access this channel',
|
||||||
Error_uploading: 'Error uploading',
|
Error_uploading: 'Error uploading',
|
||||||
Expiration_Days: 'Expiration (Days)',
|
Expiration_Days: 'Expiration (Days)',
|
||||||
|
@ -240,6 +253,7 @@ export default {
|
||||||
Has_left_the_channel: 'Has left the channel',
|
Has_left_the_channel: 'Has left the channel',
|
||||||
Hide_System_Messages: 'Hide System Messages',
|
Hide_System_Messages: 'Hide System Messages',
|
||||||
Hide_type_messages: 'Hide "{{type}}" messages',
|
Hide_type_messages: 'Hide "{{type}}" messages',
|
||||||
|
How_It_Works: 'How It Works',
|
||||||
Message_HideType_uj: 'User Join',
|
Message_HideType_uj: 'User Join',
|
||||||
Message_HideType_ul: 'User Leave',
|
Message_HideType_ul: 'User Leave',
|
||||||
Message_HideType_ru: 'User Removed',
|
Message_HideType_ru: 'User Removed',
|
||||||
|
@ -253,6 +267,7 @@ export default {
|
||||||
Message_HideType_subscription_role_removed: 'Role No Longer Defined',
|
Message_HideType_subscription_role_removed: 'Role No Longer Defined',
|
||||||
Message_HideType_room_archived: 'Room Archived',
|
Message_HideType_room_archived: 'Room Archived',
|
||||||
Message_HideType_room_unarchived: 'Room Unarchived',
|
Message_HideType_room_unarchived: 'Room Unarchived',
|
||||||
|
I_Saved_My_E2E_Password: 'I Saved My E2E Password',
|
||||||
IP: 'IP',
|
IP: 'IP',
|
||||||
In_app: 'In-app',
|
In_app: 'In-app',
|
||||||
IN_APP_AND_DESKTOP: 'IN-APP AND DESKTOP',
|
IN_APP_AND_DESKTOP: 'IN-APP AND DESKTOP',
|
||||||
|
@ -438,6 +453,10 @@ export default {
|
||||||
saving_profile: 'saving profile',
|
saving_profile: 'saving profile',
|
||||||
saving_settings: 'saving settings',
|
saving_settings: 'saving settings',
|
||||||
saved_to_gallery: 'Saved to gallery',
|
saved_to_gallery: 'Saved to gallery',
|
||||||
|
Save_Your_E2E_Password: 'Save Your E2E Password',
|
||||||
|
Save_Your_Encryption_Password: 'Save Your Encryption Password',
|
||||||
|
Save_Your_Encryption_Password_warning: 'This password is not stored anywhere so save it carefully somewhere else.',
|
||||||
|
Save_Your_Encryption_Password_info: 'Notice that you lose your password, there is no way to recover it and you will lose access to your messages.',
|
||||||
Search_Messages: 'Search Messages',
|
Search_Messages: 'Search Messages',
|
||||||
Search: 'Search',
|
Search: 'Search',
|
||||||
Search_by: 'Search by',
|
Search_by: 'Search by',
|
||||||
|
@ -585,6 +604,7 @@ export default {
|
||||||
Your_invite_link_will_expire_on__date__: 'Your invite link will expire on {{date}}.',
|
Your_invite_link_will_expire_on__date__: 'Your invite link will expire on {{date}}.',
|
||||||
Your_invite_link_will_never_expire: 'Your invite link will never expire.',
|
Your_invite_link_will_never_expire: 'Your invite link will never expire.',
|
||||||
Your_workspace: 'Your workspace',
|
Your_workspace: 'Your workspace',
|
||||||
|
Your_password_is: 'Your password is',
|
||||||
Version_no: 'Version: {{version}}',
|
Version_no: 'Version: {{version}}',
|
||||||
You_will_not_be_able_to_recover_this_message: 'You will not be able to recover this message!',
|
You_will_not_be_able_to_recover_this_message: 'You will not be able to recover this message!',
|
||||||
You_will_unset_a_certificate_for_this_server: 'You will unset a certificate for this server',
|
You_will_unset_a_certificate_for_this_server: 'You will unset a certificate for this server',
|
||||||
|
|
|
@ -192,11 +192,17 @@ export default {
|
||||||
Dont_Have_An_Account: 'Não tem uma conta?',
|
Dont_Have_An_Account: 'Não tem uma conta?',
|
||||||
Do_you_have_an_account: 'Você 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?',
|
Do_you_really_want_to_key_this_room_question_mark: 'Você quer realmente {{key}} esta sala?',
|
||||||
|
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.',
|
||||||
|
E2E_How_It_Works_info4: 'Você também pode configurar uma nova senha para sua chave de criptografia a qualquer momento em qualquer navegador em que tenha inserido a senha E2E existente.',
|
||||||
edit: 'editar',
|
edit: 'editar',
|
||||||
edited: 'editado',
|
edited: 'editado',
|
||||||
Edit: 'Editar',
|
Edit: 'Editar',
|
||||||
Edit_Invite: 'Editar convite',
|
Edit_Invite: 'Editar convite',
|
||||||
Edit_Status: 'Editar Status',
|
Edit_Status: 'Editar Status',
|
||||||
|
End_to_end_encrypted_room: 'Sala criptografada de ponta a ponta',
|
||||||
|
end_to_end_encryption: 'criptografia de ponta a ponta',
|
||||||
Email_or_password_field_is_empty: 'Email ou senha estão vazios',
|
Email_or_password_field_is_empty: 'Email ou senha estão vazios',
|
||||||
Email: 'Email',
|
Email: 'Email',
|
||||||
email: 'e-mail',
|
email: 'e-mail',
|
||||||
|
@ -205,6 +211,13 @@ export default {
|
||||||
Email_Notification_Mode_Disabled: 'Desativado',
|
Email_Notification_Mode_Disabled: 'Desativado',
|
||||||
Enable_Auto_Translate: 'Ativar a tradução automática',
|
Enable_Auto_Translate: 'Ativar a tradução automática',
|
||||||
Enable_notifications: 'Habilitar notificações',
|
Enable_notifications: 'Habilitar notificações',
|
||||||
|
Encrypted: 'Criptografado',
|
||||||
|
Encrypted_message: 'Mensagem criptografada',
|
||||||
|
Enter_Your_E2E_Password: 'Digite Sua Senha E2E',
|
||||||
|
Enter_Your_Encryption_Password_desc1: 'Isso permitirá que você acesse seus grupos privados e mensagens diretas criptografadas.',
|
||||||
|
Enter_Your_Encryption_Password_desc2: 'Você precisa inserir a senha para codificar/decodificar mensagens em todos os lugares em que usar o chat.',
|
||||||
|
Encryption_error_title: 'Sua senha de criptografia parece errada',
|
||||||
|
Encryption_error_desc: 'Não foi possível decodificar sua chave de criptografia para ser importada.',
|
||||||
Everyone_can_access_this_channel: 'Todos podem acessar este canal',
|
Everyone_can_access_this_channel: 'Todos podem acessar este canal',
|
||||||
Error_uploading: 'Erro subindo',
|
Error_uploading: 'Erro subindo',
|
||||||
Expiration_Days: 'Expira em (dias)',
|
Expiration_Days: 'Expira em (dias)',
|
||||||
|
@ -410,6 +423,10 @@ export default {
|
||||||
saving_profile: 'salvando perfil',
|
saving_profile: 'salvando perfil',
|
||||||
saving_settings: 'salvando configurações',
|
saving_settings: 'salvando configurações',
|
||||||
saved_to_gallery: 'Salvo na galeria',
|
saved_to_gallery: 'Salvo na galeria',
|
||||||
|
Save_Your_E2E_Password: 'Salve sua senha E2E',
|
||||||
|
Save_Your_Encryption_Password: 'Salve Sua Senha de Criptografia',
|
||||||
|
Save_Your_Encryption_Password_warning: 'Esta senha não é armazenada em nenhum lugar, portanto, salve-a com cuidado em outro lugar.',
|
||||||
|
Save_Your_Encryption_Password_info: 'Observe que se você perder sua senha, não há como recuperá-la e você perderá o acesso às suas mensagens.',
|
||||||
Search_Messages: 'Buscar Mensagens',
|
Search_Messages: 'Buscar Mensagens',
|
||||||
Search: 'Buscar',
|
Search: 'Buscar',
|
||||||
Search_by: 'Buscar por',
|
Search_by: 'Buscar por',
|
||||||
|
|
|
@ -77,4 +77,6 @@ export default class Message extends Model {
|
||||||
@field('tmsg') tmsg;
|
@field('tmsg') tmsg;
|
||||||
|
|
||||||
@json('blocks', sanitizer) blocks;
|
@json('blocks', sanitizer) blocks;
|
||||||
|
|
||||||
|
@field('e2e') e2e;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,8 @@ export default class Room extends Model {
|
||||||
|
|
||||||
@field('encrypted') encrypted;
|
@field('encrypted') encrypted;
|
||||||
|
|
||||||
|
@field('e2e_key_id') e2eKeyId;
|
||||||
|
|
||||||
@field('ro') ro;
|
@field('ro') ro;
|
||||||
|
|
||||||
@json('v', sanitizer) v;
|
@json('v', sanitizer) v;
|
||||||
|
|
|
@ -29,4 +29,6 @@ export default class Server extends Model {
|
||||||
@field('unique_id') uniqueID;
|
@field('unique_id') uniqueID;
|
||||||
|
|
||||||
@field('enterprise_modules') enterpriseModules;
|
@field('enterprise_modules') enterpriseModules;
|
||||||
|
|
||||||
|
@field('e2e_enable') E2E_Enable;
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,4 +109,10 @@ export default class Subscription extends Model {
|
||||||
@json('livechat_data', sanitizer) livechatData;
|
@json('livechat_data', sanitizer) livechatData;
|
||||||
|
|
||||||
@json('tags', sanitizer) tags;
|
@json('tags', sanitizer) tags;
|
||||||
|
|
||||||
|
@field('e2e_key') E2EKey;
|
||||||
|
|
||||||
|
@field('encrypted') encrypted;
|
||||||
|
|
||||||
|
@field('e2e_key_id') e2eKeyId;
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,4 +73,6 @@ export default class Thread extends Model {
|
||||||
@field('auto_translate') autoTranslate;
|
@field('auto_translate') autoTranslate;
|
||||||
|
|
||||||
@json('translations', sanitizer) translations;
|
@json('translations', sanitizer) translations;
|
||||||
|
|
||||||
|
@field('e2e') e2e;
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,4 +75,6 @@ export default class ThreadMessage extends Model {
|
||||||
@json('translations', sanitizer) translations;
|
@json('translations', sanitizer) translations;
|
||||||
|
|
||||||
@field('draft_message') draftMessage;
|
@field('draft_message') draftMessage;
|
||||||
|
|
||||||
|
@field('e2e') e2e;
|
||||||
}
|
}
|
||||||
|
|
|
@ -129,6 +129,43 @@ export default schemaMigrations({
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
toVersion: 10,
|
||||||
|
steps: [
|
||||||
|
addColumns({
|
||||||
|
table: 'subscriptions',
|
||||||
|
columns: [
|
||||||
|
{ name: 'e2e_key', type: 'string', isOptional: true },
|
||||||
|
{ name: 'encrypted', type: 'boolean', isOptional: true },
|
||||||
|
{ name: 'e2e_key_id', type: 'string', isOptional: true }
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
addColumns({
|
||||||
|
table: 'messages',
|
||||||
|
columns: [
|
||||||
|
{ name: 'e2e', type: 'string', isOptional: true }
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
addColumns({
|
||||||
|
table: 'thread_messages',
|
||||||
|
columns: [
|
||||||
|
{ name: 'e2e', type: 'string', isOptional: true }
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
addColumns({
|
||||||
|
table: 'threads',
|
||||||
|
columns: [
|
||||||
|
{ name: 'e2e', type: 'string', isOptional: true }
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
addColumns({
|
||||||
|
table: 'rooms',
|
||||||
|
columns: [
|
||||||
|
{ name: 'e2e_key_id', type: 'string', isOptional: true }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
|
@ -59,6 +59,17 @@ export default schemaMigrations({
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
toVersion: 8,
|
||||||
|
steps: [
|
||||||
|
addColumns({
|
||||||
|
table: 'servers',
|
||||||
|
columns: [
|
||||||
|
{ name: 'e2e_enable', type: 'boolean', isOptional: true }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { appSchema, tableSchema } from '@nozbe/watermelondb';
|
import { appSchema, tableSchema } from '@nozbe/watermelondb';
|
||||||
|
|
||||||
export default appSchema({
|
export default appSchema({
|
||||||
version: 9,
|
version: 10,
|
||||||
tables: [
|
tables: [
|
||||||
tableSchema({
|
tableSchema({
|
||||||
name: 'subscriptions',
|
name: 'subscriptions',
|
||||||
|
@ -49,7 +49,10 @@ export default appSchema({
|
||||||
{ name: 'department_id', type: 'string', isOptional: true },
|
{ name: 'department_id', type: 'string', isOptional: true },
|
||||||
{ name: 'served_by', type: 'string', isOptional: true },
|
{ name: 'served_by', type: 'string', isOptional: true },
|
||||||
{ name: 'livechat_data', type: 'string', isOptional: true },
|
{ name: 'livechat_data', type: 'string', isOptional: true },
|
||||||
{ name: 'tags', type: 'string', isOptional: true }
|
{ name: 'tags', type: 'string', isOptional: true },
|
||||||
|
{ name: 'e2e_key', type: 'string', isOptional: true },
|
||||||
|
{ name: 'encrypted', type: 'boolean', isOptional: true },
|
||||||
|
{ name: 'e2e_key_id', type: 'string', isOptional: true }
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
tableSchema({
|
tableSchema({
|
||||||
|
@ -63,7 +66,8 @@ export default appSchema({
|
||||||
{ name: 'department_id', type: 'string', isOptional: true },
|
{ name: 'department_id', type: 'string', isOptional: true },
|
||||||
{ name: 'served_by', type: 'string', isOptional: true },
|
{ name: 'served_by', type: 'string', isOptional: true },
|
||||||
{ name: 'livechat_data', type: 'string', isOptional: true },
|
{ name: 'livechat_data', type: 'string', isOptional: true },
|
||||||
{ name: 'tags', type: 'string', isOptional: true }
|
{ name: 'tags', type: 'string', isOptional: true },
|
||||||
|
{ name: 'e2e_key_id', type: 'string', isOptional: true }
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
tableSchema({
|
tableSchema({
|
||||||
|
@ -101,7 +105,8 @@ export default appSchema({
|
||||||
{ name: 'auto_translate', type: 'boolean', isOptional: true },
|
{ name: 'auto_translate', type: 'boolean', isOptional: true },
|
||||||
{ name: 'translations', type: 'string', isOptional: true },
|
{ name: 'translations', type: 'string', isOptional: true },
|
||||||
{ name: 'tmsg', type: 'string', isOptional: true },
|
{ name: 'tmsg', type: 'string', isOptional: true },
|
||||||
{ name: 'blocks', type: 'string', isOptional: true }
|
{ name: 'blocks', type: 'string', isOptional: true },
|
||||||
|
{ name: 'e2e', type: 'string', isOptional: true }
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
tableSchema({
|
tableSchema({
|
||||||
|
@ -137,7 +142,8 @@ export default appSchema({
|
||||||
{ name: 'channels', type: 'string', isOptional: true },
|
{ name: 'channels', type: 'string', isOptional: true },
|
||||||
{ name: 'unread', type: 'boolean', isOptional: true },
|
{ name: 'unread', type: 'boolean', isOptional: true },
|
||||||
{ name: 'auto_translate', type: 'boolean', isOptional: true },
|
{ name: 'auto_translate', type: 'boolean', isOptional: true },
|
||||||
{ name: 'translations', type: 'string', isOptional: true }
|
{ name: 'translations', type: 'string', isOptional: true },
|
||||||
|
{ name: 'e2e', type: 'string', isOptional: true }
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
tableSchema({
|
tableSchema({
|
||||||
|
@ -173,7 +179,8 @@ export default appSchema({
|
||||||
{ name: 'channels', type: 'string', isOptional: true },
|
{ name: 'channels', type: 'string', isOptional: true },
|
||||||
{ name: 'unread', type: 'boolean', isOptional: true },
|
{ name: 'unread', type: 'boolean', isOptional: true },
|
||||||
{ name: 'auto_translate', type: 'boolean', isOptional: true },
|
{ name: 'auto_translate', type: 'boolean', isOptional: true },
|
||||||
{ name: 'translations', type: 'string', isOptional: true }
|
{ name: 'translations', type: 'string', isOptional: true },
|
||||||
|
{ name: 'e2e', type: 'string', isOptional: true }
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
tableSchema({
|
tableSchema({
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { appSchema, tableSchema } from '@nozbe/watermelondb';
|
import { appSchema, tableSchema } from '@nozbe/watermelondb';
|
||||||
|
|
||||||
export default appSchema({
|
export default appSchema({
|
||||||
version: 7,
|
version: 8,
|
||||||
tables: [
|
tables: [
|
||||||
tableSchema({
|
tableSchema({
|
||||||
name: 'users',
|
name: 'users',
|
||||||
|
@ -31,7 +31,8 @@ export default appSchema({
|
||||||
{ name: 'auto_lock_time', type: 'number', isOptional: true },
|
{ name: 'auto_lock_time', type: 'number', isOptional: true },
|
||||||
{ name: 'biometry', type: 'boolean', isOptional: true },
|
{ name: 'biometry', type: 'boolean', isOptional: true },
|
||||||
{ name: 'unique_id', type: 'string', isOptional: true },
|
{ name: 'unique_id', type: 'string', isOptional: true },
|
||||||
{ name: 'enterprise_modules', type: 'string', isOptional: true }
|
{ name: 'enterprise_modules', type: 'string', isOptional: true },
|
||||||
|
{ name: 'e2e_enable', type: 'boolean', isOptional: true }
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Rocket.Chat Mobile
|
||||||
|
|
||||||
|
## E2E Encryption
|
||||||
|
|
||||||
|
> Note: This feature is currently in beta. Uploads will not be encrypted in this version.
|
||||||
|
You can check [this documentation](https://docs.rocket.chat/guides/user-guides/end-to-end-encryption) for further information about the web client.
|
||||||
|
|
||||||
|
### How it works
|
||||||
|
|
||||||
|
- Each user has a public and private key (asymmetric cryptography).
|
||||||
|
- The user private key is stored encrypted on the server and it can be decrypted on clients only using the user E2E encryption password.
|
||||||
|
- A room key is generated using the public key of each room member (symmetric cryptography).
|
||||||
|
- Users can decrypt the room key using their private key.
|
||||||
|
- Each room has a unique identifier which make users able to request a room key.
|
||||||
|
- The room unique identifier is called `e2eKeyId` and it's a property of the `room` collection.
|
||||||
|
- The room key is called `E2EKey` and it's a property of the `subscription` collection.
|
||||||
|
- After the room key is decrypted, the user is able to encrypt and decrypt messages of the room.
|
||||||
|
|
||||||
|
### User keys
|
||||||
|
|
||||||
|
* If the user doesn't have keys neither locally nor on the server, we create and encrypt them using a random password. These encrypted keys are sent to the server (so other clients can fetch) and saved locally.
|
||||||
|
* If the user have keys stored on server, but doesn't have them stored locally, we fetch them from the server and request a password to decrypt the keys.
|
||||||
|
|
||||||
|
### Room keys
|
||||||
|
|
||||||
|
* If the room has a `E2EKey`, we decrypt it using the user key.
|
||||||
|
* If the room doesn't have a `E2EKey`, but has a `e2eKeyId`, we *emit an event* on _stream-notify-room-users_ sending the `roomId` and the `e2eKeyId` requesting the `E2EKey` from any online room member.
|
||||||
|
* If the room have none of them, we create new ones and send them back to the server.
|
|
@ -0,0 +1,17 @@
|
||||||
|
export const E2E_MESSAGE_TYPE = 'e2e';
|
||||||
|
export const E2E_PUBLIC_KEY = 'RC_E2E_PUBLIC_KEY';
|
||||||
|
export const E2E_PRIVATE_KEY = 'RC_E2E_PRIVATE_KEY';
|
||||||
|
export const E2E_RANDOM_PASSWORD_KEY = 'RC_E2E_RANDOM_PASSWORD_KEY';
|
||||||
|
export const E2E_REFRESH_MESSAGES_KEY = 'E2E_REFRESH_MESSAGES_KEY';
|
||||||
|
export const E2E_STATUS = {
|
||||||
|
PENDING: 'pending',
|
||||||
|
DONE: 'done'
|
||||||
|
};
|
||||||
|
export const E2E_BANNER_TYPE = {
|
||||||
|
REQUEST_PASSWORD: 'REQUEST_PASSWORD',
|
||||||
|
SAVE_PASSWORD: 'SAVE_PASSWORD'
|
||||||
|
};
|
||||||
|
export const E2E_ROOM_TYPES = {
|
||||||
|
d: 'd',
|
||||||
|
p: 'p'
|
||||||
|
};
|
|
@ -0,0 +1,452 @@
|
||||||
|
import EJSON from 'ejson';
|
||||||
|
import SimpleCrypto from 'react-native-simple-crypto';
|
||||||
|
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
|
||||||
|
import { Q } from '@nozbe/watermelondb';
|
||||||
|
|
||||||
|
import {
|
||||||
|
toString,
|
||||||
|
utf8ToBuffer,
|
||||||
|
splitVectorData,
|
||||||
|
joinVectorData,
|
||||||
|
randomPassword
|
||||||
|
} from './utils';
|
||||||
|
import {
|
||||||
|
E2E_PUBLIC_KEY,
|
||||||
|
E2E_PRIVATE_KEY,
|
||||||
|
E2E_RANDOM_PASSWORD_KEY,
|
||||||
|
E2E_STATUS,
|
||||||
|
E2E_MESSAGE_TYPE,
|
||||||
|
E2E_BANNER_TYPE
|
||||||
|
} from './constants';
|
||||||
|
import RocketChat from '../rocketchat';
|
||||||
|
import { EncryptionRoom } from './index';
|
||||||
|
import UserPreferences from '../userPreferences';
|
||||||
|
import database from '../database';
|
||||||
|
import protectedFunction from '../methods/helpers/protectedFunction';
|
||||||
|
import Deferred from '../../utils/deferred';
|
||||||
|
import log from '../../utils/log';
|
||||||
|
import store from '../createStore';
|
||||||
|
|
||||||
|
class Encryption {
|
||||||
|
constructor() {
|
||||||
|
this.ready = false;
|
||||||
|
this.privateKey = null;
|
||||||
|
this.roomInstances = {};
|
||||||
|
this.readyPromise = new Deferred();
|
||||||
|
this.readyPromise
|
||||||
|
.then(() => {
|
||||||
|
this.ready = true;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.ready = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Encryption client
|
||||||
|
initialize = () => {
|
||||||
|
this.roomInstances = {};
|
||||||
|
|
||||||
|
// Don't await these promises
|
||||||
|
// so they can run parallelized
|
||||||
|
this.decryptPendingSubscriptions();
|
||||||
|
this.decryptPendingMessages();
|
||||||
|
|
||||||
|
// Mark Encryption client as ready
|
||||||
|
this.readyPromise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
get establishing() {
|
||||||
|
const { banner } = store.getState().encryption;
|
||||||
|
// If the password was not inserted yet
|
||||||
|
if (banner === E2E_BANNER_TYPE.REQUEST_PASSWORD) {
|
||||||
|
// We can't decrypt/encrypt, so, reject this try
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait the client ready state
|
||||||
|
return this.readyPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop Encryption client
|
||||||
|
stop = () => {
|
||||||
|
this.privateKey = null;
|
||||||
|
this.roomInstances = {};
|
||||||
|
// Cancel ongoing encryption/decryption requests
|
||||||
|
this.readyPromise.reject();
|
||||||
|
// Reset Deferred
|
||||||
|
this.ready = false;
|
||||||
|
this.readyPromise = new Deferred();
|
||||||
|
this.readyPromise
|
||||||
|
.then(() => {
|
||||||
|
this.ready = true;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.ready = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// When a new participant join and request a new room encryption key
|
||||||
|
provideRoomKeyToUser = async(keyId, rid) => {
|
||||||
|
// If the client is not ready
|
||||||
|
if (!this.ready) {
|
||||||
|
try {
|
||||||
|
// Wait for ready status
|
||||||
|
await this.establishing;
|
||||||
|
} catch {
|
||||||
|
// If it can't be initialized (missing password)
|
||||||
|
// return and don't provide a key
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomE2E = await this.getRoomInstance(rid);
|
||||||
|
return roomE2E.provideKeyToUser(keyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist keys on UserPreferences
|
||||||
|
persistKeys = async(server, publicKey, privateKey) => {
|
||||||
|
this.privateKey = await SimpleCrypto.RSA.importKey(EJSON.parse(privateKey));
|
||||||
|
await UserPreferences.setStringAsync(`${ server }-${ E2E_PUBLIC_KEY }`, EJSON.stringify(publicKey));
|
||||||
|
await UserPreferences.setStringAsync(`${ server }-${ E2E_PRIVATE_KEY }`, privateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Could not obtain public-private keypair from server.
|
||||||
|
createKeys = async(userId, server) => {
|
||||||
|
// Generate new keys
|
||||||
|
const key = await SimpleCrypto.RSA.generateKeys(2048);
|
||||||
|
|
||||||
|
// Cast these keys to the properly server format
|
||||||
|
const publicKey = await SimpleCrypto.RSA.exportKey(key.public);
|
||||||
|
const privateKey = await SimpleCrypto.RSA.exportKey(key.private);
|
||||||
|
|
||||||
|
// Persist these new keys
|
||||||
|
this.persistKeys(server, publicKey, EJSON.stringify(privateKey));
|
||||||
|
|
||||||
|
// Create a password to encode the private key
|
||||||
|
const password = await this.createRandomPassword(server);
|
||||||
|
|
||||||
|
// Encode the private key
|
||||||
|
const encodedPrivateKey = await this.encodePrivateKey(EJSON.stringify(privateKey), password, userId);
|
||||||
|
|
||||||
|
// Send the new keys to the server
|
||||||
|
await RocketChat.e2eSetUserPublicAndPrivateKeys(EJSON.stringify(publicKey), encodedPrivateKey);
|
||||||
|
|
||||||
|
// Request e2e keys of all encrypted rooms
|
||||||
|
await RocketChat.e2eRequestSubscriptionKeys();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode a private key before send it to the server
|
||||||
|
encodePrivateKey = async(privateKey, password, userId) => {
|
||||||
|
const masterKey = await this.generateMasterKey(password, userId);
|
||||||
|
|
||||||
|
const vector = await SimpleCrypto.utils.randomBytes(16);
|
||||||
|
const data = await SimpleCrypto.AES.encrypt(
|
||||||
|
utf8ToBuffer(privateKey),
|
||||||
|
masterKey,
|
||||||
|
vector
|
||||||
|
);
|
||||||
|
|
||||||
|
return EJSON.stringify(new Uint8Array(joinVectorData(vector, data)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode a private key fetched from server
|
||||||
|
decodePrivateKey = async(privateKey, password, userId) => {
|
||||||
|
const masterKey = await this.generateMasterKey(password, userId);
|
||||||
|
const [vector, cipherText] = splitVectorData(EJSON.parse(privateKey));
|
||||||
|
|
||||||
|
const privKey = await SimpleCrypto.AES.decrypt(
|
||||||
|
cipherText,
|
||||||
|
masterKey,
|
||||||
|
vector
|
||||||
|
);
|
||||||
|
|
||||||
|
return toString(privKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a user master key, this is based on userId and a password
|
||||||
|
generateMasterKey = async(password, userId) => {
|
||||||
|
const iterations = 1000;
|
||||||
|
const hash = 'SHA256';
|
||||||
|
const keyLen = 32;
|
||||||
|
|
||||||
|
const passwordBuffer = utf8ToBuffer(password);
|
||||||
|
const saltBuffer = utf8ToBuffer(userId);
|
||||||
|
|
||||||
|
const masterKey = await SimpleCrypto.PBKDF2.hash(
|
||||||
|
passwordBuffer,
|
||||||
|
saltBuffer,
|
||||||
|
iterations,
|
||||||
|
keyLen,
|
||||||
|
hash
|
||||||
|
);
|
||||||
|
|
||||||
|
return masterKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a random password to local created keys
|
||||||
|
createRandomPassword = async(server) => {
|
||||||
|
const password = randomPassword();
|
||||||
|
await UserPreferences.setStringAsync(`${ server }-${ E2E_RANDOM_PASSWORD_KEY }`, password);
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get a encryption room instance
|
||||||
|
getRoomInstance = async(rid) => {
|
||||||
|
// Prevent handshake again
|
||||||
|
if (this.roomInstances[rid]?.ready) {
|
||||||
|
return this.roomInstances[rid];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If doesn't have a instance of this room
|
||||||
|
if (!this.roomInstances[rid]) {
|
||||||
|
this.roomInstances[rid] = new EncryptionRoom(rid);
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomE2E = this.roomInstances[rid];
|
||||||
|
|
||||||
|
// Start Encryption Room instance handshake
|
||||||
|
await roomE2E.handshake();
|
||||||
|
|
||||||
|
return roomE2E;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logic to decrypt all pending messages/threads/threadMessages
|
||||||
|
// after initialize the encryption client
|
||||||
|
decryptPendingMessages = async(roomId) => {
|
||||||
|
const db = database.active;
|
||||||
|
|
||||||
|
const messagesCollection = db.collections.get('messages');
|
||||||
|
const threadsCollection = db.collections.get('threads');
|
||||||
|
const threadMessagesCollection = db.collections.get('thread_messages');
|
||||||
|
|
||||||
|
// e2e status is null or 'pending' and message type is 'e2e'
|
||||||
|
const whereClause = [
|
||||||
|
Q.where('t', E2E_MESSAGE_TYPE),
|
||||||
|
Q.or(
|
||||||
|
Q.where('e2e', null),
|
||||||
|
Q.where('e2e', E2E_STATUS.PENDING)
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
// decrypt messages of a room
|
||||||
|
if (roomId) {
|
||||||
|
whereClause.push(Q.where('rid', roomId));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find all messages/threads/threadsMessages that have pending e2e status
|
||||||
|
const messagesToDecrypt = await messagesCollection.query(...whereClause).fetch();
|
||||||
|
const threadsToDecrypt = await threadsCollection.query(...whereClause).fetch();
|
||||||
|
const threadMessagesToDecrypt = await threadMessagesCollection.query(...whereClause).fetch();
|
||||||
|
|
||||||
|
// Concat messages/threads/threadMessages
|
||||||
|
let toDecrypt = [...messagesToDecrypt, ...threadsToDecrypt, ...threadMessagesToDecrypt];
|
||||||
|
toDecrypt = await Promise.all(toDecrypt.map(async(message) => {
|
||||||
|
const { t, msg, tmsg } = message;
|
||||||
|
const { id: rid } = message.subscription;
|
||||||
|
// WM Object -> Plain Object
|
||||||
|
const newMessage = await this.decryptMessage({
|
||||||
|
t,
|
||||||
|
rid,
|
||||||
|
msg,
|
||||||
|
tmsg
|
||||||
|
});
|
||||||
|
if (message._hasPendingUpdate) {
|
||||||
|
console.log(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return message.prepareUpdate(protectedFunction((m) => {
|
||||||
|
Object.assign(m, newMessage);
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
|
||||||
|
await db.action(async() => {
|
||||||
|
await db.batch(...toDecrypt);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logic to decrypt all pending subscriptions
|
||||||
|
// after initialize the encryption client
|
||||||
|
decryptPendingSubscriptions = async() => {
|
||||||
|
const db = database.active;
|
||||||
|
const subCollection = db.collections.get('subscriptions');
|
||||||
|
try {
|
||||||
|
// Find all rooms that can have a lastMessage encrypted
|
||||||
|
// If we select only encrypted rooms we can miss some room that changed their encrypted status
|
||||||
|
const subsEncrypted = await subCollection.query(Q.where('e2e_key_id', Q.notEq(null))).fetch();
|
||||||
|
// We can't do this on database level since lastMessage is not a database object
|
||||||
|
const subsToDecrypt = subsEncrypted.filter(sub => (
|
||||||
|
// Encrypted message
|
||||||
|
sub?.lastMessage?.t === E2E_MESSAGE_TYPE
|
||||||
|
// Message pending decrypt
|
||||||
|
&& sub?.lastMessage?.e2e === E2E_STATUS.PENDING
|
||||||
|
));
|
||||||
|
await Promise.all(subsToDecrypt.map(async(sub) => {
|
||||||
|
const { rid, lastMessage } = sub;
|
||||||
|
const newSub = await this.decryptSubscription({ rid, lastMessage });
|
||||||
|
if (sub._hasPendingUpdate) {
|
||||||
|
console.log(sub);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return sub.prepareUpdate(protectedFunction((m) => {
|
||||||
|
Object.assign(m, newSub);
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
|
||||||
|
await db.action(async() => {
|
||||||
|
await db.batch(...subsToDecrypt);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt a subscription lastMessage
|
||||||
|
decryptSubscription = async(subscription) => {
|
||||||
|
// If the subscription doesn't have a lastMessage just return
|
||||||
|
if (!subscription?.lastMessage) {
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { lastMessage } = subscription;
|
||||||
|
const { t, e2e } = lastMessage;
|
||||||
|
|
||||||
|
// If it's not a encrypted message or was decrypted before
|
||||||
|
if (t !== E2E_MESSAGE_TYPE || e2e === E2E_STATUS.DONE) {
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the client is not ready
|
||||||
|
if (!this.ready) {
|
||||||
|
try {
|
||||||
|
// Wait for ready status
|
||||||
|
await this.establishing;
|
||||||
|
} catch {
|
||||||
|
// If it can't be initialized (missing password)
|
||||||
|
// return the encrypted message
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { rid } = subscription;
|
||||||
|
const db = database.active;
|
||||||
|
const subCollection = db.collections.get('subscriptions');
|
||||||
|
|
||||||
|
let subRecord;
|
||||||
|
try {
|
||||||
|
subRecord = await subCollection.find(rid);
|
||||||
|
} catch {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const batch = [];
|
||||||
|
// If the subscription doesn't exists yet
|
||||||
|
if (!subRecord) {
|
||||||
|
// Let's create the subscription with the data received
|
||||||
|
batch.push(subCollection.prepareCreate((s) => {
|
||||||
|
s._raw = sanitizedRaw({ id: rid }, subCollection.schema);
|
||||||
|
Object.assign(s, subscription);
|
||||||
|
}));
|
||||||
|
// If the subscription already exists but doesn't have the E2EKey yet
|
||||||
|
} else if (!subRecord.E2EKey && subscription.E2EKey) {
|
||||||
|
if (!subRecord._hasPendingUpdate) {
|
||||||
|
// Let's update the subscription with the received E2EKey
|
||||||
|
batch.push(subRecord.prepareUpdate((s) => {
|
||||||
|
s.E2EKey = subscription.E2EKey;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If batch has some operation
|
||||||
|
if (batch.length) {
|
||||||
|
await db.action(async() => {
|
||||||
|
await db.batch(...batch);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Abort the decryption process
|
||||||
|
// Return as received
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a instance using the subscription
|
||||||
|
const roomE2E = await this.getRoomInstance(rid);
|
||||||
|
const decryptedMessage = await roomE2E.decrypt(lastMessage);
|
||||||
|
return {
|
||||||
|
...subscription,
|
||||||
|
lastMessage: decryptedMessage
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt a message
|
||||||
|
encryptMessage = async(message) => {
|
||||||
|
const { rid } = message;
|
||||||
|
const db = database.active;
|
||||||
|
const subCollection = db.collections.get('subscriptions');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find the subscription
|
||||||
|
const subRecord = await subCollection.find(rid);
|
||||||
|
|
||||||
|
// Subscription is not encrypted at the moment
|
||||||
|
if (!subRecord.encrypted) {
|
||||||
|
// Send a non encrypted message
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the client is not ready
|
||||||
|
if (!this.ready) {
|
||||||
|
// Wait for ready status
|
||||||
|
await this.establishing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomE2E = await this.getRoomInstance(rid);
|
||||||
|
return roomE2E.encrypt(message);
|
||||||
|
} catch {
|
||||||
|
// Subscription not found
|
||||||
|
// or client can't be initialized (missing password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a non encrypted message
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt a message
|
||||||
|
decryptMessage = async(message) => {
|
||||||
|
const { t, e2e } = message;
|
||||||
|
|
||||||
|
// Prevent create a new instance if this room was encrypted sometime ago
|
||||||
|
if (t !== E2E_MESSAGE_TYPE || e2e === E2E_STATUS.DONE) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the client is not ready
|
||||||
|
if (!this.ready) {
|
||||||
|
try {
|
||||||
|
// Wait for ready status
|
||||||
|
await this.establishing;
|
||||||
|
} catch {
|
||||||
|
// If it can't be initialized (missing password)
|
||||||
|
// return the encrypted message
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { rid } = message;
|
||||||
|
const roomE2E = await this.getRoomInstance(rid);
|
||||||
|
return roomE2E.decrypt(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt multiple messages
|
||||||
|
decryptMessages = messages => Promise.all(messages.map(m => this.decryptMessage(m)))
|
||||||
|
|
||||||
|
// Decrypt multiple subscriptions
|
||||||
|
decryptSubscriptions = subscriptions => Promise.all(subscriptions.map(s => this.decryptSubscription(s)))
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryption = new Encryption();
|
||||||
|
export default encryption;
|
|
@ -0,0 +1,4 @@
|
||||||
|
import Encryption from './encryption';
|
||||||
|
import EncryptionRoom from './room';
|
||||||
|
|
||||||
|
export { Encryption, EncryptionRoom };
|
|
@ -0,0 +1,255 @@
|
||||||
|
import EJSON from 'ejson';
|
||||||
|
import { Base64 } from 'js-base64';
|
||||||
|
import SimpleCrypto from 'react-native-simple-crypto';
|
||||||
|
|
||||||
|
import {
|
||||||
|
toString,
|
||||||
|
b64ToBuffer,
|
||||||
|
bufferToUtf8,
|
||||||
|
bufferToB64,
|
||||||
|
bufferToB64URI,
|
||||||
|
utf8ToBuffer,
|
||||||
|
splitVectorData,
|
||||||
|
joinVectorData
|
||||||
|
} from './utils';
|
||||||
|
import { E2E_MESSAGE_TYPE, E2E_STATUS } from './constants';
|
||||||
|
import RocketChat from '../rocketchat';
|
||||||
|
import Deferred from '../../utils/deferred';
|
||||||
|
import debounce from '../../utils/debounce';
|
||||||
|
import { Encryption } from './index';
|
||||||
|
import database from '../database';
|
||||||
|
import log from '../../utils/log';
|
||||||
|
|
||||||
|
export default class EncryptionRoom {
|
||||||
|
constructor(roomId) {
|
||||||
|
this.ready = false;
|
||||||
|
this.roomId = roomId;
|
||||||
|
this.establishing = false;
|
||||||
|
this.readyPromise = new Deferred();
|
||||||
|
this.readyPromise.then(() => {
|
||||||
|
// Mark as ready
|
||||||
|
this.ready = true;
|
||||||
|
// Mark as established
|
||||||
|
this.establishing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the E2E room
|
||||||
|
handshake = async() => {
|
||||||
|
// If it's already ready we don't need to handshake again
|
||||||
|
if (this.ready) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's already establishing
|
||||||
|
if (this.establishing) {
|
||||||
|
// Return the ready promise to wait this client ready
|
||||||
|
return this.readyPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = database.active;
|
||||||
|
const subCollection = db.collections.get('subscriptions');
|
||||||
|
try {
|
||||||
|
// Find the subscription
|
||||||
|
const subscription = await subCollection.find(this.roomId);
|
||||||
|
|
||||||
|
const { E2EKey, e2eKeyId } = subscription;
|
||||||
|
|
||||||
|
// If this room has a E2EKey, we import it
|
||||||
|
if (E2EKey) {
|
||||||
|
// We're establishing a new room encryption client
|
||||||
|
this.establishing = true;
|
||||||
|
await this.importRoomKey(E2EKey, Encryption.privateKey);
|
||||||
|
this.readyPromise.resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it doesn't have a e2eKeyId, we need to create keys to the room
|
||||||
|
if (!e2eKeyId) {
|
||||||
|
// We're establishing a new room encryption client
|
||||||
|
this.establishing = true;
|
||||||
|
await this.createRoomKey();
|
||||||
|
this.readyPromise.resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request a E2EKey for this room to other users
|
||||||
|
await this.requestRoomKey(e2eKeyId);
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import roomKey as an AES Decrypt key
|
||||||
|
importRoomKey = async(E2EKey, privateKey) => {
|
||||||
|
const roomE2EKey = E2EKey.slice(12);
|
||||||
|
|
||||||
|
const decryptedKey = await SimpleCrypto.RSA.decrypt(roomE2EKey, privateKey);
|
||||||
|
this.sessionKeyExportedString = toString(decryptedKey);
|
||||||
|
|
||||||
|
this.keyID = Base64.encode(this.sessionKeyExportedString).slice(0, 12);
|
||||||
|
|
||||||
|
// Extract K from Web Crypto Secret Key
|
||||||
|
// K is a base64URL encoded array of bytes
|
||||||
|
// Web Crypto API uses this as a private key to decrypt/encrypt things
|
||||||
|
// Reference: https://www.javadoc.io/doc/com.nimbusds/nimbus-jose-jwt/5.1/com/nimbusds/jose/jwk/OctetSequenceKey.html
|
||||||
|
const { k } = EJSON.parse(this.sessionKeyExportedString);
|
||||||
|
this.roomKey = b64ToBuffer(k);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a key to a room
|
||||||
|
createRoomKey = async() => {
|
||||||
|
const key = await SimpleCrypto.utils.randomBytes(16);
|
||||||
|
this.roomKey = key;
|
||||||
|
|
||||||
|
// Web Crypto format of a Secret Key
|
||||||
|
const sessionKeyExported = {
|
||||||
|
// Type of Secret Key
|
||||||
|
kty: 'oct',
|
||||||
|
// Algorithm
|
||||||
|
alg: 'A128CBC',
|
||||||
|
// Base64URI encoded array of bytes
|
||||||
|
k: bufferToB64URI(this.roomKey),
|
||||||
|
// Specific Web Crypto properties
|
||||||
|
ext: true,
|
||||||
|
key_ops: ['encrypt', 'decrypt']
|
||||||
|
};
|
||||||
|
|
||||||
|
this.sessionKeyExportedString = EJSON.stringify(sessionKeyExported);
|
||||||
|
this.keyID = Base64.encode(this.sessionKeyExportedString).slice(0, 12);
|
||||||
|
|
||||||
|
await RocketChat.e2eSetRoomKeyID(this.roomId, this.keyID);
|
||||||
|
|
||||||
|
await this.encryptRoomKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request a key to this room
|
||||||
|
// We're debouncing this function to avoid multiple calls
|
||||||
|
// when you join a room with a lot of messages and nobody
|
||||||
|
// can send the encryption key at the moment.
|
||||||
|
// Each time you see a encrypted message of a room that you don't have a key
|
||||||
|
// this will be called again and run once in 5 seconds
|
||||||
|
requestRoomKey = debounce(async(e2eKeyId) => {
|
||||||
|
await RocketChat.e2eRequestRoomKey(this.roomId, e2eKeyId);
|
||||||
|
}, 5000, true)
|
||||||
|
|
||||||
|
// Create an encrypted key for this room based on users
|
||||||
|
encryptRoomKey = async() => {
|
||||||
|
const result = await RocketChat.e2eGetUsersOfRoomWithoutKey(this.roomId);
|
||||||
|
if (result.success) {
|
||||||
|
const { users } = result;
|
||||||
|
await Promise.all(users.map(user => this.encryptRoomKeyForUser(user)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt the room key to each user in
|
||||||
|
encryptRoomKeyForUser = async(user) => {
|
||||||
|
if (user?.e2e?.public_key) {
|
||||||
|
const { public_key: publicKey } = user.e2e;
|
||||||
|
const userKey = await SimpleCrypto.RSA.importKey(EJSON.parse(publicKey));
|
||||||
|
const encryptedUserKey = await SimpleCrypto.RSA.encrypt(this.sessionKeyExportedString, userKey);
|
||||||
|
await RocketChat.e2eUpdateGroupKey(user?._id, this.roomId, this.keyID + encryptedUserKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide this room key to a user
|
||||||
|
provideKeyToUser = async(keyId) => {
|
||||||
|
// Don't provide a key if the keyId received
|
||||||
|
// is different than the current one
|
||||||
|
if (this.keyID !== keyId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.encryptRoomKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt text
|
||||||
|
encryptText = async(text) => {
|
||||||
|
text = utf8ToBuffer(text);
|
||||||
|
const vector = await SimpleCrypto.utils.randomBytes(16);
|
||||||
|
const data = await SimpleCrypto.AES.encrypt(
|
||||||
|
text,
|
||||||
|
this.roomKey,
|
||||||
|
vector
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.keyID + bufferToB64(joinVectorData(vector, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt messages
|
||||||
|
encrypt = async(message) => {
|
||||||
|
if (!this.ready) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const msg = await this.encryptText(EJSON.stringify({
|
||||||
|
_id: message._id,
|
||||||
|
text: message.msg,
|
||||||
|
userId: this.userId,
|
||||||
|
ts: new Date()
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...message,
|
||||||
|
t: E2E_MESSAGE_TYPE,
|
||||||
|
e2e: E2E_STATUS.PENDING,
|
||||||
|
msg
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt text
|
||||||
|
decryptText = async(msg) => {
|
||||||
|
msg = b64ToBuffer(msg.slice(12));
|
||||||
|
const [vector, cipherText] = splitVectorData(msg);
|
||||||
|
|
||||||
|
const decrypted = await SimpleCrypto.AES.decrypt(
|
||||||
|
cipherText,
|
||||||
|
this.roomKey,
|
||||||
|
vector
|
||||||
|
);
|
||||||
|
|
||||||
|
const m = EJSON.parse(bufferToUtf8(decrypted));
|
||||||
|
|
||||||
|
return m.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt messages
|
||||||
|
decrypt = async(message) => {
|
||||||
|
if (!this.ready) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { t, e2e } = message;
|
||||||
|
|
||||||
|
// If message type is e2e and it's encrypted still
|
||||||
|
if (t === E2E_MESSAGE_TYPE && e2e !== E2E_STATUS.DONE) {
|
||||||
|
let { msg, tmsg } = message;
|
||||||
|
// Decrypt msg
|
||||||
|
msg = await this.decryptText(msg);
|
||||||
|
|
||||||
|
// Decrypt tmsg
|
||||||
|
if (tmsg) {
|
||||||
|
tmsg = await this.decryptText(tmsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...message,
|
||||||
|
tmsg,
|
||||||
|
msg,
|
||||||
|
e2e: E2E_STATUS.DONE
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
/* eslint-disable no-bitwise */
|
||||||
|
import ByteBuffer from 'bytebuffer';
|
||||||
|
import SimpleCrypto from 'react-native-simple-crypto';
|
||||||
|
|
||||||
|
import random from '../../utils/random';
|
||||||
|
import { fromByteArray, toByteArray } from '../../utils/base64-js';
|
||||||
|
|
||||||
|
const BASE64URI = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
||||||
|
|
||||||
|
export const b64ToBuffer = base64 => toByteArray(base64).buffer;
|
||||||
|
export const utf8ToBuffer = SimpleCrypto.utils.convertUtf8ToArrayBuffer;
|
||||||
|
export const bufferToB64 = arrayBuffer => fromByteArray(new Uint8Array(arrayBuffer));
|
||||||
|
// ArrayBuffer -> Base64 URI Safe
|
||||||
|
// https://github.com/herrjemand/Base64URL-ArrayBuffer/blob/master/lib/base64url-arraybuffer.js
|
||||||
|
export const bufferToB64URI = (buffer) => {
|
||||||
|
const uintArray = new Uint8Array(buffer);
|
||||||
|
const len = uintArray.length;
|
||||||
|
let base64 = '';
|
||||||
|
|
||||||
|
for (let i = 0; i < len; i += 3) {
|
||||||
|
base64 += BASE64URI[uintArray[i] >> 2];
|
||||||
|
base64 += BASE64URI[((uintArray[i] & 3) << 4) | (uintArray[i + 1] >> 4)];
|
||||||
|
base64 += BASE64URI[((uintArray[i + 1] & 15) << 2) | (uintArray[i + 2] >> 6)];
|
||||||
|
base64 += BASE64URI[uintArray[i + 2] & 63];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((len % 3) === 2) {
|
||||||
|
base64 = base64.substring(0, base64.length - 1);
|
||||||
|
} else if (len % 3 === 1) {
|
||||||
|
base64 = base64.substring(0, base64.length - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64;
|
||||||
|
};
|
||||||
|
// SimpleCrypto.utils.convertArrayBufferToUtf8 is not working with unicode emoji
|
||||||
|
export const bufferToUtf8 = (buffer) => {
|
||||||
|
const uintArray = new Uint8Array(buffer);
|
||||||
|
const encodedString = String.fromCharCode.apply(null, uintArray);
|
||||||
|
const decodedString = decodeURIComponent(escape(encodedString));
|
||||||
|
return decodedString;
|
||||||
|
};
|
||||||
|
export const splitVectorData = (text) => {
|
||||||
|
const vector = text.slice(0, 16);
|
||||||
|
const data = text.slice(16);
|
||||||
|
return [vector, data];
|
||||||
|
};
|
||||||
|
export const joinVectorData = (vector, data) => {
|
||||||
|
const output = new Uint8Array(vector.byteLength + data.byteLength);
|
||||||
|
output.set(new Uint8Array(vector), 0);
|
||||||
|
output.set(new Uint8Array(data), vector.byteLength);
|
||||||
|
return output.buffer;
|
||||||
|
};
|
||||||
|
export const toString = (thing) => {
|
||||||
|
if (typeof thing === 'string') {
|
||||||
|
return thing;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line new-cap
|
||||||
|
return new ByteBuffer.wrap(thing).toString('binary');
|
||||||
|
};
|
||||||
|
export const randomPassword = () => `${ random(3) }-${ random(3) }-${ random(3) }`.toLowerCase();
|
|
@ -11,7 +11,16 @@ import protectedFunction from './helpers/protectedFunction';
|
||||||
import fetch from '../../utils/fetch';
|
import fetch from '../../utils/fetch';
|
||||||
import { DEFAULT_AUTO_LOCK } from '../../constants/localAuthentication';
|
import { DEFAULT_AUTO_LOCK } from '../../constants/localAuthentication';
|
||||||
|
|
||||||
const serverInfoKeys = ['Site_Name', 'UI_Use_Real_Name', 'FileUpload_MediaTypeWhiteList', 'FileUpload_MaxFileSize', 'Force_Screen_Lock', 'Force_Screen_Lock_After', 'uniqueID'];
|
const serverInfoKeys = [
|
||||||
|
'Site_Name',
|
||||||
|
'UI_Use_Real_Name',
|
||||||
|
'FileUpload_MediaTypeWhiteList',
|
||||||
|
'FileUpload_MaxFileSize',
|
||||||
|
'Force_Screen_Lock',
|
||||||
|
'Force_Screen_Lock_After',
|
||||||
|
'uniqueID',
|
||||||
|
'E2E_Enable'
|
||||||
|
];
|
||||||
|
|
||||||
// these settings are used only on onboarding process
|
// these settings are used only on onboarding process
|
||||||
const loginSettings = [
|
const loginSettings = [
|
||||||
|
@ -71,6 +80,9 @@ const serverInfoUpdate = async(serverInfo, iconSetting) => {
|
||||||
if (setting._id === 'uniqueID') {
|
if (setting._id === 'uniqueID') {
|
||||||
return { ...allSettings, uniqueID: setting.valueAsString };
|
return { ...allSettings, uniqueID: setting.valueAsString };
|
||||||
}
|
}
|
||||||
|
if (setting._id === 'E2E_Enable') {
|
||||||
|
return { ...allSettings, E2E_Enable: setting.valueAsBoolean };
|
||||||
|
}
|
||||||
return allSettings;
|
return allSettings;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,10 @@ export default async(subscriptions = [], rooms = []) => {
|
||||||
departmentId: s.departmentId,
|
departmentId: s.departmentId,
|
||||||
servedBy: s.servedBy,
|
servedBy: s.servedBy,
|
||||||
livechatData: s.livechatData,
|
livechatData: s.livechatData,
|
||||||
tags: s.tags
|
tags: s.tags,
|
||||||
|
encrypted: s.encrypted,
|
||||||
|
e2eKeyId: s.e2eKeyId,
|
||||||
|
E2EKey: s.E2EKey
|
||||||
}));
|
}));
|
||||||
subscriptions = subscriptions.concat(existingSubs);
|
subscriptions = subscriptions.concat(existingSubs);
|
||||||
|
|
||||||
|
@ -75,7 +78,9 @@ export default async(subscriptions = [], rooms = []) => {
|
||||||
departmentId: r.departmentId,
|
departmentId: r.departmentId,
|
||||||
servedBy: r.servedBy,
|
servedBy: r.servedBy,
|
||||||
livechatData: r.livechatData,
|
livechatData: r.livechatData,
|
||||||
tags: r.tags
|
tags: r.tags,
|
||||||
|
encrypted: r.encrypted,
|
||||||
|
e2eKeyId: r.e2eKeyId
|
||||||
}));
|
}));
|
||||||
rooms = rooms.concat(existingRooms);
|
rooms = rooms.concat(existingRooms);
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import EJSON from 'ejson';
|
||||||
|
|
||||||
import normalizeMessage from './normalizeMessage';
|
import normalizeMessage from './normalizeMessage';
|
||||||
import findSubscriptionsRooms from './findSubscriptionsRooms';
|
import findSubscriptionsRooms from './findSubscriptionsRooms';
|
||||||
|
import { Encryption } from '../../encryption';
|
||||||
// TODO: delete and update
|
// TODO: delete and update
|
||||||
|
|
||||||
export const merge = (subscription, room) => {
|
export const merge = (subscription, room) => {
|
||||||
|
@ -27,6 +28,8 @@ export const merge = (subscription, room) => {
|
||||||
}
|
}
|
||||||
subscription.ro = room.ro;
|
subscription.ro = room.ro;
|
||||||
subscription.broadcast = room.broadcast;
|
subscription.broadcast = room.broadcast;
|
||||||
|
subscription.encrypted = room.encrypted;
|
||||||
|
subscription.e2eKeyId = room.e2eKeyId;
|
||||||
if (!subscription.roles || !subscription.roles.length) {
|
if (!subscription.roles || !subscription.roles.length) {
|
||||||
subscription.roles = [];
|
subscription.roles = [];
|
||||||
}
|
}
|
||||||
|
@ -72,17 +75,23 @@ export default async(subscriptions = [], rooms = []) => {
|
||||||
rooms = rooms.update;
|
rooms = rooms.update;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find missing rooms/subscriptions on local database
|
||||||
({ subscriptions, rooms } = await findSubscriptionsRooms(subscriptions, rooms));
|
({ subscriptions, rooms } = await findSubscriptionsRooms(subscriptions, rooms));
|
||||||
|
// Merge each subscription into a room
|
||||||
|
subscriptions = subscriptions.map((s) => {
|
||||||
|
const index = rooms.findIndex(({ _id }) => _id === s.rid);
|
||||||
|
// Room not found
|
||||||
|
if (index < 0) {
|
||||||
|
return merge(s);
|
||||||
|
}
|
||||||
|
const [room] = rooms.splice(index, 1);
|
||||||
|
return merge(s, room);
|
||||||
|
});
|
||||||
|
// Decrypt all subscriptions missing decryption
|
||||||
|
subscriptions = await Encryption.decryptSubscriptions(subscriptions);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscriptions: subscriptions.map((s) => {
|
subscriptions,
|
||||||
const index = rooms.findIndex(({ _id }) => _id === s.rid);
|
|
||||||
if (index < 0) {
|
|
||||||
return merge(s);
|
|
||||||
}
|
|
||||||
const [room] = rooms.splice(index, 1);
|
|
||||||
return merge(s, room);
|
|
||||||
}),
|
|
||||||
rooms
|
rooms
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,6 +6,7 @@ import buildMessage from './helpers/buildMessage';
|
||||||
import database from '../database';
|
import database from '../database';
|
||||||
import log from '../../utils/log';
|
import log from '../../utils/log';
|
||||||
import protectedFunction from './helpers/protectedFunction';
|
import protectedFunction from './helpers/protectedFunction';
|
||||||
|
import { Encryption } from '../encryption';
|
||||||
|
|
||||||
async function load({ tmid, offset }) {
|
async function load({ tmid, offset }) {
|
||||||
try {
|
try {
|
||||||
|
@ -32,6 +33,7 @@ export default function loadThreadMessages({ tmid, rid, offset = 0 }) {
|
||||||
InteractionManager.runAfterInteractions(async() => {
|
InteractionManager.runAfterInteractions(async() => {
|
||||||
try {
|
try {
|
||||||
data = data.map(m => buildMessage(m));
|
data = data.map(m => buildMessage(m));
|
||||||
|
data = await Encryption.decryptMessages(data);
|
||||||
const db = database.active;
|
const db = database.active;
|
||||||
const threadMessagesCollection = db.collections.get('thread_messages');
|
const threadMessagesCollection = db.collections.get('thread_messages');
|
||||||
const allThreadMessagesRecords = await threadMessagesCollection.query(Q.where('rid', tmid)).fetch();
|
const allThreadMessagesRecords = await threadMessagesCollection.query(Q.where('rid', tmid)).fetch();
|
||||||
|
|
|
@ -7,12 +7,20 @@ import { BASIC_AUTH_KEY } from '../../utils/fetch';
|
||||||
import database, { getDatabase } from '../database';
|
import database, { getDatabase } from '../database';
|
||||||
import RocketChat from '../rocketchat';
|
import RocketChat from '../rocketchat';
|
||||||
import { useSsl } from '../../utils/url';
|
import { useSsl } from '../../utils/url';
|
||||||
|
import {
|
||||||
|
E2E_PUBLIC_KEY,
|
||||||
|
E2E_PRIVATE_KEY,
|
||||||
|
E2E_RANDOM_PASSWORD_KEY
|
||||||
|
} from '../encryption/constants';
|
||||||
import UserPreferences from '../userPreferences';
|
import UserPreferences from '../userPreferences';
|
||||||
|
|
||||||
async function removeServerKeys({ server, userId }) {
|
async function removeServerKeys({ server, userId }) {
|
||||||
await UserPreferences.removeItem(`${ RocketChat.TOKEN_KEY }-${ server }`);
|
await UserPreferences.removeItem(`${ RocketChat.TOKEN_KEY }-${ server }`);
|
||||||
await UserPreferences.removeItem(`${ RocketChat.TOKEN_KEY }-${ userId }`);
|
await UserPreferences.removeItem(`${ RocketChat.TOKEN_KEY }-${ userId }`);
|
||||||
await UserPreferences.removeItem(`${ BASIC_AUTH_KEY }-${ server }`);
|
await UserPreferences.removeItem(`${ BASIC_AUTH_KEY }-${ server }`);
|
||||||
|
await UserPreferences.removeItem(`${ server }-${ E2E_PUBLIC_KEY }`);
|
||||||
|
await UserPreferences.removeItem(`${ server }-${ E2E_PRIVATE_KEY }`);
|
||||||
|
await UserPreferences.removeItem(`${ server }-${ E2E_RANDOM_PASSWORD_KEY }`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeSharedCredentials({ server }) {
|
async function removeSharedCredentials({ server }) {
|
||||||
|
|
|
@ -4,6 +4,8 @@ import messagesStatus from '../../constants/messagesStatus';
|
||||||
import database from '../database';
|
import database from '../database';
|
||||||
import log from '../../utils/log';
|
import log from '../../utils/log';
|
||||||
import random from '../../utils/random';
|
import random from '../../utils/random';
|
||||||
|
import { Encryption } from '../encryption';
|
||||||
|
import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../encryption/constants';
|
||||||
|
|
||||||
const changeMessageStatus = async(id, tmid, status, message) => {
|
const changeMessageStatus = async(id, tmid, status, message) => {
|
||||||
const db = database.active;
|
const db = database.active;
|
||||||
|
@ -44,17 +46,11 @@ const changeMessageStatus = async(id, tmid, status, message) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function sendMessageCall(message) {
|
export async function sendMessageCall(message) {
|
||||||
const {
|
const { _id, tmid } = message;
|
||||||
id: _id, subscription: { id: rid }, msg, tmid
|
|
||||||
} = message;
|
|
||||||
try {
|
try {
|
||||||
const sdk = this.shareSDK || this.sdk;
|
const sdk = this.shareSDK || this.sdk;
|
||||||
// RC 0.60.0
|
// RC 0.60.0
|
||||||
const result = await sdk.post('chat.sendMessage', {
|
const result = await sdk.post('chat.sendMessage', { message });
|
||||||
message: {
|
|
||||||
_id, rid, msg, tmid
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
return changeMessageStatus(_id, tmid, messagesStatus.SENT, result.message);
|
return changeMessageStatus(_id, tmid, messagesStatus.SENT, result.message);
|
||||||
}
|
}
|
||||||
|
@ -64,6 +60,32 @@ export async function sendMessageCall(message) {
|
||||||
return changeMessageStatus(_id, tmid, messagesStatus.ERROR);
|
return changeMessageStatus(_id, tmid, messagesStatus.ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function resendMessage(message, tmid) {
|
||||||
|
const db = database.active;
|
||||||
|
try {
|
||||||
|
await db.action(async() => {
|
||||||
|
await message.update((m) => {
|
||||||
|
m.status = messagesStatus.TEMP;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
let m = {
|
||||||
|
_id: message.id,
|
||||||
|
rid: message.subscription.id,
|
||||||
|
msg: message.msg
|
||||||
|
};
|
||||||
|
if (tmid) {
|
||||||
|
m = {
|
||||||
|
...m,
|
||||||
|
tmid
|
||||||
|
};
|
||||||
|
}
|
||||||
|
m = await Encryption.encryptMessage(m);
|
||||||
|
await sendMessageCall.call(this, m);
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default async function(rid, msg, tmid, user) {
|
export default async function(rid, msg, tmid, user) {
|
||||||
try {
|
try {
|
||||||
const db = database.active;
|
const db = database.active;
|
||||||
|
@ -73,9 +95,12 @@ export default async function(rid, msg, tmid, user) {
|
||||||
const threadMessagesCollection = db.collections.get('thread_messages');
|
const threadMessagesCollection = db.collections.get('thread_messages');
|
||||||
const messageId = random(17);
|
const messageId = random(17);
|
||||||
const batch = [];
|
const batch = [];
|
||||||
const message = {
|
|
||||||
id: messageId, subscription: { id: rid }, msg, tmid
|
let message = {
|
||||||
|
_id: messageId, rid, msg, tmid
|
||||||
};
|
};
|
||||||
|
message = await Encryption.encryptMessage(message);
|
||||||
|
|
||||||
const messageDate = new Date();
|
const messageDate = new Date();
|
||||||
let tMessageRecord;
|
let tMessageRecord;
|
||||||
|
|
||||||
|
@ -106,6 +131,10 @@ export default async function(rid, msg, tmid, user) {
|
||||||
tm._updatedAt = messageDate;
|
tm._updatedAt = messageDate;
|
||||||
tm.status = messagesStatus.SENT; // Original message was sent already
|
tm.status = messagesStatus.SENT; // Original message was sent already
|
||||||
tm.u = tMessageRecord.u;
|
tm.u = tMessageRecord.u;
|
||||||
|
tm.t = message.t;
|
||||||
|
if (message.t === E2E_MESSAGE_TYPE) {
|
||||||
|
tm.e2e = E2E_STATUS.DONE;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -124,6 +153,10 @@ export default async function(rid, msg, tmid, user) {
|
||||||
_id: user.id || '1',
|
_id: user.id || '1',
|
||||||
username: user.username
|
username: user.username
|
||||||
};
|
};
|
||||||
|
tm.t = message.t;
|
||||||
|
if (message.t === E2E_MESSAGE_TYPE) {
|
||||||
|
tm.e2e = E2E_STATUS.DONE;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -149,6 +182,10 @@ export default async function(rid, msg, tmid, user) {
|
||||||
m.tlm = messageDate;
|
m.tlm = messageDate;
|
||||||
m.tmsg = tMessageRecord.msg;
|
m.tmsg = tMessageRecord.msg;
|
||||||
}
|
}
|
||||||
|
m.t = message.t;
|
||||||
|
if (message.t === E2E_MESSAGE_TYPE) {
|
||||||
|
m.e2e = E2E_STATUS.DONE;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { addUserTyping, removeUserTyping, clearUserTyping } from '../../../actio
|
||||||
import debounce from '../../../utils/debounce';
|
import debounce from '../../../utils/debounce';
|
||||||
import RocketChat from '../../rocketchat';
|
import RocketChat from '../../rocketchat';
|
||||||
import { subscribeRoom, unsubscribeRoom } from '../../../actions/room';
|
import { subscribeRoom, unsubscribeRoom } from '../../../actions/room';
|
||||||
|
import { Encryption } from '../../encryption';
|
||||||
|
|
||||||
const WINDOW_TIME = 1000;
|
const WINDOW_TIME = 1000;
|
||||||
|
|
||||||
|
@ -162,6 +163,9 @@ export default class RoomSubscription {
|
||||||
const threadsCollection = db.collections.get('threads');
|
const threadsCollection = db.collections.get('threads');
|
||||||
const threadMessagesCollection = db.collections.get('thread_messages');
|
const threadMessagesCollection = db.collections.get('thread_messages');
|
||||||
|
|
||||||
|
// Decrypt the message if necessary
|
||||||
|
message = await Encryption.decryptMessage(message);
|
||||||
|
|
||||||
// Create or update message
|
// Create or update message
|
||||||
try {
|
try {
|
||||||
const messageRecord = await msgCollection.find(message._id);
|
const messageRecord = await msgCollection.find(message._id);
|
||||||
|
|
|
@ -16,6 +16,8 @@ import EventEmitter from '../../../utils/events';
|
||||||
import { removedRoom } from '../../../actions/room';
|
import { removedRoom } from '../../../actions/room';
|
||||||
import { setUser } from '../../../actions/login';
|
import { setUser } from '../../../actions/login';
|
||||||
import { INAPP_NOTIFICATION_EMITTER } from '../../../containers/InAppNotification';
|
import { INAPP_NOTIFICATION_EMITTER } from '../../../containers/InAppNotification';
|
||||||
|
import { Encryption } from '../../encryption';
|
||||||
|
import { E2E_MESSAGE_TYPE } from '../../encryption/constants';
|
||||||
|
|
||||||
const removeListener = listener => listener.stop();
|
const removeListener = listener => listener.stop();
|
||||||
|
|
||||||
|
@ -79,7 +81,10 @@ const createOrUpdateSubscription = async(subscription, room) => {
|
||||||
departmentId: s.departmentId,
|
departmentId: s.departmentId,
|
||||||
servedBy: s.servedBy,
|
servedBy: s.servedBy,
|
||||||
livechatData: s.livechatData,
|
livechatData: s.livechatData,
|
||||||
tags: s.tags
|
tags: s.tags,
|
||||||
|
encrypted: s.encrypted,
|
||||||
|
e2eKeyId: s.e2eKeyId,
|
||||||
|
E2EKey: s.E2EKey
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
try {
|
try {
|
||||||
|
@ -107,6 +112,7 @@ const createOrUpdateSubscription = async(subscription, room) => {
|
||||||
tags: r.tags,
|
tags: r.tags,
|
||||||
servedBy: r.servedBy,
|
servedBy: r.servedBy,
|
||||||
encrypted: r.encrypted,
|
encrypted: r.encrypted,
|
||||||
|
e2eKeyId: r.e2eKeyId,
|
||||||
broadcast: r.broadcast,
|
broadcast: r.broadcast,
|
||||||
customFields: r.customFields,
|
customFields: r.customFields,
|
||||||
departmentId: r.departmentId,
|
departmentId: r.departmentId,
|
||||||
|
@ -117,73 +123,91 @@ const createOrUpdateSubscription = async(subscription, room) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tmp = merge(subscription, room);
|
let tmp = merge(subscription, room);
|
||||||
await db.action(async() => {
|
tmp = await Encryption.decryptSubscription(tmp);
|
||||||
let sub;
|
let sub;
|
||||||
|
try {
|
||||||
|
sub = await subCollection.find(tmp.rid);
|
||||||
|
} catch (error) {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're receiving a E2EKey of a room
|
||||||
|
if (sub && !sub.E2EKey && subscription?.E2EKey) {
|
||||||
|
// Assing info from database subscription to tmp
|
||||||
|
// It should be a plain object
|
||||||
|
tmp = Object.assign(tmp, {
|
||||||
|
rid: sub.rid,
|
||||||
|
encrypted: sub.encrypted,
|
||||||
|
lastMessage: sub.lastMessage,
|
||||||
|
E2EKey: subscription.E2EKey,
|
||||||
|
e2eKeyId: sub.e2eKeyId
|
||||||
|
});
|
||||||
|
// Decrypt lastMessage using the received E2EKey
|
||||||
|
tmp = await Encryption.decryptSubscription(tmp);
|
||||||
|
// Decrypt all pending messages of this room in parallel
|
||||||
|
Encryption.decryptPendingMessages(tmp.rid);
|
||||||
|
}
|
||||||
|
|
||||||
|
const batch = [];
|
||||||
|
if (sub) {
|
||||||
try {
|
try {
|
||||||
sub = await subCollection.find(tmp.rid);
|
const update = sub.prepareUpdate((s) => {
|
||||||
|
Object.assign(s, tmp);
|
||||||
|
if (subscription.announcement) {
|
||||||
|
if (subscription.announcement !== sub.announcement) {
|
||||||
|
s.bannerClosed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
batch.push(update);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const create = subCollection.prepareCreate((s) => {
|
||||||
|
s._raw = sanitizedRaw({ id: tmp.rid }, subCollection.schema);
|
||||||
|
Object.assign(s, tmp);
|
||||||
|
if (s.roomUpdatedAt) {
|
||||||
|
s.roomUpdatedAt = new Date();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
batch.push(create);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { rooms } = store.getState().room;
|
||||||
|
if (tmp.lastMessage && !rooms.includes(tmp.rid)) {
|
||||||
|
const lastMessage = buildMessage(tmp.lastMessage);
|
||||||
|
const messagesCollection = db.collections.get('messages');
|
||||||
|
let messageRecord;
|
||||||
|
try {
|
||||||
|
messageRecord = await messagesCollection.find(lastMessage._id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
const batch = [];
|
if (messageRecord) {
|
||||||
if (sub) {
|
batch.push(
|
||||||
try {
|
messageRecord.prepareUpdate(() => {
|
||||||
const update = sub.prepareUpdate((s) => {
|
Object.assign(messageRecord, lastMessage);
|
||||||
Object.assign(s, tmp);
|
})
|
||||||
if (subscription.announcement) {
|
);
|
||||||
if (subscription.announcement !== sub.announcement) {
|
|
||||||
s.bannerClosed = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
batch.push(update);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
try {
|
batch.push(
|
||||||
const create = subCollection.prepareCreate((s) => {
|
messagesCollection.prepareCreate((m) => {
|
||||||
s._raw = sanitizedRaw({ id: tmp.rid }, subCollection.schema);
|
m._raw = sanitizedRaw({ id: lastMessage._id }, messagesCollection.schema);
|
||||||
Object.assign(s, tmp);
|
m.subscription.id = lastMessage.rid;
|
||||||
if (s.roomUpdatedAt) {
|
return Object.assign(m, lastMessage);
|
||||||
s.roomUpdatedAt = new Date();
|
})
|
||||||
}
|
);
|
||||||
});
|
|
||||||
batch.push(create);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { rooms } = store.getState().room;
|
|
||||||
if (tmp.lastMessage && !rooms.includes(tmp.rid)) {
|
|
||||||
const lastMessage = buildMessage(tmp.lastMessage);
|
|
||||||
const messagesCollection = db.collections.get('messages');
|
|
||||||
let messageRecord;
|
|
||||||
try {
|
|
||||||
messageRecord = await messagesCollection.find(lastMessage._id);
|
|
||||||
} catch (error) {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
if (messageRecord) {
|
|
||||||
batch.push(
|
|
||||||
messageRecord.prepareUpdate(() => {
|
|
||||||
Object.assign(messageRecord, lastMessage);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
batch.push(
|
|
||||||
messagesCollection.prepareCreate((m) => {
|
|
||||||
m._raw = sanitizedRaw({ id: lastMessage._id }, messagesCollection.schema);
|
|
||||||
m.subscription.id = lastMessage.rid;
|
|
||||||
return Object.assign(m, lastMessage);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.action(async() => {
|
||||||
await db.batch(...batch);
|
await db.batch(...batch);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -320,12 +344,25 @@ export default function subscribeRooms() {
|
||||||
if (/notification/.test(ev)) {
|
if (/notification/.test(ev)) {
|
||||||
const [notification] = ddpMessage.fields.args;
|
const [notification] = ddpMessage.fields.args;
|
||||||
try {
|
try {
|
||||||
const { payload: { rid } } = notification;
|
const { payload: { rid, message, sender } } = notification;
|
||||||
const room = await RocketChat.getRoom(rid);
|
const room = await RocketChat.getRoom(rid);
|
||||||
notification.title = RocketChat.getRoomTitle(room);
|
notification.title = RocketChat.getRoomTitle(room);
|
||||||
notification.avatar = RocketChat.getRoomAvatar(room);
|
notification.avatar = RocketChat.getRoomAvatar(room);
|
||||||
|
|
||||||
|
// If it's from a encrypted room
|
||||||
|
if (message.t === E2E_MESSAGE_TYPE) {
|
||||||
|
// Decrypt this message content
|
||||||
|
const { msg } = await Encryption.decryptMessage({ ...message, rid });
|
||||||
|
// If it's a direct the content is the message decrypted
|
||||||
|
if (room.t === 'd') {
|
||||||
|
notification.text = msg;
|
||||||
|
// If it's a private group we should add the sender name
|
||||||
|
} else {
|
||||||
|
notification.text = `${ RocketChat.getSenderName(sender) }: ${ msg }`;
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// do nothing
|
log(e);
|
||||||
}
|
}
|
||||||
EventEmitter.emit(INAPP_NOTIFICATION_EMITTER, notification);
|
EventEmitter.emit(INAPP_NOTIFICATION_EMITTER, notification);
|
||||||
}
|
}
|
||||||
|
@ -333,6 +370,14 @@ export default function subscribeRooms() {
|
||||||
const { type: eventType, ...args } = type;
|
const { type: eventType, ...args } = type;
|
||||||
handlePayloadUserInteraction(eventType, args);
|
handlePayloadUserInteraction(eventType, args);
|
||||||
}
|
}
|
||||||
|
if (/e2ekeyRequest/.test(ev)) {
|
||||||
|
const [roomId, keyId] = ddpMessage.fields.args;
|
||||||
|
try {
|
||||||
|
await Encryption.provideRoomKeyToUser(keyId, roomId);
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const stop = () => {
|
const stop = () => {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import buildMessage from './helpers/buildMessage';
|
||||||
import log from '../../utils/log';
|
import log from '../../utils/log';
|
||||||
import database from '../database';
|
import database from '../database';
|
||||||
import protectedFunction from './helpers/protectedFunction';
|
import protectedFunction from './helpers/protectedFunction';
|
||||||
|
import { Encryption } from '../encryption';
|
||||||
|
|
||||||
export default function updateMessages({ rid, update = [], remove = [] }) {
|
export default function updateMessages({ rid, update = [], remove = [] }) {
|
||||||
try {
|
try {
|
||||||
|
@ -13,6 +14,8 @@ export default function updateMessages({ rid, update = [], remove = [] }) {
|
||||||
}
|
}
|
||||||
const db = database.active;
|
const db = database.active;
|
||||||
return db.action(async() => {
|
return db.action(async() => {
|
||||||
|
// Decrypt these messages
|
||||||
|
update = await Encryption.decryptMessages(update);
|
||||||
const subCollection = db.collections.get('subscriptions');
|
const subCollection = db.collections.get('subscriptions');
|
||||||
let sub;
|
let sub;
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -6,12 +6,12 @@ import AsyncStorage from '@react-native-community/async-storage';
|
||||||
|
|
||||||
import reduxStore from './createStore';
|
import reduxStore from './createStore';
|
||||||
import defaultSettings from '../constants/settings';
|
import defaultSettings from '../constants/settings';
|
||||||
import messagesStatus from '../constants/messagesStatus';
|
|
||||||
import database from './database';
|
import database from './database';
|
||||||
import log from '../utils/log';
|
import log from '../utils/log';
|
||||||
import { isIOS, getBundleId } from '../utils/deviceInfo';
|
import { isIOS, getBundleId } from '../utils/deviceInfo';
|
||||||
import fetch from '../utils/fetch';
|
import fetch from '../utils/fetch';
|
||||||
|
|
||||||
|
import { encryptionInit } from '../actions/encryption';
|
||||||
import { setUser, setLoginServices, loginRequest } from '../actions/login';
|
import { setUser, setLoginServices, loginRequest } from '../actions/login';
|
||||||
import { disconnect, connectSuccess, connectRequest } from '../actions/connect';
|
import { disconnect, connectSuccess, connectRequest } from '../actions/connect';
|
||||||
import {
|
import {
|
||||||
|
@ -40,7 +40,7 @@ import loadMessagesForRoom from './methods/loadMessagesForRoom';
|
||||||
import loadMissedMessages from './methods/loadMissedMessages';
|
import loadMissedMessages from './methods/loadMissedMessages';
|
||||||
import loadThreadMessages from './methods/loadThreadMessages';
|
import loadThreadMessages from './methods/loadThreadMessages';
|
||||||
|
|
||||||
import sendMessage, { sendMessageCall } from './methods/sendMessage';
|
import sendMessage, { resendMessage } from './methods/sendMessage';
|
||||||
import { sendFileMessage, cancelUpload, isUploadActive } from './methods/sendFileMessage';
|
import { sendFileMessage, cancelUpload, isUploadActive } from './methods/sendFileMessage';
|
||||||
|
|
||||||
import callJitsi from './methods/callJitsi';
|
import callJitsi from './methods/callJitsi';
|
||||||
|
@ -53,6 +53,7 @@ import { twoFactor } from '../utils/twoFactor';
|
||||||
import { selectServerFailure } from '../actions/server';
|
import { selectServerFailure } from '../actions/server';
|
||||||
import { useSsl } from '../utils/url';
|
import { useSsl } from '../utils/url';
|
||||||
import UserPreferences from './userPreferences';
|
import UserPreferences from './userPreferences';
|
||||||
|
import { Encryption } from './encryption';
|
||||||
import EventEmitter from '../utils/events';
|
import EventEmitter from '../utils/events';
|
||||||
|
|
||||||
const TOKEN_KEY = 'reactnativemeteor_usertoken';
|
const TOKEN_KEY = 'reactnativemeteor_usertoken';
|
||||||
|
@ -80,10 +81,10 @@ const RocketChat = {
|
||||||
},
|
},
|
||||||
canOpenRoom,
|
canOpenRoom,
|
||||||
createChannel({
|
createChannel({
|
||||||
name, users, type, readOnly, broadcast
|
name, users, type, readOnly, broadcast, encrypted
|
||||||
}) {
|
}) {
|
||||||
// RC 0.51.0
|
// RC 0.51.0
|
||||||
return this.methodCallWrapper(type ? 'createPrivateGroup' : 'createChannel', name, users, readOnly, {}, { broadcast });
|
return this.methodCallWrapper(type ? 'createPrivateGroup' : 'createChannel', name, users, readOnly, {}, { broadcast, encrypted });
|
||||||
},
|
},
|
||||||
async getWebsocketInfo({ server }) {
|
async getWebsocketInfo({ server }) {
|
||||||
const sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) });
|
const sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) });
|
||||||
|
@ -307,6 +308,7 @@ const RocketChat = {
|
||||||
}
|
}
|
||||||
reduxStore.dispatch(shareSetUser(user));
|
reduxStore.dispatch(shareSetUser(user));
|
||||||
await RocketChat.login({ resume: user.token });
|
await RocketChat.login({ resume: user.token });
|
||||||
|
reduxStore.dispatch(encryptionInit());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(e);
|
log(e);
|
||||||
}
|
}
|
||||||
|
@ -321,6 +323,45 @@ const RocketChat = {
|
||||||
reduxStore.dispatch(shareSetUser({}));
|
reduxStore.dispatch(shareSetUser({}));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async e2eFetchMyKeys() {
|
||||||
|
// RC 0.70.0
|
||||||
|
const sdk = this.shareSDK || this.sdk;
|
||||||
|
const result = await sdk.get('e2e.fetchMyKeys');
|
||||||
|
// snake_case -> camelCase
|
||||||
|
if (result.success) {
|
||||||
|
return {
|
||||||
|
success: result.success,
|
||||||
|
publicKey: result.public_key,
|
||||||
|
privateKey: result.private_key
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
e2eSetUserPublicAndPrivateKeys(public_key, private_key) {
|
||||||
|
// RC 2.2.0
|
||||||
|
return this.post('e2e.setUserPublicAndPrivateKeys', { public_key, private_key });
|
||||||
|
},
|
||||||
|
e2eRequestSubscriptionKeys() {
|
||||||
|
// RC 0.72.0
|
||||||
|
return this.methodCallWrapper('e2e.requestSubscriptionKeys');
|
||||||
|
},
|
||||||
|
e2eGetUsersOfRoomWithoutKey(rid) {
|
||||||
|
// RC 0.70.0
|
||||||
|
return this.sdk.get('e2e.getUsersOfRoomWithoutKey', { rid });
|
||||||
|
},
|
||||||
|
e2eSetRoomKeyID(rid, keyID) {
|
||||||
|
// RC 0.70.0
|
||||||
|
return this.post('e2e.setRoomKeyID', { rid, keyID });
|
||||||
|
},
|
||||||
|
e2eUpdateGroupKey(uid, rid, key) {
|
||||||
|
// RC 0.70.0
|
||||||
|
return this.post('e2e.updateGroupKey', { uid, rid, key });
|
||||||
|
},
|
||||||
|
e2eRequestRoomKey(rid, e2eKeyId) {
|
||||||
|
// RC 0.70.0
|
||||||
|
return this.methodCallWrapper('stream-notify-room-users', `${ rid }/e2ekeyRequest`, rid, e2eKeyId);
|
||||||
|
},
|
||||||
|
|
||||||
updateJitsiTimeout(roomId) {
|
updateJitsiTimeout(roomId) {
|
||||||
// RC 0.74.0
|
// RC 0.74.0
|
||||||
return this.post('video-conference/jitsi.update-timeout', { roomId });
|
return this.post('video-conference/jitsi.update-timeout', { roomId });
|
||||||
|
@ -468,30 +509,7 @@ const RocketChat = {
|
||||||
sendMessage,
|
sendMessage,
|
||||||
getRooms,
|
getRooms,
|
||||||
readMessages,
|
readMessages,
|
||||||
async resendMessage(message, tmid) {
|
resendMessage,
|
||||||
const db = database.active;
|
|
||||||
try {
|
|
||||||
await db.action(async() => {
|
|
||||||
await message.update((m) => {
|
|
||||||
m.status = messagesStatus.TEMP;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
let m = {
|
|
||||||
id: message.id,
|
|
||||||
msg: message.msg,
|
|
||||||
subscription: { id: message.subscription.id }
|
|
||||||
};
|
|
||||||
if (tmid) {
|
|
||||||
m = {
|
|
||||||
...m,
|
|
||||||
tmid
|
|
||||||
};
|
|
||||||
}
|
|
||||||
await sendMessageCall.call(this, m);
|
|
||||||
} catch (e) {
|
|
||||||
log(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async search({ text, filterUsers = true, filterRooms = true }) {
|
async search({ text, filterUsers = true, filterRooms = true }) {
|
||||||
const searchText = text.trim();
|
const searchText = text.trim();
|
||||||
|
@ -641,10 +659,10 @@ const RocketChat = {
|
||||||
// RC 0.48.0
|
// RC 0.48.0
|
||||||
return this.post('chat.delete', { msgId: messageId, roomId: rid });
|
return this.post('chat.delete', { msgId: messageId, roomId: rid });
|
||||||
},
|
},
|
||||||
editMessage(message) {
|
async editMessage(message) {
|
||||||
const { id, msg, rid } = message;
|
const { rid, msg } = await Encryption.encryptMessage(message);
|
||||||
// RC 0.49.0
|
// RC 0.49.0
|
||||||
return this.post('chat.update', { roomId: rid, msgId: id, text: msg });
|
return this.post('chat.update', { roomId: rid, msgId: message.id, text: msg });
|
||||||
},
|
},
|
||||||
markAsUnread({ messageId }) {
|
markAsUnread({ messageId }) {
|
||||||
return this.post('subscriptions.unread', { firstUnreadMessage: { _id: messageId } });
|
return this.post('subscriptions.unread', { firstUnreadMessage: { _id: messageId } });
|
||||||
|
@ -1270,6 +1288,10 @@ const RocketChat = {
|
||||||
translateMessage(message, targetLanguage) {
|
translateMessage(message, targetLanguage) {
|
||||||
return this.methodCallWrapper('autoTranslate.translateMessage', message, targetLanguage);
|
return this.methodCallWrapper('autoTranslate.translateMessage', message, targetLanguage);
|
||||||
},
|
},
|
||||||
|
getSenderName(sender) {
|
||||||
|
const { UI_Use_Real_Name: useRealName } = reduxStore.getState().settings;
|
||||||
|
return useRealName ? sender.name : sender.username;
|
||||||
|
},
|
||||||
getRoomTitle(room) {
|
getRoomTitle(room) {
|
||||||
const { UI_Use_Real_Name: useRealName, UI_Allow_room_names_with_special_chars: allowSpecialChars } = reduxStore.getState().settings;
|
const { UI_Use_Real_Name: useRealName, UI_Allow_room_names_with_special_chars: allowSpecialChars } = reduxStore.getState().settings;
|
||||||
const { username } = reduxStore.getState().login.user;
|
const { username } = reduxStore.getState().login.user;
|
||||||
|
|
|
@ -6,6 +6,7 @@ import I18n from '../../i18n';
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
import Markdown from '../../containers/markdown';
|
import Markdown from '../../containers/markdown';
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
|
import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/encryption/constants';
|
||||||
|
|
||||||
const formatMsg = ({
|
const formatMsg = ({
|
||||||
lastMessage, type, showLastMessage, username, useRealName
|
lastMessage, type, showLastMessage, username, useRealName
|
||||||
|
@ -29,6 +30,11 @@ const formatMsg = ({
|
||||||
return I18n.t('User_sent_an_attachment', { user });
|
return I18n.t('User_sent_an_attachment', { user });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Encrypted message pending decrypt
|
||||||
|
if (lastMessage.t === E2E_MESSAGE_TYPE && lastMessage.e2e !== E2E_STATUS.DONE) {
|
||||||
|
lastMessage.msg = I18n.t('Encrypted_message');
|
||||||
|
}
|
||||||
|
|
||||||
if (isLastMessageSentByMe) {
|
if (isLastMessageSentByMe) {
|
||||||
prefix = I18n.t('You_colon');
|
prefix = I18n.t('You_colon');
|
||||||
} else if (type !== 'd') {
|
} else if (type !== 'd') {
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { ENCRYPTION } from '../actions/actionsTypes';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
banner: null
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function encryption(state = initialState, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case ENCRYPTION.SET_BANNER:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
banner: action.banner
|
||||||
|
};
|
||||||
|
case ENCRYPTION.INIT:
|
||||||
|
return initialState;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ import usersTyping from './usersTyping';
|
||||||
import inviteLinks from './inviteLinks';
|
import inviteLinks from './inviteLinks';
|
||||||
import createDiscussion from './createDiscussion';
|
import createDiscussion from './createDiscussion';
|
||||||
import enterpriseModules from './enterpriseModules';
|
import enterpriseModules from './enterpriseModules';
|
||||||
|
import encryption from './encryption';
|
||||||
|
|
||||||
import inquiry from '../ee/omnichannel/reducers/inquiry';
|
import inquiry from '../ee/omnichannel/reducers/inquiry';
|
||||||
|
|
||||||
|
@ -39,5 +40,6 @@ export default combineReducers({
|
||||||
inviteLinks,
|
inviteLinks,
|
||||||
createDiscussion,
|
createDiscussion,
|
||||||
inquiry,
|
inquiry,
|
||||||
enterpriseModules
|
enterpriseModules,
|
||||||
|
encryption
|
||||||
});
|
});
|
||||||
|
|
|
@ -36,8 +36,18 @@ const handleRequest = function* handleRequest({ data }) {
|
||||||
({ room: sub } = result);
|
({ room: sub } = result);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const { type, readOnly, broadcast } = data;
|
const {
|
||||||
logEvent(events.CREATE_CHANNEL_CREATE, { type: type ? 'private' : 'public', readOnly, broadcast });
|
type,
|
||||||
|
readOnly,
|
||||||
|
broadcast,
|
||||||
|
encrypted
|
||||||
|
} = data;
|
||||||
|
logEvent(events.CREATE_CHANNEL_CREATE, {
|
||||||
|
type: type ? 'private' : 'public',
|
||||||
|
readOnly,
|
||||||
|
broadcast,
|
||||||
|
encrypted
|
||||||
|
});
|
||||||
sub = yield call(createChannel, data);
|
sub = yield call(createChannel, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
import EJSON from 'ejson';
|
||||||
|
import { takeLatest, select, put } from 'redux-saga/effects';
|
||||||
|
|
||||||
|
import { ENCRYPTION } from '../actions/actionsTypes';
|
||||||
|
import { encryptionSetBanner } from '../actions/encryption';
|
||||||
|
import { Encryption } from '../lib/encryption';
|
||||||
|
import Navigation from '../lib/Navigation';
|
||||||
|
import {
|
||||||
|
E2E_PUBLIC_KEY,
|
||||||
|
E2E_PRIVATE_KEY,
|
||||||
|
E2E_BANNER_TYPE,
|
||||||
|
E2E_RANDOM_PASSWORD_KEY
|
||||||
|
} from '../lib/encryption/constants';
|
||||||
|
import database from '../lib/database';
|
||||||
|
import RocketChat from '../lib/rocketchat';
|
||||||
|
import UserPreferences from '../lib/userPreferences';
|
||||||
|
import { getUserSelector } from '../selectors/login';
|
||||||
|
import { showErrorAlert } from '../utils/info';
|
||||||
|
import I18n from '../i18n';
|
||||||
|
import log from '../utils/log';
|
||||||
|
|
||||||
|
const getServer = state => state.share.server || state.server.server;
|
||||||
|
|
||||||
|
const handleEncryptionInit = function* handleEncryptionInit() {
|
||||||
|
try {
|
||||||
|
const server = yield select(getServer);
|
||||||
|
const user = yield select(getUserSelector);
|
||||||
|
|
||||||
|
// Fetch server info to check E2E enable
|
||||||
|
const serversDB = database.servers;
|
||||||
|
const serversCollection = serversDB.collections.get('servers');
|
||||||
|
const serverInfo = yield serversCollection.find(server);
|
||||||
|
|
||||||
|
// If E2E is disabled on server, skip
|
||||||
|
if (!serverInfo?.E2E_Enable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch stored private e2e key for this server
|
||||||
|
const storedPrivateKey = yield UserPreferences.getStringAsync(`${ server }-${ E2E_PRIVATE_KEY }`);
|
||||||
|
|
||||||
|
// Fetch server stored e2e keys
|
||||||
|
const keys = yield RocketChat.e2eFetchMyKeys();
|
||||||
|
|
||||||
|
// A private key was received from the server, but it's not saved locally yet
|
||||||
|
// Show the banner asking for the password
|
||||||
|
if (!storedPrivateKey && keys?.privateKey) {
|
||||||
|
yield put(encryptionSetBanner(E2E_BANNER_TYPE.REQUEST_PASSWORD));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user has a private key stored, but never entered the password
|
||||||
|
const storedRandomPassword = yield UserPreferences.getStringAsync(`${ server }-${ E2E_RANDOM_PASSWORD_KEY }`);
|
||||||
|
if (storedRandomPassword) {
|
||||||
|
yield put(encryptionSetBanner(E2E_BANNER_TYPE.SAVE_PASSWORD));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch stored public e2e key for this server
|
||||||
|
let storedPublicKey = yield UserPreferences.getStringAsync(`${ server }-${ E2E_PUBLIC_KEY }`);
|
||||||
|
// Prevent parse undefined
|
||||||
|
if (storedPublicKey) {
|
||||||
|
storedPublicKey = EJSON.parse(storedPublicKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storedPublicKey && storedPrivateKey) {
|
||||||
|
// Persist these keys
|
||||||
|
yield Encryption.persistKeys(server, storedPublicKey, storedPrivateKey);
|
||||||
|
} else {
|
||||||
|
// Create new keys since the user doesn't have any
|
||||||
|
yield Encryption.createKeys(user.id, server);
|
||||||
|
yield put(encryptionSetBanner(E2E_BANNER_TYPE.SAVE_PASSWORD));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt all pending messages/subscriptions
|
||||||
|
Encryption.initialize();
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEncryptionStop = function* handleEncryptionStop() {
|
||||||
|
// Hide encryption banner
|
||||||
|
yield put(encryptionSetBanner());
|
||||||
|
// Stop Encryption client
|
||||||
|
Encryption.stop();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEncryptionDecodeKey = function* handleEncryptionDecodeKey({ password }) {
|
||||||
|
try {
|
||||||
|
const server = yield select(getServer);
|
||||||
|
const user = yield select(getUserSelector);
|
||||||
|
|
||||||
|
// Fetch server stored e2e keys
|
||||||
|
const keys = yield RocketChat.e2eFetchMyKeys();
|
||||||
|
|
||||||
|
const publicKey = EJSON.parse(keys?.publicKey);
|
||||||
|
|
||||||
|
// Decode the current server key
|
||||||
|
const privateKey = yield Encryption.decodePrivateKey(keys?.privateKey, password, user.id);
|
||||||
|
|
||||||
|
// Persist these decrypted keys
|
||||||
|
yield Encryption.persistKeys(server, publicKey, privateKey);
|
||||||
|
|
||||||
|
// Decrypt all pending messages/subscriptions
|
||||||
|
Encryption.initialize();
|
||||||
|
|
||||||
|
// Hide encryption banner
|
||||||
|
yield put(encryptionSetBanner());
|
||||||
|
|
||||||
|
Navigation.back();
|
||||||
|
} catch {
|
||||||
|
// Can't decrypt user private key
|
||||||
|
showErrorAlert(I18n.t('Encryption_error_desc'), I18n.t('Encryption_error_title'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const root = function* root() {
|
||||||
|
yield takeLatest(ENCRYPTION.INIT, handleEncryptionInit);
|
||||||
|
yield takeLatest(ENCRYPTION.STOP, handleEncryptionStop);
|
||||||
|
yield takeLatest(ENCRYPTION.DECODE_KEY, handleEncryptionDecodeKey);
|
||||||
|
};
|
||||||
|
export default root;
|
|
@ -10,6 +10,7 @@ import state from './state';
|
||||||
import deepLinking from './deepLinking';
|
import deepLinking from './deepLinking';
|
||||||
import inviteLinks from './inviteLinks';
|
import inviteLinks from './inviteLinks';
|
||||||
import createDiscussion from './createDiscussion';
|
import createDiscussion from './createDiscussion';
|
||||||
|
import encryption from './encryption';
|
||||||
|
|
||||||
import inquiry from '../ee/omnichannel/sagas/inquiry';
|
import inquiry from '../ee/omnichannel/sagas/inquiry';
|
||||||
|
|
||||||
|
@ -26,7 +27,8 @@ const root = function* root() {
|
||||||
deepLinking(),
|
deepLinking(),
|
||||||
inviteLinks(),
|
inviteLinks(),
|
||||||
createDiscussion(),
|
createDiscussion(),
|
||||||
inquiry()
|
inquiry(),
|
||||||
|
encryption()
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -24,10 +24,12 @@ import { inviteLinksRequest } from '../actions/inviteLinks';
|
||||||
import { showErrorAlert } from '../utils/info';
|
import { showErrorAlert } from '../utils/info';
|
||||||
import { localAuthenticate } from '../utils/localAuthentication';
|
import { localAuthenticate } from '../utils/localAuthentication';
|
||||||
import { setActiveUsers } from '../actions/activeUsers';
|
import { setActiveUsers } from '../actions/activeUsers';
|
||||||
|
import { encryptionInit, encryptionStop } from '../actions/encryption';
|
||||||
import UserPreferences from '../lib/userPreferences';
|
import UserPreferences from '../lib/userPreferences';
|
||||||
|
|
||||||
import { inquiryRequest, inquiryReset } from '../ee/omnichannel/actions/inquiry';
|
import { inquiryRequest, inquiryReset } from '../ee/omnichannel/actions/inquiry';
|
||||||
import { isOmnichannelStatusAvailable } from '../ee/omnichannel/lib';
|
import { isOmnichannelStatusAvailable } from '../ee/omnichannel/lib';
|
||||||
|
import { E2E_REFRESH_MESSAGES_KEY } from '../lib/encryption/constants';
|
||||||
|
|
||||||
const getServer = state => state.server.server;
|
const getServer = state => state.server.server;
|
||||||
const loginWithPasswordCall = args => RocketChat.loginWithPassword(args);
|
const loginWithPasswordCall = args => RocketChat.loginWithPassword(args);
|
||||||
|
@ -95,6 +97,35 @@ const fetchEnterpriseModules = function* fetchEnterpriseModules({ user }) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchRooms = function* fetchRooms({ server }) {
|
||||||
|
try {
|
||||||
|
// Read the flag to check if refresh was already done
|
||||||
|
const refreshed = yield UserPreferences.getBoolAsync(E2E_REFRESH_MESSAGES_KEY);
|
||||||
|
if (!refreshed) {
|
||||||
|
const serversDB = database.servers;
|
||||||
|
const serversCollection = serversDB.collections.get('servers');
|
||||||
|
|
||||||
|
const serverRecord = yield serversCollection.find(server);
|
||||||
|
|
||||||
|
// We need to reset roomsUpdatedAt to request all rooms again
|
||||||
|
// and save their respective E2EKeys to decrypt all pending messages and lastMessage
|
||||||
|
// that are already inserted on local database by other app version
|
||||||
|
yield serversDB.action(async() => {
|
||||||
|
await serverRecord.update((s) => {
|
||||||
|
s.roomsUpdatedAt = null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set the flag to indicate that already refreshed
|
||||||
|
yield UserPreferences.setBoolAsync(E2E_REFRESH_MESSAGES_KEY, true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
yield put(roomsRequest());
|
||||||
|
};
|
||||||
|
|
||||||
const handleLoginSuccess = function* handleLoginSuccess({ user }) {
|
const handleLoginSuccess = function* handleLoginSuccess({ user }) {
|
||||||
try {
|
try {
|
||||||
const adding = yield select(state => state.server.adding);
|
const adding = yield select(state => state.server.adding);
|
||||||
|
@ -103,7 +134,7 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
|
||||||
RocketChat.getUserPresence(user.id);
|
RocketChat.getUserPresence(user.id);
|
||||||
|
|
||||||
const server = yield select(getServer);
|
const server = yield select(getServer);
|
||||||
yield put(roomsRequest());
|
yield fork(fetchRooms, { server });
|
||||||
yield fork(fetchPermissions);
|
yield fork(fetchPermissions);
|
||||||
yield fork(fetchCustomEmojis);
|
yield fork(fetchCustomEmojis);
|
||||||
yield fork(fetchRoles);
|
yield fork(fetchRoles);
|
||||||
|
@ -111,6 +142,7 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
|
||||||
yield fork(registerPushToken);
|
yield fork(registerPushToken);
|
||||||
yield fork(fetchUsersPresence);
|
yield fork(fetchUsersPresence);
|
||||||
yield fork(fetchEnterpriseModules, { user });
|
yield fork(fetchEnterpriseModules, { user });
|
||||||
|
yield put(encryptionInit());
|
||||||
|
|
||||||
I18n.locale = user.language;
|
I18n.locale = user.language;
|
||||||
moment.locale(toMomentLocale(user.language));
|
moment.locale(toMomentLocale(user.language));
|
||||||
|
@ -173,6 +205,7 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = function* handleLogout({ forcedByServer }) {
|
const handleLogout = function* handleLogout({ forcedByServer }) {
|
||||||
|
yield put(encryptionStop());
|
||||||
yield put(appStart({ root: ROOT_LOADING, text: I18n.t('Logging_out') }));
|
yield put(appStart({ root: ROOT_LOADING, text: I18n.t('Logging_out') }));
|
||||||
const server = yield select(getServer);
|
const server = yield select(getServer);
|
||||||
if (server) {
|
if (server) {
|
||||||
|
|
|
@ -18,6 +18,7 @@ import I18n from '../i18n';
|
||||||
import { BASIC_AUTH_KEY, setBasicAuth } from '../utils/fetch';
|
import { BASIC_AUTH_KEY, setBasicAuth } from '../utils/fetch';
|
||||||
import { appStart, ROOT_INSIDE, ROOT_OUTSIDE } from '../actions/app';
|
import { appStart, ROOT_INSIDE, ROOT_OUTSIDE } from '../actions/app';
|
||||||
import UserPreferences from '../lib/userPreferences';
|
import UserPreferences from '../lib/userPreferences';
|
||||||
|
import { encryptionStop } from '../actions/encryption';
|
||||||
|
|
||||||
import { inquiryReset } from '../ee/omnichannel/actions/inquiry';
|
import { inquiryReset } from '../ee/omnichannel/actions/inquiry';
|
||||||
|
|
||||||
|
@ -67,6 +68,7 @@ const getServerInfo = function* getServerInfo({ server, raiseError = true }) {
|
||||||
const handleSelectServer = function* handleSelectServer({ server, version, fetchVersion }) {
|
const handleSelectServer = function* handleSelectServer({ server, version, fetchVersion }) {
|
||||||
try {
|
try {
|
||||||
yield put(inquiryReset());
|
yield put(inquiryReset());
|
||||||
|
yield put(encryptionStop());
|
||||||
const serversDB = database.servers;
|
const serversDB = database.servers;
|
||||||
yield UserPreferences.setStringAsync(RocketChat.CURRENT_SERVER, server);
|
yield UserPreferences.setStringAsync(RocketChat.CURRENT_SERVER, server);
|
||||||
const userId = yield UserPreferences.getStringAsync(`${ RocketChat.TOKEN_KEY }-${ server }`);
|
const userId = yield UserPreferences.getStringAsync(`${ RocketChat.TOKEN_KEY }-${ server }`);
|
||||||
|
|
|
@ -50,6 +50,13 @@ import AdminPanelView from '../views/AdminPanelView';
|
||||||
import NewMessageView from '../views/NewMessageView';
|
import NewMessageView from '../views/NewMessageView';
|
||||||
import CreateChannelView from '../views/CreateChannelView';
|
import CreateChannelView from '../views/CreateChannelView';
|
||||||
|
|
||||||
|
// E2ESaveYourPassword Stack
|
||||||
|
import E2ESaveYourPasswordView from '../views/E2ESaveYourPasswordView';
|
||||||
|
import E2EHowItWorksView from '../views/E2EHowItWorksView';
|
||||||
|
|
||||||
|
// E2EEnterYourPassword Stack
|
||||||
|
import E2EEnterYourPasswordView from '../views/E2EEnterYourPasswordView';
|
||||||
|
|
||||||
// InsideStackNavigator
|
// InsideStackNavigator
|
||||||
import AttachmentView from '../views/AttachmentView';
|
import AttachmentView from '../views/AttachmentView';
|
||||||
import ModalBlockView from '../views/ModalBlockView';
|
import ModalBlockView from '../views/ModalBlockView';
|
||||||
|
@ -302,6 +309,43 @@ const NewMessageStackNavigator = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// E2ESaveYourPasswordStackNavigator
|
||||||
|
const E2ESaveYourPasswordStack = createStackNavigator();
|
||||||
|
const E2ESaveYourPasswordStackNavigator = () => {
|
||||||
|
const { theme } = React.useContext(ThemeContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<E2ESaveYourPasswordStack.Navigator screenOptions={{ ...defaultHeader, ...themedHeader(theme), ...StackAnimation }}>
|
||||||
|
<E2ESaveYourPasswordStack.Screen
|
||||||
|
name='E2ESaveYourPasswordView'
|
||||||
|
component={E2ESaveYourPasswordView}
|
||||||
|
options={E2ESaveYourPasswordView.navigationOptions}
|
||||||
|
/>
|
||||||
|
<E2ESaveYourPasswordStack.Screen
|
||||||
|
name='E2EHowItWorksView'
|
||||||
|
component={E2EHowItWorksView}
|
||||||
|
options={E2EHowItWorksView.navigationOptions}
|
||||||
|
/>
|
||||||
|
</E2ESaveYourPasswordStack.Navigator>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// E2EEnterYourPasswordStackNavigator
|
||||||
|
const E2EEnterYourPasswordStack = createStackNavigator();
|
||||||
|
const E2EEnterYourPasswordStackNavigator = () => {
|
||||||
|
const { theme } = React.useContext(ThemeContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<E2EEnterYourPasswordStack.Navigator screenOptions={{ ...defaultHeader, ...themedHeader(theme), ...StackAnimation }}>
|
||||||
|
<E2EEnterYourPasswordStack.Screen
|
||||||
|
name='E2EEnterYourPasswordView'
|
||||||
|
component={E2EEnterYourPasswordView}
|
||||||
|
options={E2EEnterYourPasswordView.navigationOptions}
|
||||||
|
/>
|
||||||
|
</E2EEnterYourPasswordStack.Navigator>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// InsideStackNavigator
|
// InsideStackNavigator
|
||||||
const InsideStack = createStackNavigator();
|
const InsideStack = createStackNavigator();
|
||||||
const InsideStackNavigator = () => {
|
const InsideStackNavigator = () => {
|
||||||
|
@ -319,6 +363,16 @@ const InsideStackNavigator = () => {
|
||||||
component={NewMessageStackNavigator}
|
component={NewMessageStackNavigator}
|
||||||
options={{ headerShown: false }}
|
options={{ headerShown: false }}
|
||||||
/>
|
/>
|
||||||
|
<InsideStack.Screen
|
||||||
|
name='E2ESaveYourPasswordStackNavigator'
|
||||||
|
component={E2ESaveYourPasswordStackNavigator}
|
||||||
|
options={{ headerShown: false }}
|
||||||
|
/>
|
||||||
|
<InsideStack.Screen
|
||||||
|
name='E2EEnterYourPasswordStackNavigator'
|
||||||
|
component={E2EEnterYourPasswordStackNavigator}
|
||||||
|
options={{ headerShown: false }}
|
||||||
|
/>
|
||||||
<InsideStack.Screen
|
<InsideStack.Screen
|
||||||
name='AttachmentView'
|
name='AttachmentView'
|
||||||
component={AttachmentView}
|
component={AttachmentView}
|
||||||
|
|
|
@ -50,6 +50,9 @@ import ModalBlockView from '../../views/ModalBlockView';
|
||||||
import JitsiMeetView from '../../views/JitsiMeetView';
|
import JitsiMeetView from '../../views/JitsiMeetView';
|
||||||
import StatusView from '../../views/StatusView';
|
import StatusView from '../../views/StatusView';
|
||||||
import CreateDiscussionView from '../../views/CreateDiscussionView';
|
import CreateDiscussionView from '../../views/CreateDiscussionView';
|
||||||
|
import E2ESaveYourPasswordView from '../../views/E2ESaveYourPasswordView';
|
||||||
|
import E2EHowItWorksView from '../../views/E2EHowItWorksView';
|
||||||
|
import E2EEnterYourPasswordView from '../../views/E2EEnterYourPasswordView';
|
||||||
|
|
||||||
import { setKeyCommands, deleteKeyCommands } from '../../commands';
|
import { setKeyCommands, deleteKeyCommands } from '../../commands';
|
||||||
import ShareView from '../../views/ShareView';
|
import ShareView from '../../views/ShareView';
|
||||||
|
@ -256,6 +259,21 @@ const ModalStackNavigator = React.memo(({ navigation }) => {
|
||||||
name='CreateDiscussionView'
|
name='CreateDiscussionView'
|
||||||
component={CreateDiscussionView}
|
component={CreateDiscussionView}
|
||||||
/>
|
/>
|
||||||
|
<ModalStack.Screen
|
||||||
|
name='E2ESaveYourPasswordView'
|
||||||
|
component={E2ESaveYourPasswordView}
|
||||||
|
options={E2ESaveYourPasswordView.navigationOptions}
|
||||||
|
/>
|
||||||
|
<ModalStack.Screen
|
||||||
|
name='E2EHowItWorksView'
|
||||||
|
component={E2EHowItWorksView}
|
||||||
|
options={E2EHowItWorksView.navigationOptions}
|
||||||
|
/>
|
||||||
|
<ModalStack.Screen
|
||||||
|
name='E2EEnterYourPasswordView'
|
||||||
|
component={E2EEnterYourPasswordView}
|
||||||
|
options={E2EEnterYourPasswordView.navigationOptions}
|
||||||
|
/>
|
||||||
<ModalStack.Screen
|
<ModalStack.Screen
|
||||||
name='UserPreferencesView'
|
name='UserPreferencesView'
|
||||||
component={UserPreferencesView}
|
component={UserPreferencesView}
|
||||||
|
|
|
@ -0,0 +1,144 @@
|
||||||
|
/* eslint-disable no-undef */
|
||||||
|
/* eslint-disable no-bitwise */
|
||||||
|
// https://github.com/beatgammit/base64-js/tree/master/test
|
||||||
|
|
||||||
|
import {
|
||||||
|
byteLength,
|
||||||
|
toByteArray,
|
||||||
|
fromByteArray
|
||||||
|
} from './index';
|
||||||
|
|
||||||
|
const map = (arr, callback) => {
|
||||||
|
const res = [];
|
||||||
|
let kValue;
|
||||||
|
let mappedValue;
|
||||||
|
|
||||||
|
for (let k = 0, len = arr.length; k < len; k += 1) {
|
||||||
|
if ((typeof arr === 'string' && !!arr.charAt(k))) {
|
||||||
|
kValue = arr.charAt(k);
|
||||||
|
mappedValue = callback(kValue, k, arr);
|
||||||
|
res[k] = mappedValue;
|
||||||
|
} else if (typeof arr !== 'string' && k in arr) {
|
||||||
|
kValue = arr[k];
|
||||||
|
mappedValue = callback(kValue, k, arr);
|
||||||
|
res[k] = mappedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
expect.extend({
|
||||||
|
toBeEqual(a, b) {
|
||||||
|
let i;
|
||||||
|
const { length } = a;
|
||||||
|
if (length !== b.length) {
|
||||||
|
return {
|
||||||
|
pass: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
for (i = 0; i < length; i += 1) {
|
||||||
|
if ((a[i] & 0xFF) !== (b[i] & 0xFF)) {
|
||||||
|
return {
|
||||||
|
pass: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
pass: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('decode url-safe style base64 strings', () => {
|
||||||
|
const expected = [0xff, 0xff, 0xbe, 0xff, 0xef, 0xbf, 0xfb, 0xef, 0xff];
|
||||||
|
|
||||||
|
let str = '//++/++/++//';
|
||||||
|
let actual = toByteArray(str);
|
||||||
|
for (let i = 0; i < actual.length; i += 1) {
|
||||||
|
expect(actual[i]).toBe(expected[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(byteLength(str)).toBe(actual.length);
|
||||||
|
|
||||||
|
str = '__--_--_--__';
|
||||||
|
actual = toByteArray(str);
|
||||||
|
for (let i = 0; i < actual.length; i += 1) {
|
||||||
|
expect(actual[i]).toBe(expected[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(byteLength(str)).toBe(actual.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('padding bytes found inside base64 string', () => {
|
||||||
|
// See https://github.com/beatgammit/base64-js/issues/42
|
||||||
|
const str = 'SQ==QU0=';
|
||||||
|
expect(toByteArray(str)).toEqual(new Uint8Array([73]));
|
||||||
|
expect(byteLength(str)).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
const checks = [
|
||||||
|
'a',
|
||||||
|
'aa',
|
||||||
|
'aaa',
|
||||||
|
'hi',
|
||||||
|
'hi!',
|
||||||
|
'hi!!',
|
||||||
|
'sup',
|
||||||
|
'sup?',
|
||||||
|
'sup?!'
|
||||||
|
];
|
||||||
|
|
||||||
|
test('convert to base64 and back', () => {
|
||||||
|
for (let i = 0; i < checks.length; i += 1) {
|
||||||
|
const check = checks[i];
|
||||||
|
|
||||||
|
const b64Str = fromByteArray(map(check, char => char.charCodeAt(0)));
|
||||||
|
|
||||||
|
const arr = toByteArray(b64Str);
|
||||||
|
const str = map(arr, byte => String.fromCharCode(byte)).join('');
|
||||||
|
|
||||||
|
expect(check).toBe(str);
|
||||||
|
expect(byteLength(b64Str)).toBe(arr.length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
[[0, 0, 0], 'AAAA'],
|
||||||
|
[[0, 0, 1], 'AAAB'],
|
||||||
|
[[0, 1, -1], 'AAH/'],
|
||||||
|
[[1, 1, 1], 'AQEB'],
|
||||||
|
[[0, -73, 23], 'ALcX']
|
||||||
|
];
|
||||||
|
|
||||||
|
test('convert known data to string', () => {
|
||||||
|
for (let i = 0; i < data.length; i += 1) {
|
||||||
|
const bytes = data[i][0];
|
||||||
|
const expected = data[i][1];
|
||||||
|
const actual = fromByteArray(bytes);
|
||||||
|
expect(actual).toBe(expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('convert known data from string', () => {
|
||||||
|
for (let i = 0; i < data.length; i += 1) {
|
||||||
|
const expected = data[i][0];
|
||||||
|
const string = data[i][1];
|
||||||
|
const actual = toByteArray(string);
|
||||||
|
expect(actual).toBeEqual(expected);
|
||||||
|
const length = byteLength(string);
|
||||||
|
expect(length).toBe(expected.length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('convert big data to base64', () => {
|
||||||
|
let i;
|
||||||
|
let length;
|
||||||
|
const big = new Uint8Array(64 * 1024 * 1024);
|
||||||
|
for (i = 0, length = big.length; i < length; i += 1) {
|
||||||
|
big[i] = i % 256;
|
||||||
|
}
|
||||||
|
const b64str = fromByteArray(big);
|
||||||
|
const arr = toByteArray(b64str);
|
||||||
|
expect(arr).toBeEqual(big);
|
||||||
|
expect(byteLength(b64str)).toBe(arr.length);
|
||||||
|
});
|
|
@ -0,0 +1,141 @@
|
||||||
|
/* eslint-disable no-bitwise */
|
||||||
|
// https://github.com/beatgammit/base64-js/blob/master/index.js
|
||||||
|
|
||||||
|
const lookup = [];
|
||||||
|
const revLookup = [];
|
||||||
|
const Arr = typeof Uint8Array !== 'undefined' ? Uint8Array : Array;
|
||||||
|
|
||||||
|
const code = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||||
|
for (let i = 0, len = code.length; i < len; i += 1) {
|
||||||
|
lookup[i] = code[i];
|
||||||
|
revLookup[code.charCodeAt(i)] = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Support decoding URL-safe base64 strings, as Node.js does.
|
||||||
|
// See: https://en.wikipedia.org/wiki/Base64#URL_applications
|
||||||
|
revLookup['-'.charCodeAt(0)] = 62;
|
||||||
|
revLookup['_'.charCodeAt(0)] = 63;
|
||||||
|
|
||||||
|
const getLens = (b64) => {
|
||||||
|
const len = b64.length;
|
||||||
|
|
||||||
|
// We're encoding some strings not multiple of 4, so, disable this check
|
||||||
|
// if (len % 4 > 0) {
|
||||||
|
// throw new Error('Invalid string. Length must be a multiple of 4');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Trim off extra bytes after placeholder bytes are found
|
||||||
|
// See: https://github.com/beatgammit/base64-js/issues/42
|
||||||
|
let validLen = b64.indexOf('=');
|
||||||
|
if (validLen === -1) { validLen = len; }
|
||||||
|
|
||||||
|
const placeHoldersLen = validLen === len
|
||||||
|
? 0
|
||||||
|
: 4 - (validLen % 4);
|
||||||
|
|
||||||
|
return [validLen, placeHoldersLen];
|
||||||
|
};
|
||||||
|
|
||||||
|
// base64 is 4/3 + up to two characters of the original data
|
||||||
|
export const byteLength = (b64) => {
|
||||||
|
const lens = getLens(b64);
|
||||||
|
const validLen = lens[0];
|
||||||
|
const placeHoldersLen = lens[1];
|
||||||
|
return (((validLen + placeHoldersLen) * 3) / 4) - placeHoldersLen;
|
||||||
|
};
|
||||||
|
|
||||||
|
const _byteLength = (b64, validLen, placeHoldersLen) => (((validLen + placeHoldersLen) * 3) / 4) - placeHoldersLen;
|
||||||
|
|
||||||
|
export const toByteArray = (b64) => {
|
||||||
|
let tmp;
|
||||||
|
const lens = getLens(b64);
|
||||||
|
const validLen = lens[0];
|
||||||
|
const placeHoldersLen = lens[1];
|
||||||
|
|
||||||
|
const arr = new Arr(_byteLength(b64, validLen, placeHoldersLen));
|
||||||
|
|
||||||
|
let curByte = 0;
|
||||||
|
|
||||||
|
// if there are placeholders, only get up to the last complete 4 chars
|
||||||
|
const len = placeHoldersLen > 0
|
||||||
|
? validLen - 4
|
||||||
|
: validLen;
|
||||||
|
|
||||||
|
let i;
|
||||||
|
for (i = 0; i < len; i += 4) {
|
||||||
|
tmp = (revLookup[b64.charCodeAt(i)] << 18)
|
||||||
|
| (revLookup[b64.charCodeAt(i + 1)] << 12)
|
||||||
|
| (revLookup[b64.charCodeAt(i + 2)] << 6)
|
||||||
|
| revLookup[b64.charCodeAt(i + 3)];
|
||||||
|
arr[curByte] = (tmp >> 16) & 0xFF;
|
||||||
|
curByte += 1;
|
||||||
|
arr[curByte] = (tmp >> 8) & 0xFF;
|
||||||
|
curByte += 1;
|
||||||
|
arr[curByte] = tmp & 0xFF;
|
||||||
|
curByte += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (placeHoldersLen === 2) {
|
||||||
|
tmp = (revLookup[b64.charCodeAt(i)] << 2)
|
||||||
|
| (revLookup[b64.charCodeAt(i + 1)] >> 4);
|
||||||
|
arr[curByte] = tmp & 0xFF;
|
||||||
|
curByte += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (placeHoldersLen === 1) {
|
||||||
|
tmp = (revLookup[b64.charCodeAt(i)] << 10)
|
||||||
|
| (revLookup[b64.charCodeAt(i + 1)] << 4)
|
||||||
|
| (revLookup[b64.charCodeAt(i + 2)] >> 2);
|
||||||
|
arr[curByte] = (tmp >> 8) & 0xFF;
|
||||||
|
curByte += 1;
|
||||||
|
arr[curByte] = tmp & 0xFF;
|
||||||
|
curByte += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tripletToBase64 = num => lookup[(num >> 18) & 0x3F]
|
||||||
|
+ lookup[(num >> 12) & 0x3F]
|
||||||
|
+ lookup[(num >> 6) & 0x3F]
|
||||||
|
+ lookup[num & 0x3F];
|
||||||
|
|
||||||
|
const encodeChunk = (uint8, start, end) => {
|
||||||
|
let tmp;
|
||||||
|
const output = [];
|
||||||
|
for (let i = start; i < end; i += 3) {
|
||||||
|
tmp = ((uint8[i] << 16) & 0xFF0000) + ((uint8[i + 1] << 8) & 0xFF00) + (uint8[i + 2] & 0xFF);
|
||||||
|
output.push(tripletToBase64(tmp));
|
||||||
|
}
|
||||||
|
return output.join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fromByteArray = (uint8) => {
|
||||||
|
let tmp;
|
||||||
|
const len = uint8.length;
|
||||||
|
const extraBytes = len % 3; // if we have 1 byte left, pad 2 bytes
|
||||||
|
const parts = [];
|
||||||
|
const maxChunkLength = 16383; // must be multiple of 3
|
||||||
|
|
||||||
|
// go through the array every three bytes, we'll deal with trailing stuff later
|
||||||
|
for (let i = 0, len2 = len - extraBytes; i < len2; i += maxChunkLength) {
|
||||||
|
parts.push(encodeChunk(
|
||||||
|
uint8, i, (i + maxChunkLength) > len2 ? len2 : (i + maxChunkLength)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// pad the end with zeros, but make sure to not forget the extra bytes
|
||||||
|
if (extraBytes === 1) {
|
||||||
|
tmp = uint8[len - 1];
|
||||||
|
parts.push(
|
||||||
|
`${ lookup[tmp >> 2] + lookup[(tmp << 4) & 0x3F] }==`
|
||||||
|
);
|
||||||
|
} else if (extraBytes === 2) {
|
||||||
|
tmp = (uint8[len - 2] << 8) + uint8[len - 1];
|
||||||
|
parts.push(
|
||||||
|
`${ lookup[tmp >> 10] + lookup[(tmp >> 4) & 0x3F] + lookup[(tmp << 2) & 0x3F] }=`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join('');
|
||||||
|
};
|
|
@ -0,0 +1,14 @@
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Promise.jsm/Deferred
|
||||||
|
export default class Deferred {
|
||||||
|
constructor() {
|
||||||
|
const promise = new Promise((resolve, reject) => {
|
||||||
|
this.resolve = resolve;
|
||||||
|
this.reject = reject;
|
||||||
|
});
|
||||||
|
|
||||||
|
promise.resolve = this.resolve;
|
||||||
|
promise.reject = this.reject;
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
}
|
|
@ -58,6 +58,7 @@ export default {
|
||||||
RL_ADD_SERVER: 'rl_add_server',
|
RL_ADD_SERVER: 'rl_add_server',
|
||||||
RL_CHANGE_SERVER: 'rl_change_server',
|
RL_CHANGE_SERVER: 'rl_change_server',
|
||||||
RL_GO_NEW_MSG: 'rl_go_new_msg',
|
RL_GO_NEW_MSG: 'rl_go_new_msg',
|
||||||
|
RL_GO_E2E_SAVE_PASSWORD: 'rl_go_e2e_save_password',
|
||||||
RL_SEARCH: 'rl_search',
|
RL_SEARCH: 'rl_search',
|
||||||
RL_GO_DIRECTORY: 'rl_go_directory',
|
RL_GO_DIRECTORY: 'rl_go_directory',
|
||||||
RL_GO_QUEUE: 'rl_go_queue',
|
RL_GO_QUEUE: 'rl_go_queue',
|
||||||
|
@ -103,6 +104,7 @@ export default {
|
||||||
CREATE_CHANNEL_TOGGLE_TYPE: 'create_channel_toggle_type',
|
CREATE_CHANNEL_TOGGLE_TYPE: 'create_channel_toggle_type',
|
||||||
CREATE_CHANNEL_TOGGLE_READ_ONLY: 'create_channel_toggle_read_only',
|
CREATE_CHANNEL_TOGGLE_READ_ONLY: 'create_channel_toggle_read_only',
|
||||||
CREATE_CHANNEL_TOGGLE_BROADCAST: 'create_channel_toggle_broadcast',
|
CREATE_CHANNEL_TOGGLE_BROADCAST: 'create_channel_toggle_broadcast',
|
||||||
|
CREATE_CHANNEL_TOGGLE_ENCRYPTED: 'create_channel_toggle_encrypted',
|
||||||
CREATE_CHANNEL_REMOVE_USER: 'create_channel_remove_user',
|
CREATE_CHANNEL_REMOVE_USER: 'create_channel_remove_user',
|
||||||
|
|
||||||
// CREATE DISCUSSION VIEW
|
// CREATE DISCUSSION VIEW
|
||||||
|
@ -161,6 +163,7 @@ export default {
|
||||||
|
|
||||||
// ROOM VIEW
|
// ROOM VIEW
|
||||||
ROOM_SEND_MESSAGE: 'room_send_message',
|
ROOM_SEND_MESSAGE: 'room_send_message',
|
||||||
|
ROOM_ENCRYPTED_PRESS: 'room_encrypted_press',
|
||||||
ROOM_OPEN_EMOJI: 'room_open_emoji',
|
ROOM_OPEN_EMOJI: 'room_open_emoji',
|
||||||
ROOM_AUDIO_RECORD: 'room_audio_record',
|
ROOM_AUDIO_RECORD: 'room_audio_record',
|
||||||
ROOM_AUDIO_RECORD_F: 'room_audio_record_f',
|
ROOM_AUDIO_RECORD_F: 'room_audio_record_f',
|
||||||
|
@ -233,6 +236,8 @@ export default {
|
||||||
RA_LEAVE_F: 'ra_leave_f',
|
RA_LEAVE_F: 'ra_leave_f',
|
||||||
RA_TOGGLE_BLOCK_USER: 'ra_toggle_block_user',
|
RA_TOGGLE_BLOCK_USER: 'ra_toggle_block_user',
|
||||||
RA_TOGGLE_BLOCK_USER_F: 'ra_toggle_block_user_f',
|
RA_TOGGLE_BLOCK_USER_F: 'ra_toggle_block_user_f',
|
||||||
|
RA_TOGGLE_ENCRYPTED: 'ra_toggle_encrypted',
|
||||||
|
RA_TOGGLE_ENCRYPTED_F: 'ra_toggle_encrypted_f',
|
||||||
|
|
||||||
// ROOM INFO VIEW
|
// ROOM INFO VIEW
|
||||||
RI_GO_RI_EDIT: 'ri_go_ri_edit',
|
RI_GO_RI_EDIT: 'ri_go_ri_edit',
|
||||||
|
@ -244,6 +249,7 @@ export default {
|
||||||
RI_EDIT_TOGGLE_READ_ONLY: 'ri_edit_toggle_read_only',
|
RI_EDIT_TOGGLE_READ_ONLY: 'ri_edit_toggle_read_only',
|
||||||
RI_EDIT_TOGGLE_REACTIONS: 'ri_edit_toggle_reactions',
|
RI_EDIT_TOGGLE_REACTIONS: 'ri_edit_toggle_reactions',
|
||||||
RI_EDIT_TOGGLE_SYSTEM_MSG: 'ri_edit_toggle_system_msg',
|
RI_EDIT_TOGGLE_SYSTEM_MSG: 'ri_edit_toggle_system_msg',
|
||||||
|
RI_EDIT_TOGGLE_ENCRYPTED: 'ri_edit_toggle_encrypted',
|
||||||
RI_EDIT_SAVE: 'ri_edit_save',
|
RI_EDIT_SAVE: 'ri_edit_save',
|
||||||
RI_EDIT_SAVE_F: 'ri_edit_save_f',
|
RI_EDIT_SAVE_F: 'ri_edit_save_f',
|
||||||
RI_EDIT_RESET: 'ri_edit_reset',
|
RI_EDIT_RESET: 'ri_edit_reset',
|
||||||
|
@ -288,5 +294,13 @@ export default {
|
||||||
NP_DESKTOPNOTIFICATIONDURATION: 'np_desktopnotificationduration',
|
NP_DESKTOPNOTIFICATIONDURATION: 'np_desktopnotificationduration',
|
||||||
NP_DESKTOPNOTIFICATIONDURATION_F: 'np_desktopnotificationduration_f',
|
NP_DESKTOPNOTIFICATIONDURATION_F: 'np_desktopnotificationduration_f',
|
||||||
NP_EMAILNOTIFICATIONS: 'np_email_notifications',
|
NP_EMAILNOTIFICATIONS: 'np_email_notifications',
|
||||||
NP_EMAILNOTIFICATIONS_F: 'np_email_notifications_f'
|
NP_EMAILNOTIFICATIONS_F: 'np_email_notifications_f',
|
||||||
|
|
||||||
|
// E2E SAVE YOUR PASSWORD VIEW
|
||||||
|
E2E_SAVE_PW_SAVED: 'e2e_save_pw_saved',
|
||||||
|
E2E_SAVE_PW_COPY: 'e2e_save_pw_copy',
|
||||||
|
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'
|
||||||
};
|
};
|
||||||
|
|
|
@ -85,6 +85,7 @@ class CreateChannelView extends React.Component {
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
failure: PropTypes.bool,
|
failure: PropTypes.bool,
|
||||||
isFetching: PropTypes.bool,
|
isFetching: PropTypes.bool,
|
||||||
|
e2eEnabled: PropTypes.bool,
|
||||||
users: PropTypes.array.isRequired,
|
users: PropTypes.array.isRequired,
|
||||||
user: PropTypes.shape({
|
user: PropTypes.shape({
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
|
@ -97,14 +98,17 @@ class CreateChannelView extends React.Component {
|
||||||
channelName: '',
|
channelName: '',
|
||||||
type: true,
|
type: true,
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
|
encrypted: false,
|
||||||
broadcast: false
|
broadcast: false
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
const {
|
const {
|
||||||
channelName, type, readOnly, broadcast
|
channelName, type, readOnly, broadcast, encrypted
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const { users, isFetching, theme } = this.props;
|
const {
|
||||||
|
users, isFetching, e2eEnabled, theme
|
||||||
|
} = this.props;
|
||||||
if (nextProps.theme !== theme) {
|
if (nextProps.theme !== theme) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -117,12 +121,18 @@ class CreateChannelView extends React.Component {
|
||||||
if (nextState.readOnly !== readOnly) {
|
if (nextState.readOnly !== readOnly) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (nextState.encrypted !== encrypted) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (nextState.broadcast !== broadcast) {
|
if (nextState.broadcast !== broadcast) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (nextProps.isFetching !== isFetching) {
|
if (nextProps.isFetching !== isFetching) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (nextProps.e2eEnabled !== e2eEnabled) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (!equal(nextProps.users, users)) {
|
if (!equal(nextProps.users, users)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -147,7 +157,7 @@ class CreateChannelView extends React.Component {
|
||||||
|
|
||||||
submit = () => {
|
submit = () => {
|
||||||
const {
|
const {
|
||||||
channelName, type, readOnly, broadcast
|
channelName, type, readOnly, broadcast, encrypted
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const { users: usersProps, isFetching, create } = this.props;
|
const { users: usersProps, isFetching, create } = this.props;
|
||||||
|
|
||||||
|
@ -160,7 +170,7 @@ class CreateChannelView extends React.Component {
|
||||||
|
|
||||||
// create channel
|
// create channel
|
||||||
create({
|
create({
|
||||||
name: channelName, users, type, readOnly, broadcast
|
name: channelName, users, type, readOnly, broadcast, encrypted
|
||||||
});
|
});
|
||||||
|
|
||||||
Review.pushPositiveEvent();
|
Review.pushPositiveEvent();
|
||||||
|
@ -198,7 +208,8 @@ class CreateChannelView extends React.Component {
|
||||||
label: 'Private_Channel',
|
label: 'Private_Channel',
|
||||||
onValueChange: (value) => {
|
onValueChange: (value) => {
|
||||||
logEvent(events.CREATE_CHANNEL_TOGGLE_TYPE);
|
logEvent(events.CREATE_CHANNEL_TOGGLE_TYPE);
|
||||||
this.setState({ type: value });
|
// If we set the channel as public, encrypted status should be false
|
||||||
|
this.setState(({ encrypted }) => ({ type: value, encrypted: value && encrypted }));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -217,6 +228,26 @@ class CreateChannelView extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderEncrypted() {
|
||||||
|
const { type, encrypted } = this.state;
|
||||||
|
const { e2eEnabled } = this.props;
|
||||||
|
|
||||||
|
if (!e2eEnabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.renderSwitch({
|
||||||
|
id: 'encrypted',
|
||||||
|
value: encrypted,
|
||||||
|
label: 'Encrypted',
|
||||||
|
onValueChange: (value) => {
|
||||||
|
logEvent(events.CREATE_CHANNEL_TOGGLE_ENCRYPTED);
|
||||||
|
this.setState({ encrypted: value });
|
||||||
|
},
|
||||||
|
disabled: !type
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
renderBroadcast() {
|
renderBroadcast() {
|
||||||
const { broadcast, readOnly } = this.state;
|
const { broadcast, readOnly } = this.state;
|
||||||
return this.renderSwitch({
|
return this.renderSwitch({
|
||||||
|
@ -315,6 +346,8 @@ class CreateChannelView extends React.Component {
|
||||||
{this.renderFormSeparator()}
|
{this.renderFormSeparator()}
|
||||||
{this.renderReadOnly()}
|
{this.renderReadOnly()}
|
||||||
{this.renderFormSeparator()}
|
{this.renderFormSeparator()}
|
||||||
|
{this.renderEncrypted()}
|
||||||
|
{this.renderFormSeparator()}
|
||||||
{this.renderBroadcast()}
|
{this.renderBroadcast()}
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.invitedHeader}>
|
<View style={styles.invitedHeader}>
|
||||||
|
@ -333,6 +366,7 @@ class CreateChannelView extends React.Component {
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
baseUrl: state.server.server,
|
baseUrl: state.server.server,
|
||||||
isFetching: state.createChannel.isFetching,
|
isFetching: state.createChannel.isFetching,
|
||||||
|
e2eEnabled: state.settings.E2E_Enable,
|
||||||
users: state.selectedUsers.users,
|
users: state.selectedUsers.users,
|
||||||
user: getUserSelector(state)
|
user: getUserSelector(state)
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Text, StyleSheet, ScrollView } from 'react-native';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import I18n from '../i18n';
|
||||||
|
import sharedStyles from './Styles';
|
||||||
|
import { withTheme } from '../theme';
|
||||||
|
import Button from '../containers/Button';
|
||||||
|
import { themes } from '../constants/colors';
|
||||||
|
import TextInput from '../containers/TextInput';
|
||||||
|
import SafeAreaView from '../containers/SafeAreaView';
|
||||||
|
import { CloseModalButton } from '../containers/HeaderButton';
|
||||||
|
import { encryptionDecodeKey as encryptionDecodeKeyAction } from '../actions/encryption';
|
||||||
|
import scrollPersistTaps from '../utils/scrollPersistTaps';
|
||||||
|
import KeyboardView from '../presentation/KeyboardView';
|
||||||
|
import StatusBar from '../containers/StatusBar';
|
||||||
|
import { logEvent, events } from '../utils/log';
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
padding: 28
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
fontSize: 14,
|
||||||
|
marginVertical: 8,
|
||||||
|
...sharedStyles.textRegular
|
||||||
|
}
|
||||||
|
});
|
||||||
|
class E2EEnterYourPasswordView extends React.Component {
|
||||||
|
static navigationOptions = ({ navigation }) => ({
|
||||||
|
headerLeft: () => <CloseModalButton navigation={navigation} testID='e2e-enter-your-password-view-close' />,
|
||||||
|
title: I18n.t('Enter_Your_E2E_Password')
|
||||||
|
})
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
encryptionDecodeKey: PropTypes.func,
|
||||||
|
theme: PropTypes.string
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
password: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
submit = () => {
|
||||||
|
logEvent(events.E2E_ENTER_PW_SUBMIT);
|
||||||
|
const { password } = this.state;
|
||||||
|
const { encryptionDecodeKey } = this.props;
|
||||||
|
encryptionDecodeKey(password);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { password } = this.state;
|
||||||
|
const { theme } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<KeyboardView
|
||||||
|
style={{ backgroundColor: themes[theme].backgroundColor }}
|
||||||
|
contentContainerStyle={sharedStyles.container}
|
||||||
|
keyboardVerticalOffset={128}
|
||||||
|
>
|
||||||
|
<StatusBar theme={theme} />
|
||||||
|
<ScrollView {...scrollPersistTaps} style={sharedStyles.container} contentContainerStyle={[sharedStyles.containerScrollView, styles.scrollView]}>
|
||||||
|
<SafeAreaView theme={theme} style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]}>
|
||||||
|
<TextInput
|
||||||
|
inputRef={(e) => { this.passwordInput = e; }}
|
||||||
|
placeholder={I18n.t('Password')}
|
||||||
|
returnKeyType='send'
|
||||||
|
secureTextEntry
|
||||||
|
onSubmitEditing={this.submit}
|
||||||
|
onChangeText={value => this.setState({ password: value })}
|
||||||
|
testID='e2e-enter-your-password-view-password'
|
||||||
|
textContentType='password'
|
||||||
|
autoCompleteType='password'
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onPress={this.submit}
|
||||||
|
title={I18n.t('Confirm')}
|
||||||
|
disabled={!password}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
</SafeAreaView>
|
||||||
|
</ScrollView>
|
||||||
|
</KeyboardView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
encryptionDecodeKey: password => dispatch(encryptionDecodeKeyAction(password))
|
||||||
|
});
|
||||||
|
export default connect(null, mapDispatchToProps)(withTheme(E2EEnterYourPasswordView));
|
|
@ -0,0 +1,73 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { StyleSheet } from 'react-native';
|
||||||
|
|
||||||
|
import SafeAreaView from '../containers/SafeAreaView';
|
||||||
|
import { themes } from '../constants/colors';
|
||||||
|
import { CloseModalButton } from '../containers/HeaderButton';
|
||||||
|
import Markdown from '../containers/markdown';
|
||||||
|
import { withTheme } from '../theme';
|
||||||
|
import I18n from '../i18n';
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
padding: 44,
|
||||||
|
paddingTop: 32
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
fontSize: 14,
|
||||||
|
marginVertical: 8
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
class E2EHowItWorksView extends React.Component {
|
||||||
|
static navigationOptions = ({ route, navigation }) => {
|
||||||
|
const showCloseModal = route.params?.showCloseModal;
|
||||||
|
return {
|
||||||
|
title: I18n.t('How_It_Works'),
|
||||||
|
headerLeft: showCloseModal ? () => <CloseModalButton navigation={navigation} /> : undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
theme: PropTypes.string
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { theme } = this.props;
|
||||||
|
|
||||||
|
const infoStyle = [styles.info, { color: themes[theme].bodyText }];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView
|
||||||
|
style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]}
|
||||||
|
testID='e2e-how-it-works-view'
|
||||||
|
theme={theme}
|
||||||
|
>
|
||||||
|
<Markdown
|
||||||
|
msg={I18n.t('E2E_How_It_Works_info1')}
|
||||||
|
style={infoStyle}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
<Markdown
|
||||||
|
msg={I18n.t('E2E_How_It_Works_info2')}
|
||||||
|
style={infoStyle}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
<Markdown
|
||||||
|
msg={I18n.t('E2E_How_It_Works_info3')}
|
||||||
|
style={infoStyle}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
<Markdown
|
||||||
|
msg={I18n.t('E2E_How_It_Works_info4')}
|
||||||
|
style={infoStyle}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withTheme(E2EHowItWorksView);
|
|
@ -0,0 +1,172 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
Text,
|
||||||
|
View,
|
||||||
|
Clipboard,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet
|
||||||
|
} from 'react-native';
|
||||||
|
|
||||||
|
import { encryptionSetBanner as encryptionSetBannerAction } from '../actions/encryption';
|
||||||
|
import { E2E_RANDOM_PASSWORD_KEY } from '../lib/encryption/constants';
|
||||||
|
import { CloseModalButton } from '../containers/HeaderButton';
|
||||||
|
import scrollPersistTaps from '../utils/scrollPersistTaps';
|
||||||
|
import SafeAreaView from '../containers/SafeAreaView';
|
||||||
|
import UserPreferences from '../lib/userPreferences';
|
||||||
|
import { logEvent, events } from '../utils/log';
|
||||||
|
import StatusBar from '../containers/StatusBar';
|
||||||
|
import { LISTENER } from '../containers/Toast';
|
||||||
|
import { themes } from '../constants/colors';
|
||||||
|
import EventEmitter from '../utils/events';
|
||||||
|
import Button from '../containers/Button';
|
||||||
|
import { withTheme } from '../theme';
|
||||||
|
import sharedStyles from './Styles';
|
||||||
|
import I18n from '../i18n';
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
padding: 44,
|
||||||
|
paddingTop: 32
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
marginVertical: 68,
|
||||||
|
alignItems: 'center'
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
fontSize: 14,
|
||||||
|
...sharedStyles.textMedium
|
||||||
|
},
|
||||||
|
passwordText: {
|
||||||
|
marginBottom: 8,
|
||||||
|
...sharedStyles.textAlignCenter
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
fontSize: 24,
|
||||||
|
marginBottom: 24,
|
||||||
|
...sharedStyles.textBold
|
||||||
|
},
|
||||||
|
copyButton: {
|
||||||
|
width: 72,
|
||||||
|
height: 32
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
fontSize: 14,
|
||||||
|
marginBottom: 64,
|
||||||
|
...sharedStyles.textRegular
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
class E2ESaveYourPasswordView extends React.Component {
|
||||||
|
static navigationOptions = ({ navigation }) => ({
|
||||||
|
headerLeft: () => <CloseModalButton navigation={navigation} testID='e2e-save-your-password-view-close' />,
|
||||||
|
title: I18n.t('Save_Your_E2E_Password')
|
||||||
|
})
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
server: PropTypes.string,
|
||||||
|
navigation: PropTypes.object,
|
||||||
|
encryptionSetBanner: PropTypes.func,
|
||||||
|
theme: PropTypes.string
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.mounted = false;
|
||||||
|
this.state = { password: '' };
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.mounted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
init = async() => {
|
||||||
|
const { server } = this.props;
|
||||||
|
try {
|
||||||
|
// Set stored password on local state
|
||||||
|
const password = await UserPreferences.getStringAsync(`${ server }-${ E2E_RANDOM_PASSWORD_KEY }`);
|
||||||
|
if (this.mounted) {
|
||||||
|
this.setState({ password });
|
||||||
|
} else {
|
||||||
|
this.state.password = password;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSaved = async() => {
|
||||||
|
logEvent(events.E2E_SAVE_PW_SAVED);
|
||||||
|
const { navigation, server, encryptionSetBanner } = this.props;
|
||||||
|
// Remove stored password
|
||||||
|
await UserPreferences.removeItem(`${ server }-${ E2E_RANDOM_PASSWORD_KEY }`);
|
||||||
|
// Hide encryption banner
|
||||||
|
encryptionSetBanner();
|
||||||
|
navigation.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
onCopy = () => {
|
||||||
|
logEvent(events.E2E_SAVE_PW_COPY);
|
||||||
|
const { password } = this.state;
|
||||||
|
Clipboard.setString(password);
|
||||||
|
EventEmitter.emit(LISTENER, { message: I18n.t('Copied_to_clipboard') });
|
||||||
|
}
|
||||||
|
|
||||||
|
onHowItWorks = () => {
|
||||||
|
logEvent(events.E2E_SAVE_PW_HOW_IT_WORKS);
|
||||||
|
const { navigation } = this.props;
|
||||||
|
navigation.navigate('E2EHowItWorksView');
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { password } = this.state;
|
||||||
|
const { theme } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView theme={theme} style={{ backgroundColor: themes[theme].backgroundColor }}>
|
||||||
|
<StatusBar theme={theme} />
|
||||||
|
<ScrollView {...scrollPersistTaps} style={sharedStyles.container} contentContainerStyle={sharedStyles.containerScrollView}>
|
||||||
|
<View style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]}>
|
||||||
|
<Text style={[styles.warning, { color: themes[theme].dangerColor }]}>{I18n.t('Save_Your_Encryption_Password_warning')}</Text>
|
||||||
|
<View style={styles.content}>
|
||||||
|
<Text style={[styles.passwordText, { color: themes[theme].bodyText }]}>{I18n.t('Your_password_is')}</Text>
|
||||||
|
<Text style={[styles.password, { color: themes[theme].bodyText }]}>{password}</Text>
|
||||||
|
<Button
|
||||||
|
onPress={this.onCopy}
|
||||||
|
style={[styles.copyButton, { backgroundColor: themes[theme].auxiliaryBackground }]}
|
||||||
|
title={I18n.t('Copy')}
|
||||||
|
type='secondary'
|
||||||
|
fontSize={12}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<Text style={[styles.info, { color: themes[theme].bodyText }]}>{I18n.t('Save_Your_Encryption_Password_info')}</Text>
|
||||||
|
<Button
|
||||||
|
onPress={this.onHowItWorks}
|
||||||
|
style={{ backgroundColor: themes[theme].auxiliaryBackground }}
|
||||||
|
title={I18n.t('How_It_Works')}
|
||||||
|
type='secondary'
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onPress={this.onSaved}
|
||||||
|
title={I18n.t('I_Saved_My_E2E_Password')}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
server: state.server.server
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
encryptionSetBanner: () => dispatch(encryptionSetBannerAction())
|
||||||
|
});
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(E2ESaveYourPasswordView));
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {
|
import {
|
||||||
View, SectionList, Text, Alert, Share
|
View, SectionList, Text, Alert, Share, Switch
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
@ -21,13 +21,16 @@ import scrollPersistTaps from '../../utils/scrollPersistTaps';
|
||||||
import { CustomIcon } from '../../lib/Icons';
|
import { CustomIcon } from '../../lib/Icons';
|
||||||
import DisclosureIndicator from '../../containers/DisclosureIndicator';
|
import DisclosureIndicator from '../../containers/DisclosureIndicator';
|
||||||
import StatusBar from '../../containers/StatusBar';
|
import StatusBar from '../../containers/StatusBar';
|
||||||
import { themes } from '../../constants/colors';
|
import { themes, SWITCH_TRACK_COLOR } from '../../constants/colors';
|
||||||
import { withTheme } from '../../theme';
|
import { withTheme } from '../../theme';
|
||||||
import { CloseModalButton } from '../../containers/HeaderButton';
|
import { CloseModalButton } from '../../containers/HeaderButton';
|
||||||
import { getUserSelector } from '../../selectors/login';
|
import { getUserSelector } from '../../selectors/login';
|
||||||
import Markdown from '../../containers/markdown';
|
import Markdown from '../../containers/markdown';
|
||||||
import { showConfirmationAlert, showErrorAlert } from '../../utils/info';
|
import { showConfirmationAlert, showErrorAlert } from '../../utils/info';
|
||||||
import SafeAreaView from '../../containers/SafeAreaView';
|
import SafeAreaView from '../../containers/SafeAreaView';
|
||||||
|
import { E2E_ROOM_TYPES } from '../../lib/encryption/constants';
|
||||||
|
import protectedFunction from '../../lib/methods/helpers/protectedFunction';
|
||||||
|
import database from '../../lib/database';
|
||||||
|
|
||||||
class RoomActionsView extends React.Component {
|
class RoomActionsView extends React.Component {
|
||||||
static navigationOptions = ({ navigation, isMasterDetail }) => {
|
static navigationOptions = ({ navigation, isMasterDetail }) => {
|
||||||
|
@ -50,6 +53,7 @@ class RoomActionsView extends React.Component {
|
||||||
}),
|
}),
|
||||||
leaveRoom: PropTypes.func,
|
leaveRoom: PropTypes.func,
|
||||||
jitsiEnabled: PropTypes.bool,
|
jitsiEnabled: PropTypes.bool,
|
||||||
|
e2eEnabled: PropTypes.bool,
|
||||||
setLoadingInvite: PropTypes.func,
|
setLoadingInvite: PropTypes.func,
|
||||||
closeRoom: PropTypes.func,
|
closeRoom: PropTypes.func,
|
||||||
theme: PropTypes.string
|
theme: PropTypes.string
|
||||||
|
@ -72,7 +76,8 @@ class RoomActionsView extends React.Component {
|
||||||
canAddUser: false,
|
canAddUser: false,
|
||||||
canInviteUser: false,
|
canInviteUser: false,
|
||||||
canForwardGuest: false,
|
canForwardGuest: false,
|
||||||
canReturnQueue: false
|
canReturnQueue: false,
|
||||||
|
canEdit: false
|
||||||
};
|
};
|
||||||
if (room && room.observe && room.rid) {
|
if (room && room.observe && room.rid) {
|
||||||
this.roomObservable = room.observe();
|
this.roomObservable = room.observe();
|
||||||
|
@ -120,6 +125,7 @@ class RoomActionsView extends React.Component {
|
||||||
|
|
||||||
this.canAddUser();
|
this.canAddUser();
|
||||||
this.canInviteUser();
|
this.canInviteUser();
|
||||||
|
this.canEdit();
|
||||||
|
|
||||||
// livechat permissions
|
// livechat permissions
|
||||||
if (room.t === 'l') {
|
if (room.t === 'l') {
|
||||||
|
@ -184,6 +190,15 @@ class RoomActionsView extends React.Component {
|
||||||
this.setState({ canInviteUser });
|
this.setState({ canInviteUser });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canEdit = async() => {
|
||||||
|
const { room } = this.state;
|
||||||
|
const { rid } = room;
|
||||||
|
const permissions = await RocketChat.hasPermission(['edit-room'], rid);
|
||||||
|
|
||||||
|
const canEdit = permissions && permissions['edit-room'];
|
||||||
|
this.setState({ canEdit });
|
||||||
|
}
|
||||||
|
|
||||||
canViewMembers = async() => {
|
canViewMembers = async() => {
|
||||||
const { room } = this.state;
|
const { room } = this.state;
|
||||||
const { rid, t, broadcast } = room;
|
const { rid, t, broadcast } = room;
|
||||||
|
@ -227,11 +242,11 @@ class RoomActionsView extends React.Component {
|
||||||
|
|
||||||
get sections() {
|
get sections() {
|
||||||
const {
|
const {
|
||||||
room, member, membersCount, canViewMembers, canAddUser, canInviteUser, joined, canAutoTranslate, canForwardGuest, canReturnQueue
|
room, member, membersCount, canViewMembers, canAddUser, canInviteUser, joined, canAutoTranslate, canForwardGuest, canReturnQueue, canEdit
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const { jitsiEnabled } = this.props;
|
const { jitsiEnabled, e2eEnabled } = this.props;
|
||||||
const {
|
const {
|
||||||
rid, t, blocker
|
rid, t, blocker, encrypted
|
||||||
} = room;
|
} = room;
|
||||||
const isGroupChat = RocketChat.isGroupChat(room);
|
const isGroupChat = RocketChat.isGroupChat(room);
|
||||||
|
|
||||||
|
@ -240,7 +255,8 @@ class RoomActionsView extends React.Component {
|
||||||
name: I18n.t('Notifications'),
|
name: I18n.t('Notifications'),
|
||||||
route: 'NotificationPrefView',
|
route: 'NotificationPrefView',
|
||||||
params: { rid, room },
|
params: { rid, room },
|
||||||
testID: 'room-actions-notifications'
|
testID: 'room-actions-notifications',
|
||||||
|
right: this.renderDisclosure
|
||||||
};
|
};
|
||||||
|
|
||||||
const jitsiActions = jitsiEnabled ? [
|
const jitsiActions = jitsiEnabled ? [
|
||||||
|
@ -248,13 +264,15 @@ class RoomActionsView extends React.Component {
|
||||||
icon: 'phone',
|
icon: 'phone',
|
||||||
name: I18n.t('Voice_call'),
|
name: I18n.t('Voice_call'),
|
||||||
event: () => RocketChat.callJitsi(rid, true),
|
event: () => RocketChat.callJitsi(rid, true),
|
||||||
testID: 'room-actions-voice'
|
testID: 'room-actions-voice',
|
||||||
|
right: this.renderDisclosure
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'camera',
|
icon: 'camera',
|
||||||
name: I18n.t('Video_call'),
|
name: I18n.t('Video_call'),
|
||||||
event: () => RocketChat.callJitsi(rid),
|
event: () => RocketChat.callJitsi(rid),
|
||||||
testID: 'room-actions-video'
|
testID: 'room-actions-video',
|
||||||
|
right: this.renderDisclosure
|
||||||
}
|
}
|
||||||
] : [];
|
] : [];
|
||||||
|
|
||||||
|
@ -281,41 +299,47 @@ class RoomActionsView extends React.Component {
|
||||||
name: I18n.t('Files'),
|
name: I18n.t('Files'),
|
||||||
route: 'MessagesView',
|
route: 'MessagesView',
|
||||||
params: { rid, t, name: 'Files' },
|
params: { rid, t, name: 'Files' },
|
||||||
testID: 'room-actions-files'
|
testID: 'room-actions-files',
|
||||||
|
right: this.renderDisclosure
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'mention',
|
icon: 'mention',
|
||||||
name: I18n.t('Mentions'),
|
name: I18n.t('Mentions'),
|
||||||
route: 'MessagesView',
|
route: 'MessagesView',
|
||||||
params: { rid, t, name: 'Mentions' },
|
params: { rid, t, name: 'Mentions' },
|
||||||
testID: 'room-actions-mentioned'
|
testID: 'room-actions-mentioned',
|
||||||
|
right: this.renderDisclosure
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'star',
|
icon: 'star',
|
||||||
name: I18n.t('Starred'),
|
name: I18n.t('Starred'),
|
||||||
route: 'MessagesView',
|
route: 'MessagesView',
|
||||||
params: { rid, t, name: 'Starred' },
|
params: { rid, t, name: 'Starred' },
|
||||||
testID: 'room-actions-starred'
|
testID: 'room-actions-starred',
|
||||||
|
right: this.renderDisclosure
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'search',
|
icon: 'search',
|
||||||
name: I18n.t('Search'),
|
name: I18n.t('Search'),
|
||||||
route: 'SearchMessagesView',
|
route: 'SearchMessagesView',
|
||||||
params: { rid },
|
params: { rid, encrypted },
|
||||||
testID: 'room-actions-search'
|
testID: 'room-actions-search',
|
||||||
|
right: this.renderDisclosure
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'share',
|
icon: 'share',
|
||||||
name: I18n.t('Share'),
|
name: I18n.t('Share'),
|
||||||
event: this.handleShare,
|
event: this.handleShare,
|
||||||
testID: 'room-actions-share'
|
testID: 'room-actions-share',
|
||||||
|
right: this.renderDisclosure
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'pin',
|
icon: 'pin',
|
||||||
name: I18n.t('Pinned'),
|
name: I18n.t('Pinned'),
|
||||||
route: 'MessagesView',
|
route: 'MessagesView',
|
||||||
params: { rid, t, name: 'Pinned' },
|
params: { rid, t, name: 'Pinned' },
|
||||||
testID: 'room-actions-pinned'
|
testID: 'room-actions-pinned',
|
||||||
|
right: this.renderDisclosure
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
renderItem: this.renderItem
|
renderItem: this.renderItem
|
||||||
|
@ -327,7 +351,8 @@ class RoomActionsView extends React.Component {
|
||||||
name: I18n.t('Auto_Translate'),
|
name: I18n.t('Auto_Translate'),
|
||||||
route: 'AutoTranslateView',
|
route: 'AutoTranslateView',
|
||||||
params: { rid, room },
|
params: { rid, room },
|
||||||
testID: 'room-actions-auto-translate'
|
testID: 'room-actions-auto-translate',
|
||||||
|
right: this.renderDisclosure
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -338,7 +363,8 @@ class RoomActionsView extends React.Component {
|
||||||
description: membersCount > 0 ? `${ membersCount } ${ I18n.t('members') }` : null,
|
description: membersCount > 0 ? `${ membersCount } ${ I18n.t('members') }` : null,
|
||||||
route: 'RoomMembersView',
|
route: 'RoomMembersView',
|
||||||
params: { rid, room },
|
params: { rid, room },
|
||||||
testID: 'room-actions-members'
|
testID: 'room-actions-members',
|
||||||
|
right: this.renderDisclosure
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -350,7 +376,8 @@ class RoomActionsView extends React.Component {
|
||||||
name: I18n.t(`${ blocker ? 'Unblock' : 'Block' }_user`),
|
name: I18n.t(`${ blocker ? 'Unblock' : 'Block' }_user`),
|
||||||
type: 'danger',
|
type: 'danger',
|
||||||
event: this.toggleBlockUser,
|
event: this.toggleBlockUser,
|
||||||
testID: 'room-actions-block-user'
|
testID: 'room-actions-block-user',
|
||||||
|
right: this.renderDisclosure
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
renderItem: this.renderItem
|
renderItem: this.renderItem
|
||||||
|
@ -366,7 +393,8 @@ class RoomActionsView extends React.Component {
|
||||||
description: membersCount > 0 ? `${ membersCount } ${ I18n.t('members') }` : null,
|
description: membersCount > 0 ? `${ membersCount } ${ I18n.t('members') }` : null,
|
||||||
route: 'RoomMembersView',
|
route: 'RoomMembersView',
|
||||||
params: { rid, room },
|
params: { rid, room },
|
||||||
testID: 'room-actions-members'
|
testID: 'room-actions-members',
|
||||||
|
right: this.renderDisclosure
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -380,7 +408,8 @@ class RoomActionsView extends React.Component {
|
||||||
title: I18n.t('Add_users'),
|
title: I18n.t('Add_users'),
|
||||||
nextAction: this.addUser
|
nextAction: this.addUser
|
||||||
},
|
},
|
||||||
testID: 'room-actions-add-user'
|
testID: 'room-actions-add-user',
|
||||||
|
right: this.renderDisclosure
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (canInviteUser) {
|
if (canInviteUser) {
|
||||||
|
@ -391,7 +420,8 @@ class RoomActionsView extends React.Component {
|
||||||
params: {
|
params: {
|
||||||
rid
|
rid
|
||||||
},
|
},
|
||||||
testID: 'room-actions-invite-user'
|
testID: 'room-actions-invite-user',
|
||||||
|
right: this.renderDisclosure
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
sections[2].data = [...actions, ...sections[2].data];
|
sections[2].data = [...actions, ...sections[2].data];
|
||||||
|
@ -405,7 +435,8 @@ class RoomActionsView extends React.Component {
|
||||||
name: I18n.t('Leave_channel'),
|
name: I18n.t('Leave_channel'),
|
||||||
type: 'danger',
|
type: 'danger',
|
||||||
event: this.leaveChannel,
|
event: this.leaveChannel,
|
||||||
testID: 'room-actions-leave-channel'
|
testID: 'room-actions-leave-channel',
|
||||||
|
right: this.renderDisclosure
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
renderItem: this.renderItem
|
renderItem: this.renderItem
|
||||||
|
@ -418,7 +449,8 @@ class RoomActionsView extends React.Component {
|
||||||
sections[2].data.push({
|
sections[2].data.push({
|
||||||
icon: 'close',
|
icon: 'close',
|
||||||
name: I18n.t('Close'),
|
name: I18n.t('Close'),
|
||||||
event: this.closeLivechat
|
event: this.closeLivechat,
|
||||||
|
right: this.renderDisclosure
|
||||||
});
|
});
|
||||||
|
|
||||||
if (canForwardGuest) {
|
if (canForwardGuest) {
|
||||||
|
@ -426,7 +458,8 @@ class RoomActionsView extends React.Component {
|
||||||
icon: 'user-forward',
|
icon: 'user-forward',
|
||||||
name: I18n.t('Forward'),
|
name: I18n.t('Forward'),
|
||||||
route: 'ForwardLivechatView',
|
route: 'ForwardLivechatView',
|
||||||
params: { rid }
|
params: { rid },
|
||||||
|
right: this.renderDisclosure
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -434,7 +467,8 @@ class RoomActionsView extends React.Component {
|
||||||
sections[2].data.push({
|
sections[2].data.push({
|
||||||
icon: 'undo',
|
icon: 'undo',
|
||||||
name: I18n.t('Return'),
|
name: I18n.t('Return'),
|
||||||
event: this.returnLivechat
|
event: this.returnLivechat,
|
||||||
|
right: this.renderDisclosure
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -442,7 +476,8 @@ class RoomActionsView extends React.Component {
|
||||||
icon: 'history',
|
icon: 'history',
|
||||||
name: I18n.t('Navigation_history'),
|
name: I18n.t('Navigation_history'),
|
||||||
route: 'VisitorNavigationView',
|
route: 'VisitorNavigationView',
|
||||||
params: { rid }
|
params: { rid },
|
||||||
|
right: this.renderDisclosure
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -452,14 +487,47 @@ class RoomActionsView extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If can edit this room
|
||||||
|
// If this room type can be Encrypted
|
||||||
|
// If e2e is enabled for this server
|
||||||
|
if (canEdit && E2E_ROOM_TYPES[t] && e2eEnabled) {
|
||||||
|
sections.splice(2, 0, {
|
||||||
|
data: [{
|
||||||
|
icon: 'encrypted',
|
||||||
|
name: I18n.t('Encrypted'),
|
||||||
|
testID: 'room-actions-encrypt',
|
||||||
|
right: this.renderEncryptedSwitch
|
||||||
|
}],
|
||||||
|
renderItem: this.renderItem
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return sections;
|
return sections;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderDisclosure = () => {
|
||||||
|
const { theme } = this.props;
|
||||||
|
return <DisclosureIndicator theme={theme} />;
|
||||||
|
}
|
||||||
|
|
||||||
renderSeparator = () => {
|
renderSeparator = () => {
|
||||||
const { theme } = this.props;
|
const { theme } = this.props;
|
||||||
return <View style={[styles.separator, { backgroundColor: themes[theme].separatorColor }]} />;
|
return <View style={[styles.separator, { backgroundColor: themes[theme].separatorColor }]} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderEncryptedSwitch = () => {
|
||||||
|
const { room } = this.state;
|
||||||
|
const { encrypted } = room;
|
||||||
|
return (
|
||||||
|
<Switch
|
||||||
|
value={encrypted}
|
||||||
|
trackColor={SWITCH_TRACK_COLOR}
|
||||||
|
onValueChange={this.toggleEncrypted}
|
||||||
|
style={styles.encryptedSwitch}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
closeLivechat = () => {
|
closeLivechat = () => {
|
||||||
const { room: { rid } } = this.state;
|
const { room: { rid } } = this.state;
|
||||||
const { closeRoom } = this.props;
|
const { closeRoom } = this.props;
|
||||||
|
@ -514,19 +582,58 @@ class RoomActionsView extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleBlockUser = () => {
|
toggleBlockUser = async() => {
|
||||||
logEvent(events.RA_TOGGLE_BLOCK_USER);
|
logEvent(events.RA_TOGGLE_BLOCK_USER);
|
||||||
const { room } = this.state;
|
const { room } = this.state;
|
||||||
const { rid, blocker } = room;
|
const { rid, blocker } = room;
|
||||||
const { member } = this.state;
|
const { member } = this.state;
|
||||||
try {
|
try {
|
||||||
RocketChat.toggleBlockUser(rid, member._id, !blocker);
|
await RocketChat.toggleBlockUser(rid, member._id, !blocker);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logEvent(events.RA_TOGGLE_BLOCK_USER_F);
|
logEvent(events.RA_TOGGLE_BLOCK_USER_F);
|
||||||
log(e);
|
log(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleEncrypted = async() => {
|
||||||
|
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.action(async() => {
|
||||||
|
await room.update(protectedFunction((r) => {
|
||||||
|
r.encrypted = encrypted;
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Send new room setting value to server
|
||||||
|
const { result } = await RocketChat.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.action(async() => {
|
||||||
|
await room.update(protectedFunction((r) => {
|
||||||
|
r.encrypted = room.encrypted;
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logEvent(events.RA_TOGGLE_ENCRYPTED_F);
|
||||||
|
log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleShare = () => {
|
handleShare = () => {
|
||||||
logEvent(events.RA_SHARE);
|
logEvent(events.RA_SHARE);
|
||||||
const { room } = this.state;
|
const { room } = this.state;
|
||||||
|
@ -638,7 +745,7 @@ class RoomActionsView extends React.Component {
|
||||||
<CustomIcon name={item.icon} size={24} style={[styles.sectionItemIcon, { color: themes[theme].bodyText }]} />
|
<CustomIcon name={item.icon} size={24} style={[styles.sectionItemIcon, { color: themes[theme].bodyText }]} />
|
||||||
<Text style={[styles.sectionItemName, { color: themes[theme].bodyText }]}>{ item.name }</Text>
|
<Text style={[styles.sectionItemName, { color: themes[theme].bodyText }]}>{ item.name }</Text>
|
||||||
{item.description ? <Text style={[styles.sectionItemDescription, { color: themes[theme].auxiliaryText }]}>{ item.description }</Text> : null}
|
{item.description ? <Text style={[styles.sectionItemDescription, { color: themes[theme].auxiliaryText }]}>{ item.description }</Text> : null}
|
||||||
<DisclosureIndicator theme={theme} />
|
{item?.right?.()}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
return this.renderTouchableItem(subview, item);
|
return this.renderTouchableItem(subview, item);
|
||||||
|
@ -679,7 +786,8 @@ class RoomActionsView extends React.Component {
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
user: getUserSelector(state),
|
user: getUserSelector(state),
|
||||||
baseUrl: state.server.server,
|
baseUrl: state.server.server,
|
||||||
jitsiEnabled: state.settings.Jitsi_Enabled || false
|
jitsiEnabled: state.settings.Jitsi_Enabled || false,
|
||||||
|
e2eEnabled: state.settings.E2E_Enable || false
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
|
|
@ -58,5 +58,8 @@ export default StyleSheet.create({
|
||||||
paddingRight: 16,
|
paddingRight: 16,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center'
|
alignItems: 'center'
|
||||||
|
},
|
||||||
|
encryptedSwitch: {
|
||||||
|
marginHorizontal: 16
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -56,6 +56,7 @@ class RoomInfoEditView extends React.Component {
|
||||||
route: PropTypes.object,
|
route: PropTypes.object,
|
||||||
deleteRoom: PropTypes.func,
|
deleteRoom: PropTypes.func,
|
||||||
serverVersion: PropTypes.string,
|
serverVersion: PropTypes.string,
|
||||||
|
e2eEnabled: PropTypes.bool,
|
||||||
theme: PropTypes.string
|
theme: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -76,7 +77,8 @@ class RoomInfoEditView extends React.Component {
|
||||||
reactWhenReadOnly: false,
|
reactWhenReadOnly: false,
|
||||||
archived: false,
|
archived: false,
|
||||||
systemMessages: [],
|
systemMessages: [],
|
||||||
enableSysMes: false
|
enableSysMes: false,
|
||||||
|
encrypted: false
|
||||||
};
|
};
|
||||||
this.loadRoom();
|
this.loadRoom();
|
||||||
}
|
}
|
||||||
|
@ -123,7 +125,7 @@ class RoomInfoEditView extends React.Component {
|
||||||
|
|
||||||
init = (room) => {
|
init = (room) => {
|
||||||
const {
|
const {
|
||||||
description, topic, announcement, t, ro, reactWhenReadOnly, joinCodeRequired, sysMes
|
description, topic, announcement, t, ro, reactWhenReadOnly, joinCodeRequired, sysMes, encrypted
|
||||||
} = room;
|
} = room;
|
||||||
// fake password just to user knows about it
|
// fake password just to user knows about it
|
||||||
this.randomValue = random(15);
|
this.randomValue = random(15);
|
||||||
|
@ -139,7 +141,8 @@ class RoomInfoEditView extends React.Component {
|
||||||
joinCode: joinCodeRequired ? this.randomValue : '',
|
joinCode: joinCodeRequired ? this.randomValue : '',
|
||||||
archived: room.archived,
|
archived: room.archived,
|
||||||
systemMessages: sysMes,
|
systemMessages: sysMes,
|
||||||
enableSysMes: sysMes && sysMes.length > 0
|
enableSysMes: sysMes && sysMes.length > 0,
|
||||||
|
encrypted
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,7 +160,7 @@ class RoomInfoEditView extends React.Component {
|
||||||
|
|
||||||
formIsChanged = () => {
|
formIsChanged = () => {
|
||||||
const {
|
const {
|
||||||
room, name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCode, systemMessages, enableSysMes
|
room, name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCode, systemMessages, enableSysMes, encrypted
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const { joinCodeRequired } = room;
|
const { joinCodeRequired } = room;
|
||||||
return !(room.name === name
|
return !(room.name === name
|
||||||
|
@ -170,6 +173,7 @@ class RoomInfoEditView extends React.Component {
|
||||||
&& room.reactWhenReadOnly === reactWhenReadOnly
|
&& room.reactWhenReadOnly === reactWhenReadOnly
|
||||||
&& isEqual(room.sysMes, systemMessages)
|
&& isEqual(room.sysMes, systemMessages)
|
||||||
&& enableSysMes === (room.sysMes && room.sysMes.length > 0)
|
&& enableSysMes === (room.sysMes && room.sysMes.length > 0)
|
||||||
|
&& room.encrypted === encrypted
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,7 +181,7 @@ class RoomInfoEditView extends React.Component {
|
||||||
logEvent(events.RI_EDIT_SAVE);
|
logEvent(events.RI_EDIT_SAVE);
|
||||||
Keyboard.dismiss();
|
Keyboard.dismiss();
|
||||||
const {
|
const {
|
||||||
room, name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCode, systemMessages
|
room, name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCode, systemMessages, encrypted
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
this.setState({ saving: true });
|
this.setState({ saving: true });
|
||||||
|
@ -231,6 +235,11 @@ class RoomInfoEditView extends React.Component {
|
||||||
params.joinCode = joinCode;
|
params.joinCode = joinCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Encrypted
|
||||||
|
if (room.encrypted !== encrypted) {
|
||||||
|
params.encrypted = encrypted;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await RocketChat.saveRoomSettings(room.rid, params);
|
await RocketChat.saveRoomSettings(room.rid, params);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -340,7 +349,7 @@ class RoomInfoEditView extends React.Component {
|
||||||
|
|
||||||
toggleRoomType = (value) => {
|
toggleRoomType = (value) => {
|
||||||
logEvent(events.RI_EDIT_TOGGLE_ROOM_TYPE);
|
logEvent(events.RI_EDIT_TOGGLE_ROOM_TYPE);
|
||||||
this.setState({ t: value });
|
this.setState(({ encrypted }) => ({ t: value, encrypted: value && encrypted }));
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleReadOnly = (value) => {
|
toggleReadOnly = (value) => {
|
||||||
|
@ -358,11 +367,16 @@ class RoomInfoEditView extends React.Component {
|
||||||
this.setState(({ systemMessages }) => ({ enableSysMes: value, systemMessages: value ? systemMessages : [] }));
|
this.setState(({ systemMessages }) => ({ enableSysMes: value, systemMessages: value ? systemMessages : [] }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleEncrypted = (value) => {
|
||||||
|
logEvent(events.RI_EDIT_TOGGLE_ENCRYPTED);
|
||||||
|
this.setState({ encrypted: value });
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
name, nameError, description, topic, announcement, t, ro, reactWhenReadOnly, room, joinCode, saving, permissions, archived, enableSysMes
|
name, nameError, description, topic, announcement, t, ro, reactWhenReadOnly, room, joinCode, saving, permissions, archived, enableSysMes, encrypted
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const { serverVersion, theme } = this.props;
|
const { serverVersion, e2eEnabled, theme } = this.props;
|
||||||
const { dangerColor } = themes[theme];
|
const { dangerColor } = themes[theme];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -487,6 +501,19 @@ class RoomInfoEditView extends React.Component {
|
||||||
{this.renderSystemMessages()}
|
{this.renderSystemMessages()}
|
||||||
</SwitchContainer>
|
</SwitchContainer>
|
||||||
) : null}
|
) : null}
|
||||||
|
{e2eEnabled ? (
|
||||||
|
<SwitchContainer
|
||||||
|
value={encrypted}
|
||||||
|
disabled={!t}
|
||||||
|
leftLabelPrimary={I18n.t('Encrypted')}
|
||||||
|
leftLabelSecondary={I18n.t('End_to_end_encrypted_room')}
|
||||||
|
theme={theme}
|
||||||
|
testID='room-info-edit-switch-encrypted'
|
||||||
|
onValueChange={this.toggleEncrypted}
|
||||||
|
labelContainerStyle={styles.hideSystemMessages}
|
||||||
|
leftLabelStyle={styles.systemMessagesLabel}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.buttonContainer,
|
styles.buttonContainer,
|
||||||
|
@ -577,7 +604,8 @@ class RoomInfoEditView extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
serverVersion: state.server.version
|
serverVersion: state.server.version,
|
||||||
|
e2eEnabled: state.settings.E2E_Enable || false
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
import List from './List';
|
import List from './List';
|
||||||
import database from '../../lib/database';
|
import database from '../../lib/database';
|
||||||
import RocketChat from '../../lib/rocketchat';
|
import RocketChat from '../../lib/rocketchat';
|
||||||
|
import { Encryption } from '../../lib/encryption';
|
||||||
import Message from '../../containers/message';
|
import Message from '../../containers/message';
|
||||||
import MessageActions from '../../containers/MessageActions';
|
import MessageActions from '../../containers/MessageActions';
|
||||||
import MessageErrorActions from '../../containers/MessageErrorActions';
|
import MessageErrorActions from '../../containers/MessageErrorActions';
|
||||||
|
@ -55,6 +56,7 @@ import Navigation from '../../lib/Navigation';
|
||||||
import SafeAreaView from '../../containers/SafeAreaView';
|
import SafeAreaView from '../../containers/SafeAreaView';
|
||||||
import { withDimensions } from '../../dimensions';
|
import { withDimensions } from '../../dimensions';
|
||||||
import { getHeaderTitlePosition } from '../../containers/Header';
|
import { getHeaderTitlePosition } from '../../containers/Header';
|
||||||
|
import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/encryption/constants';
|
||||||
|
|
||||||
import { takeInquiry } from '../../ee/omnichannel/lib';
|
import { takeInquiry } from '../../ee/omnichannel/lib';
|
||||||
|
|
||||||
|
@ -582,6 +584,18 @@ class RoomView extends React.Component {
|
||||||
this.setState({ selectedMessage: {}, reactionsModalVisible: false });
|
this.setState({ selectedMessage: {}, reactionsModalVisible: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onEncryptedPress = () => {
|
||||||
|
logEvent(events.ROOM_ENCRYPTED_PRESS);
|
||||||
|
const { navigation, isMasterDetail } = this.props;
|
||||||
|
|
||||||
|
const screen = { screen: 'E2EHowItWorksView', params: { showCloseModal: true } };
|
||||||
|
|
||||||
|
if (isMasterDetail) {
|
||||||
|
return navigation.navigate('ModalStackNavigator', screen);
|
||||||
|
}
|
||||||
|
navigation.navigate('E2ESaveYourPasswordStackNavigator', screen);
|
||||||
|
}
|
||||||
|
|
||||||
onDiscussionPress = debounce((item) => {
|
onDiscussionPress = debounce((item) => {
|
||||||
const { navigation } = this.props;
|
const { navigation } = this.props;
|
||||||
navigation.push('RoomView', {
|
navigation.push('RoomView', {
|
||||||
|
@ -616,8 +630,12 @@ class RoomView extends React.Component {
|
||||||
if (!item.tmsg) {
|
if (!item.tmsg) {
|
||||||
await this.fetchThreadName(item.tmid, item.id);
|
await this.fetchThreadName(item.tmid, item.id);
|
||||||
}
|
}
|
||||||
|
let name = item.tmsg;
|
||||||
|
if (item.t === E2E_MESSAGE_TYPE && item.e2e !== E2E_STATUS.DONE) {
|
||||||
|
name = I18n.t('Encrypted_message');
|
||||||
|
}
|
||||||
navigation.push('RoomView', {
|
navigation.push('RoomView', {
|
||||||
rid: item.subscription.id, tmid: item.tmid, name: item.tmsg, t: 'thread'
|
rid: item.subscription.id, tmid: item.tmid, name, t: 'thread'
|
||||||
});
|
});
|
||||||
} else if (item.tlm) {
|
} else if (item.tlm) {
|
||||||
navigation.push('RoomView', {
|
navigation.push('RoomView', {
|
||||||
|
@ -723,7 +741,8 @@ class RoomView extends React.Component {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const { message: thread } = await RocketChat.getSingleMessage(tmid);
|
let { message: thread } = await RocketChat.getSingleMessage(tmid);
|
||||||
|
thread = await Encryption.decryptMessage(thread);
|
||||||
await db.action(async() => {
|
await db.action(async() => {
|
||||||
await db.batch(
|
await db.batch(
|
||||||
threadCollection.prepareCreate((t) => {
|
threadCollection.prepareCreate((t) => {
|
||||||
|
@ -858,6 +877,7 @@ class RoomView extends React.Component {
|
||||||
onReactionPress={this.onReactionPress}
|
onReactionPress={this.onReactionPress}
|
||||||
onReactionLongPress={this.onReactionLongPress}
|
onReactionLongPress={this.onReactionLongPress}
|
||||||
onLongPress={this.onMessageLongPress}
|
onLongPress={this.onMessageLongPress}
|
||||||
|
onEncryptedPress={this.onEncryptedPress}
|
||||||
onDiscussionPress={this.onDiscussionPress}
|
onDiscussionPress={this.onDiscussionPress}
|
||||||
onThreadPress={this.onThreadPress}
|
onThreadPress={this.onThreadPress}
|
||||||
showAttachment={this.showAttachment}
|
showAttachment={this.showAttachment}
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Text } from 'react-native';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { BorderlessButton } from 'react-native-gesture-handler';
|
||||||
|
|
||||||
|
import { withTheme } from '../../../theme';
|
||||||
|
import { CustomIcon } from '../../../lib/Icons';
|
||||||
|
import { themes } from '../../../constants/colors';
|
||||||
|
import I18n from '../../../i18n';
|
||||||
|
import styles from '../styles';
|
||||||
|
import { E2E_BANNER_TYPE } from '../../../lib/encryption/constants';
|
||||||
|
|
||||||
|
const Encryption = React.memo(({
|
||||||
|
searching,
|
||||||
|
goEncryption,
|
||||||
|
encryptionBanner,
|
||||||
|
theme
|
||||||
|
}) => {
|
||||||
|
if (searching > 0 || !encryptionBanner) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = I18n.t('Save_Your_Encryption_Password');
|
||||||
|
if (encryptionBanner === E2E_BANNER_TYPE.REQUEST_PASSWORD) {
|
||||||
|
text = I18n.t('Enter_Your_E2E_Password');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BorderlessButton style={[styles.encryptionButton, { backgroundColor: themes[theme].actionTintColor }]} theme={theme} onPress={goEncryption}>
|
||||||
|
<CustomIcon name='encrypted' size={24} color={themes[theme].buttonText} style={styles.encryptionIcon} />
|
||||||
|
<Text style={[styles.encryptionText, { color: themes[theme].buttonText }]}>{text}</Text>
|
||||||
|
</BorderlessButton>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Encryption.propTypes = {
|
||||||
|
searching: PropTypes.bool,
|
||||||
|
goEncryption: PropTypes.func,
|
||||||
|
encryptionBanner: PropTypes.string,
|
||||||
|
theme: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withTheme(Encryption);
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import Sort from './Sort';
|
import Sort from './Sort';
|
||||||
|
import Encryption from './Encryption';
|
||||||
|
|
||||||
import OmnichannelStatus from '../../../ee/omnichannel/containers/OmnichannelStatus';
|
import OmnichannelStatus from '../../../ee/omnichannel/containers/OmnichannelStatus';
|
||||||
|
|
||||||
|
@ -9,12 +10,15 @@ const ListHeader = React.memo(({
|
||||||
searching,
|
searching,
|
||||||
sortBy,
|
sortBy,
|
||||||
toggleSort,
|
toggleSort,
|
||||||
|
goEncryption,
|
||||||
goQueue,
|
goQueue,
|
||||||
queueSize,
|
queueSize,
|
||||||
inquiryEnabled,
|
inquiryEnabled,
|
||||||
|
encryptionBanner,
|
||||||
user
|
user
|
||||||
}) => (
|
}) => (
|
||||||
<>
|
<>
|
||||||
|
<Encryption searching={searching} goEncryption={goEncryption} encryptionBanner={encryptionBanner} />
|
||||||
<Sort searching={searching} sortBy={sortBy} toggleSort={toggleSort} />
|
<Sort searching={searching} sortBy={sortBy} toggleSort={toggleSort} />
|
||||||
<OmnichannelStatus searching={searching} goQueue={goQueue} inquiryEnabled={inquiryEnabled} queueSize={queueSize} user={user} />
|
<OmnichannelStatus searching={searching} goQueue={goQueue} inquiryEnabled={inquiryEnabled} queueSize={queueSize} user={user} />
|
||||||
</>
|
</>
|
||||||
|
@ -24,9 +28,11 @@ ListHeader.propTypes = {
|
||||||
searching: PropTypes.bool,
|
searching: PropTypes.bool,
|
||||||
sortBy: PropTypes.string,
|
sortBy: PropTypes.string,
|
||||||
toggleSort: PropTypes.func,
|
toggleSort: PropTypes.func,
|
||||||
|
goEncryption: PropTypes.func,
|
||||||
goQueue: PropTypes.func,
|
goQueue: PropTypes.func,
|
||||||
queueSize: PropTypes.number,
|
queueSize: PropTypes.number,
|
||||||
inquiryEnabled: PropTypes.bool,
|
inquiryEnabled: PropTypes.bool,
|
||||||
|
encryptionBanner: PropTypes.string,
|
||||||
user: PropTypes.object
|
user: PropTypes.object
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -63,6 +63,7 @@ import SafeAreaView from '../../containers/SafeAreaView';
|
||||||
import Header, { getHeaderTitlePosition } from '../../containers/Header';
|
import Header, { getHeaderTitlePosition } from '../../containers/Header';
|
||||||
import { withDimensions } from '../../dimensions';
|
import { withDimensions } from '../../dimensions';
|
||||||
import { showErrorAlert, showConfirmationAlert } from '../../utils/info';
|
import { showErrorAlert, showConfirmationAlert } from '../../utils/info';
|
||||||
|
import { E2E_BANNER_TYPE } from '../../lib/encryption/constants';
|
||||||
|
|
||||||
import { getInquiryQueueSelector } from '../../ee/omnichannel/selectors/inquiry';
|
import { getInquiryQueueSelector } from '../../ee/omnichannel/selectors/inquiry';
|
||||||
import { changeLivechatStatus, isOmnichannelStatusAvailable } from '../../ee/omnichannel/lib';
|
import { changeLivechatStatus, isOmnichannelStatusAvailable } from '../../ee/omnichannel/lib';
|
||||||
|
@ -98,7 +99,8 @@ const shouldUpdateProps = [
|
||||||
'isMasterDetail',
|
'isMasterDetail',
|
||||||
'refreshing',
|
'refreshing',
|
||||||
'queueSize',
|
'queueSize',
|
||||||
'inquiryEnabled'
|
'inquiryEnabled',
|
||||||
|
'encryptionBanner'
|
||||||
];
|
];
|
||||||
const getItemLayout = (data, index) => ({
|
const getItemLayout = (data, index) => ({
|
||||||
length: ROW_HEIGHT,
|
length: ROW_HEIGHT,
|
||||||
|
@ -143,7 +145,8 @@ class RoomsListView extends React.Component {
|
||||||
width: PropTypes.number,
|
width: PropTypes.number,
|
||||||
insets: PropTypes.object,
|
insets: PropTypes.object,
|
||||||
queueSize: PropTypes.number,
|
queueSize: PropTypes.number,
|
||||||
inquiryEnabled: PropTypes.bool
|
inquiryEnabled: PropTypes.bool,
|
||||||
|
encryptionBanner: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -803,6 +806,20 @@ class RoomsListView extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
goEncryption = () => {
|
||||||
|
logEvent(events.RL_GO_E2E_SAVE_PASSWORD);
|
||||||
|
const { navigation, isMasterDetail, encryptionBanner } = this.props;
|
||||||
|
|
||||||
|
const isSavePassword = encryptionBanner === E2E_BANNER_TYPE.SAVE_PASSWORD;
|
||||||
|
if (isMasterDetail) {
|
||||||
|
const screen = isSavePassword ? 'E2ESaveYourPasswordView' : 'E2EEnterYourPasswordView';
|
||||||
|
navigation.navigate('ModalStackNavigator', { screen });
|
||||||
|
} else {
|
||||||
|
const screen = isSavePassword ? 'E2ESaveYourPasswordStackNavigator' : 'E2EEnterYourPasswordStackNavigator';
|
||||||
|
navigation.navigate(screen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleCommands = ({ event }) => {
|
handleCommands = ({ event }) => {
|
||||||
const { navigation, server, isMasterDetail } = this.props;
|
const { navigation, server, isMasterDetail } = this.props;
|
||||||
const { input } = event;
|
const { input } = event;
|
||||||
|
@ -848,17 +865,18 @@ class RoomsListView extends React.Component {
|
||||||
renderListHeader = () => {
|
renderListHeader = () => {
|
||||||
const { searching } = this.state;
|
const { searching } = this.state;
|
||||||
const {
|
const {
|
||||||
sortBy, queueSize, inquiryEnabled, user
|
sortBy, queueSize, inquiryEnabled, encryptionBanner, user
|
||||||
} = this.props;
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<ListHeader
|
<ListHeader
|
||||||
searching={searching}
|
searching={searching}
|
||||||
sortBy={sortBy}
|
sortBy={sortBy}
|
||||||
toggleSort={this.toggleSort}
|
toggleSort={this.toggleSort}
|
||||||
goDirectory={this.goDirectory}
|
goEncryption={this.goEncryption}
|
||||||
goQueue={this.goQueue}
|
goQueue={this.goQueue}
|
||||||
queueSize={queueSize}
|
queueSize={queueSize}
|
||||||
inquiryEnabled={inquiryEnabled}
|
inquiryEnabled={inquiryEnabled}
|
||||||
|
encryptionBanner={encryptionBanner}
|
||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -1028,7 +1046,8 @@ const mapStateToProps = state => ({
|
||||||
StoreLastMessage: state.settings.Store_Last_Message,
|
StoreLastMessage: state.settings.Store_Last_Message,
|
||||||
rooms: state.room.rooms,
|
rooms: state.room.rooms,
|
||||||
queueSize: getInquiryQueueSelector(state).length,
|
queueSize: getInquiryQueueSelector(state).length,
|
||||||
inquiryEnabled: state.inquiry.enabled
|
inquiryEnabled: state.inquiry.enabled,
|
||||||
|
encryptionBanner: state.encryption.banner
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
|
|
@ -126,6 +126,21 @@ export default StyleSheet.create({
|
||||||
height: StyleSheet.hairlineWidth,
|
height: StyleSheet.hairlineWidth,
|
||||||
marginLeft: 72
|
marginLeft: 72
|
||||||
},
|
},
|
||||||
|
encryptionButton: {
|
||||||
|
width: '100%',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 12
|
||||||
|
},
|
||||||
|
encryptionIcon: {
|
||||||
|
...sharedStyles.textMedium
|
||||||
|
},
|
||||||
|
encryptionText: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 16,
|
||||||
|
marginHorizontal: 16,
|
||||||
|
...sharedStyles.textMedium
|
||||||
|
},
|
||||||
omnichannelToggle: {
|
omnichannelToggle: {
|
||||||
marginRight: 12
|
marginRight: 12
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { View, FlatList, Text } from 'react-native';
|
import { View, FlatList, Text } from 'react-native';
|
||||||
|
import { Q } from '@nozbe/watermelondb';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import equal from 'deep-equal';
|
import equal from 'deep-equal';
|
||||||
|
|
||||||
|
@ -20,6 +21,7 @@ import { withTheme } from '../../theme';
|
||||||
import { getUserSelector } from '../../selectors/login';
|
import { getUserSelector } from '../../selectors/login';
|
||||||
import SafeAreaView from '../../containers/SafeAreaView';
|
import SafeAreaView from '../../containers/SafeAreaView';
|
||||||
import { CloseModalButton } from '../../containers/HeaderButton';
|
import { CloseModalButton } from '../../containers/HeaderButton';
|
||||||
|
import database from '../../lib/database';
|
||||||
|
|
||||||
class SearchMessagesView extends React.Component {
|
class SearchMessagesView extends React.Component {
|
||||||
static navigationOptions = ({ navigation, route }) => {
|
static navigationOptions = ({ navigation, route }) => {
|
||||||
|
@ -50,6 +52,7 @@ class SearchMessagesView extends React.Component {
|
||||||
searchText: ''
|
searchText: ''
|
||||||
};
|
};
|
||||||
this.rid = props.route.params?.rid;
|
this.rid = props.route.params?.rid;
|
||||||
|
this.encrypted = props.route.params?.encrypted;
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
|
@ -71,21 +74,40 @@ class SearchMessagesView extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.search.stop();
|
this.search?.stop?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle encrypted rooms search messages
|
||||||
|
searchMessages = async(searchText) => {
|
||||||
|
// If it's a encrypted, room we'll search only on the local stored messages
|
||||||
|
if (this.encrypted) {
|
||||||
|
const db = database.active;
|
||||||
|
const messagesCollection = db.collections.get('messages');
|
||||||
|
return messagesCollection
|
||||||
|
.query(
|
||||||
|
// Messages of this room
|
||||||
|
Q.where('rid', this.rid),
|
||||||
|
// Message content is like the search text
|
||||||
|
Q.where('msg', Q.like(`%${ Q.sanitizeLikeString(searchText) }%`))
|
||||||
|
)
|
||||||
|
.fetch();
|
||||||
|
}
|
||||||
|
// If it's not a encrypted room, search messages on the server
|
||||||
|
const result = await RocketChat.searchMessages(this.rid, searchText);
|
||||||
|
if (result.success) {
|
||||||
|
return result.messages;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react/sort-comp
|
|
||||||
search = debounce(async(searchText) => {
|
search = debounce(async(searchText) => {
|
||||||
this.setState({ searchText, loading: true, messages: [] });
|
this.setState({ searchText, loading: true, messages: [] });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await RocketChat.searchMessages(this.rid, searchText);
|
const messages = await this.searchMessages(searchText);
|
||||||
if (result.success) {
|
this.setState({
|
||||||
this.setState({
|
messages: messages || [],
|
||||||
messages: result.messages || [],
|
loading: false
|
||||||
loading: false
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.setState({ loading: false });
|
this.setState({ loading: false });
|
||||||
log(e);
|
log(e);
|
||||||
|
|
|
@ -13,7 +13,7 @@ async function waitForToast() {
|
||||||
// await expect(element(by.id('toast'))).toBeVisible();
|
// await expect(element(by.id('toast'))).toBeVisible();
|
||||||
// await waitFor(element(by.id('toast'))).toBeNotVisible().withTimeout(10000);
|
// await waitFor(element(by.id('toast'))).toBeNotVisible().withTimeout(10000);
|
||||||
// await expect(element(by.id('toast'))).toBeNotVisible();
|
// await expect(element(by.id('toast'))).toBeNotVisible();
|
||||||
await sleep(1);
|
await sleep(300);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Profile screen', () => {
|
describe('Profile screen', () => {
|
||||||
|
|
|
@ -7,7 +7,7 @@ const data = require('../../data');
|
||||||
const testuser = data.users.regular
|
const testuser = data.users.regular
|
||||||
|
|
||||||
async function waitForToast() {
|
async function waitForToast() {
|
||||||
await sleep(1);
|
await sleep(300);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Status screen', () => {
|
describe('Status screen', () => {
|
||||||
|
|
|
@ -245,7 +245,7 @@ describe('Room actions screen', () => {
|
||||||
//Back into Room Actions
|
//Back into Room Actions
|
||||||
await element(by.id('room-view-header-actions')).tap();
|
await element(by.id('room-view-header-actions')).tap();
|
||||||
await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(5000);
|
await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(5000);
|
||||||
|
await element(by.type('UIScrollView')).atIndex(1).scrollTo('bottom');
|
||||||
await waitFor(element(by.id('room-actions-pinned'))).toExist();
|
await waitFor(element(by.id('room-actions-pinned'))).toExist();
|
||||||
await element(by.id('room-actions-pinned')).tap();
|
await element(by.id('room-actions-pinned')).tap();
|
||||||
await waitFor(element(by.id('pinned-messages-view'))).toExist().withTimeout(2000);
|
await waitFor(element(by.id('pinned-messages-view'))).toExist().withTimeout(2000);
|
||||||
|
@ -281,6 +281,7 @@ describe('Room actions screen', () => {
|
||||||
|
|
||||||
describe('Notification', async() => {
|
describe('Notification', async() => {
|
||||||
it('should navigate to notification preference view', async() => {
|
it('should navigate to notification preference view', async() => {
|
||||||
|
await element(by.type('UIScrollView')).atIndex(1).scrollTo('bottom');
|
||||||
await waitFor(element(by.id('room-actions-notifications'))).toExist().withTimeout(2000);
|
await waitFor(element(by.id('room-actions-notifications'))).toExist().withTimeout(2000);
|
||||||
await element(by.id('room-actions-notifications')).tap();
|
await element(by.id('room-actions-notifications')).tap();
|
||||||
await waitFor(element(by.id('notification-preference-view'))).toExist().withTimeout(2000);
|
await waitFor(element(by.id('notification-preference-view'))).toExist().withTimeout(2000);
|
||||||
|
|
|
@ -28,7 +28,7 @@ async function waitForToast() {
|
||||||
// await expect(element(by.id('toast'))).toExist();
|
// await expect(element(by.id('toast'))).toExist();
|
||||||
// await waitFor(element(by.id('toast'))).toBeNotVisible().withTimeout(10000);
|
// await waitFor(element(by.id('toast'))).toBeNotVisible().withTimeout(10000);
|
||||||
// await expect(element(by.id('toast'))).toBeNotVisible();
|
// await expect(element(by.id('toast'))).toBeNotVisible();
|
||||||
await sleep(1);
|
await sleep(300);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Room info screen', () => {
|
describe('Room info screen', () => {
|
||||||
|
|
|
@ -386,6 +386,9 @@ PODS:
|
||||||
- React
|
- React
|
||||||
- react-native-safe-area-context (3.1.1):
|
- react-native-safe-area-context (3.1.1):
|
||||||
- React
|
- React
|
||||||
|
- react-native-simple-crypto (0.3.1):
|
||||||
|
- OpenSSL-Universal
|
||||||
|
- React
|
||||||
- react-native-slider (3.0.2):
|
- react-native-slider (3.0.2):
|
||||||
- React
|
- React
|
||||||
- react-native-webview (10.3.2):
|
- react-native-webview (10.3.2):
|
||||||
|
@ -596,6 +599,7 @@ DEPENDENCIES:
|
||||||
- react-native-notifications (from `../node_modules/react-native-notifications`)
|
- react-native-notifications (from `../node_modules/react-native-notifications`)
|
||||||
- react-native-orientation-locker (from `../node_modules/react-native-orientation-locker`)
|
- react-native-orientation-locker (from `../node_modules/react-native-orientation-locker`)
|
||||||
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
|
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
|
||||||
|
- react-native-simple-crypto (from `../node_modules/react-native-simple-crypto`)
|
||||||
- "react-native-slider (from `../node_modules/@react-native-community/slider`)"
|
- "react-native-slider (from `../node_modules/@react-native-community/slider`)"
|
||||||
- react-native-webview (from `../node_modules/react-native-webview`)
|
- react-native-webview (from `../node_modules/react-native-webview`)
|
||||||
- React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
|
- React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
|
||||||
|
@ -756,6 +760,8 @@ EXTERNAL SOURCES:
|
||||||
:path: "../node_modules/react-native-orientation-locker"
|
:path: "../node_modules/react-native-orientation-locker"
|
||||||
react-native-safe-area-context:
|
react-native-safe-area-context:
|
||||||
:path: "../node_modules/react-native-safe-area-context"
|
:path: "../node_modules/react-native-safe-area-context"
|
||||||
|
react-native-simple-crypto:
|
||||||
|
:path: "../node_modules/react-native-simple-crypto"
|
||||||
react-native-slider:
|
react-native-slider:
|
||||||
:path: "../node_modules/@react-native-community/slider"
|
:path: "../node_modules/@react-native-community/slider"
|
||||||
react-native-webview:
|
react-native-webview:
|
||||||
|
@ -918,6 +924,7 @@ SPEC CHECKSUMS:
|
||||||
react-native-notifications: ee8fd739853e72694f3af8b374c8ccb106b7b227
|
react-native-notifications: ee8fd739853e72694f3af8b374c8ccb106b7b227
|
||||||
react-native-orientation-locker: f0ca1a8e5031dab6b74bfb4ab33a17ed2c2fcb0d
|
react-native-orientation-locker: f0ca1a8e5031dab6b74bfb4ab33a17ed2c2fcb0d
|
||||||
react-native-safe-area-context: 344b969c45af3d8464d36e8dea264942992ef033
|
react-native-safe-area-context: 344b969c45af3d8464d36e8dea264942992ef033
|
||||||
|
react-native-simple-crypto: 5e4f2877f71675d95baabf5823cd0cc0c379d0e6
|
||||||
react-native-slider: 0221b417686c5957f6e77cd9ac22c1478a165355
|
react-native-slider: 0221b417686c5957f6e77cd9ac22c1478a165355
|
||||||
react-native-webview: 679b6f400176e2ea8a785acf7ae16cf282e7d1eb
|
react-native-webview: 679b6f400176e2ea8a785acf7ae16cf282e7d1eb
|
||||||
React-RCTActionSheet: 1702a1a85e550b5c36e2e03cb2bd3adea053de95
|
React-RCTActionSheet: 1702a1a85e550b5c36e2e03cb2bd3adea053de95
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/react-native-simple-crypto/ios/RCTCrypto/lib/Aes.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/react-native-simple-crypto/ios/RCTCrypto/lib/Hmac.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/react-native-simple-crypto/ios/RCTCrypto/lib/Pbkdf2.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/react-native-simple-crypto/ios/RCTCrypto/RCTAes.h
|
1
ios/Pods/Headers/Private/react-native-simple-crypto/RCTCrypto-Bridging-Header.h
generated
Symbolic link
1
ios/Pods/Headers/Private/react-native-simple-crypto/RCTCrypto-Bridging-Header.h
generated
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/react-native-simple-crypto/ios/RCTCrypto/RCTCrypto-Bridging-Header.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/react-native-simple-crypto/ios/RCTCrypto/RCTHmac.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/react-native-simple-crypto/ios/RCTCrypto/RCTPbkdf2.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/react-native-simple-crypto/ios/RCTCrypto/RCTRsa.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/react-native-simple-crypto/ios/RCTCrypto/RCTSha.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/react-native-simple-crypto/ios/RCTCrypto/RNRandomBytes.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/react-native-simple-crypto/ios/RCTCrypto/lib/Rsa.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/react-native-simple-crypto/ios/RCTCrypto/lib/RsaFormatter.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/react-native-simple-crypto/ios/RCTCrypto/lib/Sha.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/react-native-simple-crypto/ios/RCTCrypto/lib/Shared.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/react-native-simple-crypto/ios/RCTCrypto/lib/Aes.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/react-native-simple-crypto/ios/RCTCrypto/lib/Hmac.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/react-native-simple-crypto/ios/RCTCrypto/lib/Pbkdf2.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/react-native-simple-crypto/ios/RCTCrypto/RCTAes.h
|
1
ios/Pods/Headers/Public/react-native-simple-crypto/RCTCrypto-Bridging-Header.h
generated
Symbolic link
1
ios/Pods/Headers/Public/react-native-simple-crypto/RCTCrypto-Bridging-Header.h
generated
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/react-native-simple-crypto/ios/RCTCrypto/RCTCrypto-Bridging-Header.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/react-native-simple-crypto/ios/RCTCrypto/RCTHmac.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/react-native-simple-crypto/ios/RCTCrypto/RCTPbkdf2.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/react-native-simple-crypto/ios/RCTCrypto/RCTRsa.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/react-native-simple-crypto/ios/RCTCrypto/RCTSha.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/react-native-simple-crypto/ios/RCTCrypto/RNRandomBytes.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/react-native-simple-crypto/ios/RCTCrypto/lib/Rsa.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/react-native-simple-crypto/ios/RCTCrypto/lib/RsaFormatter.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/react-native-simple-crypto/ios/RCTCrypto/lib/Sha.h
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../node_modules/react-native-simple-crypto/ios/RCTCrypto/lib/Shared.h
|
1
ios/Pods/Headers/Public/react_native_simple_crypto/react-native-simple-crypto-umbrella.h
generated
Symbolic link
1
ios/Pods/Headers/Public/react_native_simple_crypto/react-native-simple-crypto-umbrella.h
generated
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../../Target Support Files/react-native-simple-crypto/react-native-simple-crypto-umbrella.h
|
1
ios/Pods/Headers/Public/react_native_simple_crypto/react-native-simple-crypto.modulemap
generated
Symbolic link
1
ios/Pods/Headers/Public/react_native_simple_crypto/react-native-simple-crypto.modulemap
generated
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../../Target Support Files/react-native-simple-crypto/react-native-simple-crypto.modulemap
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"name": "react-native-simple-crypto",
|
||||||
|
"version": "0.3.1",
|
||||||
|
"summary": "A simpler React-Native crypto library",
|
||||||
|
"authors": "Gary Button <gary.button.public@gmail.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires_arc": true,
|
||||||
|
"homepage": "https://github.com/ghbutton/react-native-simple-crypto",
|
||||||
|
"source": {
|
||||||
|
"git": "git+https://github.com/ghbutton/react-native-simple-crypto.git"
|
||||||
|
},
|
||||||
|
"platforms": {
|
||||||
|
"ios": "8.0"
|
||||||
|
},
|
||||||
|
"source_files": "ios/**/*.{h,m,swift}",
|
||||||
|
"dependencies": {
|
||||||
|
"React": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"OpenSSL-Universal": [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue