From eb35608fa1b3bb4367514ab829163c65341d0885 Mon Sep 17 00:00:00 2001 From: jack <212554440+jackjackbits@users.noreply.github.com> Date: Fri, 17 Oct 2025 22:58:56 +0200 Subject: [PATCH] Remove unused helpers and add cross-platform logging fallbacks (#811) --- bitchat/Services/BLEService.swift | 24 --- bitchat/Services/CommandProcessor.swift | 20 +-- bitchat/Services/KeychainManager.swift | 28 --- bitchat/ViewModels/ChatViewModel.swift | 170 ------------------ .../BitLogger/Sources/OSLog+Categories.swift | 4 +- .../BitLogger/Sources/SecureLogger.swift | 46 +++++ localPackages/Tor/Sources/TorManager.swift | 166 ++--------------- 7 files changed, 65 insertions(+), 393 deletions(-) diff --git a/bitchat/Services/BLEService.swift b/bitchat/Services/BLEService.swift index e9f672a2..4f5d1f7f 100644 --- a/bitchat/Services/BLEService.swift +++ b/bitchat/Services/BLEService.swift @@ -2149,19 +2149,6 @@ extension BLEService { } } - private func sendData(_ data: Data, to peripheral: CBPeripheral) { - // Fire-and-forget: Simple send without complex fallback logic - guard peripheral.state == .connected else { return } - - let peripheralUUID = peripheral.identifier.uuidString - guard let state = peripherals[peripheralUUID], - let characteristic = state.characteristic else { return } - - // Fire-and-forget principle: always use .withoutResponse for speed - // CoreBluetooth will handle fragmentation at L2CAP layer - writeOrEnqueue(data, to: peripheral, characteristic: characteristic) - } - // MARK: Fragmentation (Required for messages > BLE MTU) private func sendFragmentedPacket(_ packet: BitchatPacket, pad: Bool, maxChunk: Int? = nil, directedOnlyPeer: PeerID? = nil) { @@ -2848,17 +2835,6 @@ extension BLEService { // MARK: Helper Functions - private func sendLeave() { - SecureLogger.debug("👋 Sending leave announcement", category: .session) - let packet = BitchatPacket( - type: MessageType.leave.rawValue, - ttl: messageTTL, - senderID: myPeerID, - payload: Data(myNickname.utf8) - ) - broadcastPacket(packet) - } - private func sendAnnounce(forceSend: Bool = false) { // Throttle announces to prevent flooding let now = Date() diff --git a/bitchat/Services/CommandProcessor.swift b/bitchat/Services/CommandProcessor.swift index 3ac8d23c..f621e749 100644 --- a/bitchat/Services/CommandProcessor.swift +++ b/bitchat/Services/CommandProcessor.swift @@ -65,14 +65,11 @@ final class CommandProcessor { case "/unfav": if inGeoPublic || inGeoDM { return .error(message: "favorites are only for mesh peers in #mesh") } return handleFavorite(args, add: false) - // - case "/help", "/h": - return .error(message: "unknown command: \(cmd)") default: return .error(message: "unknown command: \(cmd)") } } - + // MARK: - Command Handlers private func handleMessage(_ args: String) -> CommandResult { @@ -311,19 +308,4 @@ final class CommandProcessor { } } - private func handleHelp() -> CommandResult { - let helpText = """ - commands: - /msg @name - start private chat - /who - list who's online - /clear - clear messages - /hug @name - send a hug - /slap @name - slap with a trout - /fav @name - add to favorites - /unfav @name - remove from favorites - /block @name - block - /unblock @name - unblock - """ - return .success(message: helpText) - } } diff --git a/bitchat/Services/KeychainManager.swift b/bitchat/Services/KeychainManager.swift index cbc379f4..be6ed784 100644 --- a/bitchat/Services/KeychainManager.swift +++ b/bitchat/Services/KeychainManager.swift @@ -27,34 +27,6 @@ final class KeychainManager: KeychainManagerProtocol { private let service = BitchatApp.bundleID private let appGroup = "group.\(BitchatApp.bundleID)" - private func isSandboxed() -> Bool { - #if os(macOS) - // More robust sandbox detection using multiple methods - - // Method 1: Check environment variable (can be spoofed) - let environment = ProcessInfo.processInfo.environment - let hasEnvVar = environment["APP_SANDBOX_CONTAINER_ID"] != nil - - // Method 2: Check if we can access a path outside sandbox - let homeDir = FileManager.default.homeDirectoryForCurrentUser - let testPath = homeDir.appendingPathComponent("../../../tmp/bitchat_sandbox_test_\(UUID().uuidString)") - let canWriteOutsideSandbox = FileManager.default.createFile(atPath: testPath.path, contents: nil, attributes: nil) - if canWriteOutsideSandbox { - try? FileManager.default.removeItem(at: testPath) - } - - // Method 3: Check container path - let containerPath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first?.path ?? "" - let hasContainerPath = containerPath.contains("/Containers/") - - // If any method indicates sandbox, we consider it sandboxed - return hasEnvVar || !canWriteOutsideSandbox || hasContainerPath - #else - // iOS is always sandboxed - return true - #endif - } - // MARK: - Identity Keys func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool { diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index 85711db3..4fc17af0 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -2164,16 +2164,6 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { return "anon#\(suffix)" } - // Helper: display name for current active channel (for notifications) - private func activeChannelDisplayName() -> String { - switch activeChannel { - case .mesh: - return "#mesh" - case .location(let ch): - return "#\(ch.geohash)" - } - } - // Dedup helper with small memory cap private func recordProcessedEvent(_ id: String) { processedNostrEvents.insert(id) @@ -5351,61 +5341,6 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { ) } - @MainActor - private func handleNostrAcknowledgment(content: String, from senderPubkey: String) { - // Parse ACK format: "ACK:TYPE:MESSAGE_ID" - let parts = content.split(separator: ":", maxSplits: 2) - guard parts.count >= 3 else { - SecureLogger.warning("⚠️ Invalid ACK format: \(content)", category: .session) - return - } - - let ackType = String(parts[1]) - let messageId = String(parts[2]) - - // Check if we've already processed this ACK - let ackKey = "\(messageId):\(ackType):\(senderPubkey)" - if processedNostrAcks.contains(ackKey) { - // Skip duplicate ACK - return - } - processedNostrAcks.insert(ackKey) - - SecureLogger.debug("📨 Received \(ackType) ACK for message \(messageId.prefix(16))... from \(senderPubkey.prefix(16))...", category: .session) - - // Verify the sender has a valid Noise key - guard findNoiseKey(for: senderPubkey) != nil else { - // Cannot find Noise key for ACK sender - return - } - - // Find and update the message status in ALL private chats (both stable and ephemeral) - var messageFound = false - for (chatPeerID, messages) in privateChats { - if let index = messages.firstIndex(where: { $0.id == messageId }) { - // Update delivery status based on ACK type - switch ackType { - case "DELIVERED": - privateChats[chatPeerID]?[index].deliveryStatus = .delivered(to: "recipient", at: Date()) - case "READ": - privateChats[chatPeerID]?[index].deliveryStatus = .read(by: "recipient", at: Date()) - default: - SecureLogger.warning("⚠️ Unknown ACK type: \(ackType)", category: .session) - } - - messageFound = true - SecureLogger.info("✅ Updated message \(messageId.prefix(16))... status to \(ackType) in chat \(chatPeerID.id.prefix(16))...", category: .session) - // Don't break - continue to update in all chats where this message exists - } - } - - if messageFound { - objectWillChange.send() - } else { - SecureLogger.warning("⚠️ Could not find message \(messageId) to update status from ACK", category: .session) - } - } - // MARK: - Base64URL utils private static func base64URLDecode(_ s: String) -> Data? { var str = s.replacingOccurrences(of: "-", with: "+") @@ -5464,111 +5399,6 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } } - @MainActor - private func handleNostrMessageFromUnknownSender( - messageId: String, - content: String, - senderPubkey: String, - senderNickname: String? = nil, - timestamp: Date - ) { - // Check if we already have this message in local storage - for (_, messages) in privateChats { - if messages.contains(where: { $0.id == messageId }) { - return // Skipping duplicate message - } - } - - // Check if we've read this message before (in a previous session) - let wasReadBefore = sentReadReceipts.contains(messageId) - - // Try to find sender by checking all known peers for nickname matches - // This is a fallback when we receive Nostr messages from someone not in favorites - - // For now, create a temporary peer ID based on Nostr pubkey - // This allows the message to be displayed even without Noise key mapping - let tempPeerID = PeerID(nostr_: senderPubkey) - - // Check if we're viewing this unknown sender's chat - let isViewingThisChat = selectedPrivateChatPeer == tempPeerID - - // Check if message is recent (less than 30 seconds old) - let messageAgeSeconds = Date().timeIntervalSince(timestamp) - let isRecentMessage = messageAgeSeconds < 30 - - // Determine if we should mark as unread BEFORE adding to chats - // During startup phase, only block OLD messages from being marked as unread - // Recent messages should always be marked as unread if not previously read - let shouldMarkAsUnread = !wasReadBefore && !isViewingThisChat && (isRecentMessage || !isStartupPhase) - - // Use provided nickname or try to extract from previous messages - var finalSenderNickname = senderNickname ?? "Unknown" - - // If no nickname provided, check if we have any previous messages from this Nostr key - if senderNickname == nil { - for (_, messages) in privateChats { - if let previousMessage = messages.first(where: { - $0.senderPeerID == tempPeerID - }) { - finalSenderNickname = previousMessage.sender - break - } - } - } - - // Create the message - let message = BitchatMessage( - id: messageId, - sender: finalSenderNickname, - content: content, - timestamp: timestamp, - isRelay: false, - originalSender: nil, - isPrivate: true, - recipientNickname: nickname, - senderPeerID: tempPeerID, - mentions: nil, - deliveryStatus: .delivered(to: nickname, at: Date()) - ) - - // Store in private chats - if privateChats[tempPeerID] == nil { - privateChats[tempPeerID] = [] - } - privateChats[tempPeerID]?.append(message) - - // For unknown senders (no Noise key), skip sending Nostr ACKs - - // Handle based on read status - if wasReadBefore { - // Message was read in a previous session - don't mark as unread or notify - // Not marking previously-read message as unread - } else if isViewingThisChat { - // Viewing this chat - mark as read - // No read ACKs for unknown senders - } else { - // Not viewing and not previously read - // Use pre-calculated shouldMarkAsUnread to avoid UI flicker - if shouldMarkAsUnread { - unreadPrivateMessages.insert(tempPeerID) - - // Only notify if it's a recent message - if isRecentMessage { - NotificationService.shared.sendPrivateMessageNotification( - from: finalSenderNickname, - message: content, - peerID: tempPeerID.id - ) - } else { - // Not notifying for old message - } - } - // Not notifying for old message - } - - SecureLogger.info("📬 Stored Nostr message from unknown sender \(finalSenderNickname) in temporary peer \(tempPeerID)", category: .session) - } - @MainActor private func findNoiseKey(for nostrPubkey: String) -> Data? { // Convert hex to npub if needed for comparison diff --git a/localPackages/BitLogger/Sources/OSLog+Categories.swift b/localPackages/BitLogger/Sources/OSLog+Categories.swift index 7ebe4da0..91e7eeb1 100644 --- a/localPackages/BitLogger/Sources/OSLog+Categories.swift +++ b/localPackages/BitLogger/Sources/OSLog+Categories.swift @@ -6,11 +6,13 @@ // For more information, see // +#if canImport(os.log) import os.log +#endif public extension OSLog { private static let subsystem = "chat.bitchat" - + static let noise = OSLog(subsystem: subsystem, category: "noise") static let encryption = OSLog(subsystem: subsystem, category: "encryption") static let keychain = OSLog(subsystem: subsystem, category: "keychain") diff --git a/localPackages/BitLogger/Sources/SecureLogger.swift b/localPackages/BitLogger/Sources/SecureLogger.swift index e5f60cbf..84bb997d 100644 --- a/localPackages/BitLogger/Sources/SecureLogger.swift +++ b/localPackages/BitLogger/Sources/SecureLogger.swift @@ -7,7 +7,53 @@ // import Foundation +#if canImport(os.log) import os.log +#else +public struct OSLog { + public let subsystem: String + public let category: String + + public init(subsystem: String, category: String) { + self.subsystem = subsystem + self.category = category + } +} + +public struct OSLogType: CustomStringConvertible { + private let label: String + + private init(_ label: String) { + self.label = label + } + + public var description: String { label } + + public static let debug = OSLogType("debug") + public static let info = OSLogType("info") + public static let `default` = OSLogType("default") + public static let error = OSLogType("error") + public static let fault = OSLogType("fault") +} + +@usableFromInline +let secureLoggerFallbackFormatter: ISO8601DateFormatter = { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + return formatter +}() + +@usableFromInline +func os_log(_ message: StaticString, log: OSLog, type: OSLogType, _ args: CVarArg...) { + let rawFormat = String(describing: message) + let format = rawFormat + .replacingOccurrences(of: "%{public}@", with: "%@") + .replacingOccurrences(of: "%{private}@", with: "%@") + let formatted = String(format: format, arguments: args) + let timestamp = secureLoggerFallbackFormatter.string(from: Date()) + print("[\(timestamp)] [\(log.subsystem)::\(log.category)] [\(type.description)] \(formatted)") +} +#endif /// Centralized security-aware logging framework /// Provides safe logging that filters sensitive data and security events diff --git a/localPackages/Tor/Sources/TorManager.swift b/localPackages/Tor/Sources/TorManager.swift index 00c4c4c7..32154b86 100644 --- a/localPackages/Tor/Sources/TorManager.swift +++ b/localPackages/Tor/Sources/TorManager.swift @@ -1,11 +1,23 @@ import BitLogger import Foundation +#if canImport(Network) import Network +#endif +#if canImport(Darwin) import Darwin +#elseif canImport(Glibc) +import Glibc +#endif -// Declare C entrypoint for Tor when statically linked from an xcframework. -@_silgen_name("tor_main") -private func tor_main_c(_ argc: Int32, _ argv: UnsafeMutablePointer?>?) -> Int32 +#if !canImport(Network) +private final class NWPathMonitor { + var pathUpdateHandler: ((Any) -> Void)? + + func start(queue: DispatchQueue) { + // Path monitoring is unavailable on this platform; nothing to do. + } +} +#endif // Preferred: tiny C glue that uses Tor's embedding API (tor_api.h) @_silgen_name("tor_host_start") @@ -286,150 +298,6 @@ public final class TorManager: ObservableObject { } } - // MARK: - Dynamic loader path (no Swift module required) - - /// Attempt to locate an embedded tor framework binary and launch Tor via `tor_run_main`. - /// Returns true if the attempt started and port probing was scheduled. - private func startTorViaDlopen() -> Bool { - guard let fwURL = frameworkBinaryURL() else { - SecureLogger.warning("TorManager: no embedded tor framework found", category: .session) - return false - } - - // Load the library - let mode = RTLD_NOW | RTLD_LOCAL - SecureLogger.info("TorManager: dlopen(\(fwURL.lastPathComponent))…", category: .session) - guard let handle = dlopen(fwURL.path, mode) else { - let err = String(cString: dlerror()) - self.lastError = NSError(domain: "TorManager", code: -10, userInfo: [NSLocalizedDescriptionKey: "dlopen failed: \(err)"]) - self.isStarting = false - return false - } - - // Resolve tor_main(argc, argv) - typealias TorMainType = @convention(c) (Int32, UnsafeMutablePointer?>?) -> Int32 - guard let sym = dlsym(handle, "tor_main") else { - // Keep handle open but report error - let err = String(cString: dlerror()) - self.lastError = NSError(domain: "TorManager", code: -11, userInfo: [NSLocalizedDescriptionKey: "dlsym tor_main failed: \(err)"]) - self.isStarting = false - return false - } - let torMain = unsafeBitCast(sym, to: TorMainType.self) - self._dlHandle = handle - - // Prepare args: tor -f - var argv: [String] = ["tor"] - if let torrc = torrcURL()?.path { - argv.append(contentsOf: ["-f", torrc]) - } - // Run Tor on a background thread to avoid blocking the main actor - SecureLogger.info("TorManager: launching tor_main with torrc", category: .session) - let argc = Int32(argv.count) - DispatchQueue.global(qos: .utility).async { - // Build stable C argv in this thread - let cStrings: [UnsafeMutablePointer?] = argv.map { strdup($0) } - let cArgv = UnsafeMutablePointer?>.allocate(capacity: cStrings.count + 1) - for i in 0.. URL? { - // Try common embedded locations for the framework binary name - let candidates = [ - "tor-nolzma.framework/tor-nolzma", - "Tor.framework/Tor", - ] - if let base = Bundle.main.privateFrameworksURL { - for rel in candidates { - let url = base.appendingPathComponent(rel) - if FileManager.default.fileExists(atPath: url.path) { return url } - } - } - // For macOS apps, also try Contents/Frameworks explicitly - #if os(macOS) - if let appURL = Bundle.main.bundleURL as URL?, - let frameworksURL = Optional(appURL.appendingPathComponent("Contents/Frameworks", isDirectory: true)) { - for rel in candidates { - let url = frameworksURL.appendingPathComponent(rel) - if FileManager.default.fileExists(atPath: url.path) { return url } - } - } - #endif - return nil - } - - // MARK: - Static-link path (no module import) - private func startTorViaLinkedSymbol() -> Bool { - // Attempt to start tor_run_main directly (statically linked). If the - // symbol is not present at link-time, builds will fail — which is - // expected when the xcframework is absent. - var argv: [String] = ["tor"] - if let torrc = torrcURL()?.path { argv.append(contentsOf: ["-f", torrc]) } - - SecureLogger.info("TorManager: starting tor_main (static)", category: .session) - let argc = Int32(argv.count) - DispatchQueue.global(qos: .utility).async { - // Build stable C argv in this thread - let cStrings: [UnsafeMutablePointer?] = argv.map { strdup($0) } - let cArgv = UnsafeMutablePointer?>.allocate(capacity: cStrings.count + 1) - for i in 0.. Bool { false } - // iOS: Poll GETINFO periodically to track bootstrap progress without long-lived control readers. private func bootstrapPollLoop() async { let deadline = Date().addingTimeInterval(75)