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 = () =>