diff --git a/app/build.gradle b/app/build.gradle index 9192d48c56..db9c42d43a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -133,6 +133,7 @@ android { apply plugin: 'com.google.gms.google-services' ext.websiteUpdateUrl = "null" buildConfigField "boolean", "PLAY_STORE_DISABLED", "false" + buildConfigField "String", "DEVICE", "\"android\"" buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl" } @@ -140,6 +141,7 @@ android { dimension "distribution" ext.websiteUpdateUrl = "null" buildConfigField "boolean", "PLAY_STORE_DISABLED", "true" + buildConfigField "String", "DEVICE", "\"huawei\"" buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl" } @@ -147,6 +149,7 @@ android { dimension "distribution" ext.websiteUpdateUrl = "https://github.com/oxen-io/session-android/releases" buildConfigField "boolean", "PLAY_STORE_DISABLED", "true" + buildConfigField "String", "DEVICE", "\"android\"" buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\"" } } diff --git a/app/src/huawei/AndroidManifest.xml b/app/src/huawei/AndroidManifest.xml new file mode 100644 index 0000000000..4745c454b2 --- /dev/null +++ b/app/src/huawei/AndroidManifest.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushModule.kt b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushModule.kt index 17c40ea826..e10ed77abf 100644 --- a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushModule.kt +++ b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushModule.kt @@ -21,7 +21,7 @@ object HuaweiPushModule { @Module @InstallIn(SingletonComponent::class) -abstract class FirebaseBindingModule { +abstract class HuaweiBindingModule { @Binds - abstract fun bindPushManager(firebasePushManager: HuaweiPushManager): PushManager -} \ No newline at end of file + abstract fun bindPushManager(pushManager: HuaweiPushManager): PushManager +} diff --git a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt new file mode 100644 index 0000000000..dd475c8344 --- /dev/null +++ b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt @@ -0,0 +1,59 @@ +package org.thoughtcrime.securesms.notifications + +import android.os.Bundle +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import com.huawei.hms.push.HmsMessageService +import com.huawei.hms.push.RemoteMessage +import dagger.hilt.android.AndroidEntryPoint +import org.session.libsession.messaging.jobs.BatchMessageReceiveJob +import org.session.libsession.messaging.jobs.JobQueue +import org.session.libsession.messaging.jobs.MessageReceiveParameters +import org.session.libsession.messaging.utilities.MessageWrapper +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsignal.utilities.Base64 +import org.session.libsignal.utilities.Log +import javax.inject.Inject + +@AndroidEntryPoint +class HuaweiPushNotificationService: HmsMessageService() { + + @Inject token + + override fun onNewToken(token: String?, bundle: Bundle?) { + Log.d("Loki", "New HCM token: $token.") + + if (!token.isNullOrEmpty()) { + val userPublicKey = TextSecurePreferences.getLocalNumber(this) ?: return + PushManager.register(token, userPublicKey, this, false) + } + } + + override fun onMessageReceived(message: RemoteMessage?) { + Log.d("Loki", "Received a push notification.") + val base64EncodedData = message?.data + val data = base64EncodedData?.let { Base64.decode(it) } + if (data != null) { + try { + val envelopeAsData = MessageWrapper.unwrap(data).toByteArray() + val job = BatchMessageReceiveJob(listOf(MessageReceiveParameters(envelopeAsData)), null) + JobQueue.shared.add(job) + } catch (e: Exception) { + Log.d("Loki", "Failed to unwrap data for message due to error: $e.") + } + } else { + Log.d("Loki", "Failed to decode data for message.") + val builder = NotificationCompat.Builder(this, NotificationChannels.OTHER) + .setSmallIcon(network.loki.messenger.R.drawable.ic_notification) + .setColor(this.getResources().getColor(network.loki.messenger.R.color.textsecure_primary)) + .setContentTitle("Session") + .setContentText("You've got a new message.") + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setAutoCancel(true) + with(NotificationManagerCompat.from(this)) { + notify(11111, builder.build()) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushHandler.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushHandler.kt new file mode 100644 index 0000000000..be4832104d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushHandler.kt @@ -0,0 +1,107 @@ +package org.thoughtcrime.securesms.notifications + +import android.content.Context +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import com.goterl.lazysodium.LazySodiumAndroid +import com.goterl.lazysodium.SodiumAndroid +import com.goterl.lazysodium.interfaces.AEAD +import com.goterl.lazysodium.utils.Key +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import org.session.libsession.messaging.jobs.BatchMessageReceiveJob +import org.session.libsession.messaging.jobs.JobQueue +import org.session.libsession.messaging.jobs.MessageReceiveParameters +import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationMetadata +import org.session.libsession.messaging.utilities.MessageWrapper +import org.session.libsession.messaging.utilities.SodiumUtilities +import org.session.libsession.utilities.bencode.Bencode +import org.session.libsession.utilities.bencode.BencodeList +import org.session.libsession.utilities.bencode.BencodeString +import org.session.libsignal.utilities.Base64 +import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil +import javax.inject.Inject + +private const val TAG = "PushHandler" + +class PushHandler @Inject constructor(@ApplicationContext val context: Context) { + private val sodium = LazySodiumAndroid(SodiumAndroid()) + + fun onPush(dataMap: Map) { + val data: ByteArray? = if (dataMap.containsKey("spns")) { + // this is a v2 push notification + try { + decrypt(Base64.decode(dataMap["enc_payload"])) + } catch(e: Exception) { + Log.e(TAG, "Invalid push notification: ${e.message}") + return + } + } else { + // old v1 push notification; we still need this for receiving legacy closed group notifications + dataMap.get("ENCRYPTED_DATA")?.let(Base64::decode) + } + data?.let { onPush(data) } ?: onPush() + + } + + fun onPush() { + Log.d(TAG, "Failed to decode data for message.") + val builder = NotificationCompat.Builder(context, NotificationChannels.OTHER) + .setSmallIcon(network.loki.messenger.R.drawable.ic_notification) + .setColor(context.getColor(network.loki.messenger.R.color.textsecure_primary)) + .setContentTitle("Session") + .setContentText("You've got a new message.") + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setAutoCancel(true) + NotificationManagerCompat.from(context).notify(11111, builder.build()) + } + + fun onPush(data: ByteArray) { + try { + val envelopeAsData = MessageWrapper.unwrap(data).toByteArray() + val job = BatchMessageReceiveJob(listOf(MessageReceiveParameters(envelopeAsData)), null) + JobQueue.shared.add(job) + } catch (e: Exception) { + Log.d(TAG, "Failed to unwrap data for message due to error: $e.") + } + } + + fun decrypt(encPayload: ByteArray): ByteArray? { + Log.d(TAG, "decrypt() called") + + val encKey = getOrCreateNotificationKey() + val nonce = encPayload.take(AEAD.XCHACHA20POLY1305_IETF_NPUBBYTES).toByteArray() + val payload = encPayload.drop(AEAD.XCHACHA20POLY1305_IETF_NPUBBYTES).toByteArray() + val padded = SodiumUtilities.decrypt(payload, encKey.asBytes, nonce) + ?: error("Failed to decrypt push notification") + val decrypted = padded.dropLastWhile { it.toInt() == 0 }.toByteArray() + val bencoded = Bencode.Decoder(decrypted) + val expectedList = (bencoded.decode() as? BencodeList)?.values + ?: error("Failed to decode bencoded list from payload") + + val metadataJson = (expectedList[0] as? BencodeString)?.value ?: error("no metadata") + val metadata: PushNotificationMetadata = Json.decodeFromString(String(metadataJson)) + + val content: ByteArray? = if (expectedList.size >= 2) (expectedList[1] as? BencodeString)?.value else null + // null content is valid only if we got a "data_too_long" flag + if (content == null) + check(metadata.data_too_long) { "missing message data, but no too-long flag" } + else + check(metadata.data_len == content.size) { "wrong message data size" } + + Log.d(TAG, "Received push for ${metadata.account}/${metadata.namespace}, msg ${metadata.msg_hash}, ${metadata.data_len}B") + + return content + } + + fun getOrCreateNotificationKey(): Key { + if (IdentityKeyUtil.retrieve(context, IdentityKeyUtil.NOTIFICATION_KEY) == null) { + // generate the key and store it + val key = sodium.keygen(AEAD.Method.XCHACHA20_POLY1305_IETF) + IdentityKeyUtil.save(context, IdentityKeyUtil.NOTIFICATION_KEY, key.asHexString) + } + return Key.fromHexString(IdentityKeyUtil.retrieve(context, IdentityKeyUtil.NOTIFICATION_KEY)) + } +} diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/ExpiryManager.kt b/app/src/main/kotlin/org/thoughtcrime/securesms/notifications/ExpiryManager.kt similarity index 100% rename from app/src/play/kotlin/org/thoughtcrime/securesms/notifications/ExpiryManager.kt rename to app/src/main/kotlin/org/thoughtcrime/securesms/notifications/ExpiryManager.kt diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt b/app/src/main/kotlin/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt similarity index 100% rename from app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt rename to app/src/main/kotlin/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt index 89712c2e85..df15efe80c 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt @@ -40,6 +40,7 @@ import org.session.libsignal.utilities.emptyPromise import org.session.libsignal.utilities.retryIfNeeded import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.KeyPairUtilities +import javax.inject.Inject private const val TAG = "FirebasePushManager" @@ -47,7 +48,7 @@ class FirebasePushManager( private val context: Context ): PushManager { - private val pushManagerV2 = PushManagerV2(context) + @Inject lateinit var pushManagerV2: PushManagerV2 companion object { const val maxRetryCount = 4 @@ -132,6 +133,4 @@ class FirebasePushManager( } success { tokenManager.fcmToken = null } - - fun decrypt(decode: ByteArray) = pushManagerV2.decrypt(decode) } diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushManagerV2.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushManagerV2.kt index 66c7590e5f..4fe885758f 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushManagerV2.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushManagerV2.kt @@ -39,7 +39,7 @@ import org.thoughtcrime.securesms.crypto.IdentityKeyUtil private const val TAG = "PushManagerV2" -class PushManagerV2(private val context: Context) { +class PushManagerV2(private val pushHandler: PushHandler) { private val sodium = LazySodiumAndroid(SodiumAndroid()) fun register( @@ -48,7 +48,7 @@ class PushManagerV2(private val context: Context) { userEd25519Key: KeyPair, namespaces: List ): Promise { - val pnKey = getOrCreateNotificationKey() + val pnKey = pushHandler.getOrCreateNotificationKey() val timestamp = SnodeAPI.nowWithOffset / 1000 // get timestamp in ms -> s // if we want to support passing namespace list, here is the place to do it @@ -117,41 +117,4 @@ class PushManagerV2(private val context: Context) { .also { if (it.isFailure()) throw Exception("error: ${it.message}.") } } } - - private fun getOrCreateNotificationKey(): Key { - if (IdentityKeyUtil.retrieve(context, IdentityKeyUtil.NOTIFICATION_KEY) == null) { - // generate the key and store it - val key = sodium.keygen(AEAD.Method.XCHACHA20_POLY1305_IETF) - IdentityKeyUtil.save(context, IdentityKeyUtil.NOTIFICATION_KEY, key.asHexString) - } - return Key.fromHexString(IdentityKeyUtil.retrieve(context, IdentityKeyUtil.NOTIFICATION_KEY)) - } - - fun decrypt(encPayload: ByteArray): ByteArray? { - Log.d(TAG, "decrypt() called") - - val encKey = getOrCreateNotificationKey() - val nonce = encPayload.take(AEAD.XCHACHA20POLY1305_IETF_NPUBBYTES).toByteArray() - val payload = encPayload.drop(AEAD.XCHACHA20POLY1305_IETF_NPUBBYTES).toByteArray() - val padded = SodiumUtilities.decrypt(payload, encKey.asBytes, nonce) - ?: error("Failed to decrypt push notification") - val decrypted = padded.dropLastWhile { it.toInt() == 0 }.toByteArray() - val bencoded = Bencode.Decoder(decrypted) - val expectedList = (bencoded.decode() as? BencodeList)?.values - ?: error("Failed to decode bencoded list from payload") - - val metadataJson = (expectedList[0] as? BencodeString)?.value ?: error("no metadata") - val metadata: PushNotificationMetadata = Json.decodeFromString(String(metadataJson)) - - val content: ByteArray? = if (expectedList.size >= 2) (expectedList[1] as? BencodeString)?.value else null - // null content is valid only if we got a "data_too_long" flag - if (content == null) - check(metadata.data_too_long) { "missing message data, but no too-long flag" } - else - check(metadata.data_len == content.size) { "wrong message data size" } - - Log.d(TAG, "Received push for ${metadata.account}/${metadata.namespace}, msg ${metadata.msg_hash}, ${metadata.data_len}B") - - return content - } } diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt index 4c6ce9f4ea..e546053fce 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt @@ -8,7 +8,6 @@ import dagger.hilt.android.AndroidEntryPoint import org.session.libsession.messaging.jobs.BatchMessageReceiveJob import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.MessageReceiveParameters -import org.session.libsession.messaging.sending_receiving.notifications.PushManagerV1 import org.session.libsession.messaging.utilities.MessageWrapper import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.Base64 @@ -21,6 +20,7 @@ private const val TAG = "PushNotificationService" class PushNotificationService : FirebaseMessagingService() { @Inject lateinit var pushManager: FirebasePushManager + @Inject lateinit var pushHandler: PushHandler override fun onNewToken(token: String) { super.onNewToken(token) @@ -32,37 +32,7 @@ class PushNotificationService : FirebaseMessagingService() { override fun onMessageReceived(message: RemoteMessage) { Log.d(TAG, "Received a push notification.") - val data: ByteArray? = if (message.data.containsKey("spns")) { - // this is a v2 push notification - try { - pushManager.decrypt(Base64.decode(message.data["enc_payload"])) - } catch(e: Exception) { - Log.e(TAG, "Invalid push notification: ${e.message}") - return - } - } else { - // old v1 push notification; we still need this for receiving legacy closed group notifications - message.data?.get("ENCRYPTED_DATA")?.let(Base64::decode) - } - if (data != null) { - try { - val envelopeAsData = MessageWrapper.unwrap(data).toByteArray() - val job = BatchMessageReceiveJob(listOf(MessageReceiveParameters(envelopeAsData)), null) - JobQueue.shared.add(job) - } catch (e: Exception) { - Log.d(TAG, "Failed to unwrap data for message due to error: $e.") - } - } else { - Log.d(TAG, "Failed to decode data for message.") - val builder = NotificationCompat.Builder(this, NotificationChannels.OTHER) - .setSmallIcon(network.loki.messenger.R.drawable.ic_notification) - .setColor(resources.getColor(network.loki.messenger.R.color.textsecure_primary)) - .setContentTitle("Session") - .setContentText("You've got a new message.") - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .setAutoCancel(true) - NotificationManagerCompat.from(this).notify(11111, builder.build()) - } + pushHandler.onPush(message.data) } override fun onDeletedMessages() { diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt index e112cd2682..1533002d6c 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt @@ -52,10 +52,13 @@ object PushManagerV1 { ) val url = "${server.url}/register_legacy_groups_only" - val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) + val body = RequestBody.create( + MediaType.get("application/json"), + JsonUtil.toJson(parameters) + ) val request = Request.Builder().url(url).post(body).build() - return sendOnionRequest(request) sideEffect { response -> + return sendOnionRequest(request) sideEffect { response -> when (response.code) { null, 0 -> throw Exception("error: ${response.message}.") } @@ -73,7 +76,7 @@ object PushManagerV1 { val token = TextSecurePreferences.getFCMToken(context) ?: emptyPromise() return retryIfNeeded(maxRetryCount) { - val parameters = mapOf( "token" to token ) + val parameters = mapOf("token" to token) val url = "${server.url}/unregister" val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val request = Request.Builder().url(url).post(body).build() @@ -110,7 +113,7 @@ object PushManagerV1 { closedGroupPublicKey: String, publicKey: String ): Promise<*, Exception> { - val parameters = mapOf( "closedGroupPublicKey" to closedGroupPublicKey, "pubKey" to publicKey ) + val parameters = mapOf("closedGroupPublicKey" to closedGroupPublicKey, "pubKey" to publicKey) val url = "${server.url}/$operation" val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val request = Request.Builder().url(url).post(body).build() @@ -118,7 +121,7 @@ object PushManagerV1 { return retryIfNeeded(maxRetryCount) { sendOnionRequest(request) sideEffect { when (it.code) { - 0, null -> throw Exception(it.message) + 0, null -> throw Exception(it.message) } } }