Skip to content

Commit

Permalink
Merge branch 'develop' into chore-migrate-to-hooks-messages-view
Browse files Browse the repository at this point in the history
  • Loading branch information
OtavioStasiak authored Nov 12, 2024
2 parents 0ec9a58 + 8eceb25 commit 5afc864
Show file tree
Hide file tree
Showing 76 changed files with 1,474 additions and 971 deletions.
2 changes: 1 addition & 1 deletion .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { mockedStore as store } from '../app/reducers/mockedStore';
import { setUser } from '../app/actions/login';

const baseUrl = 'https://open.rocket.chat';
store.dispatch(selectServerRequest(baseUrl));
store.dispatch(selectServerRequest(baseUrl, '7.0.0'));
store.dispatch(setUser({ id: 'abc', username: 'rocket.cat', name: 'Rocket Cat' }));

const preview: Preview = {
Expand Down
2 changes: 1 addition & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode VERSIONCODE as Integer
versionName "4.54.0"
versionName "4.55.0"
vectorDrawables.useSupportLibrary = true
if (!isFoss) {
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
Expand Down
7 changes: 7 additions & 0 deletions android/app/src/play/java/chat/rocket/reactnative/Ejson.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public class Ejson {

String tmid;

Content content;

private MMKV mmkv;

private String TOKEN_KEY = "reactnativemeteor_usertoken-";
Expand Down Expand Up @@ -102,4 +104,9 @@ public class Sender {
String username;
String _id;
}

public class Content {
String ciphertext;
String algorithm;
}
}
131 changes: 85 additions & 46 deletions android/app/src/play/java/chat/rocket/reactnative/Encryption.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.WritableMap;
import com.google.gson.Gson;
import com.nozbe.watermelondb.Database;
import com.pedrouid.crypto.RCTAes;
import com.pedrouid.crypto.RCTRsaUtils;
import com.pedrouid.crypto.RSA;
import com.pedrouid.crypto.Util;

import java.io.File;
import java.lang.reflect.Field;
import java.security.SecureRandom;
import java.util.Arrays;
Expand All @@ -31,6 +31,14 @@ class Message {
}
}

class DecryptedContent {
String msg;

DecryptedContent(String msg) {
this.msg = msg;
}
}

class PrivateKey {
String d;
String dp;
Expand Down Expand Up @@ -58,44 +66,66 @@ class Room {

class Encryption {
private Gson gson = new Gson();
private String E2ERoomKey;
private String keyId;

public static Encryption shared = new Encryption();
private ReactApplicationContext reactContext;

public Room readRoom(final Ejson ejson) throws NoSuchFieldException {
public Room readRoom(final Ejson ejson) {
String dbName = getDatabaseName(ejson.serverURL());
SQLiteDatabase db = null;

try {
db = SQLiteDatabase.openDatabase(dbName, null, SQLiteDatabase.OPEN_READONLY);
String[] queryArgs = {ejson.rid};

Cursor cursor = db.rawQuery("SELECT * FROM subscriptions WHERE id == ? LIMIT 1", queryArgs);

if (cursor.getCount() == 0) {
cursor.close();
return null;
}

cursor.moveToFirst();
String e2eKey = cursor.getString(cursor.getColumnIndex("e2e_key"));
Boolean encrypted = cursor.getInt(cursor.getColumnIndex("encrypted")) > 0;
cursor.close();

return new Room(e2eKey, encrypted);

} catch (Exception e) {
Log.e("[ENCRYPTION]", "Error reading room", e);
return null;

} finally {
if (db != null) {
db.close();
}
}
}

private String getDatabaseName(String serverUrl) {
int resId = reactContext.getResources().getIdentifier("rn_config_reader_custom_package", "string", reactContext.getPackageName());
String className = reactContext.getString(resId);
Class clazz = null;
Boolean isOfficial = false;

try {
clazz = Class.forName(className + ".BuildConfig");
Class<?> clazz = Class.forName(className + ".BuildConfig");
Field IS_OFFICIAL = clazz.getField("IS_OFFICIAL");
isOfficial = (Boolean) IS_OFFICIAL.get(null);
} catch (ClassNotFoundException | IllegalAccessException e) {
} catch (Exception e) {
e.printStackTrace();
}
String dbName = ejson.serverURL().replace("https://", "");

String dbName = serverUrl.replace("https://", "");
if (!isOfficial) {
dbName += "-experimental";
}
dbName += ".db";
Database database = new Database(dbName, reactContext, SQLiteDatabase.CREATE_IF_NECESSARY);
String[] query = {ejson.rid};
Cursor cursor = database.rawQuery("select * from subscriptions where id == ? limit 1", query);

// Room not found
if (cursor.getCount() == 0) {
return null;
}

cursor.moveToFirst();
String e2eKey = cursor.getString(cursor.getColumnIndex("e2e_key"));
Boolean encrypted = cursor.getInt(cursor.getColumnIndex("encrypted")) > 0;
cursor.close();

return new Room(e2eKey, encrypted);
// Old issue. Safer to accept it then to migrate away from it.
dbName += ".db.db";
// https://github.com/Nozbe/WatermelonDB/blob/a757e646141437ad9a06f7314ad5555a8a4d252e/native/android-jsi/src/main/java/com/nozbe/watermelondb/jsi/JSIInstaller.java#L18
File databasePath = new File(reactContext.getDatabasePath(dbName).getPath().replace("/databases", ""));
return databasePath.getPath();
}

public String readUserKey(final Ejson ejson) throws Exception {
Expand All @@ -120,7 +150,7 @@ public String readUserKey(final Ejson ejson) throws Exception {
}

public String decryptRoomKey(final String e2eKey, final Ejson ejson) throws Exception {
String key = e2eKey.substring(12, e2eKey.length());
String key = e2eKey.substring(12);
keyId = e2eKey.substring(0, 12);

String userKey = readUserKey(ejson);
Expand All @@ -138,6 +168,15 @@ public String decryptRoomKey(final String e2eKey, final Ejson ejson) throws Exce
return Util.bytesToHex(decoded);
}

private String decryptText(String text, String e2eKey) throws Exception {
String msg = text.substring(12);
byte[] msgData = Base64.decode(msg, Base64.NO_WRAP);
String b64 = Base64.encodeToString(Arrays.copyOfRange(msgData, 16, msgData.length), Base64.DEFAULT);
String decrypted = RCTAes.decrypt(b64, e2eKey, Util.bytesToHex(Arrays.copyOfRange(msgData, 0, 16)));
byte[] data = Base64.decode(decrypted, Base64.NO_WRAP);
return new String(data, "UTF-8");
}

public String decryptMessage(final Ejson ejson, final ReactApplicationContext reactContext) {
try {
this.reactContext = reactContext;
Expand All @@ -151,19 +190,22 @@ public String decryptMessage(final Ejson ejson, final ReactApplicationContext re
return null;
}

String message = ejson.msg;
String msg = message.substring(12, message.length());
byte[] msgData = Base64.decode(msg, Base64.NO_WRAP);

String b64 = Base64.encodeToString(Arrays.copyOfRange(msgData, 16, msgData.length), Base64.DEFAULT);

String decrypted = RCTAes.decrypt(b64, e2eKey, Util.bytesToHex(Arrays.copyOfRange(msgData, 0, 16)));
byte[] data = Base64.decode(decrypted, Base64.NO_WRAP);
Message m = gson.fromJson(new String(data, "UTF-8"), Message.class);
if (ejson.msg != null && !ejson.msg.isEmpty()) {
String message = ejson.msg;
String decryptedText = decryptText(message, e2eKey);
Message m = gson.fromJson(decryptedText, Message.class);
return m.text;
} else if (ejson.content != null && "rc.v1.aes-sha2".equals(ejson.content.algorithm)) {
String message = ejson.content.ciphertext;
String decryptedText = decryptText(message, e2eKey);
DecryptedContent m = gson.fromJson(decryptedText, DecryptedContent.class);
return m.msg;
} else {
return null;
}

return m.text;
} catch (Exception e) {
Log.d("[ROCKETCHAT][E2E]", Log.getStackTraceString(e));
Log.e("[ROCKETCHAT][E2E]", Log.getStackTraceString(e));
}

return null;
Expand Down Expand Up @@ -193,29 +235,26 @@ public String encryptMessage(final String message, final String id, final Ejson

return keyId + Base64.encodeToString(concat(bytes, data), Base64.NO_WRAP);
} catch (Exception e) {
Log.d("[ROCKETCHAT][E2E]", Log.getStackTraceString(e));
Log.e("[ROCKETCHAT][E2E]", Log.getStackTraceString(e));
}

return message;
}

static byte[] concat(byte[]... arrays) {
// Determine the length of the result array
int totalLength = 0;
for (int i = 0; i < arrays.length; i++) {
totalLength += arrays[i].length;
for (byte[] array : arrays) {
totalLength += array.length;
}

// create the result array
byte[] result = new byte[totalLength];

// copy the source arrays into the result array
int currentIndex = 0;
for (int i = 0; i < arrays.length; i++) {
System.arraycopy(arrays[i], 0, result, currentIndex, arrays[i].length);
currentIndex += arrays[i].length;

for (byte[] array : arrays) {
System.arraycopy(array, 0, result, currentIndex, array.length);
currentIndex += array.length;
}

return result;
}
}
}
4 changes: 2 additions & 2 deletions app/actions/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { SERVER } from './actionsTypes';

export interface ISelectServerAction extends Action {
server: string;
version?: string;
version: string;
fetchVersion: boolean;
changeServer: boolean;
}
Expand All @@ -29,7 +29,7 @@ export type TActionServer = ISelectServerAction & ISelectServerSuccess & IServer

export function selectServerRequest(
server: string,
version?: string,
version: string,
fetchVersion = true,
changeServer = false
): ISelectServerAction {
Expand Down
7 changes: 3 additions & 4 deletions app/containers/MessageComposer/MessageComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { sanitizeLikeString } from '../../lib/database/utils';
import { generateTriggerId } from '../../lib/methods';
import { Services } from '../../lib/services';
import log from '../../lib/methods/helpers/log';
import { prepareQuoteMessage } from './helpers';
import { prepareQuoteMessage, insertEmojiAtCursor } from './helpers';
import { RecordAudio } from './components/RecordAudio';
import { useKeyboardListener } from './hooks';
import { emitter } from '../../lib/methods/helpers/emitter';
Expand Down Expand Up @@ -179,9 +179,8 @@ export const MessageComposer = ({
} else {
emojiText = `:${emoji.name}:`;
}
newText = `${text.substr(0, cursor)}${emojiText}${text.substr(cursor)}`;
newCursor = cursor + emojiText.length;
composerInputComponentRef.current.setInput(newText, { start: newCursor, end: newCursor });
const { updatedCursor, updatedText } = insertEmojiAtCursor(text, emojiText, cursor);
composerInputComponentRef.current.setInput(updatedText, { start: updatedCursor, end: updatedCursor });
break;
case EventTypes.SEARCH_PRESSED:
openSearchEmojiKeyboard();
Expand Down
4 changes: 3 additions & 1 deletion app/containers/MessageComposer/components/ComposerInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { RouteProp, useFocusEffect, useRoute } from '@react-navigation/native';

import I18n from '../../../i18n';
import { IAutocompleteItemProps, IComposerInput, IComposerInputProps, IInputSelection, TSetInput } from '../interfaces';
import { useAutocompleteParams, useFocused, useMessageComposerApi } from '../context';
import { useAutocompleteParams, useFocused, useMessageComposerApi, useMicOrSend } from '../context';
import { fetchIsAllOrHere, getMentionRegexp } from '../helpers';
import { useSubscription, useAutoSaveDraft } from '../hooks';
import sharedStyles from '../../../views/Styles';
Expand Down Expand Up @@ -58,6 +58,8 @@ export const ComposerInput = memo(
const usedCannedResponse = route.params?.usedCannedResponse;
const prevAction = usePrevious(action);

// subscribe to changes on mic state to update draft after a message is sent
useMicOrSend();
const { saveMessageDraft } = useAutoSaveDraft(textRef.current);

// Draft/Canned Responses
Expand Down
1 change: 1 addition & 0 deletions app/containers/MessageComposer/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './fetchIsAllOrHere';
export * from './forceJpgExtension';
export * from './getMentionRegexp';
export * from './prepareQuoteMessage';
export * from './insertEmojiAtCursor';
51 changes: 51 additions & 0 deletions app/containers/MessageComposer/helpers/insertEmojiAtCursor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { insertEmojiAtCursor } from './insertEmojiAtCursor';

describe('insertEmojiAtCursor', () => {
it('should insert emoji at the beginning of the text', () => {
const result = insertEmojiAtCursor('Hello world', ':test2:', 0);
expect(result).toEqual({
updatedText: ':test2: Hello world',
updatedCursor: 8
});
});

it('should insert emoji at the end of the text', () => {
const result = insertEmojiAtCursor('Hello world', ':test2:', 11);
expect(result).toEqual({
updatedText: 'Hello world :test2:',
updatedCursor: 19
});
});

it('should insert emoji in the middle of the text with spaces', () => {
const result = insertEmojiAtCursor('Hello world', ':test2:', 5);
expect(result).toEqual({
updatedText: 'Hello :test2: world',
updatedCursor: 13
});
});

it('should add a space before the emoji if missing', () => {
const result = insertEmojiAtCursor('Hello', ':test2:', 5);
expect(result).toEqual({
updatedText: 'Hello :test2:',
updatedCursor: 13
});
});

it('should add a space after the emoji if missing', () => {
const result = insertEmojiAtCursor('Hello', ':test2:', 0);
expect(result).toEqual({
updatedText: ':test2: Hello',
updatedCursor: 8
});
});

it('should not add extra spaces if they are already present', () => {
const result = insertEmojiAtCursor('Hello ', ':test2:', 6);
expect(result).toEqual({
updatedText: 'Hello :test2:',
updatedCursor: 13
});
});
});
18 changes: 18 additions & 0 deletions app/containers/MessageComposer/helpers/insertEmojiAtCursor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export const insertEmojiAtCursor = (text: string, emojiText: string, cursor: number) => {
let updatedCursor = cursor + emojiText.length;

const firstPart = text.substr(0, cursor);
const lastPart = text.substr(cursor);

const spaceBefore = firstPart.endsWith(' ') || firstPart.length === 0 ? '' : ' ';
const spaceAfter = lastPart.startsWith(' ') || lastPart.length === 0 ? '' : ' ';

const updatedText = `${firstPart}${spaceBefore}${emojiText}${spaceAfter}${lastPart}`;
updatedCursor += spaceBefore ? 1 : 0;
updatedCursor += spaceAfter ? 1 : 0;

return {
updatedCursor,
updatedText
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const SupportedVersionsExpired = () => {
r.supportedVersionsWarningAt = null;
});
});
dispatch(selectServerRequest(server));
dispatch(selectServerRequest(server, serverRecord.version));
// forces loading state a little longer until redux is finished
await new Promise(res => setTimeout(res, checkAgainTimeout));
}
Expand Down
Loading

0 comments on commit 5afc864

Please sign in to comment.