diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx
index 666c6e30f3..e336aab9b3 100644
--- a/.storybook/preview.tsx
+++ b/.storybook/preview.tsx
@@ -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 = {
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 11cbd8bd9d..31c7158397 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -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]
diff --git a/android/app/src/main/assets/fonts/custom.ttf b/android/app/src/main/assets/fonts/custom.ttf
index 0a3f5acaf2..68f92d398a 100644
Binary files a/android/app/src/main/assets/fonts/custom.ttf and b/android/app/src/main/assets/fonts/custom.ttf differ
diff --git a/android/app/src/play/java/chat/rocket/reactnative/Ejson.java b/android/app/src/play/java/chat/rocket/reactnative/Ejson.java
index c5ee7595cb..56d0b09489 100644
--- a/android/app/src/play/java/chat/rocket/reactnative/Ejson.java
+++ b/android/app/src/play/java/chat/rocket/reactnative/Ejson.java
@@ -36,6 +36,8 @@ public class Ejson {
String tmid;
+ Content content;
+
private MMKV mmkv;
private String TOKEN_KEY = "reactnativemeteor_usertoken-";
@@ -102,4 +104,9 @@ public class Sender {
String username;
String _id;
}
+
+ public class Content {
+ String ciphertext;
+ String algorithm;
+ }
}
diff --git a/android/app/src/play/java/chat/rocket/reactnative/Encryption.java b/android/app/src/play/java/chat/rocket/reactnative/Encryption.java
index 7ddab1979c..2e6e4a79dd 100644
--- a/android/app/src/play/java/chat/rocket/reactnative/Encryption.java
+++ b/android/app/src/play/java/chat/rocket/reactnative/Encryption.java
@@ -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;
@@ -31,6 +31,14 @@ class Message {
}
}
+class DecryptedContent {
+ String msg;
+
+ DecryptedContent(String msg) {
+ this.msg = msg;
+ }
+}
+
class PrivateKey {
String d;
String dp;
@@ -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 {
@@ -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);
@@ -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;
@@ -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;
@@ -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;
}
-}
+}
\ No newline at end of file
diff --git a/app/actions/server.ts b/app/actions/server.ts
index 9d5fde27e8..af3f41d04d 100644
--- a/app/actions/server.ts
+++ b/app/actions/server.ts
@@ -4,7 +4,7 @@ import { SERVER } from './actionsTypes';
export interface ISelectServerAction extends Action {
server: string;
- version?: string;
+ version: string;
fetchVersion: boolean;
changeServer: boolean;
}
@@ -29,7 +29,7 @@ export type TActionServer = ISelectServerAction & ISelectServerSuccess & IServer
export function selectServerRequest(
server: string,
- version?: string,
+ version: string,
fetchVersion = true,
changeServer = false
): ISelectServerAction {
diff --git a/app/containers/Button/Button.stories.tsx b/app/containers/Button/Button.stories.tsx
index 2ecf791037..a61061ad53 100644
--- a/app/containers/Button/Button.stories.tsx
+++ b/app/containers/Button/Button.stories.tsx
@@ -23,6 +23,8 @@ export const DisabledButton = () => ;
export const DisabledLoadingButton = () => ;
+export const SmallButton = () => ;
+
export const CustomButton = () => (