diff --git a/app/src/main/java/com/bitchat/android/model/PeerCapabilities.kt b/app/src/main/java/com/bitchat/android/model/PeerCapabilities.kt index d53f3d20..e64fc390 100644 --- a/app/src/main/java/com/bitchat/android/model/PeerCapabilities.kt +++ b/app/src/main/java/com/bitchat/android/model/PeerCapabilities.kt @@ -29,9 +29,23 @@ data class PeerCapabilities(val rawValue: Long) : Parcelable { companion object { val NONE = PeerCapabilities(0) + val PREKEYS = PeerCapabilities(1L shl 0) + val WIFI_BULK = PeerCapabilities(1L shl 1) + val GATEWAY = PeerCapabilities(1L shl 2) + val GROUPS = PeerCapabilities(1L shl 3) + val BOARD = PeerCapabilities(1L shl 4) + val VOUCH = PeerCapabilities(1L shl 5) + val MESH_DIAGNOSTICS = PeerCapabilities(1L shl 6) + val BRIDGE = PeerCapabilities(1L shl 7) + /** Noise-encrypted private BitchatFilePacket using payload type 0x20. */ val PRIVATE_MEDIA = PeerCapabilities(1L shl 8) + val PRIVATE_MEDIA_RECEIPTS = PeerCapabilities(1L shl 9) + + /** Reserved by iOS; decode it but do not advertise or act on it. */ + val NON_DESTRUCTIVE_NOISE_REPLACEMENT = PeerCapabilities(1L shl 10) + /** Capabilities implemented by this Android build. */ val LOCAL_SUPPORTED = PRIVATE_MEDIA diff --git a/app/src/test/kotlin/com/bitchat/android/model/IdentityAnnouncementTest.kt b/app/src/test/kotlin/com/bitchat/android/model/IdentityAnnouncementTest.kt index 78d72bee..892f6565 100644 --- a/app/src/test/kotlin/com/bitchat/android/model/IdentityAnnouncementTest.kt +++ b/app/src/test/kotlin/com/bitchat/android/model/IdentityAnnouncementTest.kt @@ -12,9 +12,38 @@ class IdentityAnnouncementTest { private val signingKey = ByteArray(32) { 0x22 } @Test - fun `private media capability uses iOS little-endian bytes`() { + fun `capability assignments and encoding match iOS`() { + assertEquals(1L shl 0, PeerCapabilities.PREKEYS.rawValue) + assertEquals(1L shl 1, PeerCapabilities.WIFI_BULK.rawValue) + assertEquals(1L shl 2, PeerCapabilities.GATEWAY.rawValue) + assertEquals(1L shl 3, PeerCapabilities.GROUPS.rawValue) + assertEquals(1L shl 4, PeerCapabilities.BOARD.rawValue) + assertEquals(1L shl 5, PeerCapabilities.VOUCH.rawValue) + assertEquals(1L shl 6, PeerCapabilities.MESH_DIAGNOSTICS.rawValue) + assertEquals(1L shl 7, PeerCapabilities.BRIDGE.rawValue) + assertEquals(1L shl 8, PeerCapabilities.PRIVATE_MEDIA.rawValue) + assertEquals(1L shl 9, PeerCapabilities.PRIVATE_MEDIA_RECEIPTS.rawValue) + assertEquals(1L shl 10, PeerCapabilities.NON_DESTRUCTIVE_NOISE_REPLACEMENT.rawValue) + + val assigned = PeerCapabilities((1L shl 11) - 1) + assertArrayEquals(byteArrayOf(0xFF.toByte(), 0x07), assigned.encoded()) + assertEquals(assigned, PeerCapabilities.decode(assigned.encoded())) + } + + @Test + fun `capability encoding is minimal and preserves low 64 unknown bits`() { + assertArrayEquals(byteArrayOf(0x00), PeerCapabilities.NONE.encoded()) + assertArrayEquals(byteArrayOf(0x01), PeerCapabilities.PREKEYS.encoded()) assertArrayEquals(byteArrayOf(0x00, 0x01), PeerCapabilities.PRIVATE_MEDIA.encoded()) - assertTrue(PeerCapabilities.decode(byteArrayOf(0x00, 0x01)).contains(PeerCapabilities.PRIVATE_MEDIA)) + + val future = PeerCapabilities.decode( + byteArrayOf(0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80.toByte(), 0x55) + ) + assertEquals(Long.MIN_VALUE or 1L, future.rawValue) + assertArrayEquals( + byteArrayOf(0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80.toByte()), + future.encoded() + ) } @Test @@ -71,5 +100,6 @@ class IdentityAnnouncementTest { .capabilities!! .contains(PeerCapabilities.PRIVATE_MEDIA) ) + assertEquals(PeerCapabilities.PRIVATE_MEDIA, PeerCapabilities.LOCAL_SUPPORTED) } } diff --git a/docs/ANNOUNCEMENT_GOSSIP.md b/docs/ANNOUNCEMENT_GOSSIP.md index 25593c6f..5bad0711 100644 --- a/docs/ANNOUNCEMENT_GOSSIP.md +++ b/docs/ANNOUNCEMENT_GOSSIP.md @@ -29,9 +29,13 @@ Existing TLVs (unchanged): Other optional extension: -- `0x05` CAPABILITIES: Minimal little-endian feature bitfield. Bit 8 advertises - authenticated Noise private media (`PRIVATE_MEDIA_V1`); its exact value is - `00 01`. An empty value is valid and means no advertised capabilities. +- `0x05` CAPABILITIES: Minimal little-endian feature bitfield. Assigned bits + match iOS: 0 prekeys, 1 Wi-Fi bulk, 2 gateway, 3 private groups, 4 board, + 5 vouch, 6 mesh diagnostics, 7 bridge, 8 authenticated Noise private media, + 9 private-media receipts, and 10 reserved non-destructive Noise replacement. + Bit 8's exact value is `00 01`. An empty value is valid and means no + advertised capabilities. Unknown bits and bytes beyond the low 64 bits must + be tolerated for forward compatibility. New TLV (optional): @@ -69,6 +73,9 @@ This matches the on‑wire 8‑byte `senderID`/`recipientID` encoding used in th security-sensitive bit is trusted only after the signed announcement key is bound to the matching remote static key from a live Noise handshake; see `PRIVATE_MEDIA_V1.md`. +- Capability bits are independent discovery hints. Implementations advertise + only wire features they implement and must not infer support from platform, + app version, or unrelated transports with similar names. - If a `0x04` TLV is present: - Interpret the value as `N = length / 8` peer IDs (ignore trailing non‑aligned bytes). - Each 8‑byte chunk is decoded back to a 16‑hex‑char peer ID string (lowercase).