Unverified Commit 01955065 authored by Djorkaeff Alexandre's avatar Djorkaeff Alexandre Committed by GitHub
Browse files

[NEW] E2E Encryption push (Android) (#2481)



* poc push encryption android

* eof

* format code

* react-native-simple-crypto update

* prevent find sub twice

* remove storage and use ejson storage

* invalidate yarn cache

* Bump crypto and fix db path

* Fix google-services path

Co-authored-by: default avatarDiego Mello <diegolmello@gmail.com>
parent 8643f17f
......@@ -170,7 +170,7 @@ jobs:
if [[ $KEYSTORE ]]; then
echo $GOOGLE_SERVICES_ANDROID | base64 --decode > google-services.json
fi
working_directory: android/app
working_directory: android/app/src/play
- run:
name: Config variables
......
package chat.rocket.reactnative;
import android.os.Bundle;
import androidx.annotation.Nullable;
public class Callback {
......
......@@ -14,12 +14,14 @@ import android.graphics.drawable.Icon;
import android.os.Build;
import android.os.Bundle;
import android.app.Person;
import androidx.annotation.Nullable;
import com.google.gson.Gson;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
import com.bumptech.glide.request.RequestOptions;
import java.util.concurrent.ExecutionException;
import java.lang.InterruptedException;
......@@ -84,6 +86,15 @@ public class CustomPushNotification extends PushNotification {
boolean hasSender = loadedEjson.sender != null;
String title = bundle.getString("title");
// If it has a encrypted message
if (loadedEjson.msg != null) {
// Override message with the decrypted content
String decrypted = Encryption.shared.decryptMessage(loadedEjson, reactApplicationContext);
if (decrypted != null) {
bundle.putString("message", decrypted);
}
}
bundle.putLong("time", new Date().getTime());
bundle.putString("username", hasSender ? loadedEjson.sender.username : title);
bundle.putString("senderId", hasSender ? loadedEjson.sender._id : "1");
......@@ -114,12 +125,12 @@ public class CustomPushNotification extends PushNotification {
Ejson ejson = new Gson().fromJson(bundle.getString("ejson", "{}"), Ejson.class);
notification
.setContentTitle(title)
.setContentText(message)
.setContentIntent(intent)
.setPriority(Notification.PRIORITY_HIGH)
.setDefaults(Notification.DEFAULT_ALL)
.setAutoCancel(true);
.setContentTitle(title)
.setContentText(message)
.setContentIntent(intent)
.setPriority(Notification.PRIORITY_HIGH)
.setDefaults(Notification.DEFAULT_ALL)
.setAutoCancel(true);
Integer notificationId = Integer.parseInt(notId);
notificationColor(notification);
......@@ -132,7 +143,7 @@ public class CustomPushNotification extends PushNotification {
notificationStyle(notification, notificationId, bundle);
notificationReply(notification, notificationId, bundle);
// message couldn't be loaded from server (Fallback notification)
// message couldn't be loaded from server (Fallback notification)
} else {
Gson gson = new Gson();
// iterate over the current notification ids to dismiss fallback notifications from same server
......@@ -163,11 +174,11 @@ public class CustomPushNotification extends PushNotification {
private Bitmap getAvatar(String uri) {
try {
return Glide.with(mContext)
.asBitmap()
.apply(RequestOptions.bitmapTransform(new RoundedCorners(10)))
.load(uri)
.submit(100, 100)
.get();
.asBitmap()
.apply(RequestOptions.bitmapTransform(new RoundedCorners(10)))
.load(uri)
.submit(100, 100)
.get();
} catch (final ExecutionException | InterruptedException e) {
return largeIcon();
}
......@@ -203,8 +214,8 @@ public class CustomPushNotification extends PushNotification {
String CHANNEL_NAME = "All";
NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT);
CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT);
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.createNotificationChannel(channel);
......@@ -253,9 +264,9 @@ public class CustomPushNotification extends PushNotification {
messageStyle = new Notification.MessagingStyle("");
} else {
Person sender = new Person.Builder()
.setKey("")
.setName("")
.build();
.setKey("")
.setName("")
.build();
messageStyle = new Notification.MessagingStyle(sender);
}
......@@ -279,9 +290,14 @@ public class CustomPushNotification extends PushNotification {
} else {
Bitmap avatar = getAvatar(avatarUri);
String name = username;
if (ejson.senderName != null) {
name = ejson.senderName;
}
Person.Builder sender = new Person.Builder()
.setKey(senderId)
.setName(username);
.setKey(senderId)
.setName(name);
if (avatar != null) {
sender.setIcon(Icon.createWithBitmap(avatar));
......@@ -317,18 +333,18 @@ public class CustomPushNotification extends PushNotification {
PendingIntent replyPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT);
RemoteInput remoteInput = new RemoteInput.Builder(KEY_REPLY)
.setLabel(label)
.build();
.setLabel(label)
.build();
CharSequence title = label;
Notification.Action replyAction = new Notification.Action.Builder(smallIconResId, title, replyPendingIntent)
.addRemoteInput(remoteInput)
.setAllowGeneratedReplies(true)
.build();
.addRemoteInput(remoteInput)
.setAllowGeneratedReplies(true)
.build();
notification
.setShowWhen(true)
.addAction(replyAction);
.setShowWhen(true)
.addAction(replyAction);
}
private void notificationDismiss(Notification.Builder notification, int notificationId) {
......
......@@ -5,9 +5,9 @@ import android.content.Context;
import android.content.Intent;
public class DismissNotification extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
int notId = intent.getExtras().getInt(CustomPushNotification.NOTIFICATION_ID);
CustomPushNotification.clearMessages(notId);
}
@Override
public void onReceive(Context context, Intent intent) {
int notId = intent.getExtras().getInt(CustomPushNotification.NOTIFICATION_ID);
CustomPushNotification.clearMessages(notId);
}
}
......@@ -31,6 +31,8 @@ public class Ejson {
Sender sender;
String messageId;
String notificationType;
String senderName;
String msg;
private MMKV mmkv;
......@@ -82,6 +84,14 @@ public class Ejson {
return "";
}
public String privateKey() {
String serverURL = serverURL();
if (mmkv != null && serverURL != null) {
return mmkv.decodeString(serverURL.concat("-RC_E2E_PRIVATE_KEY"));
}
return null;
}
public String serverURL() {
String url = this.host;
if (url != null && url.endsWith("/")) {
......
package chat.rocket.reactnative;
import android.util.Log;
import android.util.Base64;
import android.database.Cursor;
import com.pedrouid.crypto.RSA;
import com.pedrouid.crypto.RCTAes;
import com.pedrouid.crypto.RCTRsaUtils;
import com.pedrouid.crypto.Util;
import com.google.gson.Gson;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.nozbe.watermelondb.Database;
import java.util.Arrays;
import java.security.SecureRandom;
class Message {
String _id;
String userId;
String text;
Message(String id, String userId, String text) {
this._id = id;
this.userId = userId;
this.text = text;
}
}
class PrivateKey {
String d;
String dp;
String dq;
String e;
String n;
String p;
String q;
String qi;
}
class RoomKey {
String k;
}
class Room {
String e2eKey;
Boolean encrypted;
Room(String e2eKey, Boolean encrypted) {
this.e2eKey = e2eKey;
this.encrypted = encrypted;
}
}
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) {
Database database = new Database(ejson.serverURL().replace("https://", "") + "-experimental.db", reactContext);
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);
}
public String readUserKey(final Ejson ejson) throws Exception {
String privateKey = ejson.privateKey();
if (privateKey == null) {
return null;
}
PrivateKey privKey = gson.fromJson(privateKey, PrivateKey.class);
WritableMap jwk = Arguments.createMap();
jwk.putString("n", privKey.n);
jwk.putString("e", privKey.e);
jwk.putString("d", privKey.d);
jwk.putString("p", privKey.p);
jwk.putString("q", privKey.q);
jwk.putString("dp", privKey.dp);
jwk.putString("dq", privKey.dq);
jwk.putString("qi", privKey.qi);
return new RCTRsaUtils().jwkToPrivatePkcs1(jwk);
}
public String decryptRoomKey(final String e2eKey, final Ejson ejson) throws Exception {
String key = e2eKey.substring(12, e2eKey.length());
keyId = e2eKey.substring(0, 12);
String userKey = readUserKey(ejson);
if (userKey == null) {
return null;
}
RSA rsa = new RSA();
rsa.setPrivateKey(userKey);
String decrypted = rsa.decrypt(key);
RoomKey roomKey = gson.fromJson(decrypted, RoomKey.class);
byte[] decoded = Base64.decode(roomKey.k, Base64.NO_PADDING | Base64.NO_WRAP | Base64.URL_SAFE);
return Util.bytesToHex(decoded);
}
public String decryptMessage(final Ejson ejson, final ReactApplicationContext reactContext) {
try {
this.reactContext = reactContext;
Room room = readRoom(ejson);
if (room == null || room.e2eKey == null) {
return null;
}
String e2eKey = decryptRoomKey(room.e2eKey, ejson);
if (e2eKey == null) {
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);
return m.text;
} catch (Exception e) {
Log.d("[ROCKETCHAT][ENCRYPTION]", Log.getStackTraceString(e));
}
return null;
}
public String encryptMessage(final String message, final String id, final Ejson ejson) {
try {
Room room = readRoom(ejson);
if (room == null || !room.encrypted || room.e2eKey == null) {
return message;
}
String e2eKey = decryptRoomKey(room.e2eKey, ejson);
if (e2eKey == null) {
return message;
}
Message m = new Message(id, ejson.userId(), message);
String cypher = gson.toJson(m);
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[16];
random.nextBytes(bytes);
String encrypted = RCTAes.encrypt(Base64.encodeToString(cypher.getBytes("UTF-8"), Base64.NO_WRAP), e2eKey, Util.bytesToHex(bytes));
byte[] data = Base64.decode(encrypted, Base64.NO_WRAP);
return keyId + Base64.encodeToString(concat(bytes, data), Base64.NO_WRAP);
} catch (Exception e) {
Log.d("[ROCKETCHAT][ENCRYPTION]", 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;
}
// 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;
}
return result;
}
}
......@@ -11,89 +11,90 @@ import okhttp3.Response;
import okhttp3.Interceptor;
import com.google.gson.Gson;
import java.io.IOException;
import com.facebook.react.bridge.ReactApplicationContext;
class JsonResponse {
Data data;
class Data {
Notification notification;
class Notification {
String notId;
String title;
String text;
Payload payload;
class Payload {
String host;
String rid;
String type;
Sender sender;
String messageId;
String notificationType;
String name;
String messageType;
class Sender {
String _id;
String username;
String name;
Data data;
class Data {
Notification notification;
class Notification {
String notId;
String title;
String text;
Payload payload;
class Payload {
String host;
String rid;
String type;
Sender sender;
String messageId;
String notificationType;
String name;
String messageType;
class Sender {
String _id;
String username;
String name;
}
}
}
}
}
}
}
public class LoadNotification {
private static int RETRY_COUNT = 0;
private static int[] TIMEOUT = new int[]{ 0, 1, 3, 5, 10 };
private static String TOKEN_KEY = "reactnativemeteor_usertoken-";
public static void load(ReactApplicationContext reactApplicationContext, final Ejson ejson, Callback callback) {
final OkHttpClient client = new OkHttpClient();
HttpUrl.Builder url = HttpUrl.parse(ejson.serverURL().concat("/api/v1/push.get")).newBuilder();
Request request = new Request.Builder()
.header("x-user-id", ejson.userId())
.header("x-auth-token", ejson.token())
.url(url.addQueryParameter("id", ejson.messageId).build())
.build();
runRequest(client, request, callback);
}
private static void runRequest(OkHttpClient client, Request request, Callback callback) {
try {
Thread.sleep(TIMEOUT[RETRY_COUNT] * 1000);
Response response = client.newCall(request).execute();
String body = response.body().string();
if (!response.isSuccessful()) {
throw new Exception("Error");
}
Gson gson = new Gson();
JsonResponse json = gson.fromJson(body, JsonResponse.class);
Bundle bundle = new Bundle();
bundle.putString("notId", json.data.notification.notId);
bundle.putString("title", json.data.notification.title);
bundle.putString("message", json.data.notification.text);
bundle.putString("ejson", gson.toJson(json.data.notification.payload));
bundle.putBoolean("notificationLoaded", true);
callback.call(bundle);
} catch (Exception e) {
if (RETRY_COUNT <= TIMEOUT.length) {
RETRY_COUNT++;
private static int RETRY_COUNT = 0;
private static int[] TIMEOUT = new int[]{0, 1, 3, 5, 10};
private static String TOKEN_KEY = "reactnativemeteor_usertoken-";
public static void load(ReactApplicationContext reactApplicationContext, final Ejson ejson, Callback callback) {
final OkHttpClient client = new OkHttpClient();
HttpUrl.Builder url = HttpUrl.parse(ejson.serverURL().concat("/api/v1/push.get")).newBuilder();
Request request = new Request.Builder()
.header("x-user-id", ejson.userId())
.header("x-auth-token", ejson.token())
.url(url.addQueryParameter("id", ejson.messageId).build())
.build();
runRequest(client, request, callback);
} else {
callback.call(null);
}
}
}
private static void runRequest(OkHttpClient client, Request request, Callback callback) {
try {
Thread.sleep(TIMEOUT[RETRY_COUNT] * 1000);
Response response = client.newCall(request).execute();
String body = response.body().string();
if (!response.isSuccessful()) {
throw new Exception("Error");
}
Gson gson = new Gson();
JsonResponse json = gson.fromJson(body, JsonResponse.class);
Bundle bundle = new Bundle();
bundle.putString("notId", json.data.notification.notId);
bundle.putString("title", json.data.notification.title);
bundle.putString("message", json.data.notification.text);
bundle.putString("ejson", gson.toJson(json.data.notification.payload));
bundle.putBoolean("notificationLoaded", true);
callback.call(bundle);
} catch (Exception e) {
if (RETRY_COUNT <= TIMEOUT.length) {
RETRY_COUNT++;
runRequest(client, request, callback);
} else {
callback.call(null);
}
}
}
}
......@@ -11,10 +11,12 @@ import android.content.res.Resources;
import android.os.Build;