From ba6eca2443f06291a3d91e8df90cd65338020f34 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 9 Jun 2023 09:52:11 +0930 Subject: [PATCH] Add FirebasePushManager#unregister --- .../securesms/notifications/PushManager.kt | 1 - .../notifications/FirebasePushManager.kt | 113 +++++++++++------- .../notifications/NoOpPushManager.kt | 6 +- .../sending_receiving/notifications/Models.kt | 26 ++++ .../utilities/TextSecurePreferences.kt | 2 +- 5 files changed, 100 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushManager.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushManager.kt index 36aca1c912..9d4ef23fe9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushManager.kt @@ -2,5 +2,4 @@ package org.thoughtcrime.securesms.notifications interface PushManager { fun register(force: Boolean) - fun unregister(token: String) } \ No newline at end of file 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 12e62cee45..4f9a6292f3 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt @@ -1,6 +1,8 @@ package org.thoughtcrime.securesms.notifications import android.content.Context +import com.google.android.gms.tasks.Task +import com.google.firebase.iid.InstanceIdResult import com.goterl.lazysodium.LazySodiumAndroid import com.goterl.lazysodium.SodiumAndroid import com.goterl.lazysodium.interfaces.AEAD @@ -21,6 +23,8 @@ import org.session.libsession.messaging.sending_receiving.notifications.PushNoti import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationMetadata import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionRequest import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionResponse +import org.session.libsession.messaging.sending_receiving.notifications.UnsubscribeResponse +import org.session.libsession.messaging.sending_receiving.notifications.UnsubscriptionRequest import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.SnodeAPI @@ -74,7 +78,7 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS val metadataJson = (expectedList[0] as? BencodeString)?.value ?: error("no metadata") - val metadata:PushNotificationMetadata = Json.decodeFromString(String(metadataJson)) + 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 @@ -90,41 +94,71 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS } override fun register(force: Boolean) { - val currentInstanceIdJob = firebaseInstanceIdJob - if (currentInstanceIdJob != null && currentInstanceIdJob.isActive && !force) return - - if (force && currentInstanceIdJob != null) { - currentInstanceIdJob.cancel(null) + firebaseInstanceIdJob?.apply { + if (force) cancel() else if (isActive) return } - 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) + firebaseInstanceIdJob = getFcmInstanceId { register(it, force) } + } + + private fun register(task: Task, force: Boolean) { + // context in here is Dispatchers.IO + if (!task.isSuccessful) { + Log.w( + "Loki", + "FirebaseInstanceId.getInstance().getInstanceId() failed." + task.exception + ) + return + } + + val token: String = task.result?.token ?: return + val userPublicKey = getLocalNumber(context) ?: return + val userEdKey = KeyPairUtilities.getUserED25519KeyPair(context) ?: return + + when { + prefs.isUsingFCM() -> register(token, userPublicKey, userEdKey, force) + prefs.getFCMToken() != null -> unregister(token, userPublicKey, userEdKey) + } + } + + private fun unregister(token: String, userPublicKey: String, userEdKey: KeyPair) { + 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 = "UNSUBSCRIBE${userPublicKey}${timestamp}".encodeToByteArray() + val signature = ByteArray(Sign.BYTES) + sodium.cryptoSignDetached(signature, sigData, sigData.size.toLong(), userEdKey.secretKey.asBytes) + + val requestParameters = UnsubscriptionRequest( + pubkey = userPublicKey, + session_ed25519 = userEdKey.publicKey.asHexString, + service = "firebase", + sig_ts = timestamp, + signature = Base64.encodeBytes(signature), + service_info = mapOf("token" to token), + ) + + val url = "${PushNotificationAPI.server}/unsubscribe" + 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.success == true) { + TextSecurePreferences.setIsUsingFCM(context, false) + TextSecurePreferences.setFCMToken(context, null) + Log.d("Loki", "Unsubscribe FCM success") + } else { + Log.e("Loki", "Couldn't unregister for FCM due to error: ${response.message}") + } + }.fail { exception -> + Log.e("Loki", "Couldn't unregister for FCM due to error: ${exception}.", exception) } } } - override fun unregister(token: String) { - TODO("Not yet implemented") - } - - fun register(token: String, publicKey: String, userEd25519Key: KeyPair, force: Boolean, namespaces: List = listOf(Namespace.DEFAULT)) { + private fun register(token: String, publicKey: String, userEd25519Key: KeyPair, force: Boolean, namespaces: List = listOf(Namespace.DEFAULT)) { val oldToken = TextSecurePreferences.getFCMToken(context) val lastUploadDate = TextSecurePreferences.getLastFCMUploadTime(context) - if (!force && token == oldToken && System.currentTimeMillis() - lastUploadDate < tokenExpirationInterval) { return } + if (!force && token == oldToken && System.currentTimeMillis() - lastUploadDate < tokenExpirationInterval) return val pnKey = getOrCreateNotificationKey() @@ -133,7 +167,7 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS 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 ( + val requestParameters = SubscriptionRequest( pubkey = publicKey, session_ed25519 = userEd25519Key.publicKey.asHexString, namespaces = listOf(Namespace.DEFAULT), @@ -149,9 +183,8 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS 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 -> + getResponseBody(request.build()).map { response -> if (response.isSuccess()) { - TextSecurePreferences.setIsUsingFCM(context, true) TextSecurePreferences.setFCMToken(context, token) TextSecurePreferences.setLastFCMUploadTime(context, System.currentTimeMillis()) } else { @@ -159,18 +192,16 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS Log.e("Loki", "Couldn't register for FCM due to error: $message.") } }.fail { exception -> - Log.e("Loki", "Couldn't register for FCM due to error: ${exception}.") + Log.e("Loki", "Couldn't register for FCM due to error: ${exception}.", exception) } } } - private fun getResponseBody(request: Request): Promise { - return OnionRequestAPI.sendOnionRequest(request, + private inline fun getResponseBody(request: Request): Promise = + OnionRequestAPI.sendOnionRequest( + request, PushNotificationAPI.server, - PushNotificationAPI.serverPublicKey, Version.V4).map { response -> - Json.decodeFromStream(response.body!!.inputStream()) - } - } - - -} \ No newline at end of file + PushNotificationAPI.serverPublicKey, + Version.V4 + ).map { response -> Json.decodeFromStream(response.body!!.inputStream()) } +} diff --git a/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushManager.kt b/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushManager.kt index edbf7d710d..59f1b9df4b 100644 --- a/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushManager.kt +++ b/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushManager.kt @@ -7,8 +7,4 @@ class NoOpPushManager: PushManager { override fun register(force: Boolean) { Log.d("NoOpPushManager", "Push notifications not supported, not registering for push notifications") } - - override fun unregister(token: String) { - Log.d("NoOpPushManager", "Push notifications not supported, not unregistering for push notifications") - } -} \ No newline at end of file +} diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Models.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Models.kt index e7a0ad79a2..141202ef66 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Models.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Models.kt @@ -36,6 +36,24 @@ data class SubscriptionRequest( val enc_key: String ) +@Serializable +data class UnsubscriptionRequest( + /** the 33-byte account being subscribed to; typically a session ID */ + val pubkey: String, + /** when the pubkey starts with 05 (i.e. a session ID) this is the ed25519 32-byte pubkey associated with the session ID */ + val session_ed25519: String?, + /** 32-byte swarm authentication subkey; omitted (or null) when not using subkey auth (new closed groups) */ + val subkey_tag: String? = null, + /** the signature unix timestamp in seconds, not ms */ + val sig_ts: Long, + /** the 64-byte ed25519 signature */ + val signature: String, + /** the string identifying the notification service, "firebase" for android (currently) */ + val service: String, + /** dict of service-specific data, currently just "token" field with device-specific token but different services might have other requirements */ + val service_info: Map, +) + @Serializable data class SubscriptionResponse( val error: Int? = null, @@ -60,6 +78,14 @@ data class SubscriptionResponse( } else null to null } +@Serializable +data class UnsubscribeResponse( + val error: Int? = null, + val message: String? = null, + val success: Boolean? = null, + val removed: Boolean? = null, +) + @Serializable data class PushNotificationMetadata( /** Account ID (such as Session ID or closed group ID) where the message arrived **/ diff --git a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt index 1f05e4f135..b1ab1090f7 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt @@ -318,7 +318,7 @@ interface TextSecurePreferences { } @JvmStatic - fun setFCMToken(context: Context, value: String) { + fun setFCMToken(context: Context, value: String?) { setStringPreference(context, FCM_TOKEN, value) }