From d2e80c3157c368d1cd9c7bc330a6bb21f295ec6b Mon Sep 17 00:00:00 2001 From: 0x330a <92654767+0x330a@users.noreply.github.com> Date: Fri, 28 Oct 2022 15:17:18 +1100 Subject: [PATCH] feat: re-add bencode utility and fix tests to use bytearray instead of assuming utf-8 encoding for strings --- .../notifications/FirebasePushManager.kt | 36 ++++ .../notifications/FirebasePushModule.kt | 10 +- .../notifications/PushNotificationService.kt | 4 +- .../libsession/utilities/bencode/Bencode.kt | 169 ++++++++++++++++++ .../libsession/utilities/BencoderTest.kt | 107 +++++++++++ 5 files changed, 323 insertions(+), 3 deletions(-) create mode 100644 libsession/src/main/java/org/session/libsession/utilities/bencode/Bencode.kt create mode 100644 libsession/src/test/java/org/session/libsession/utilities/BencoderTest.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 0865c25c90..39705c75d6 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt @@ -19,11 +19,16 @@ 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.messaging.utilities.SodiumUtilities 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.libsession.utilities.bencode.Bencode +import org.session.libsession.utilities.bencode.BencodeDict +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.session.libsignal.utilities.Namespace @@ -55,6 +60,37 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS ) } + fun decrypt(encPayload: ByteArray) { + val encKey = getOrCreateNotificationKey() + val nonce = encPayload.take(AEAD.XCHACHA20POLY1305_IETF_NPUBBYTES).toByteArray() + val payload = encPayload.drop(AEAD.XCHACHA20POLY1305_IETF_NPUBBYTES).toByteArray() + val decrypted = SodiumUtilities.decrypt(payload, encKey.asBytes, nonce) + ?: return Log.e("Loki", "Failed to decrypt push notification") + val bencoded = Bencode.Decoder(decrypted) + val expectedList = (bencoded.decode() as? BencodeList) + ?: return Log.e("Loki", "Failed to decode bencoded list from payload") + + val (metadata, content) = expectedList.values + val metadataDict = (metadata as? BencodeDict)?.values + ?: return Log.e("Loki", "Failed to decode metadata dict") + + val push = """ + Push metadata received was: + @: ${metadataDict["@"]} + #: ${metadataDict["#"]} + n: ${metadataDict["n"]} + l: ${metadataDict["l"]} + B: ${metadataDict["B"]} + """.trimIndent() + + Log.d("Loki", "push") + + val contentBytes = (content as? BencodeString)?.value + ?: return Log.e("Loki", "Failed to decode content string") + + // TODO: something with contentBytes + } + override fun register(force: Boolean) { val currentInstanceIdJob = firebaseInstanceIdJob if (currentInstanceIdJob != null && currentInstanceIdJob.isActive && !force) return diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushModule.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushModule.kt index 983973d964..30045d6c25 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushModule.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushModule.kt @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.notifications import android.content.Context +import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -17,5 +18,12 @@ object FirebasePushModule { fun provideFirebasePushManager( @ApplicationContext context: Context, prefs: TextSecurePreferences, - ): PushManager = FirebasePushManager(context, prefs) + ) = FirebasePushManager(context, prefs) +} + +@Module +@InstallIn(SingletonComponent::class) +abstract class FirebaseBindingModule { + @Binds + abstract fun bindPushManager(firebasePushManager: FirebasePushManager): PushManager } \ No newline at end of file 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 6bf18131e9..4c268ce180 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt @@ -17,7 +17,7 @@ import javax.inject.Inject @AndroidEntryPoint class PushNotificationService : FirebaseMessagingService() { - @Inject lateinit var pushManager: PushManager + @Inject lateinit var pushManager: FirebasePushManager override fun onNewToken(token: String) { super.onNewToken(token) @@ -32,7 +32,7 @@ class PushNotificationService : FirebaseMessagingService() { // assume this is the new push notification content // deal with the enc payload (probably decrypting through the PushManager? Log.d("Loki", "TODO: deal with the enc_payload\n${message.data["enc_payload"]}") - pushManager.decrypt(message.data) + pushManager.decrypt(Base64.decode(message.data["enc_payload"])) return } val base64EncodedData = message.data?.get("ENCRYPTED_DATA") diff --git a/libsession/src/main/java/org/session/libsession/utilities/bencode/Bencode.kt b/libsession/src/main/java/org/session/libsession/utilities/bencode/Bencode.kt new file mode 100644 index 0000000000..427e80691a --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/utilities/bencode/Bencode.kt @@ -0,0 +1,169 @@ +package org.session.libsession.utilities.bencode + +import java.util.LinkedList + +object Bencode { + class Decoder(source: ByteArray) { + + private val iterator = LinkedList().apply { + addAll(source.asIterable()) + } + + /** + * Decode an element based on next marker assumed to be string/int/list/dict or return null + */ + fun decode(): BencodeElement? { + val result = when (iterator.peek()?.toInt()?.toChar()) { + in NUMBERS -> decodeString() + INT_INDICATOR -> decodeInt() + LIST_INDICATOR -> decodeList() + DICT_INDICATOR -> decodeDict() + else -> { + null + } + } + return result + } + + /** + * Decode a string element from iterator assumed to have structure `{length}:{data}` + */ + private fun decodeString(): BencodeString? { + val lengthStrings = buildString { + while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != SEPARATOR) { + append(iterator.pop().toInt().toChar()) + } + } + iterator.pop() // drop `:` + val length = lengthStrings.toIntOrNull(10) ?: return null + val remaining = (0 until length).map { iterator.pop() }.toByteArray() + return BencodeString(remaining) + } + + /** + * Decode an int element from iterator assumed to have structure `i{int}e` + */ + private fun decodeInt(): BencodeElement? { + iterator.pop() // drop `i` + val intString = buildString { + while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != END_INDICATOR) { + append(iterator.pop().toInt().toChar()) + } + } + val asInt = intString.toIntOrNull(10) ?: return null + iterator.pop() // drop `e` + return BencodeInteger(asInt) + } + + /** + * Decode a list element from iterator assumed to have structure `l{data}e` + */ + private fun decodeList(): BencodeElement { + iterator.pop() // drop `l` + val listElements = mutableListOf() + while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != END_INDICATOR) { + decode()?.let { nextElement -> + listElements += nextElement + } + } + iterator.pop() // drop `e` + return BencodeList(listElements) + } + + /** + * Decode a dict element from iterator assumed to have structure `d{data}e` + */ + private fun decodeDict(): BencodeElement? { + iterator.pop() // drop `d` + val dictElements = mutableMapOf() + while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != END_INDICATOR) { + val key = decodeString() ?: return null + val value = decode() ?: return null + dictElements += key.value.decodeToString() to value + } + iterator.pop() // drop `e` + return BencodeDict(dictElements) + } + + companion object { + private val NUMBERS = arrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9') + private const val INT_INDICATOR = 'i' + private const val LIST_INDICATOR = 'l' + private const val DICT_INDICATOR = 'd' + private const val END_INDICATOR = 'e' + private const val SEPARATOR = ':' + } + + } + +} + +sealed class BencodeElement { + abstract fun encode(): ByteArray +} + +fun String.bencode() = BencodeString(this.encodeToByteArray()) +fun Int.bencode() = BencodeInteger(this) + +data class BencodeString(val value: ByteArray): BencodeElement() { + override fun encode(): ByteArray = buildString { + append(value.size.toString()) + append(':') + }.toByteArray() + value + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as BencodeString + + if (!value.contentEquals(other.value)) return false + + return true + } + + override fun hashCode(): Int { + return value.contentHashCode() + } +} +data class BencodeInteger(val value: Int): BencodeElement() { + override fun encode(): ByteArray = buildString { + append('i') + append(value.toString()) + append('e') + }.toByteArray() +} +data class BencodeList(val values: List): BencodeElement() { + + constructor(vararg values: BencodeElement) : this(values.toList()) + + override fun encode(): ByteArray = "l".toByteArray() + + values.fold(byteArrayOf()) { array, element -> array + element.encode() } + + "e".toByteArray() +} +data class BencodeDict(val values: Map): BencodeElement() { + + constructor(vararg values: Pair) : this(values.toMap()) + + override fun encode(): ByteArray = "d".toByteArray() + + values.entries.fold(byteArrayOf()) { array, (key, value) -> + array + key.bencode().encode() + value.encode() + } + "e".toByteArray() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as BencodeDict + + if (values != other.values) return false + + return true + } + + override fun hashCode(): Int { + return values.hashCode() + } + + +} \ No newline at end of file diff --git a/libsession/src/test/java/org/session/libsession/utilities/BencoderTest.kt b/libsession/src/test/java/org/session/libsession/utilities/BencoderTest.kt new file mode 100644 index 0000000000..d96fa6658f --- /dev/null +++ b/libsession/src/test/java/org/session/libsession/utilities/BencoderTest.kt @@ -0,0 +1,107 @@ +package org.session.libsession.utilities + +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Test +import org.session.libsession.utilities.bencode.Bencode +import org.session.libsession.utilities.bencode.BencodeDict +import org.session.libsession.utilities.bencode.BencodeInteger +import org.session.libsession.utilities.bencode.BencodeList +import org.session.libsession.utilities.bencode.bencode + +class BencoderTest { + + @Test + fun `it should decode a basic string`() { + val basicString = "5:howdy".toByteArray() + val bencoder = Bencode.Decoder(basicString) + val result = bencoder.decode() + assertEquals("howdy".bencode(), result) + } + + @Test + fun `it should decode a basic integer`() { + val basicInteger = "i3e".toByteArray() + val bencoder = Bencode.Decoder(basicInteger) + val result = bencoder.decode() + assertEquals(BencodeInteger(3), result) + } + + @Test + fun `it should decode a list of integers`() { + val basicIntList = "li1ei2ee".toByteArray() + val bencoder = Bencode.Decoder(basicIntList) + val result = bencoder.decode() + assertEquals( + BencodeList( + 1.bencode(), + 2.bencode() + ), + result + ) + } + + @Test + fun `it should decode a basic dict`() { + val basicDict = "d4:spaml1:a1:bee".toByteArray() + val bencoder = Bencode.Decoder(basicDict) + val result = bencoder.decode() + assertEquals( + BencodeDict( + "spam" to BencodeList( + "a".bencode(), + "b".bencode() + ) + ), + result + ) + } + + @Test + fun `it should encode a basic string`() { + val basicString = "5:howdy".toByteArray() + val element = "howdy".bencode() + assertArrayEquals(basicString, element.encode()) + } + + @Test + fun `it should encode a basic int`() { + val basicInt = "i3e".toByteArray() + val element = 3.bencode() + assertArrayEquals(basicInt, element.encode()) + } + + @Test + fun `it should encode a basic list`() { + val basicList = "li1ei2ee".toByteArray() + val element = BencodeList(1.bencode(),2.bencode()) + assertArrayEquals(basicList, element.encode()) + } + + @Test + fun `it should encode a basic dict`() { + val basicDict = "d4:spaml1:a1:bee".toByteArray() + val element = BencodeDict( + "spam" to BencodeList( + "a".bencode(), + "b".bencode() + ) + ) + assertArrayEquals(basicDict, element.encode()) + } + + @Test + fun `it should encode a more complex real world case`() { + val source = "d15:lastReadMessaged66:031122334455667788990011223344556677889900112233445566778899001122i1234568790e66:051122334455667788990011223344556677889900112233445566778899001122i1234568790ee5:seqNoi1ee".toByteArray() + val result = Bencode.Decoder(source).decode() + val expected = BencodeDict( + "lastReadMessage" to BencodeDict( + "051122334455667788990011223344556677889900112233445566778899001122" to 1234568790.bencode(), + "031122334455667788990011223344556677889900112233445566778899001122" to 1234568790.bencode() + ), + "seqNo" to BencodeInteger(1) + ) + assertEquals(expected, result) + } + +} \ No newline at end of file