mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 23:45:18 +00:00
* Add capability bits to announce TLV Announces now carry an optional capabilities TLV (0x05): a little-endian bitfield with named bits for upcoming features (prekeys, wifiBulk, gateway, groups, board, vouch, meshDiagnostics). Old clients skip the unknown TLV; peers without it decode as nil so features can distinguish "legacy peer" from "advertises nothing". PeerCapabilities lives in BitFoundation with a minimal-length encoding that preserves unknown bits for forward compatibility. Peer capabilities are stored in the BLE peer registry on verified announce and exposed via BLEService.peerCapabilities(_:). The local advertisement set is empty until each feature ships its bit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Private groups: creator-managed encrypted group chat over the mesh Small encrypted crews (hard cap 16) between public broadcast and 1:1 DMs: Protocol - MessageType.groupMessage = 0x25: broadcast packets with a cleartext 16-byte group ID + epoch, ChaCha20-Poly1305 ciphertext (epoch bound as AEAD AAD), inner Ed25519 sender signature over "bitchat-group-msg-v1"|groupID|messageID|timestamp|content - NoisePayloadType.groupInvite = 0x06 / .groupKeyUpdate = 0x07: creator-signed group state (key, epoch, roster) 1:1 over Noise; signature over "bitchat-group-v1"|groupID|epoch|key-hash|roster-hash and the Noise session peer must BE the creator - SyncTypeFlags bit 10 (groupMessage): variable-length LE bitfield widens 1 -> 2 bytes inside the length-prefixed REQUEST_SYNC TLV; old clients ignore unknown bits and answer with types they know - PeerCapabilities.localSupported now advertises .groups Storage - GroupStore: symmetric keys in the keychain, roster/name/epoch as protected JSON in Application Support; wiped in panicClearAllData() Behavior - Non-members relay 0x25 like any broadcast but cannot read it; group messages join gossip-sync backfill with the public-message window - Receivers drop wrong-epoch envelopes, bad sender signatures, and senders missing from the creator-signed roster - Fire-and-flood delivery (no per-member acks in v1) UI - Groups open as chat windows through the private-chat sheet (virtual "group_" peer IDs); groups section in the people sheet; /group create/invite/remove/leave/list commands; invitees get a system message + notification and the group appears in their people sheet Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Private groups: fix TLV truncation, roster downgrade, removal notice, block, media, signable bytes Addresses the Codex review and adversarial-review findings on #1383: - TLV encoding now throws GroupTLVError.valueTooLong instead of clamping to 65535 and truncating, so an oversize group message fails to seal and surfaces send_failed rather than shipping ciphertext recipients drop. - Roster nicknames truncate on a Character boundary (never mid-scalar), so a multi-byte nickname can no longer make the whole signed roster undecodable. - Invites now bump the epoch (rotate the key) like removals, giving every roster change a strictly-increasing epoch so out-of-order invite states no longer last-writer-wins a just-added member back out. - Removing a member now sends them a creator-signed roster-without-them under a throwaway all-zero key (never the rotated key), so their client deactivates the group and surfaces "removed" instead of going silently dark. - /block is enforced in the group receive path: a blocked member's messages are dropped from display and notifications, consistent with every other inbound path. - Media affordances are disabled in group chats (both computed sites) so the composer can't strand a media placeholder that never sends; media-in-groups is a documented v2 item. - Creator signature now covers the group name and the sender signature covers the epoch (wire-format-affecting; needs Android parity before ship). - Explicit isGroup guard in markPrivateMessagesAsRead so read/delivered receipts can never leak into group conversations under a future refactor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
80 lines
3.3 KiB
Swift
80 lines
3.3 KiB
Swift
//
|
|
// SyncTypeFlagsBoardTests.swift
|
|
// bitchatTests
|
|
//
|
|
// This is free and unencumbered software released into the public domain.
|
|
// For more information, see <https://unlicense.org>
|
|
//
|
|
|
|
import BitFoundation
|
|
import Foundation
|
|
import Testing
|
|
@testable import bitchat
|
|
|
|
/// The board sync flag is the first bit outside the original single byte of
|
|
/// type flags. These tests pin down the wire compatibility contract: the
|
|
/// types TLV has been a variable-length (1-8 byte) little-endian bitfield
|
|
/// since type-aware sync, so widening to two bytes must decode everywhere
|
|
/// and unknown bits must be ignored, not rejected.
|
|
struct SyncTypeFlagsBoardTests {
|
|
|
|
@Test func boardFlagEncodesIntoSecondByte() throws {
|
|
let data = try #require(SyncTypeFlags.board.toData())
|
|
// Little-endian: low byte first, board bit (bit 8) in byte 2.
|
|
#expect(data == Data([0x00, 0x01]))
|
|
}
|
|
|
|
@Test func boardFlagRoundTrips() throws {
|
|
let flags = SyncTypeFlags(messageTypes: [.message, .boardPost])
|
|
let data = try #require(flags.toData())
|
|
let decoded = try #require(SyncTypeFlags.decode(data))
|
|
#expect(decoded.contains(.message))
|
|
#expect(decoded.contains(.boardPost))
|
|
#expect(!decoded.contains(.fragment))
|
|
#expect(Set(decoded.toMessageTypes()) == Set([.message, .boardPost]))
|
|
}
|
|
|
|
/// An old decoder is modeled by bits it has no mapping for: the shared
|
|
/// decode path accepts the bytes and simply maps unknown bits to no
|
|
/// message type, so a board-only request reads as "nothing I can serve".
|
|
@Test func unknownBitsDecodeToNoTypes() throws {
|
|
// Bits 11-15 are unassigned (bit 8 = board, bit 9 = prekeyBundle,
|
|
// bit 10 = groupMessage); a future (or unknown) two-byte bitfield must
|
|
// decode without error and yield no known types.
|
|
let decoded = try #require(SyncTypeFlags.decode(Data([0x00, 0xF8])))
|
|
#expect(decoded.toMessageTypes().isEmpty)
|
|
for type in [MessageType.announce, .message, .fragment, .fileTransfer, .boardPost, .prekeyBundle, .groupMessage] {
|
|
#expect(!decoded.contains(type))
|
|
}
|
|
}
|
|
|
|
@Test func mixedKnownAndUnknownBitsKeepKnownTypes() throws {
|
|
// Known low-byte flags survive alongside unknown high bits (11-15).
|
|
let decoded = try #require(SyncTypeFlags.decode(Data([0x03, 0xF8])))
|
|
#expect(decoded.contains(.announce))
|
|
#expect(decoded.contains(.message))
|
|
#expect(Set(decoded.toMessageTypes()) == Set([.announce, .message]))
|
|
}
|
|
|
|
@Test func requestSyncPacketRoundTripsBoardFlag() throws {
|
|
let request = RequestSyncPacket(
|
|
p: 4,
|
|
m: 128,
|
|
data: Data([0xAB, 0xCD]),
|
|
types: SyncTypeFlags(messageTypes: [.boardPost])
|
|
)
|
|
let decoded = try #require(RequestSyncPacket.decode(from: request.encode()))
|
|
let types = try #require(decoded.types)
|
|
#expect(types.contains(.boardPost))
|
|
#expect(!types.contains(.message))
|
|
}
|
|
|
|
@Test func singleByteLegacyEncodingStillDecodes() throws {
|
|
// Requests from old clients keep the one-byte bitfield.
|
|
let decoded = try #require(SyncTypeFlags.decode(Data([0x03])))
|
|
#expect(decoded.contains(.announce))
|
|
#expect(decoded.contains(.message))
|
|
#expect(!decoded.contains(.boardPost))
|
|
}
|
|
}
|