This commit is contained in:
andrew 2023-07-25 14:49:41 +09:30
parent 01e9d15872
commit 34990b13d3
11 changed files with 202 additions and 82 deletions

View File

@ -133,6 +133,7 @@ android {
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'
ext.websiteUpdateUrl = "null" ext.websiteUpdateUrl = "null"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false" buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
buildConfigField "String", "DEVICE", "\"android\""
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl" buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
} }
@ -140,6 +141,7 @@ android {
dimension "distribution" dimension "distribution"
ext.websiteUpdateUrl = "null" ext.websiteUpdateUrl = "null"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true" buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
buildConfigField "String", "DEVICE", "\"huawei\""
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl" buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
} }
@ -147,6 +149,7 @@ android {
dimension "distribution" dimension "distribution"
ext.websiteUpdateUrl = "https://github.com/oxen-io/session-android/releases" ext.websiteUpdateUrl = "https://github.com/oxen-io/session-android/releases"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true" buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
buildConfigField "String", "DEVICE", "\"android\""
buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\"" buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\""
} }
} }

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application tools:node="merge">
<service
android:name="org.thoughtcrime.securesms.notifications.HuaweiPushNotificationService"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
</manifest>

View File

@ -21,7 +21,7 @@ object HuaweiPushModule {
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
abstract class FirebaseBindingModule { abstract class HuaweiBindingModule {
@Binds @Binds
abstract fun bindPushManager(firebasePushManager: HuaweiPushManager): PushManager abstract fun bindPushManager(pushManager: HuaweiPushManager): PushManager
} }

View File

@ -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())
}
}
}
}

View File

@ -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<String, String>) {
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))
}
}

View File

@ -40,6 +40,7 @@ import org.session.libsignal.utilities.emptyPromise
import org.session.libsignal.utilities.retryIfNeeded import org.session.libsignal.utilities.retryIfNeeded
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.crypto.KeyPairUtilities import org.thoughtcrime.securesms.crypto.KeyPairUtilities
import javax.inject.Inject
private const val TAG = "FirebasePushManager" private const val TAG = "FirebasePushManager"
@ -47,7 +48,7 @@ class FirebasePushManager(
private val context: Context private val context: Context
): PushManager { ): PushManager {
private val pushManagerV2 = PushManagerV2(context) @Inject lateinit var pushManagerV2: PushManagerV2
companion object { companion object {
const val maxRetryCount = 4 const val maxRetryCount = 4
@ -132,6 +133,4 @@ class FirebasePushManager(
} success { } success {
tokenManager.fcmToken = null tokenManager.fcmToken = null
} }
fun decrypt(decode: ByteArray) = pushManagerV2.decrypt(decode)
} }

View File

@ -39,7 +39,7 @@ import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
private const val TAG = "PushManagerV2" private const val TAG = "PushManagerV2"
class PushManagerV2(private val context: Context) { class PushManagerV2(private val pushHandler: PushHandler) {
private val sodium = LazySodiumAndroid(SodiumAndroid()) private val sodium = LazySodiumAndroid(SodiumAndroid())
fun register( fun register(
@ -48,7 +48,7 @@ class PushManagerV2(private val context: Context) {
userEd25519Key: KeyPair, userEd25519Key: KeyPair,
namespaces: List<Int> namespaces: List<Int>
): Promise<SubscriptionResponse, Exception> { ): Promise<SubscriptionResponse, Exception> {
val pnKey = getOrCreateNotificationKey() val pnKey = pushHandler.getOrCreateNotificationKey()
val timestamp = SnodeAPI.nowWithOffset / 1000 // get timestamp in ms -> s 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 // 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}.") } .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
}
} }

View File

@ -8,7 +8,6 @@ import dagger.hilt.android.AndroidEntryPoint
import org.session.libsession.messaging.jobs.BatchMessageReceiveJob import org.session.libsession.messaging.jobs.BatchMessageReceiveJob
import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.jobs.MessageReceiveParameters 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.messaging.utilities.MessageWrapper
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Base64
@ -21,6 +20,7 @@ private const val TAG = "PushNotificationService"
class PushNotificationService : FirebaseMessagingService() { class PushNotificationService : FirebaseMessagingService() {
@Inject lateinit var pushManager: FirebasePushManager @Inject lateinit var pushManager: FirebasePushManager
@Inject lateinit var pushHandler: PushHandler
override fun onNewToken(token: String) { override fun onNewToken(token: String) {
super.onNewToken(token) super.onNewToken(token)
@ -32,37 +32,7 @@ class PushNotificationService : FirebaseMessagingService() {
override fun onMessageReceived(message: RemoteMessage) { override fun onMessageReceived(message: RemoteMessage) {
Log.d(TAG, "Received a push notification.") Log.d(TAG, "Received a push notification.")
val data: ByteArray? = if (message.data.containsKey("spns")) { pushHandler.onPush(message.data)
// 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())
}
} }
override fun onDeletedMessages() { override fun onDeletedMessages() {

View File

@ -52,10 +52,13 @@ object PushManagerV1 {
) )
val url = "${server.url}/register_legacy_groups_only" 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() val request = Request.Builder().url(url).post(body).build()
return sendOnionRequest(request) sideEffect { response -> return sendOnionRequest(request) sideEffect { response ->
when (response.code) { when (response.code) {
null, 0 -> throw Exception("error: ${response.message}.") null, 0 -> throw Exception("error: ${response.message}.")
} }
@ -73,7 +76,7 @@ object PushManagerV1 {
val token = TextSecurePreferences.getFCMToken(context) ?: emptyPromise() val token = TextSecurePreferences.getFCMToken(context) ?: emptyPromise()
return retryIfNeeded(maxRetryCount) { return retryIfNeeded(maxRetryCount) {
val parameters = mapOf( "token" to token ) val parameters = mapOf("token" to token)
val url = "${server.url}/unregister" val url = "${server.url}/unregister"
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() val request = Request.Builder().url(url).post(body).build()
@ -110,7 +113,7 @@ object PushManagerV1 {
closedGroupPublicKey: String, closedGroupPublicKey: String,
publicKey: String publicKey: String
): Promise<*, Exception> { ): 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 url = "${server.url}/$operation"
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() val request = Request.Builder().url(url).post(body).build()
@ -118,7 +121,7 @@ object PushManagerV1 {
return retryIfNeeded(maxRetryCount) { return retryIfNeeded(maxRetryCount) {
sendOnionRequest(request) sideEffect { sendOnionRequest(request) sideEffect {
when (it.code) { when (it.code) {
0, null -> throw Exception(it.message) 0, null -> throw Exception(it.message)
} }
} }
} }