feat: add support for firebase and split out google services as a dependency for only the play version of the app. Add support for requests in new pn server

This commit is contained in:
0x330a
2023-04-20 17:12:38 +10:00
parent 2246a5d9ce
commit 8d4f2445f2
21 changed files with 381 additions and 510 deletions

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.PushNotificationService"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
</manifest>

View File

@@ -0,0 +1,17 @@
@file:JvmName("FcmUtils")
package org.thoughtcrime.securesms.notifications
import com.google.android.gms.tasks.Task
import com.google.firebase.iid.FirebaseInstanceId
import com.google.firebase.iid.InstanceIdResult
import kotlinx.coroutines.*
fun getFcmInstanceId(body: (Task<InstanceIdResult>)->Unit): Job = MainScope().launch(Dispatchers.IO) {
val task = FirebaseInstanceId.getInstance().instanceId
while (!task.isComplete && isActive) {
// wait for task to complete while we are active
}
if (!isActive) return@launch // don't 'complete' task if we were canceled
body(task)
}

View File

@@ -0,0 +1,142 @@
package org.thoughtcrime.securesms.notifications
import android.content.Context
import com.goterl.lazysodium.LazySodiumAndroid
import com.goterl.lazysodium.SodiumAndroid
import com.goterl.lazysodium.interfaces.AEAD
import com.goterl.lazysodium.interfaces.Sign
import com.goterl.lazysodium.utils.Key
import com.goterl.lazysodium.utils.KeyPair
import kotlinx.coroutines.Job
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.map
import okhttp3.MediaType
import okhttp3.Request
import okhttp3.RequestBody
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionRequest
import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionResponse
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.Version
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Namespace
import org.session.libsignal.utilities.retryIfNeeded
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
class FirebasePushManager(private val context: Context, private val prefs: TextSecurePreferences): PushManager {
companion object {
private const val maxRetryCount = 4
private const val tokenExpirationInterval = 12 * 60 * 60 * 1000
}
private var firebaseInstanceIdJob: Job? = null
private val sodium = LazySodiumAndroid(SodiumAndroid())
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
)
)
}
override fun register(force: Boolean) {
val currentInstanceIdJob = firebaseInstanceIdJob
if (currentInstanceIdJob != null && currentInstanceIdJob.isActive && !force) return
if (force && currentInstanceIdJob != null) {
currentInstanceIdJob.cancel(null)
}
firebaseInstanceIdJob = getFcmInstanceId { task ->
// context in here is Dispatchers.IO
if (!task.isSuccessful) {
Log.w(
"Loki",
"FirebaseInstanceId.getInstance().getInstanceId() failed." + task.exception
)
return@getFcmInstanceId
}
val token: String = task.result?.token ?: return@getFcmInstanceId
val userPublicKey = getLocalNumber(context) ?: return@getFcmInstanceId
val userEdKey = KeyPairUtilities.getUserED25519KeyPair(context) ?: return@getFcmInstanceId
if (prefs.isUsingFCM()) {
register(token, userPublicKey, userEdKey, force)
} else {
unregister(token)
}
}
}
override fun unregister(token: String) {
TODO("Not yet implemented")
}
fun register(token: String, publicKey: String, userEd25519Key: KeyPair, force: Boolean, namespaces: List<Int> = listOf(Namespace.DEFAULT)) {
val oldToken = TextSecurePreferences.getFCMToken(context)
val lastUploadDate = TextSecurePreferences.getLastFCMUploadTime(context)
if (!force && token == oldToken && System.currentTimeMillis() - lastUploadDate < tokenExpirationInterval) { return }
val pnKey = 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
val sigData = "MONITOR${publicKey}${timestamp}1${namespaces.joinToString(separator = ",")}".encodeToByteArray()
val signature = ByteArray(Sign.BYTES)
sodium.cryptoSignDetached(signature, sigData, sigData.size.toLong(), userEd25519Key.secretKey.asBytes)
val requestParameters = SubscriptionRequest (
pubkey = publicKey,
session_ed25519 = userEd25519Key.publicKey.asHexString,
namespaces = listOf(Namespace.DEFAULT),
data = true, // only permit data subscription for now (?)
service = "firebase",
sig_ts = timestamp,
signature = Base64.encodeBytes(signature),
service_info = mapOf("token" to token),
enc_key = pnKey.asHexString,
)
val url = "${PushNotificationAPI.server}/subscribe"
val body = RequestBody.create(MediaType.get("application/json"), Json.encodeToString(requestParameters))
val request = Request.Builder().url(url).post(body)
retryIfNeeded(maxRetryCount) {
getResponseBody(request.build()).map { response ->
if (response.isSuccess()) {
TextSecurePreferences.setIsUsingFCM(context, true)
TextSecurePreferences.setFCMToken(context, token)
TextSecurePreferences.setLastFCMUploadTime(context, System.currentTimeMillis())
} else {
val (_, message) = response.errorInfo()
Log.d("Loki", "Couldn't register for FCM due to error: $message.")
}
}.fail { exception ->
Log.d("Loki", "Couldn't register for FCM due to error: ${exception}.")
}
}
}
private fun getResponseBody(request: Request): Promise<SubscriptionResponse, Exception> {
return OnionRequestAPI.sendOnionRequest(request,
PushNotificationAPI.server,
PushNotificationAPI.serverPublicKey, Version.V4).map { response ->
Json.decodeFromStream(response.body!!.inputStream())
}
}
}

View File

@@ -0,0 +1,21 @@
package org.thoughtcrime.securesms.notifications
import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import org.session.libsession.utilities.TextSecurePreferences
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object FirebasePushModule {
@Provides
@Singleton
fun provideFirebasePushManager(
@ApplicationContext context: Context,
prefs: TextSecurePreferences,
): PushManager = FirebasePushManager(context, prefs)
}