mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-09-19 04:59:59 +00:00
Merge cc17c00282b77d512cb66d9493de571df20e4b2a into c127eb83ab94c069c32d37530d2faecd381cd2a8
This commit is contained in:
commit
44b128b1fc
@ -78,7 +78,7 @@ class NostrDirectMessageHandler(
|
||||
if (!content.startsWith("bitchat1:")) return@launch
|
||||
|
||||
val base64Content = content.removePrefix("bitchat1:")
|
||||
val packetData = base64URLDecode(base64Content) ?: return@launch
|
||||
val packetData = NostrEmbeddedPacketDecoder.decodeBounded(base64Content) ?: return@launch
|
||||
val packet = BitchatPacket.fromBinaryData(packetData) ?: return@launch
|
||||
|
||||
if (packet.type != com.bitchat.android.protocol.MessageType.NOISE_ENCRYPTED.value) return@launch
|
||||
@ -304,19 +304,4 @@ class NostrDirectMessageHandler(
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun base64URLDecode(input: String): ByteArray? {
|
||||
return try {
|
||||
val padded = input.replace("-", "+")
|
||||
.replace("_", "/")
|
||||
.let { str ->
|
||||
val padding = (4 - str.length % 4) % 4
|
||||
str + "=".repeat(padding)
|
||||
}
|
||||
android.util.Base64.decode(padded, android.util.Base64.DEFAULT)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to decode base64url: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,50 @@
|
||||
package com.bitchat.android.nostr
|
||||
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import com.bitchat.android.util.AppConstants
|
||||
|
||||
/**
|
||||
* Decode a `bitchat1:` base64url payload with an explicit size ceiling.
|
||||
*
|
||||
* iOS rejects oversized embedded packets before allocating
|
||||
* (`NostrInboundPipeline.decodeEmbeddedBitChatPacket`). Without a bound,
|
||||
* a malicious gift wrap can force an arbitrarily large Base64 decode.
|
||||
*/
|
||||
object NostrEmbeddedPacketDecoder {
|
||||
private const val TAG = "NostrEmbeddedPacketDecoder"
|
||||
|
||||
fun decodeBounded(
|
||||
base64Url: String,
|
||||
maxBytes: Int = AppConstants.Protocol.MAX_PAYLOAD_LENGTH
|
||||
): ByteArray? {
|
||||
if (maxBytes <= 0) return null
|
||||
// Base64 expands 3 bytes -> 4 chars; reject oversized encodings first.
|
||||
val maxEncoded = ((maxBytes.toLong() + 2L) / 3L) * 4L
|
||||
if (base64Url.length.toLong() > maxEncoded) {
|
||||
Log.w(TAG, "Rejecting embedded packet encoding longer than $maxEncoded chars")
|
||||
return null
|
||||
}
|
||||
val decoded = base64URLDecode(base64Url) ?: return null
|
||||
if (decoded.size > maxBytes) {
|
||||
Log.w(TAG, "Rejecting embedded packet of ${decoded.size} bytes (max $maxBytes)")
|
||||
return null
|
||||
}
|
||||
return decoded
|
||||
}
|
||||
|
||||
internal fun base64URLDecode(input: String): ByteArray? {
|
||||
return try {
|
||||
val padded = input.replace("-", "+")
|
||||
.replace("_", "/")
|
||||
.let { str ->
|
||||
val padding = (4 - str.length % 4) % 4
|
||||
str + "=".repeat(padding)
|
||||
}
|
||||
Base64.decode(padded, Base64.DEFAULT)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to decode base64url: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
package com.bitchat.android.nostr
|
||||
|
||||
import android.util.Base64
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import android.os.Build
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [Build.VERSION_CODES.P], manifest = Config.NONE)
|
||||
class NostrEmbeddedPacketDecoderTest {
|
||||
|
||||
@Test
|
||||
fun `decodes a small base64url payload`() {
|
||||
val raw = byteArrayOf(1, 2, 3, 4)
|
||||
val encoded = Base64.encodeToString(raw, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
|
||||
.replace("=", "")
|
||||
assertArrayEquals(raw, NostrEmbeddedPacketDecoder.decodeBounded(encoded, maxBytes = 64))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rejects an encoding that cannot fit under the byte ceiling`() {
|
||||
val maxBytes = 16
|
||||
// 4 chars encode at most 3 bytes; 24 chars would decode to ~18 bytes.
|
||||
val oversizedEncoding = "A".repeat(24)
|
||||
assertNull(NostrEmbeddedPacketDecoder.decodeBounded(oversizedEncoding, maxBytes = maxBytes))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rejects a decode that expands past the byte ceiling`() {
|
||||
val maxBytes = 8
|
||||
// 12 chars of valid base64url decode to 9 bytes.
|
||||
val nineBytes = ByteArray(9) { 0x41 }
|
||||
val encoded = Base64.encodeToString(nineBytes, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
|
||||
.replace("=", "")
|
||||
assertNull(NostrEmbeddedPacketDecoder.decodeBounded(encoded, maxBytes = maxBytes))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `accepts a payload that fills the ceiling exactly`() {
|
||||
val maxBytes = 9
|
||||
val nineBytes = ByteArray(9) { 0x42 }
|
||||
val encoded = Base64.encodeToString(nineBytes, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
|
||||
.replace("=", "")
|
||||
assertNotNull(NostrEmbeddedPacketDecoder.decodeBounded(encoded, maxBytes = maxBytes))
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user