mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 17:45:21 +00:00
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>
This commit is contained in:
@@ -221,6 +221,13 @@ private struct ContentPeopleListView: View {
|
||||
}
|
||||
)
|
||||
} else {
|
||||
GroupChatList(
|
||||
groups: peerListModel.groupRows,
|
||||
onTapGroup: { peerID in
|
||||
peerListModel.startConversation(with: peerID)
|
||||
showSidebar = true
|
||||
}
|
||||
)
|
||||
MeshPeerList(
|
||||
onTapPeer: { peerID in
|
||||
peerListModel.startConversation(with: peerID)
|
||||
@@ -455,6 +462,10 @@ private struct ContentPrivateChatSheetView: View {
|
||||
}
|
||||
|
||||
private var privacyCaptionText: String {
|
||||
// Group chats are ChaCha20-Poly1305 sealed to the roster's shared key.
|
||||
if privateConversationModel.selectedPeerID?.isGroup == true {
|
||||
return String(localized: "content.private.caption_group", comment: "Caption above the group chat composer noting messages are encrypted to group members")
|
||||
}
|
||||
// Geohash DMs are NIP-17 gift-wrapped — always end-to-end encrypted,
|
||||
// even though they carry no Noise session status. Mesh DMs earn the
|
||||
// "encrypted" claim only once the Noise handshake has secured.
|
||||
@@ -503,30 +514,39 @@ private struct ContentPrivateHeaderInfoButton: View {
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
// A group has no single fingerprint to show.
|
||||
guard !headerState.isGroupConversation else { return }
|
||||
appChromeModel.showFingerprint(for: headerState.headerPeerID)
|
||||
}) {
|
||||
HStack(spacing: 6) {
|
||||
switch headerState.availability {
|
||||
case .bluetoothConnected:
|
||||
Image(systemName: "dot.radiowaves.left.and.right")
|
||||
if headerState.isGroupConversation {
|
||||
Image(systemName: "person.3.fill")
|
||||
.font(.bitchatSystem(size: 14))
|
||||
.foregroundColor(palette.primary)
|
||||
.accessibilityLabel(String(localized: "content.accessibility.connected_mesh", comment: "Accessibility label for mesh-connected peer indicator"))
|
||||
case .meshReachable:
|
||||
Image(systemName: "point.3.filled.connected.trianglepath.dotted")
|
||||
.font(.bitchatSystem(size: 14))
|
||||
.foregroundColor(palette.primary)
|
||||
.accessibilityLabel(String(localized: "content.accessibility.reachable_mesh", comment: "Accessibility label for mesh-reachable peer indicator"))
|
||||
case .nostrAvailable:
|
||||
Image(systemName: "globe")
|
||||
.font(.bitchatSystem(size: 14))
|
||||
.foregroundColor(.purple)
|
||||
.accessibilityLabel(String(localized: "content.accessibility.available_nostr", comment: "Accessibility label for Nostr-available peer indicator"))
|
||||
case .offline:
|
||||
// Absence of a glyph was the only offline signal; say it.
|
||||
Text("mesh_peers.state.offline")
|
||||
.bitchatFont(size: 11)
|
||||
.foregroundColor(palette.secondary)
|
||||
.accessibilityLabel(String(localized: "content.accessibility.group_chat", comment: "Accessibility label for the group chat indicator"))
|
||||
} else {
|
||||
switch headerState.availability {
|
||||
case .bluetoothConnected:
|
||||
Image(systemName: "dot.radiowaves.left.and.right")
|
||||
.font(.bitchatSystem(size: 14))
|
||||
.foregroundColor(palette.primary)
|
||||
.accessibilityLabel(String(localized: "content.accessibility.connected_mesh", comment: "Accessibility label for mesh-connected peer indicator"))
|
||||
case .meshReachable:
|
||||
Image(systemName: "point.3.filled.connected.trianglepath.dotted")
|
||||
.font(.bitchatSystem(size: 14))
|
||||
.foregroundColor(palette.primary)
|
||||
.accessibilityLabel(String(localized: "content.accessibility.reachable_mesh", comment: "Accessibility label for mesh-reachable peer indicator"))
|
||||
case .nostrAvailable:
|
||||
Image(systemName: "globe")
|
||||
.font(.bitchatSystem(size: 14))
|
||||
.foregroundColor(.purple)
|
||||
.accessibilityLabel(String(localized: "content.accessibility.available_nostr", comment: "Accessibility label for Nostr-available peer indicator"))
|
||||
case .offline:
|
||||
// Absence of a glyph was the only offline signal; say it.
|
||||
Text("mesh_peers.state.offline")
|
||||
.bitchatFont(size: 11)
|
||||
.foregroundColor(palette.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Text(headerState.displayName)
|
||||
@@ -571,7 +591,9 @@ private struct ContentPrivateHeaderInfoButton: View {
|
||||
)
|
||||
)
|
||||
.accessibilityHint(
|
||||
String(localized: "content.accessibility.view_fingerprint_hint", comment: "Accessibility hint for viewing encryption fingerprint")
|
||||
headerState.isGroupConversation
|
||||
? ""
|
||||
: String(localized: "content.accessibility.view_fingerprint_hint", comment: "Accessibility hint for viewing encryption fingerprint")
|
||||
)
|
||||
.frame(minHeight: headerHeight)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user