Page MenuHomePhabricator

D8566.id28842.diff
No OneTemporary

D8566.id28842.diff

diff --git a/native/android/app/src/main/java/app/comm/android/notifications/CommAndroidNotificationsBlobServiceClient.java b/native/android/app/src/main/java/app/comm/android/notifications/CommAndroidNotificationsBlobServiceClient.java
new file mode 100644
--- /dev/null
+++ b/native/android/app/src/main/java/app/comm/android/notifications/CommAndroidNotificationsBlobServiceClient.java
@@ -0,0 +1,65 @@
+package app.comm.android.notifications;
+
+import android.util.Log;
+import com.google.firebase.messaging.RemoteMessage;
+import java.io.IOException;
+import java.lang.OutOfMemoryError;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+
+public class CommAndroidNotificationsBlobServiceClient {
+ private static final String BLOB_SERVICE_URL =
+ "https://blob.commtechnologies.org";
+
+ // OkHttp docs advise to share OkHttpClient instances
+ // https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/#okhttpclients-should-be-shared
+ private static final OkHttpClient httpClient =
+ // The FirebaseMessagingService docs state that message
+ // processing should complete within 20 seconds window
+ // https://firebase.google.com/docs/cloud-messaging/android/receive#handling_messages
+ new OkHttpClient.Builder().callTimeout(20, TimeUnit.SECONDS).build();
+
+ @FunctionalInterface
+ public interface BlobServiceMessageConsumer {
+ void accept(RemoteMessage message, byte[] blob);
+ }
+
+ public void getAndConsumeAsync(
+ String blobHash,
+ RemoteMessage message,
+ BlobServiceMessageConsumer successConsumer,
+ Consumer<RemoteMessage> fallbackConsumer) {
+ Request request = new Request.Builder()
+ .get()
+ .url(BLOB_SERVICE_URL + "/blob/" + blobHash)
+ .build();
+
+ httpClient.newCall(request).enqueue(new Callback() {
+ @Override
+ public void onFailure(Call call, IOException e) {
+ fallbackConsumer.accept(message);
+ Log.w("COMM", "Failed to download blob from blob service.", e);
+ call.cancel();
+ }
+
+ @Override
+ public void onResponse(Call call, Response response) {
+ try {
+ successConsumer.accept(message, response.body().bytes());
+ } catch (OutOfMemoryError e) {
+ fallbackConsumer.accept(message);
+ Log.w("COMM", "Notification payload exceeds available memory.", e);
+ } catch (IOException e) {
+ fallbackConsumer.accept(message);
+ Log.w("COMM", "Unable to get payload from HTTP response.", e);
+ }
+ }
+ });
+ }
+}
diff --git a/native/android/app/src/main/java/app/comm/android/notifications/CommNotificationsHandler.java b/native/android/app/src/main/java/app/comm/android/notifications/CommNotificationsHandler.java
--- a/native/android/app/src/main/java/app/comm/android/notifications/CommNotificationsHandler.java
+++ b/native/android/app/src/main/java/app/comm/android/notifications/CommNotificationsHandler.java
@@ -9,7 +9,6 @@
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.service.notification.StatusBarNotification;
-import android.util.JsonReader;
import android.util.Log;
import androidx.core.app.NotificationCompat;
import androidx.lifecycle.Lifecycle;
@@ -18,6 +17,7 @@
import app.comm.android.ExpoUtils;
import app.comm.android.MainActivity;
import app.comm.android.R;
+import app.comm.android.aescrypto.AESCryptoModuleCompat;
import app.comm.android.fbjni.CommSecureStore;
import app.comm.android.fbjni.GlobalDBSingleton;
import app.comm.android.fbjni.MessageOperationsUtilities;
@@ -27,6 +27,10 @@
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
import java.io.File;
+import java.io.IOException;
+import java.lang.Thread;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
import me.leolin.shortcutbadger.ShortcutBadger;
import org.json.JSONException;
import org.json.JSONObject;
@@ -39,12 +43,15 @@
private static final String NOTIF_ID_KEY = "id";
private static final String ENCRYPTED_PAYLOAD_KEY = "encryptedPayload";
private static final String ENCRYPTION_FAILED_KEY = "encryptionFailed";
+ private static final String BLOB_HASH_KEY = "blobHash";
+ private static final String AES_ENCRYPTION_KEY_KEY = "encryptionKey";
private static final String CHANNEL_ID = "default";
private static final long[] VIBRATION_SPEC = {500, 500};
private Bitmap displayableNotificationLargeIcon;
private NotificationManager notificationManager;
private LocalBroadcastManager localBroadcastManager;
+ private CommAndroidNotificationsBlobServiceClient blobServiceClient;
public static final String RESCIND_KEY = "rescind";
public static final String RESCIND_ID_KEY = "rescindID";
@@ -67,6 +74,7 @@
localBroadcastManager = LocalBroadcastManager.getInstance(this);
displayableNotificationLargeIcon = BitmapFactory.decodeResource(
this.getApplicationContext().getResources(), R.mipmap.ic_launcher);
+ blobServiceClient = new CommAndroidNotificationsBlobServiceClient();
}
@Override
@@ -80,7 +88,7 @@
public void onMessageReceived(RemoteMessage message) {
if (message.getData().get(ENCRYPTED_PAYLOAD_KEY) != null) {
try {
- message = this.decryptRemoteMessage(message);
+ message = this.olmDecryptRemoteMessage(message);
} catch (JSONException e) {
Log.w("COMM", "Malformed notification JSON payload.", e);
return;
@@ -88,7 +96,7 @@
Log.w("COMM", "Android notification type violation.", e);
return;
} catch (Exception e) {
- Log.w("COMM", "Notification decryption failure.", e);
+ Log.w("COMM", "Notification olm decryption failure.", e);
return;
}
} else if ("1".equals(message.getData().get(ENCRYPTION_FAILED_KEY))) {
@@ -97,6 +105,34 @@
"Received unencrypted notification for client with existing olm session for notifications");
}
+ String blobHash = message.getData().get(BLOB_HASH_KEY);
+ if (blobHash != null) {
+ blobServiceClient.getAndConsumeAsync(
+ blobHash,
+ message,
+ (remoteMessage, blob)
+ -> {
+ try {
+ remoteMessage = aesDecryptRemoteMessage(remoteMessage, blob);
+ persistAndDisplayUnencryptedMessage(remoteMessage);
+ } catch (JSONException e) {
+ Log.w("COMM", "Malformed notification JSON payload.", e);
+ } catch (IllegalStateException e) {
+ Log.w("COMM", "Android notification type violation.", e);
+ } catch (Exception e) {
+ Log.w("COMM", "Notification aes decryption failure.", e);
+ }
+ },
+ (remoteMessage) -> {
+ persistAndDisplayUnencryptedMessage(remoteMessage);
+ });
+ return;
+ }
+
+ persistAndDisplayUnencryptedMessage(message);
+ }
+
+ private void persistAndDisplayUnencryptedMessage(RemoteMessage message) {
String rescind = message.getData().get(RESCIND_KEY);
if ("true".equals(rescind) &&
android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
@@ -288,15 +324,10 @@
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
}
- private RemoteMessage decryptRemoteMessage(RemoteMessage message)
+ private RemoteMessage updateRemoteMessageWithDecryptedPayload(
+ RemoteMessage message,
+ String decryptedSerializedPayload)
throws JSONException, IllegalStateException {
- String encryptedSerializedPayload =
- message.getData().get(ENCRYPTED_PAYLOAD_KEY);
- String decryptedSerializedPayload = NotificationsCryptoModule.decrypt(
- encryptedSerializedPayload,
- NotificationsCryptoModule.olmEncryptedTypeMessage(),
- "CommNotificationsHandler");
-
JSONObject decryptedPayload = new JSONObject(decryptedSerializedPayload);
((Iterable<String>)() -> decryptedPayload.keys())
@@ -313,6 +344,37 @@
return message;
}
+ private RemoteMessage olmDecryptRemoteMessage(RemoteMessage message)
+ throws JSONException, IllegalStateException {
+ String encryptedSerializedPayload =
+ message.getData().get(ENCRYPTED_PAYLOAD_KEY);
+ String decryptedSerializedPayload = NotificationsCryptoModule.decrypt(
+ encryptedSerializedPayload,
+ NotificationsCryptoModule.olmEncryptedTypeMessage(),
+ "CommNotificationsHandler");
+
+ return updateRemoteMessageWithDecryptedPayload(
+ message, decryptedSerializedPayload);
+ }
+
+ private RemoteMessage
+ aesDecryptRemoteMessage(RemoteMessage message, byte[] blob)
+ throws JSONException, IllegalStateException {
+ String aesEncryptionKey = message.getData().get(AES_ENCRYPTION_KEY_KEY);
+ AESCryptoModuleCompat obj = new AESCryptoModuleCompat();
+ // On the keyserver AES key is generated as raw bytes
+ // so to send it in JSON it is encoded to Base64 string.
+ byte[] aesEncryptionKeyBytes = Base64.getDecoder().decode(aesEncryptionKey);
+ // On the keyserver notification is a string so it is
+ // first encoded into UTF8 bytes. Therefore bytes
+ // obtained from blob decryption are correct UTF8 bytes.
+ String decryptedSerializedPayload = new String(
+ obj.decrypt(aesEncryptionKeyBytes, blob), StandardCharsets.UTF_8);
+
+ return updateRemoteMessageWithDecryptedPayload(
+ message, decryptedSerializedPayload);
+ }
+
private Bundle
serializeMessageDataForIntentAttachment(RemoteMessage message) {
Bundle bundle = new Bundle();

File Metadata

Mime Type
text/plain
Expires
Tue, Nov 26, 1:46 PM (5 h, 6 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2584947
Default Alt Text
D8566.id28842.diff (9 KB)

Event Timeline