feat: re-add bencode utility and fix tests to use bytearray instead of assuming utf-8 encoding for strings

This commit is contained in:
0x330a 2022-10-28 15:17:18 +11:00
parent 7762d534bb
commit d2e80c3157
No known key found for this signature in database
GPG Key ID: 267811D6E6A2698C
5 changed files with 323 additions and 3 deletions

View File

@ -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

View File

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

View File

@ -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")

View File

@ -0,0 +1,169 @@
package org.session.libsession.utilities.bencode
import java.util.LinkedList
object Bencode {
class Decoder(source: ByteArray) {
private val iterator = LinkedList<Byte>().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<BencodeElement>()
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<String,BencodeElement>()
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>): 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<String, BencodeElement>): BencodeElement() {
constructor(vararg values: Pair<String, BencodeElement>) : 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()
}
}

View File

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