From a039c7db8fc7819af5f85e76a29898aa55a646ac Mon Sep 17 00:00:00 2001 From: Taksh Date: Fri, 4 Sep 2026 22:08:49 +0530 Subject: [PATCH 1/3] Reject odd-length and 0x-prefixed hex the way iOS does dataFromHexString used length/2, so an odd-length string silently dropped the trailing nibble. Match iOS Data(hexString:): trim, strip an optional 0x/0X prefix, reject odd length, and treat empty as empty bytes. --- .../com/bitchat/android/util/HexStringTest.kt | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 app/src/test/java/com/bitchat/android/util/HexStringTest.kt diff --git a/app/src/test/java/com/bitchat/android/util/HexStringTest.kt b/app/src/test/java/com/bitchat/android/util/HexStringTest.kt new file mode 100644 index 00000000..b5b449bc --- /dev/null +++ b/app/src/test/java/com/bitchat/android/util/HexStringTest.kt @@ -0,0 +1,58 @@ +package com.bitchat.android.util + +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +class HexStringTest { + + @Test + fun `decodes even-length lowercase hex`() { + assertArrayEquals( + byteArrayOf(0x0a, 0xbc.toByte()), + "0abc".dataFromHexString() + ) + } + + @Test + fun `rejects odd-length hex instead of truncating`() { + // Previously length/2 silently dropped the trailing nibble. + assertNull("abc".dataFromHexString()) + assertNull("0ab".dataFromHexString()) + } + + @Test + fun `strips optional 0x prefix like iOS`() { + assertArrayEquals( + byteArrayOf(0xde.toByte(), 0xad.toByte()), + "0xdead".dataFromHexString() + ) + assertArrayEquals( + byteArrayOf(0xbe.toByte(), 0xef.toByte()), + "0Xbeef".dataFromHexString() + ) + } + + @Test + fun `empty string and empty after prefix yield empty array`() { + assertArrayEquals(ByteArray(0), "".dataFromHexString()) + assertArrayEquals(ByteArray(0), "0x".dataFromHexString()) + assertTrue(" ".dataFromHexString()!!.isEmpty()) + } + + @Test + fun `rejects non-hex characters`() { + assertNull("zz".dataFromHexString()) + assertNull("0xgg".dataFromHexString()) + } + + @Test + fun `round-trips with hexEncodedString`() { + val original = byteArrayOf(0x00, 0x7f, 0xff.toByte()) + val encoded = original.hexEncodedString() + assertEquals("007fff", encoded) + assertArrayEquals(original, encoded.dataFromHexString()) + } +} From 53fdacca8336c08d348efcfe15b2089cb7123db4 Mon Sep 17 00:00:00 2001 From: Taksh Date: Fri, 4 Sep 2026 22:09:36 +0530 Subject: [PATCH 2/3] Apply the hex decoder changes covered by HexStringTest --- .../android/util/BinaryEncodingUtils.kt | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/util/BinaryEncodingUtils.kt b/app/src/main/java/com/bitchat/android/util/BinaryEncodingUtils.kt index 549f5971..d2637403 100644 --- a/app/src/main/java/com/bitchat/android/util/BinaryEncodingUtils.kt +++ b/app/src/main/java/com/bitchat/android/util/BinaryEncodingUtils.kt @@ -19,16 +19,27 @@ fun ByteArray.hexEncodedString(): String { } fun String.dataFromHexString(): ByteArray? { - val len = this.length / 2 - val data = ByteArray(len) - var index = 0 - - for (i in 0 until len) { - val hexByte = this.substring(i * 2, i * 2 + 2) - val byte = hexByte.toIntOrNull(16)?.toByte() ?: return null - data[index++] = byte + // Match iOS Data(hexString:): trim, optional 0x/0X prefix, reject odd length. + var hex = this.trim() + if (hex.startsWith("0x", ignoreCase = true)) { + hex = hex.substring(2) } - + if (hex.isEmpty()) { + return ByteArray(0) + } + if (hex.length % 2 != 0) { + return null + } + + val len = hex.length / 2 + val data = ByteArray(len) + + for (i in 0 until len) { + val hexByte = hex.substring(i * 2, i * 2 + 2) + val byte = hexByte.toIntOrNull(16)?.toByte() ?: return null + data[i] = byte + } + return data } From 3754d416d2ce070a1b6164b4626c0b2914323bb2 Mon Sep 17 00:00:00 2001 From: Taksh Date: Sat, 12 Sep 2026 12:21:04 +0530 Subject: [PATCH 3/3] Reject signed and non-ASCII hexadecimal byte chunks --- .../java/com/bitchat/android/util/BinaryEncodingUtils.kt | 3 ++- .../test/java/com/bitchat/android/util/HexStringTest.kt | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/bitchat/android/util/BinaryEncodingUtils.kt b/app/src/main/java/com/bitchat/android/util/BinaryEncodingUtils.kt index d2637403..06615332 100644 --- a/app/src/main/java/com/bitchat/android/util/BinaryEncodingUtils.kt +++ b/app/src/main/java/com/bitchat/android/util/BinaryEncodingUtils.kt @@ -27,7 +27,8 @@ fun String.dataFromHexString(): ByteArray? { if (hex.isEmpty()) { return ByteArray(0) } - if (hex.length % 2 != 0) { + // Radix parsing accepts signs and Unicode digits; encoded bytes require ASCII hex. + if (hex.length % 2 != 0 || hex.any { it !in '0'..'9' && it !in 'a'..'f' && it !in 'A'..'F' }) { return null } diff --git a/app/src/test/java/com/bitchat/android/util/HexStringTest.kt b/app/src/test/java/com/bitchat/android/util/HexStringTest.kt index b5b449bc..6472ede8 100644 --- a/app/src/test/java/com/bitchat/android/util/HexStringTest.kt +++ b/app/src/test/java/com/bitchat/android/util/HexStringTest.kt @@ -48,6 +48,13 @@ class HexStringTest { assertNull("0xgg".dataFromHexString()) } + @Test + fun `rejects signed byte chunks and non-ascii digits`() { + for (invalid in listOf("0x-1", "0x+1", "-1", "+1", "00-1", "0X+100", "01")) { + assertNull(invalid.dataFromHexString()) + } + } + @Test fun `round-trips with hexEncodedString`() { val original = byteArrayOf(0x00, 0x7f, 0xff.toByte())