mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 23:45:18 +00:00
Implement password-protected rooms with encryption
- Room creators can set/remove passwords for their rooms - Passwords are used to derive encryption keys via PBKDF2 - All messages in password-protected rooms are encrypted with AES-GCM - Room protection status is announced to all peers - Password and room data persists across app launches - Visual lock indicators for password-protected rooms - Password prompts when joining protected rooms - Only room creators can modify password settings - Encrypted messages show placeholder text if password is unknown
This commit is contained in:
@@ -180,14 +180,14 @@ extension BitchatMessage {
|
||||
var data = Data()
|
||||
|
||||
// Message format:
|
||||
// - Flags: 1 byte (bit 0: isRelay, bit 1: isPrivate, bit 2: hasOriginalSender, bit 3: hasRecipientNickname, bit 4: hasSenderPeerID, bit 5: hasMentions, bit 6: hasVoiceNote)
|
||||
// - Flags: 1 byte (bit 0: isRelay, bit 1: isPrivate, bit 2: hasOriginalSender, bit 3: hasRecipientNickname, bit 4: hasSenderPeerID, bit 5: hasMentions, bit 6: hasRoom, bit 7: isEncrypted)
|
||||
// - Timestamp: 8 bytes (seconds since epoch)
|
||||
// - ID length: 1 byte
|
||||
// - ID: variable
|
||||
// - Sender length: 1 byte
|
||||
// - Sender: variable
|
||||
// - Content length: 2 bytes
|
||||
// - Content: variable
|
||||
// - Content: variable (or encrypted content if isEncrypted)
|
||||
// Optional fields based on flags:
|
||||
// - Original sender length + data
|
||||
// - Recipient nickname length + data
|
||||
@@ -203,6 +203,7 @@ extension BitchatMessage {
|
||||
if senderPeerID != nil { flags |= 0x10 }
|
||||
if mentions != nil && !mentions!.isEmpty { flags |= 0x20 }
|
||||
if room != nil { flags |= 0x40 }
|
||||
if isEncrypted { flags |= 0x80 }
|
||||
|
||||
data.append(flags)
|
||||
|
||||
@@ -229,8 +230,14 @@ extension BitchatMessage {
|
||||
data.append(0)
|
||||
}
|
||||
|
||||
// Content
|
||||
if let contentData = content.data(using: .utf8) {
|
||||
// Content or encrypted content
|
||||
if isEncrypted, let encryptedContent = encryptedContent {
|
||||
let length = UInt16(min(encryptedContent.count, 65535))
|
||||
// Encode length as 2 bytes, big-endian
|
||||
data.append(UInt8((length >> 8) & 0xFF))
|
||||
data.append(UInt8(length & 0xFF))
|
||||
data.append(encryptedContent.prefix(Int(length)))
|
||||
} else if let contentData = content.data(using: .utf8) {
|
||||
let length = UInt16(min(contentData.count, 65535))
|
||||
// Encode length as 2 bytes, big-endian
|
||||
data.append(UInt8((length >> 8) & 0xFF))
|
||||
@@ -301,6 +308,7 @@ extension BitchatMessage {
|
||||
let hasSenderPeerID = (flags & 0x10) != 0
|
||||
let hasMentions = (flags & 0x20) != 0
|
||||
let hasRoom = (flags & 0x40) != 0
|
||||
let isEncrypted = (flags & 0x80) != 0
|
||||
|
||||
// Timestamp
|
||||
guard offset + 8 <= dataCopy.count else {
|
||||
@@ -347,7 +355,19 @@ extension BitchatMessage {
|
||||
guard offset + contentLength <= dataCopy.count else {
|
||||
return nil
|
||||
}
|
||||
let content = String(data: dataCopy[offset..<offset+contentLength], encoding: .utf8) ?? ""
|
||||
|
||||
let content: String
|
||||
let encryptedContent: Data?
|
||||
|
||||
if isEncrypted {
|
||||
// Content is encrypted, store as Data
|
||||
encryptedContent = dataCopy[offset..<offset+contentLength]
|
||||
content = "" // Empty placeholder
|
||||
} else {
|
||||
// Normal string content
|
||||
content = String(data: dataCopy[offset..<offset+contentLength], encoding: .utf8) ?? ""
|
||||
encryptedContent = nil
|
||||
}
|
||||
offset += contentLength
|
||||
|
||||
// Optional fields
|
||||
@@ -418,7 +438,9 @@ extension BitchatMessage {
|
||||
recipientNickname: recipientNickname,
|
||||
senderPeerID: senderPeerID,
|
||||
mentions: mentions,
|
||||
room: room
|
||||
room: room,
|
||||
encryptedContent: encryptedContent,
|
||||
isEncrypted: isEncrypted
|
||||
)
|
||||
return message
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ enum MessageType: UInt8 {
|
||||
case fragmentStart = 0x05
|
||||
case fragmentContinue = 0x06
|
||||
case fragmentEnd = 0x07
|
||||
case roomAnnounce = 0x08 // Announce password-protected room status
|
||||
}
|
||||
|
||||
// Special recipient ID for broadcast messages
|
||||
@@ -134,8 +135,10 @@ struct BitchatMessage: Codable, Equatable {
|
||||
let senderPeerID: String?
|
||||
let mentions: [String]? // Array of mentioned nicknames
|
||||
let room: String? // Room hashtag (e.g., "#general")
|
||||
let encryptedContent: Data? // For password-protected rooms
|
||||
let isEncrypted: Bool // Flag to indicate if content is encrypted
|
||||
|
||||
init(sender: String, content: String, timestamp: Date, isRelay: Bool, originalSender: String? = nil, isPrivate: Bool = false, recipientNickname: String? = nil, senderPeerID: String? = nil, mentions: [String]? = nil, room: String? = nil) {
|
||||
init(sender: String, content: String, timestamp: Date, isRelay: Bool, originalSender: String? = nil, isPrivate: Bool = false, recipientNickname: String? = nil, senderPeerID: String? = nil, mentions: [String]? = nil, room: String? = nil, encryptedContent: Data? = nil, isEncrypted: Bool = false) {
|
||||
self.id = UUID().uuidString
|
||||
self.sender = sender
|
||||
self.content = content
|
||||
@@ -147,6 +150,8 @@ struct BitchatMessage: Codable, Equatable {
|
||||
self.senderPeerID = senderPeerID
|
||||
self.mentions = mentions
|
||||
self.room = room
|
||||
self.encryptedContent = encryptedContent
|
||||
self.isEncrypted = isEncrypted
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,6 +161,8 @@ protocol BitchatDelegate: AnyObject {
|
||||
func didDisconnectFromPeer(_ peerID: String)
|
||||
func didUpdatePeerList(_ peers: [String])
|
||||
func didReceiveRoomLeave(_ room: String, from peerID: String)
|
||||
func didReceivePasswordProtectedRoomAnnouncement(_ room: String, isProtected: Bool, creatorID: String?)
|
||||
func decryptRoomMessage(_ encryptedContent: Data, room: String) -> String?
|
||||
|
||||
// Optional method to check if a fingerprint belongs to a favorite peer
|
||||
func isFavorite(fingerprint: String) -> Bool
|
||||
@@ -170,4 +177,13 @@ extension BitchatDelegate {
|
||||
func didReceiveRoomLeave(_ room: String, from peerID: String) {
|
||||
// Default empty implementation
|
||||
}
|
||||
|
||||
func didReceivePasswordProtectedRoomAnnouncement(_ room: String, isProtected: Bool, creatorID: String?) {
|
||||
// Default empty implementation
|
||||
}
|
||||
|
||||
func decryptRoomMessage(_ encryptedContent: Data, room: String) -> String? {
|
||||
// Default returns nil (unable to decrypt)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -685,6 +685,88 @@ class BluetoothMeshService: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
func announcePasswordProtectedRoom(_ room: String, isProtected: Bool = true, creatorID: String? = nil) {
|
||||
messageQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
// Payload format: room|isProtected|creatorID
|
||||
let protectedFlag = isProtected ? "1" : "0"
|
||||
let creator = creatorID ?? self.myPeerID
|
||||
let payload = "\(room)|\(protectedFlag)|\(creator)"
|
||||
|
||||
let packet = BitchatPacket(
|
||||
type: MessageType.roomAnnounce.rawValue,
|
||||
senderID: Data(self.myPeerID.utf8),
|
||||
recipientID: SpecialRecipients.broadcast,
|
||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||
payload: Data(payload.utf8),
|
||||
signature: nil,
|
||||
ttl: 5 // Allow wider propagation for room announcements
|
||||
)
|
||||
|
||||
bitchatLog("Announcing room \(room) protection status: \(isProtected)", category: "room")
|
||||
self.broadcastPacket(packet)
|
||||
}
|
||||
}
|
||||
|
||||
func sendEncryptedRoomMessage(_ content: String, mentions: [String], room: String, roomKey: SymmetricKey) {
|
||||
messageQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
let nickname = self.delegate as? ChatViewModel
|
||||
let senderNick = nickname?.nickname ?? self.myPeerID
|
||||
|
||||
// Encrypt the content
|
||||
guard let contentData = content.data(using: .utf8) else { return }
|
||||
|
||||
do {
|
||||
let sealedBox = try AES.GCM.seal(contentData, using: roomKey)
|
||||
let encryptedData = sealedBox.combined!
|
||||
|
||||
// Create message with encrypted content
|
||||
let message = BitchatMessage(
|
||||
sender: senderNick,
|
||||
content: "", // Empty placeholder since actual content is encrypted
|
||||
timestamp: Date(),
|
||||
isRelay: false,
|
||||
originalSender: nil,
|
||||
isPrivate: false,
|
||||
recipientNickname: nil,
|
||||
senderPeerID: self.myPeerID,
|
||||
mentions: mentions.isEmpty ? nil : mentions,
|
||||
room: room,
|
||||
encryptedContent: encryptedData,
|
||||
isEncrypted: true
|
||||
)
|
||||
|
||||
if let messageData = message.toBinaryPayload() {
|
||||
// Sign the message payload
|
||||
let signature: Data?
|
||||
do {
|
||||
signature = try self.encryptionService.sign(messageData)
|
||||
} catch {
|
||||
signature = nil
|
||||
}
|
||||
|
||||
let packet = BitchatPacket(
|
||||
type: MessageType.message.rawValue,
|
||||
senderID: Data(self.myPeerID.utf8),
|
||||
recipientID: SpecialRecipients.broadcast,
|
||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||
payload: messageData,
|
||||
signature: signature,
|
||||
ttl: self.adaptiveTTL
|
||||
)
|
||||
|
||||
self.broadcastPacket(packet)
|
||||
bitchatLog("Sent encrypted message to room \(room)", category: "room")
|
||||
}
|
||||
} catch {
|
||||
bitchatLog("Failed to encrypt room message: \(error)", category: "crypto")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func sendAnnouncementToPeer(_ peerID: String) {
|
||||
guard let vm = delegate as? ChatViewModel else { return }
|
||||
|
||||
@@ -1224,9 +1306,21 @@ class BluetoothMeshService: NSObject {
|
||||
peerNicknames[senderID] = message.sender
|
||||
peerNicknamesLock.unlock()
|
||||
|
||||
// Handle encrypted room messages
|
||||
var finalContent = message.content
|
||||
if message.isEncrypted, let room = message.room, let encryptedData = message.encryptedContent {
|
||||
// Try to decrypt the content
|
||||
if let decryptedContent = self.delegate?.decryptRoomMessage(encryptedData, room: room) {
|
||||
finalContent = decryptedContent
|
||||
} else {
|
||||
// Unable to decrypt - show placeholder
|
||||
finalContent = "[Encrypted message - password required]"
|
||||
}
|
||||
}
|
||||
|
||||
let messageWithPeerID = BitchatMessage(
|
||||
sender: message.sender,
|
||||
content: message.content,
|
||||
content: finalContent,
|
||||
timestamp: message.timestamp,
|
||||
isRelay: message.isRelay,
|
||||
originalSender: message.originalSender,
|
||||
@@ -1234,7 +1328,9 @@ class BluetoothMeshService: NSObject {
|
||||
recipientNickname: nil,
|
||||
senderPeerID: senderID,
|
||||
mentions: message.mentions,
|
||||
room: message.room
|
||||
room: message.room,
|
||||
encryptedContent: message.encryptedContent,
|
||||
isEncrypted: message.isEncrypted
|
||||
)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
@@ -1664,6 +1760,30 @@ class BluetoothMeshService: NSObject {
|
||||
self.broadcastPacket(relayPacket)
|
||||
}
|
||||
|
||||
case .roomAnnounce:
|
||||
if let payloadStr = String(data: packet.payload, encoding: .utf8) {
|
||||
// Parse payload: room|isProtected|creatorID
|
||||
let components = payloadStr.split(separator: "|").map(String.init)
|
||||
if components.count >= 3 {
|
||||
let room = components[0]
|
||||
let isProtected = components[1] == "1"
|
||||
let creatorID = components[2]
|
||||
|
||||
bitchatLog("Received room announcement: \(room) is \(isProtected ? "protected" : "public") by \(creatorID)", category: "room")
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.didReceivePasswordProtectedRoomAnnouncement(room, isProtected: isProtected, creatorID: creatorID)
|
||||
}
|
||||
|
||||
// Relay announcement
|
||||
if packet.ttl > 1 {
|
||||
var relayPacket = packet
|
||||
relayPacket.ttl -= 1
|
||||
self.broadcastPacket(relayPacket)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import Foundation
|
||||
import SwiftUI
|
||||
import Combine
|
||||
import CryptoKit
|
||||
import CommonCrypto
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
#endif
|
||||
@@ -40,12 +41,21 @@ class ChatViewModel: ObservableObject {
|
||||
@Published var roomMessages: [String: [BitchatMessage]] = [:] // room -> messages
|
||||
@Published var unreadRoomMessages: [String: Int] = [:] // room -> unread count
|
||||
@Published var roomMembers: [String: Set<String>] = [:] // room -> set of peer IDs who have sent messages
|
||||
@Published var roomPasswords: [String: String] = [:] // room -> password (stored locally only)
|
||||
@Published var roomKeys: [String: SymmetricKey] = [:] // room -> derived encryption key
|
||||
@Published var passwordProtectedRooms: Set<String> = [] // Set of rooms that require passwords
|
||||
@Published var roomCreators: [String: String] = [:] // room -> creator peerID
|
||||
@Published var showPasswordPrompt: Bool = false
|
||||
@Published var passwordPromptRoom: String? = nil
|
||||
|
||||
let meshService = BluetoothMeshService()
|
||||
private let userDefaults = UserDefaults.standard
|
||||
private let nicknameKey = "bitchat.nickname"
|
||||
private let favoritesKey = "bitchat.favorites"
|
||||
private let joinedRoomsKey = "bitchat.joinedRooms"
|
||||
private let passwordProtectedRoomsKey = "bitchat.passwordProtectedRooms"
|
||||
private let roomCreatorsKey = "bitchat.roomCreators"
|
||||
private let roomPasswordsKey = "bitchat.roomPasswords"
|
||||
private var nicknameSaveTimer: Timer?
|
||||
|
||||
@Published var favoritePeers: Set<String> = [] // Now stores public key fingerprints instead of peer IDs
|
||||
@@ -57,6 +67,7 @@ class ChatViewModel: ObservableObject {
|
||||
loadNickname()
|
||||
loadFavorites()
|
||||
loadJoinedRooms()
|
||||
loadRoomData()
|
||||
meshService.delegate = self
|
||||
|
||||
// Log startup info
|
||||
@@ -109,9 +120,53 @@ class ChatViewModel: ObservableObject {
|
||||
userDefaults.synchronize()
|
||||
}
|
||||
|
||||
func joinRoom(_ room: String) {
|
||||
private func loadRoomData() {
|
||||
// Load password protected rooms
|
||||
if let savedProtectedRooms = userDefaults.stringArray(forKey: passwordProtectedRoomsKey) {
|
||||
passwordProtectedRooms = Set(savedProtectedRooms)
|
||||
}
|
||||
|
||||
// Load room creators
|
||||
if let savedCreators = userDefaults.dictionary(forKey: roomCreatorsKey) as? [String: String] {
|
||||
roomCreators = savedCreators
|
||||
}
|
||||
|
||||
// Load room passwords (encrypted would be better, but keeping simple for now)
|
||||
if let savedPasswords = userDefaults.dictionary(forKey: roomPasswordsKey) as? [String: String] {
|
||||
roomPasswords = savedPasswords
|
||||
// Derive keys for all saved passwords
|
||||
for (room, password) in savedPasswords {
|
||||
roomKeys[room] = deriveRoomKey(from: password, roomName: room)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func saveRoomData() {
|
||||
userDefaults.set(Array(passwordProtectedRooms), forKey: passwordProtectedRoomsKey)
|
||||
userDefaults.set(roomCreators, forKey: roomCreatorsKey)
|
||||
userDefaults.set(roomPasswords, forKey: roomPasswordsKey)
|
||||
userDefaults.synchronize()
|
||||
}
|
||||
|
||||
func joinRoom(_ room: String, password: String? = nil) {
|
||||
// Ensure room starts with #
|
||||
let roomTag = room.hasPrefix("#") ? room : "#\(room)"
|
||||
|
||||
// If room is password protected and we don't have the key yet
|
||||
if passwordProtectedRooms.contains(roomTag) && roomKeys[roomTag] == nil {
|
||||
if let password = password {
|
||||
// Derive key from password
|
||||
let key = deriveRoomKey(from: password, roomName: roomTag)
|
||||
roomKeys[roomTag] = key
|
||||
roomPasswords[roomTag] = password // Store for convenience (local only)
|
||||
} else {
|
||||
// Show password prompt
|
||||
passwordPromptRoom = roomTag
|
||||
showPasswordPrompt = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
joinedRooms.insert(roomTag)
|
||||
saveJoinedRooms()
|
||||
|
||||
@@ -144,6 +199,87 @@ class ChatViewModel: ObservableObject {
|
||||
unreadRoomMessages.removeValue(forKey: room)
|
||||
roomMessages.removeValue(forKey: room)
|
||||
roomMembers.removeValue(forKey: room)
|
||||
roomKeys.removeValue(forKey: room)
|
||||
roomPasswords.removeValue(forKey: room)
|
||||
}
|
||||
|
||||
// Password management
|
||||
func setRoomPassword(_ password: String, for room: String) {
|
||||
guard joinedRooms.contains(room) else { return }
|
||||
|
||||
// If room has no creator yet, make current user the creator
|
||||
if roomCreators[room] == nil {
|
||||
roomCreators[room] = meshService.myPeerID
|
||||
}
|
||||
|
||||
// Only room creator can set password
|
||||
guard roomCreators[room] == meshService.myPeerID else {
|
||||
bitchatLog("Only room creator can set password", category: "room")
|
||||
return
|
||||
}
|
||||
|
||||
// Derive encryption key from password
|
||||
let key = deriveRoomKey(from: password, roomName: room)
|
||||
roomKeys[room] = key
|
||||
roomPasswords[room] = password
|
||||
passwordProtectedRooms.insert(room)
|
||||
|
||||
// Save room data
|
||||
saveRoomData()
|
||||
|
||||
// Announce that this room is now password protected
|
||||
meshService.announcePasswordProtectedRoom(room, creatorID: meshService.myPeerID)
|
||||
|
||||
bitchatLog("Set password for room \(room)", category: "room")
|
||||
}
|
||||
|
||||
func removeRoomPassword(for room: String) {
|
||||
// Only room creator can remove password
|
||||
guard roomCreators[room] == meshService.myPeerID else {
|
||||
bitchatLog("Only room creator can remove password", category: "room")
|
||||
return
|
||||
}
|
||||
|
||||
roomKeys.removeValue(forKey: room)
|
||||
roomPasswords.removeValue(forKey: room)
|
||||
passwordProtectedRooms.remove(room)
|
||||
|
||||
// Save room data
|
||||
saveRoomData()
|
||||
|
||||
// Announce that this room is no longer password protected
|
||||
meshService.announcePasswordProtectedRoom(room, isProtected: false, creatorID: meshService.myPeerID)
|
||||
|
||||
bitchatLog("Removed password for room \(room)", category: "room")
|
||||
}
|
||||
|
||||
private func deriveRoomKey(from password: String, roomName: String) -> SymmetricKey {
|
||||
// Use PBKDF2 to derive a key from the password
|
||||
let salt = roomName.data(using: .utf8)! // Use room name as salt for consistency
|
||||
let keyData = pbkdf2(password: password, salt: salt, iterations: 100000, keyLength: 32)
|
||||
return SymmetricKey(data: keyData)
|
||||
}
|
||||
|
||||
private func pbkdf2(password: String, salt: Data, iterations: Int, keyLength: Int) -> Data {
|
||||
var derivedKey = Data(count: keyLength)
|
||||
let passwordData = password.data(using: .utf8)!
|
||||
|
||||
derivedKey.withUnsafeMutableBytes { derivedKeyBytes in
|
||||
salt.withUnsafeBytes { saltBytes in
|
||||
passwordData.withUnsafeBytes { passwordBytes in
|
||||
CCKeyDerivationPBKDF(
|
||||
CCPBKDFAlgorithm(kCCPBKDF2),
|
||||
passwordBytes.baseAddress, passwordData.count,
|
||||
saltBytes.baseAddress, salt.count,
|
||||
CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256),
|
||||
UInt32(iterations),
|
||||
derivedKeyBytes.baseAddress, keyLength
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return derivedKey
|
||||
}
|
||||
|
||||
func switchToRoom(_ room: String?) {
|
||||
@@ -271,8 +407,14 @@ class ChatViewModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
// Send via mesh with mentions and room
|
||||
meshService.sendMessage(content, mentions: mentions, room: messageRoom)
|
||||
// Check if room is password protected and encrypt if needed
|
||||
if let room = messageRoom, roomKeys[room] != nil {
|
||||
// Send encrypted room message
|
||||
meshService.sendEncryptedRoomMessage(content, mentions: mentions, room: room, roomKey: roomKeys[room]!)
|
||||
} else {
|
||||
// Send via mesh with mentions and room (unencrypted)
|
||||
meshService.sendMessage(content, mentions: mentions, room: messageRoom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -644,6 +786,41 @@ extension ChatViewModel: BitchatDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func didReceivePasswordProtectedRoomAnnouncement(_ room: String, isProtected: Bool, creatorID: String?) {
|
||||
if isProtected {
|
||||
passwordProtectedRooms.insert(room)
|
||||
if let creator = creatorID {
|
||||
roomCreators[room] = creator
|
||||
}
|
||||
} else {
|
||||
passwordProtectedRooms.remove(room)
|
||||
// If we're in this room and it's no longer protected, clear the key
|
||||
roomKeys.removeValue(forKey: room)
|
||||
roomPasswords.removeValue(forKey: room)
|
||||
}
|
||||
|
||||
// Save updated room data
|
||||
saveRoomData()
|
||||
|
||||
bitchatLog("Room \(room) is now \(isProtected ? "password protected" : "public")", category: "room")
|
||||
}
|
||||
|
||||
func decryptRoomMessage(_ encryptedContent: Data, room: String) -> String? {
|
||||
guard let roomKey = roomKeys[room] else {
|
||||
bitchatLog("No key available for encrypted room \(room)", category: "room")
|
||||
return nil
|
||||
}
|
||||
|
||||
do {
|
||||
let sealedBox = try AES.GCM.SealedBox(combined: encryptedContent)
|
||||
let decryptedData = try AES.GCM.open(sealedBox, using: roomKey)
|
||||
return String(data: decryptedData, encoding: .utf8)
|
||||
} catch {
|
||||
bitchatLog("Failed to decrypt room message: \(error)", category: "crypto")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func didReceiveMessage(_ message: BitchatMessage) {
|
||||
bitchatLog("Received message from \(message.sender), room: \(message.room ?? "nil"), senderPeerID: \(message.senderPeerID ?? "nil")", category: "message")
|
||||
|
||||
|
||||
@@ -18,6 +18,11 @@ struct ContentView: View {
|
||||
@State private var showSidebar = false
|
||||
@State private var sidebarDragOffset: CGFloat = 0
|
||||
@State private var showAppInfo = false
|
||||
@State private var showPasswordInput = false
|
||||
@State private var passwordInputRoom: String? = nil
|
||||
@State private var passwordInput = ""
|
||||
@State private var showPasswordPrompt = false
|
||||
@State private var passwordPromptInput = ""
|
||||
|
||||
private var backgroundColor: Color {
|
||||
colorScheme == .dark ? Color.black : Color.white
|
||||
@@ -160,6 +165,40 @@ struct ContentView: View {
|
||||
.sheet(isPresented: $showAppInfo) {
|
||||
AppInfoView()
|
||||
}
|
||||
.alert("Set Room Password", isPresented: $showPasswordInput) {
|
||||
SecureField("Password", text: $passwordInput)
|
||||
Button("Cancel", role: .cancel) {
|
||||
passwordInput = ""
|
||||
passwordInputRoom = nil
|
||||
}
|
||||
Button("Set Password") {
|
||||
if let room = passwordInputRoom, !passwordInput.isEmpty {
|
||||
viewModel.setRoomPassword(passwordInput, for: room)
|
||||
passwordInput = ""
|
||||
passwordInputRoom = nil
|
||||
}
|
||||
}
|
||||
} message: {
|
||||
Text("Enter a password to protect \(passwordInputRoom ?? "room"). Others will need this password to read messages.")
|
||||
}
|
||||
.alert("Enter Room Password", isPresented: Binding(
|
||||
get: { viewModel.showPasswordPrompt },
|
||||
set: { viewModel.showPasswordPrompt = $0 }
|
||||
)) {
|
||||
SecureField("Password", text: $passwordPromptInput)
|
||||
Button("Cancel", role: .cancel) {
|
||||
passwordPromptInput = ""
|
||||
viewModel.passwordPromptRoom = nil
|
||||
}
|
||||
Button("Join") {
|
||||
if let room = viewModel.passwordPromptRoom, !passwordPromptInput.isEmpty {
|
||||
viewModel.joinRoom(room, password: passwordPromptInput)
|
||||
passwordPromptInput = ""
|
||||
}
|
||||
}
|
||||
} message: {
|
||||
Text("This room is password protected. Enter the password to join.")
|
||||
}
|
||||
}
|
||||
|
||||
private var headerView: some View {
|
||||
@@ -517,6 +556,13 @@ struct ContentView: View {
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
// Lock icon for password protected rooms
|
||||
if viewModel.passwordProtectedRooms.contains(room) {
|
||||
Image(systemName: "lock.fill")
|
||||
.font(.system(size: 10))
|
||||
.foregroundColor(secondaryTextColor)
|
||||
}
|
||||
|
||||
Text(room)
|
||||
.font(.system(size: 14, design: .monospaced))
|
||||
.foregroundColor(viewModel.currentRoom == room ? Color.blue : textColor)
|
||||
@@ -534,22 +580,49 @@ struct ContentView: View {
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
|
||||
// Leave button
|
||||
// Room controls
|
||||
if viewModel.currentRoom == room {
|
||||
Button(action: {
|
||||
viewModel.leaveRoom(room)
|
||||
}) {
|
||||
Text("leave")
|
||||
.font(.system(size: 10, design: .monospaced))
|
||||
.foregroundColor(secondaryTextColor)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 2)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.stroke(secondaryTextColor.opacity(0.5), lineWidth: 1)
|
||||
)
|
||||
HStack(spacing: 4) {
|
||||
// Password button for room creator
|
||||
if viewModel.roomCreators[room] == viewModel.meshService.myPeerID {
|
||||
Button(action: {
|
||||
// Toggle password protection
|
||||
if viewModel.passwordProtectedRooms.contains(room) {
|
||||
viewModel.removeRoomPassword(for: room)
|
||||
} else {
|
||||
// Show password input
|
||||
showPasswordInput = true
|
||||
passwordInputRoom = room
|
||||
}
|
||||
}) {
|
||||
Image(systemName: viewModel.passwordProtectedRooms.contains(room) ? "lock.open" : "lock")
|
||||
.font(.system(size: 10))
|
||||
.foregroundColor(secondaryTextColor)
|
||||
.padding(4)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.stroke(secondaryTextColor.opacity(0.5), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
// Leave button
|
||||
Button(action: {
|
||||
viewModel.leaveRoom(room)
|
||||
}) {
|
||||
Text("leave")
|
||||
.font(.system(size: 10, design: .monospaced))
|
||||
.foregroundColor(secondaryTextColor)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 2)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.stroke(secondaryTextColor.opacity(0.5), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
|
||||
Reference in New Issue
Block a user