mirror of
https://github.com/oxen-io/session-android.git
synced 2025-05-21 19:28:22 +00:00
New SPNS subscription and notifications
Finishes the WIP for subscribing to push notifications and handling the new-style pushes we get.
This commit is contained in:
parent
d2e80c3157
commit
46acd7878d
@ -8,6 +8,7 @@ import com.goterl.lazysodium.interfaces.Sign
|
|||||||
import com.goterl.lazysodium.utils.Key
|
import com.goterl.lazysodium.utils.Key
|
||||||
import com.goterl.lazysodium.utils.KeyPair
|
import com.goterl.lazysodium.utils.KeyPair
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.decodeFromStream
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
@ -17,6 +18,7 @@ import okhttp3.MediaType
|
|||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||||
|
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.SubscriptionRequest
|
||||||
import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionResponse
|
import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionResponse
|
||||||
import org.session.libsession.messaging.utilities.SodiumUtilities
|
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||||
@ -26,7 +28,6 @@ import org.session.libsession.snode.Version
|
|||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber
|
import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber
|
||||||
import org.session.libsession.utilities.bencode.Bencode
|
import org.session.libsession.utilities.bencode.Bencode
|
||||||
import org.session.libsession.utilities.bencode.BencodeDict
|
|
||||||
import org.session.libsession.utilities.bencode.BencodeList
|
import org.session.libsession.utilities.bencode.BencodeList
|
||||||
import org.session.libsession.utilities.bencode.BencodeString
|
import org.session.libsession.utilities.bencode.BencodeString
|
||||||
import org.session.libsignal.utilities.Base64
|
import org.session.libsignal.utilities.Base64
|
||||||
@ -60,35 +61,32 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun decrypt(encPayload: ByteArray) {
|
fun decrypt(encPayload: ByteArray): ByteArray? {
|
||||||
val encKey = getOrCreateNotificationKey()
|
val encKey = getOrCreateNotificationKey()
|
||||||
val nonce = encPayload.take(AEAD.XCHACHA20POLY1305_IETF_NPUBBYTES).toByteArray()
|
val nonce = encPayload.take(AEAD.XCHACHA20POLY1305_IETF_NPUBBYTES).toByteArray()
|
||||||
val payload = encPayload.drop(AEAD.XCHACHA20POLY1305_IETF_NPUBBYTES).toByteArray()
|
val payload = encPayload.drop(AEAD.XCHACHA20POLY1305_IETF_NPUBBYTES).toByteArray()
|
||||||
val decrypted = SodiumUtilities.decrypt(payload, encKey.asBytes, nonce)
|
val padded = SodiumUtilities.decrypt(payload, encKey.asBytes, nonce)
|
||||||
?: return Log.e("Loki", "Failed to decrypt push notification")
|
?: error("Failed to decrypt push notification")
|
||||||
|
val decrypted = padded.dropLastWhile { it.toInt() == 0 }.toByteArray()
|
||||||
val bencoded = Bencode.Decoder(decrypted)
|
val bencoded = Bencode.Decoder(decrypted)
|
||||||
val expectedList = (bencoded.decode() as? BencodeList)
|
val expectedList = (bencoded.decode() as? BencodeList)?.values
|
||||||
?: return Log.e("Loki", "Failed to decode bencoded list from payload")
|
?: error("Failed to decode bencoded list from payload")
|
||||||
|
|
||||||
val (metadata, content) = expectedList.values
|
val metadataJson = (expectedList[0] as? BencodeString)?.value
|
||||||
val metadataDict = (metadata as? BencodeDict)?.values
|
?: error("no metadata")
|
||||||
?: return Log.e("Loki", "Failed to decode metadata dict")
|
val metadata:PushNotificationMetadata = Json.decodeFromString(String(metadataJson))
|
||||||
|
|
||||||
val push = """
|
val content: ByteArray? = if (expectedList.size >= 2) (expectedList[1] as? BencodeString)?.value else null
|
||||||
Push metadata received was:
|
// null content is valid only if we got a "data_too_long" flag
|
||||||
@: ${metadataDict["@"]}
|
if (content == null)
|
||||||
#: ${metadataDict["#"]}
|
check(metadata.data_too_long) { "missing message data, but no too-long flag" }
|
||||||
n: ${metadataDict["n"]}
|
else
|
||||||
l: ${metadataDict["l"]}
|
check(metadata.data_len == content.size) { "wrong message data size" }
|
||||||
B: ${metadataDict["B"]}
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
Log.d("Loki", "push")
|
Log.d("Loki",
|
||||||
|
"Received push for ${metadata.account}/${metadata.namespace}, msg ${metadata.msg_hash}, ${metadata.data_len}B")
|
||||||
|
|
||||||
val contentBytes = (content as? BencodeString)?.value
|
return content
|
||||||
?: return Log.e("Loki", "Failed to decode content string")
|
|
||||||
|
|
||||||
// TODO: something with contentBytes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun register(force: Boolean) {
|
override fun register(force: Boolean) {
|
||||||
@ -158,10 +156,10 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS
|
|||||||
TextSecurePreferences.setLastFCMUploadTime(context, System.currentTimeMillis())
|
TextSecurePreferences.setLastFCMUploadTime(context, System.currentTimeMillis())
|
||||||
} else {
|
} else {
|
||||||
val (_, message) = response.errorInfo()
|
val (_, message) = response.errorInfo()
|
||||||
Log.d("Loki", "Couldn't register for FCM due to error: $message.")
|
Log.e("Loki", "Couldn't register for FCM due to error: $message.")
|
||||||
}
|
}
|
||||||
}.fail { exception ->
|
}.fail { exception ->
|
||||||
Log.d("Loki", "Couldn't register for FCM due to error: ${exception}.")
|
Log.e("Loki", "Couldn't register for FCM due to error: ${exception}.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,15 +28,19 @@ class PushNotificationService : FirebaseMessagingService() {
|
|||||||
|
|
||||||
override fun onMessageReceived(message: RemoteMessage) {
|
override fun onMessageReceived(message: RemoteMessage) {
|
||||||
Log.d("Loki", "Received a push notification.")
|
Log.d("Loki", "Received a push notification.")
|
||||||
if (message.data.containsKey("spns")) {
|
val data: ByteArray? = if (message.data.containsKey("spns")) {
|
||||||
// assume this is the new push notification content
|
// this is a v2 push notification
|
||||||
// deal with the enc payload (probably decrypting through the PushManager?
|
try {
|
||||||
Log.d("Loki", "TODO: deal with the enc_payload\n${message.data["enc_payload"]}")
|
|
||||||
pushManager.decrypt(Base64.decode(message.data["enc_payload"]))
|
pushManager.decrypt(Base64.decode(message.data["enc_payload"]))
|
||||||
|
} catch(e: Exception) {
|
||||||
|
Log.e("Loki", "Invalid push notification: ${e.message}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// old v1 push notification; we still need this for receiving legacy closed group notifications
|
||||||
val base64EncodedData = message.data?.get("ENCRYPTED_DATA")
|
val base64EncodedData = message.data?.get("ENCRYPTED_DATA")
|
||||||
val data = base64EncodedData?.let { Base64.decode(it) }
|
base64EncodedData?.let { Base64.decode(it) }
|
||||||
|
}
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
try {
|
try {
|
||||||
val envelopeAsData = MessageWrapper.unwrap(data).toByteArray()
|
val envelopeAsData = MessageWrapper.unwrap(data).toByteArray()
|
||||||
|
@ -7,7 +7,8 @@ import kotlinx.serialization.Serializable
|
|||||||
/**
|
/**
|
||||||
* N.B. all of these variable names will be named the same as the actual JSON utf-8 request/responses expected from the server.
|
* N.B. all of these variable names will be named the same as the actual JSON utf-8 request/responses expected from the server.
|
||||||
* Changing the variable names will break how data is serialized/deserialized.
|
* Changing the variable names will break how data is serialized/deserialized.
|
||||||
* If it's less than ideally named we can use [SerialName]
|
* If it's less than ideally named we can use [SerialName], such as for the push metadata which uses
|
||||||
|
* single-letter keys to be as compact as possible.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@ -37,11 +38,11 @@ data class SubscriptionRequest(
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class SubscriptionResponse(
|
data class SubscriptionResponse(
|
||||||
val error: Int?,
|
val error: Int? = null,
|
||||||
val message: String?,
|
val message: String? = null,
|
||||||
val success: Boolean?,
|
val success: Boolean? = null,
|
||||||
val added: Boolean?,
|
val added: Boolean? = null,
|
||||||
val updated: Boolean?,
|
val updated: Boolean? = null,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
/** invalid values, missing reuqired arguments etc, details in message */
|
/** invalid values, missing reuqired arguments etc, details in message */
|
||||||
@ -59,6 +60,32 @@ data class SubscriptionResponse(
|
|||||||
} else null to null
|
} else null to null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PushNotificationMetadata(
|
||||||
|
/** Account ID (such as Session ID or closed group ID) where the message arrived **/
|
||||||
|
@SerialName("@")
|
||||||
|
val account: String,
|
||||||
|
|
||||||
|
/** The hash of the message in the swarm. */
|
||||||
|
@SerialName("#")
|
||||||
|
val msg_hash: String,
|
||||||
|
|
||||||
|
/** The swarm namespace in which this message arrived. */
|
||||||
|
@SerialName("n")
|
||||||
|
val namespace: Int,
|
||||||
|
|
||||||
|
/** The length of the message data. This is always included, even if the message content
|
||||||
|
* itself was too large to fit into the push notification. */
|
||||||
|
@SerialName("l")
|
||||||
|
val data_len: Int,
|
||||||
|
|
||||||
|
/** This will be true if the data was omitted because it was too long to fit in a push
|
||||||
|
* notification (around 2.5kB of raw data), in which case the push notification includes
|
||||||
|
* only this metadata but not the message content itself. */
|
||||||
|
@SerialName("B")
|
||||||
|
val data_too_long : Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PushNotificationServerObject(
|
data class PushNotificationServerObject(
|
||||||
val enc_payload: String,
|
val enc_payload: String,
|
||||||
|
@ -17,7 +17,7 @@ import org.session.libsignal.utilities.retryIfNeeded
|
|||||||
object PushNotificationAPI {
|
object PushNotificationAPI {
|
||||||
val context = MessagingModuleConfiguration.shared.context
|
val context = MessagingModuleConfiguration.shared.context
|
||||||
val server = "https://push.getsession.org"
|
val server = "https://push.getsession.org"
|
||||||
val serverPublicKey: String = TODO("get the new server pubkey here")
|
val serverPublicKey: String = "d7557fe563e2610de876c0ac7341b62f3c82d5eea4b62c702392ea4368f51b3b"
|
||||||
private val legacyServer = "https://live.apns.getsession.org"
|
private val legacyServer = "https://live.apns.getsession.org"
|
||||||
private val legacyServerPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049"
|
private val legacyServerPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049"
|
||||||
private val maxRetryCount = 4
|
private val maxRetryCount = 4
|
||||||
|
@ -205,7 +205,7 @@ object SodiumUtilities {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun decrypt(ciphertext: ByteArray, decryptionKey: ByteArray, nonce: ByteArray): ByteArray? {
|
fun decrypt(ciphertext: ByteArray, decryptionKey: ByteArray, nonce: ByteArray): ByteArray? {
|
||||||
val plaintextSize = ciphertext.size - AEAD.CHACHA20POLY1305_ABYTES
|
val plaintextSize = ciphertext.size - AEAD.XCHACHA20POLY1305_IETF_ABYTES
|
||||||
val plaintext = ByteArray(plaintextSize)
|
val plaintext = ByteArray(plaintextSize)
|
||||||
return if (sodium.cryptoAeadXChaCha20Poly1305IetfDecrypt(
|
return if (sodium.cryptoAeadXChaCha20Poly1305IetfDecrypt(
|
||||||
plaintext,
|
plaintext,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user