Make logging categories static properties of OSLog

So we can use `.<category name>` to simplify the code.

Search/Replace Strategy:
Search text: `category: SecureLogger.`
Replace text: `category: .`
This commit is contained in:
islam
2025-09-11 19:02:32 +01:00
parent ee16ff5ff4
commit 5ca9222fc2
21 changed files with 354 additions and 345 deletions
@@ -160,7 +160,7 @@ final class SecureIdentityStateManager {
cache = try JSONDecoder().decode(IdentityCache.self, from: decryptedData) cache = try JSONDecoder().decode(IdentityCache.self, from: decryptedData)
} catch { } catch {
// Log error but continue with empty cache // Log error but continue with empty cache
SecureLogger.logError(error, context: "Failed to load identity cache", category: SecureLogger.security) SecureLogger.logError(error, context: "Failed to load identity cache", category: .security)
} }
} }
@@ -191,10 +191,10 @@ final class SecureIdentityStateManager {
let sealedBox = try AES.GCM.seal(data, using: encryptionKey) let sealedBox = try AES.GCM.seal(data, using: encryptionKey)
let saved = keychain.saveIdentityKey(sealedBox.combined!, forKey: cacheKey) let saved = keychain.saveIdentityKey(sealedBox.combined!, forKey: cacheKey)
if saved { if saved {
SecureLogger.log("Identity cache saved to keychain", category: SecureLogger.security, level: .debug) SecureLogger.log("Identity cache saved to keychain", category: .security, level: .debug)
} }
} catch { } catch {
SecureLogger.logError(error, context: "Failed to save identity cache", category: SecureLogger.security) SecureLogger.logError(error, context: "Failed to save identity cache", category: .security)
} }
} }
@@ -395,7 +395,7 @@ final class SecureIdentityStateManager {
} }
func setBlocked(_ fingerprint: String, isBlocked: Bool) { func setBlocked(_ fingerprint: String, isBlocked: Bool) {
SecureLogger.log("User \(isBlocked ? "blocked" : "unblocked"): \(fingerprint)", category: SecureLogger.security, level: .info) SecureLogger.log("User \(isBlocked ? "blocked" : "unblocked"): \(fingerprint)", category: .security, level: .info)
queue.async(flags: .barrier) { queue.async(flags: .barrier) {
if var identity = self.cache.socialIdentities[fingerprint] { if var identity = self.cache.socialIdentities[fingerprint] {
@@ -519,7 +519,7 @@ final class SecureIdentityStateManager {
// MARK: - Cleanup // MARK: - Cleanup
func clearAllIdentityData() { func clearAllIdentityData() {
SecureLogger.log("Clearing all identity data", category: SecureLogger.security, level: .warning) SecureLogger.log("Clearing all identity data", category: .security, level: .warning)
queue.async(flags: .barrier) { queue.async(flags: .barrier) {
self.cache = IdentityCache() self.cache = IdentityCache()
@@ -543,7 +543,7 @@ final class SecureIdentityStateManager {
// MARK: - Verification // MARK: - Verification
func setVerified(fingerprint: String, verified: Bool) { func setVerified(fingerprint: String, verified: Bool) {
SecureLogger.log("Fingerprint \(verified ? "verified" : "unverified"): \(fingerprint)", category: SecureLogger.security, level: .info) SecureLogger.log("Fingerprint \(verified ? "verified" : "unverified"): \(fingerprint)", category: .security, level: .info)
queue.async(flags: .barrier) { queue.async(flags: .barrier) {
if verified { if verified {
+16 -16
View File
@@ -69,7 +69,7 @@ final class NoiseHandshakeCoordinator {
case .initiating(_, let lastAttempt): case .initiating(_, let lastAttempt):
if Date().timeIntervalSince(lastAttempt) > handshakeTimeout { if Date().timeIntervalSince(lastAttempt) > handshakeTimeout {
SecureLogger.log("Forcing new handshake with \(remotePeerID) - previous stuck in initiating", SecureLogger.log("Forcing new handshake with \(remotePeerID) - previous stuck in initiating",
category: SecureLogger.handshake, level: .warning) category: .handshake, level: .warning)
return true return true
} }
default: default:
@@ -78,7 +78,7 @@ final class NoiseHandshakeCoordinator {
} }
SecureLogger.log("Already in active handshake with \(remotePeerID), state: \(state)", SecureLogger.log("Already in active handshake with \(remotePeerID), state: \(state)",
category: SecureLogger.handshake, level: .debug) category: .handshake, level: .debug)
return false return false
} }
@@ -108,7 +108,7 @@ final class NoiseHandshakeCoordinator {
let attempt = self.getCurrentAttempt(for: peerID) + 1 let attempt = self.getCurrentAttempt(for: peerID) + 1
self.handshakeStates[peerID] = .initiating(attempt: attempt, lastAttempt: Date()) self.handshakeStates[peerID] = .initiating(attempt: attempt, lastAttempt: Date())
SecureLogger.log("Recording handshake initiation with \(peerID), attempt \(attempt)", SecureLogger.log("Recording handshake initiation with \(peerID), attempt \(attempt)",
category: SecureLogger.handshake, level: .info) category: .handshake, level: .info)
} }
} }
@@ -117,7 +117,7 @@ final class NoiseHandshakeCoordinator {
handshakeQueue.async(flags: .barrier) { handshakeQueue.async(flags: .barrier) {
self.handshakeStates[peerID] = .responding(since: Date()) self.handshakeStates[peerID] = .responding(since: Date())
SecureLogger.log("Recording handshake response to \(peerID)", SecureLogger.log("Recording handshake response to \(peerID)",
category: SecureLogger.handshake, level: .info) category: .handshake, level: .info)
} }
} }
@@ -126,7 +126,7 @@ final class NoiseHandshakeCoordinator {
handshakeQueue.async(flags: .barrier) { handshakeQueue.async(flags: .barrier) {
self.handshakeStates[peerID] = .established(since: Date()) self.handshakeStates[peerID] = .established(since: Date())
SecureLogger.log("Handshake successfully established with \(peerID)", SecureLogger.log("Handshake successfully established with \(peerID)",
category: SecureLogger.handshake, level: .info) category: .handshake, level: .info)
} }
} }
@@ -137,7 +137,7 @@ final class NoiseHandshakeCoordinator {
let canRetry = attempts < self.maxHandshakeAttempts let canRetry = attempts < self.maxHandshakeAttempts
self.handshakeStates[peerID] = .failed(reason: reason, canRetry: canRetry, lastAttempt: Date()) self.handshakeStates[peerID] = .failed(reason: reason, canRetry: canRetry, lastAttempt: Date())
SecureLogger.log("Handshake failed with \(peerID): \(reason), canRetry: \(canRetry)", SecureLogger.log("Handshake failed with \(peerID): \(reason), canRetry: \(canRetry)",
category: SecureLogger.handshake, level: .warning) category: .handshake, level: .warning)
} }
} }
@@ -147,7 +147,7 @@ final class NoiseHandshakeCoordinator {
// If we're already established, reject new handshakes // If we're already established, reject new handshakes
if case .established = handshakeStates[remotePeerID] { if case .established = handshakeStates[remotePeerID] {
SecureLogger.log("Rejecting handshake from \(remotePeerID) - already established", SecureLogger.log("Rejecting handshake from \(remotePeerID) - already established",
category: SecureLogger.handshake, level: .debug) category: .handshake, level: .debug)
return false return false
} }
@@ -158,7 +158,7 @@ final class NoiseHandshakeCoordinator {
if case .initiating = handshakeStates[remotePeerID] { if case .initiating = handshakeStates[remotePeerID] {
// They shouldn't be initiating, but accept it to recover from race condition // They shouldn't be initiating, but accept it to recover from race condition
SecureLogger.log("Accepting handshake from \(remotePeerID) despite being initiator (race condition recovery)", SecureLogger.log("Accepting handshake from \(remotePeerID) despite being initiator (race condition recovery)",
category: SecureLogger.handshake, level: .warning) category: .handshake, level: .warning)
return true return true
} }
} }
@@ -216,7 +216,7 @@ final class NoiseHandshakeCoordinator {
handshakeQueue.async(flags: .barrier) { handshakeQueue.async(flags: .barrier) {
self.handshakeStates.removeValue(forKey: peerID) self.handshakeStates.removeValue(forKey: peerID)
SecureLogger.log("Reset handshake state for \(peerID)", SecureLogger.log("Reset handshake state for \(peerID)",
category: SecureLogger.handshake, level: .debug) category: .handshake, level: .debug)
} }
} }
@@ -257,7 +257,7 @@ final class NoiseHandshakeCoordinator {
if isStale { if isStale {
stalePeerIDs.append(peerID) stalePeerIDs.append(peerID)
SecureLogger.log("Found stale handshake state for \(peerID): \(state)", SecureLogger.log("Found stale handshake state for \(peerID): \(state)",
category: SecureLogger.handshake, level: .warning) category: .handshake, level: .warning)
} }
} }
@@ -271,7 +271,7 @@ final class NoiseHandshakeCoordinator {
let peerID = sortedSessions[i].peerID let peerID = sortedSessions[i].peerID
stalePeerIDs.append(peerID) stalePeerIDs.append(peerID)
SecureLogger.log("Removing old established session for \(peerID) to maintain session limit", SecureLogger.log("Removing old established session for \(peerID) to maintain session limit",
category: SecureLogger.handshake, level: .info) category: .handshake, level: .info)
} }
} }
@@ -282,7 +282,7 @@ final class NoiseHandshakeCoordinator {
if !stalePeerIDs.isEmpty { if !stalePeerIDs.isEmpty {
SecureLogger.log("Cleaned up \(stalePeerIDs.count) stale handshake states", SecureLogger.log("Cleaned up \(stalePeerIDs.count) stale handshake states",
category: SecureLogger.handshake, level: .info) category: .handshake, level: .info)
} }
return stalePeerIDs return stalePeerIDs
@@ -333,7 +333,7 @@ final class NoiseHandshakeCoordinator {
/// Log current handshake states for debugging /// Log current handshake states for debugging
func logHandshakeStates() { func logHandshakeStates() {
handshakeQueue.sync { handshakeQueue.sync {
SecureLogger.log("=== Handshake States ===", category: SecureLogger.handshake, level: .debug) SecureLogger.log("=== Handshake States ===", category: .handshake, level: .debug)
for (peerID, state) in handshakeStates { for (peerID, state) in handshakeStates {
let stateDesc: String let stateDesc: String
switch state { switch state {
@@ -352,16 +352,16 @@ final class NoiseHandshakeCoordinator {
case .failed(let reason, let canRetry, let lastAttempt): case .failed(let reason, let canRetry, let lastAttempt):
stateDesc = "failed: \(reason) (canRetry: \(canRetry), last: \(lastAttempt))" stateDesc = "failed: \(reason) (canRetry: \(canRetry), last: \(lastAttempt))"
} }
SecureLogger.log(" \(peerID): \(stateDesc)", category: SecureLogger.handshake, level: .debug) SecureLogger.log(" \(peerID): \(stateDesc)", category: .handshake, level: .debug)
} }
SecureLogger.log("========================", category: SecureLogger.handshake, level: .debug) SecureLogger.log("========================", category: .handshake, level: .debug)
} }
} }
/// Clear all handshake states - used during panic mode /// Clear all handshake states - used during panic mode
func clearAllHandshakeStates() { func clearAllHandshakeStates() {
handshakeQueue.async(flags: .barrier) { handshakeQueue.async(flags: .barrier) {
SecureLogger.log("Clearing all handshake states for panic mode", category: SecureLogger.handshake, level: .warning) SecureLogger.log("Clearing all handshake states for panic mode", category: .handshake, level: .warning)
self.handshakeStates.removeAll() self.handshakeStates.removeAll()
self.processedHandshakeMessages.removeAll() self.processedHandshakeMessages.removeAll()
} }
+6 -6
View File
@@ -285,7 +285,7 @@ final class NoiseCipherState {
// Log high nonce values that might indicate issues // Log high nonce values that might indicate issues
if currentNonce > Self.HIGH_NONCE_WARNING_THRESHOLD { if currentNonce > Self.HIGH_NONCE_WARNING_THRESHOLD {
SecureLogger.log("High nonce value detected: \(currentNonce) - consider rekeying", category: SecureLogger.encryption, level: .warning) SecureLogger.log("High nonce value detected: \(currentNonce) - consider rekeying", category: .encryption, level: .warning)
} }
return combinedPayload return combinedPayload
@@ -342,7 +342,7 @@ final class NoiseCipherState {
// Log high nonce values that might indicate issues // Log high nonce values that might indicate issues
if decryptionNonce > Self.HIGH_NONCE_WARNING_THRESHOLD { if decryptionNonce > Self.HIGH_NONCE_WARNING_THRESHOLD {
SecureLogger.log("High nonce value detected: \(decryptionNonce) - consider rekeying", category: SecureLogger.encryption, level: .warning) SecureLogger.log("High nonce value detected: \(decryptionNonce) - consider rekeying", category: .encryption, level: .warning)
} }
do { do {
@@ -357,7 +357,7 @@ final class NoiseCipherState {
} catch { } catch {
SecureLogger.log("Decrypt failed: \(error) for nonce \(decryptionNonce)") SecureLogger.log("Decrypt failed: \(error) for nonce \(decryptionNonce)")
// Log authentication failures with nonce info // Log authentication failures with nonce info
SecureLogger.log("Decryption failed at nonce \(decryptionNonce)", category: SecureLogger.encryption, level: .error) SecureLogger.log("Decryption failed at nonce \(decryptionNonce)", category: .encryption, level: .error)
throw error throw error
} }
} }
@@ -661,7 +661,7 @@ final class NoiseHandshakeState {
do { do {
remoteEphemeralPublic = try NoiseHandshakeState.validatePublicKey(ephemeralData) remoteEphemeralPublic = try NoiseHandshakeState.validatePublicKey(ephemeralData)
} catch { } catch {
SecureLogger.log("Invalid ephemeral public key received", category: SecureLogger.security, level: .warning) SecureLogger.log("Invalid ephemeral public key received", category: .security, level: .warning)
throw NoiseError.invalidMessage throw NoiseError.invalidMessage
} }
symmetricState.mixHash(ephemeralData) symmetricState.mixHash(ephemeralData)
@@ -877,7 +877,7 @@ extension NoiseHandshakeState {
// Check against known bad points // Check against known bad points
if lowOrderPoints.contains(keyData) { if lowOrderPoints.contains(keyData) {
SecureLogger.log("Low-order point detected", category: SecureLogger.security, level: .warning) SecureLogger.log("Low-order point detected", category: .security, level: .warning)
throw NoiseError.invalidPublicKey throw NoiseError.invalidPublicKey
} }
@@ -887,7 +887,7 @@ extension NoiseHandshakeState {
return publicKey return publicKey
} catch { } catch {
// If CryptoKit rejects it, it's invalid // If CryptoKit rejects it, it's invalid
SecureLogger.log("CryptoKit validation failed", category: SecureLogger.security, level: .warning) SecureLogger.log("CryptoKit validation failed", category: .security, level: .warning)
throw NoiseError.invalidPublicKey throw NoiseError.invalidPublicKey
} }
} }
@@ -153,7 +153,7 @@ final class NoiseRateLimiter {
// Check global rate limit first // Check global rate limit first
globalHandshakeTimestamps = globalHandshakeTimestamps.filter { $0 > oneMinuteAgo } globalHandshakeTimestamps = globalHandshakeTimestamps.filter { $0 > oneMinuteAgo }
if globalHandshakeTimestamps.count >= NoiseSecurityConstants.maxGlobalHandshakesPerMinute { if globalHandshakeTimestamps.count >= NoiseSecurityConstants.maxGlobalHandshakesPerMinute {
SecureLogger.log("Global handshake rate limit exceeded: \(globalHandshakeTimestamps.count)/\(NoiseSecurityConstants.maxGlobalHandshakesPerMinute) per minute", category: SecureLogger.security, level: .warning) SecureLogger.log("Global handshake rate limit exceeded: \(globalHandshakeTimestamps.count)/\(NoiseSecurityConstants.maxGlobalHandshakesPerMinute) per minute", category: .security, level: .warning)
return false return false
} }
@@ -162,7 +162,7 @@ final class NoiseRateLimiter {
timestamps = timestamps.filter { $0 > oneMinuteAgo } timestamps = timestamps.filter { $0 > oneMinuteAgo }
if timestamps.count >= NoiseSecurityConstants.maxHandshakesPerMinute { if timestamps.count >= NoiseSecurityConstants.maxHandshakesPerMinute {
SecureLogger.log("Per-peer handshake rate limit exceeded for \(peerID): \(timestamps.count)/\(NoiseSecurityConstants.maxHandshakesPerMinute) per minute", category: SecureLogger.security, level: .warning) SecureLogger.log("Per-peer handshake rate limit exceeded for \(peerID): \(timestamps.count)/\(NoiseSecurityConstants.maxHandshakesPerMinute) per minute", category: .security, level: .warning)
return false return false
} }
@@ -182,7 +182,7 @@ final class NoiseRateLimiter {
// Check global rate limit first // Check global rate limit first
globalMessageTimestamps = globalMessageTimestamps.filter { $0 > oneSecondAgo } globalMessageTimestamps = globalMessageTimestamps.filter { $0 > oneSecondAgo }
if globalMessageTimestamps.count >= NoiseSecurityConstants.maxGlobalMessagesPerSecond { if globalMessageTimestamps.count >= NoiseSecurityConstants.maxGlobalMessagesPerSecond {
SecureLogger.log("Global message rate limit exceeded: \(globalMessageTimestamps.count)/\(NoiseSecurityConstants.maxGlobalMessagesPerSecond) per second", category: SecureLogger.security, level: .warning) SecureLogger.log("Global message rate limit exceeded: \(globalMessageTimestamps.count)/\(NoiseSecurityConstants.maxGlobalMessagesPerSecond) per second", category: .security, level: .warning)
return false return false
} }
@@ -191,7 +191,7 @@ final class NoiseRateLimiter {
timestamps = timestamps.filter { $0 > oneSecondAgo } timestamps = timestamps.filter { $0 > oneSecondAgo }
if timestamps.count >= NoiseSecurityConstants.maxMessagesPerSecond { if timestamps.count >= NoiseSecurityConstants.maxMessagesPerSecond {
SecureLogger.log("Per-peer message rate limit exceeded for \(peerID): \(timestamps.count)/\(NoiseSecurityConstants.maxMessagesPerSecond) per second", category: SecureLogger.security, level: .warning) SecureLogger.log("Per-peer message rate limit exceeded for \(peerID): \(timestamps.count)/\(NoiseSecurityConstants.maxMessagesPerSecond) per second", category: .security, level: .warning)
return false return false
} }
+7 -7
View File
@@ -92,7 +92,7 @@ class NoiseSession {
func processHandshakeMessage(_ message: Data) throws -> Data? { func processHandshakeMessage(_ message: Data) throws -> Data? {
return try sessionQueue.sync(flags: .barrier) { return try sessionQueue.sync(flags: .barrier) {
SecureLogger.log("NoiseSession[\(peerID)]: Processing handshake message, current state: \(state), role: \(role)", category: SecureLogger.noise, level: .debug) SecureLogger.log("NoiseSession[\(peerID)]: Processing handshake message, current state: \(state), role: \(role)", category: .noise, level: .debug)
// Initialize handshake state if needed (for responders) // Initialize handshake state if needed (for responders)
if state == .uninitialized && role == .responder { if state == .uninitialized && role == .responder {
@@ -103,7 +103,7 @@ class NoiseSession {
remoteStaticKey: nil remoteStaticKey: nil
) )
state = .handshaking state = .handshaking
SecureLogger.log("NoiseSession[\(peerID)]: Initialized handshake state for responder", category: SecureLogger.noise, level: .debug) SecureLogger.log("NoiseSession[\(peerID)]: Initialized handshake state for responder", category: .noise, level: .debug)
} }
guard case .handshaking = state, let handshake = handshakeState else { guard case .handshaking = state, let handshake = handshakeState else {
@@ -112,7 +112,7 @@ class NoiseSession {
// Process incoming message // Process incoming message
_ = try handshake.readMessage(message) _ = try handshake.readMessage(message)
SecureLogger.log("NoiseSession[\(peerID)]: Read handshake message, checking if complete", category: SecureLogger.noise, level: .debug) SecureLogger.log("NoiseSession[\(peerID)]: Read handshake message, checking if complete", category: .noise, level: .debug)
// Check if handshake is complete // Check if handshake is complete
if handshake.isHandshakeComplete() { if handshake.isHandshakeComplete() {
@@ -130,7 +130,7 @@ class NoiseSession {
state = .established state = .established
handshakeState = nil // Clear handshake state handshakeState = nil // Clear handshake state
SecureLogger.log("NoiseSession[\(peerID)]: Handshake complete (no response needed), transitioning to established", category: SecureLogger.noise, level: .debug) SecureLogger.log("NoiseSession[\(peerID)]: Handshake complete (no response needed), transitioning to established", category: .noise, level: .debug)
SecureLogger.logSecurityEvent(.handshakeCompleted(peerID: peerID)) SecureLogger.logSecurityEvent(.handshakeCompleted(peerID: peerID))
return nil return nil
@@ -138,7 +138,7 @@ class NoiseSession {
// Generate response // Generate response
let response = try handshake.writeMessage() let response = try handshake.writeMessage()
sentHandshakeMessages.append(response) sentHandshakeMessages.append(response)
SecureLogger.log("NoiseSession[\(peerID)]: Generated handshake response of size \(response.count)", category: SecureLogger.noise, level: .debug) SecureLogger.log("NoiseSession[\(peerID)]: Generated handshake response of size \(response.count)", category: .noise, level: .debug)
// Check if handshake is complete after writing // Check if handshake is complete after writing
if handshake.isHandshakeComplete() { if handshake.isHandshakeComplete() {
@@ -156,7 +156,7 @@ class NoiseSession {
state = .established state = .established
handshakeState = nil // Clear handshake state handshakeState = nil // Clear handshake state
SecureLogger.log("NoiseSession[\(peerID)]: Handshake complete after writing response, transitioning to established", category: SecureLogger.noise, level: .debug) SecureLogger.log("NoiseSession[\(peerID)]: Handshake complete after writing response, transitioning to established", category: .noise, level: .debug)
SecureLogger.logSecurityEvent(.handshakeCompleted(peerID: peerID)) SecureLogger.logSecurityEvent(.handshakeCompleted(peerID: peerID))
} }
@@ -349,7 +349,7 @@ final class NoiseSessionManager {
// We should accept the new handshake to re-establish encryption // We should accept the new handshake to re-establish encryption
if existing.isEstablished() { if existing.isEstablished() {
SecureLogger.log("Accepting handshake from \(peerID) despite existing session - peer likely cleared their session", SecureLogger.log("Accepting handshake from \(peerID) despite existing session - peer likely cleared their session",
category: SecureLogger.session, level: .info) category: .session, level: .info)
_ = sessions.removeValue(forKey: peerID) _ = sessions.removeValue(forKey: peerID)
shouldCreateNew = true shouldCreateNew = true
} else { } else {
+5 -5
View File
@@ -54,7 +54,7 @@ final class GeoRelayDirectory {
Task.detached { Task.detached {
let ready = await TorManager.shared.awaitReady() let ready = await TorManager.shared.awaitReady()
if !ready { if !ready {
SecureLogger.log("GeoRelayDirectory: Tor not ready; skipping remote fetch (fail-closed)", category: SecureLogger.session, level: .warning) SecureLogger.log("GeoRelayDirectory: Tor not ready; skipping remote fetch (fail-closed)", category: .session, level: .warning)
return return
} }
let task = TorURLSession.shared.session.dataTask(with: req) { [weak self] data, _, error in let task = TorURLSession.shared.session.dataTask(with: req) { [weak self] data, _, error in
@@ -66,12 +66,12 @@ final class GeoRelayDirectory {
self.entries = parsed self.entries = parsed
self.persistCache(text) self.persistCache(text)
UserDefaults.standard.set(Date(), forKey: self.lastFetchKey) UserDefaults.standard.set(Date(), forKey: self.lastFetchKey)
SecureLogger.log("GeoRelayDirectory: refreshed \(parsed.count) relays from remote", category: SecureLogger.session, level: .info) SecureLogger.log("GeoRelayDirectory: refreshed \(parsed.count) relays from remote", category: .session, level: .info)
} }
return return
} }
} }
SecureLogger.log("GeoRelayDirectory: remote fetch failed; keeping local entries", category: SecureLogger.session, level: .warning) SecureLogger.log("GeoRelayDirectory: remote fetch failed; keeping local entries", category: .session, level: .warning)
} }
task.resume() task.resume()
} }
@@ -82,7 +82,7 @@ final class GeoRelayDirectory {
do { do {
try text.data(using: .utf8)?.write(to: url, options: .atomic) try text.data(using: .utf8)?.write(to: url, options: .atomic)
} catch { } catch {
SecureLogger.log("GeoRelayDirectory: failed to write cache: \(error)", category: SecureLogger.session, level: .warning) SecureLogger.log("GeoRelayDirectory: failed to write cache: \(error)", category: .session, level: .warning)
} }
} }
@@ -113,7 +113,7 @@ final class GeoRelayDirectory {
let text = String(data: data, encoding: .utf8) { let text = String(data: data, encoding: .utf8) {
return Self.parseCSV(text) return Self.parseCSV(text)
} }
SecureLogger.log("GeoRelayDirectory: no local CSV found; entries empty", category: SecureLogger.session, level: .warning) SecureLogger.log("GeoRelayDirectory: no local CSV found; entries empty", category: .session, level: .warning)
return [] return []
} }
+2 -2
View File
@@ -79,7 +79,7 @@ struct NostrProtocol {
// Successfully unwrapped gift wrap // Successfully unwrapped gift wrap
} catch { } catch {
SecureLogger.log("❌ Failed to unwrap gift wrap: \(error)", SecureLogger.log("❌ Failed to unwrap gift wrap: \(error)",
category: SecureLogger.session, level: .error) category: .session, level: .error)
throw error throw error
} }
@@ -93,7 +93,7 @@ struct NostrProtocol {
// Successfully opened seal // Successfully opened seal
} catch { } catch {
SecureLogger.log("❌ Failed to open seal: \(error)", SecureLogger.log("❌ Failed to open seal: \(error)",
category: SecureLogger.session, level: .error) category: .session, level: .error)
throw error throw error
} }
+21 -21
View File
@@ -82,10 +82,10 @@ final class NostrRelayManager: ObservableObject {
let ready = await TorManager.shared.awaitReady() let ready = await TorManager.shared.awaitReady()
await MainActor.run { await MainActor.run {
if !ready { if !ready {
SecureLogger.log("❌ Tor not ready; aborting relay connections (fail-closed)", category: SecureLogger.session, level: .error) SecureLogger.log("❌ Tor not ready; aborting relay connections (fail-closed)", category: .session, level: .error)
return return
} }
SecureLogger.log("🌐 Connecting to \(self.relays.count) Nostr relays (via Tor)", category: SecureLogger.session, level: .debug) SecureLogger.log("🌐 Connecting to \(self.relays.count) Nostr relays (via Tor)", category: .session, level: .debug)
for relay in self.relays { for relay in self.relays {
self.connectToRelay(relay.url) self.connectToRelay(relay.url)
} }
@@ -231,12 +231,12 @@ final class NostrRelayManager: ObservableObject {
do { do {
let message = try encoder.encode(req) let message = try encoder.encode(req)
guard let messageString = String(data: message, encoding: .utf8) else { guard let messageString = String(data: message, encoding: .utf8) else {
SecureLogger.log("❌ Failed to encode subscription request", category: SecureLogger.session, level: .error) SecureLogger.log("❌ Failed to encode subscription request", category: .session, level: .error)
return return
} }
// SecureLogger.log("📋 Subscription filter JSON: \(messageString.prefix(200))...", // SecureLogger.log("📋 Subscription filter JSON: \(messageString.prefix(200))...",
// category: SecureLogger.session, level: .debug) // category: .session, level: .debug)
// Target specific relays if provided; else default. Filter permanently failed relays. // Target specific relays if provided; else default. Filter permanently failed relays.
let baseUrls = relayUrls ?? Self.defaultRelays let baseUrls = relayUrls ?? Self.defaultRelays
@@ -252,7 +252,7 @@ final class NostrRelayManager: ObservableObject {
self.pendingSubscriptions[url] = map self.pendingSubscriptions[url] = map
} }
SecureLogger.log("📋 Queued subscription id=\(id) for \(urls.count) relay(s)", SecureLogger.log("📋 Queued subscription id=\(id) for \(urls.count) relay(s)",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
// Ensure we actually have sockets opening to these relays so queued REQs can flush // Ensure we actually have sockets opening to these relays so queued REQs can flush
ensureConnections(to: urls) ensureConnections(to: urls)
// If some targets are already connected, flush immediately for them // If some targets are already connected, flush immediately for them
@@ -263,7 +263,7 @@ final class NostrRelayManager: ObservableObject {
} }
} catch { } catch {
SecureLogger.log("❌ Failed to encode subscription request: \(error)", SecureLogger.log("❌ Failed to encode subscription request: \(error)",
category: SecureLogger.session, level: .error) category: .session, level: .error)
} }
} }
@@ -295,7 +295,7 @@ final class NostrRelayManager: ObservableObject {
private func connectToRelay(_ urlString: String) { private func connectToRelay(_ urlString: String) {
guard let url = URL(string: urlString) else { guard let url = URL(string: urlString) else {
SecureLogger.log("Invalid relay URL: \(urlString)", category: SecureLogger.session, level: .warning) SecureLogger.log("Invalid relay URL: \(urlString)", category: .session, level: .warning)
return return
} }
@@ -321,7 +321,7 @@ final class NostrRelayManager: ObservableObject {
let ready = await TorManager.shared.awaitReady() let ready = await TorManager.shared.awaitReady()
await MainActor.run { await MainActor.run {
if ready { self.connectToRelay(urlString) } if ready { self.connectToRelay(urlString) }
else { SecureLogger.log("❌ Tor not ready; skipping connection to \(urlString)", category: SecureLogger.session, level: .error) } else { SecureLogger.log("❌ Tor not ready; skipping connection to \(urlString)", category: .session, level: .error) }
} }
} }
return return
@@ -341,13 +341,13 @@ final class NostrRelayManager: ObservableObject {
DispatchQueue.main.async { DispatchQueue.main.async {
if error == nil { if error == nil {
SecureLogger.log("✅ Connected to Nostr relay: \(urlString)", SecureLogger.log("✅ Connected to Nostr relay: \(urlString)",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
self?.updateRelayStatus(urlString, isConnected: true) self?.updateRelayStatus(urlString, isConnected: true)
// Flush any pending subscriptions for this relay // Flush any pending subscriptions for this relay
self?.flushPendingSubscriptions(for: urlString) self?.flushPendingSubscriptions(for: urlString)
} else { } else {
SecureLogger.log("❌ Failed to connect to Nostr relay \(urlString): \(error?.localizedDescription ?? "Unknown error")", SecureLogger.log("❌ Failed to connect to Nostr relay \(urlString): \(error?.localizedDescription ?? "Unknown error")",
category: SecureLogger.session, level: .error) category: .session, level: .error)
self?.updateRelayStatus(urlString, isConnected: false, error: error) self?.updateRelayStatus(urlString, isConnected: false, error: error)
// Trigger disconnection handler for proper backoff // Trigger disconnection handler for proper backoff
self?.handleDisconnection(relayUrl: urlString, error: error ?? NSError(domain: "NostrRelay", code: -1, userInfo: nil)) self?.handleDisconnection(relayUrl: urlString, error: error ?? NSError(domain: "NostrRelay", code: -1, userInfo: nil))
@@ -365,7 +365,7 @@ final class NostrRelayManager: ObservableObject {
connection.send(.string(messageString)) { error in connection.send(.string(messageString)) { error in
if let error = error { if let error = error {
SecureLogger.log("❌ Failed to send pending subscription to \(relayUrl): \(error)", SecureLogger.log("❌ Failed to send pending subscription to \(relayUrl): \(error)",
category: SecureLogger.session, level: .error) category: .session, level: .error)
} else { } else {
Task { @MainActor in Task { @MainActor in
var subs = self.subscriptions[relayUrl] ?? Set<String>() var subs = self.subscriptions[relayUrl] ?? Set<String>()
@@ -415,7 +415,7 @@ final class NostrRelayManager: ObservableObject {
case .event(let subId, let event): case .event(let subId, let event):
if event.kind != 1059 { if event.kind != 1059 {
SecureLogger.log("📥 Event kind=\(event.kind) id=\(event.id.prefix(16))… relay=\(relayUrl)", SecureLogger.log("📥 Event kind=\(event.kind) id=\(event.id.prefix(16))… relay=\(relayUrl)",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
} }
if let index = self.relays.firstIndex(where: { $0.url == relayUrl }) { if let index = self.relays.firstIndex(where: { $0.url == relayUrl }) {
self.relays[index].messagesReceived += 1 self.relays[index].messagesReceived += 1
@@ -424,7 +424,7 @@ final class NostrRelayManager: ObservableObject {
handler(event) handler(event)
} else { } else {
SecureLogger.log("⚠️ No handler for subscription \(subId)", SecureLogger.log("⚠️ No handler for subscription \(subId)",
category: SecureLogger.session, level: .warning) category: .session, level: .warning)
} }
case .eose: case .eose:
// No-op for now // No-op for now
@@ -433,11 +433,11 @@ final class NostrRelayManager: ObservableObject {
if success { if success {
_ = Self.pendingGiftWrapIDs.remove(eventId) _ = Self.pendingGiftWrapIDs.remove(eventId)
SecureLogger.log("✅ Accepted id=\(eventId.prefix(16))… relay=\(relayUrl)", SecureLogger.log("✅ Accepted id=\(eventId.prefix(16))… relay=\(relayUrl)",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
} else { } else {
let isGiftWrap = Self.pendingGiftWrapIDs.remove(eventId) != nil let isGiftWrap = Self.pendingGiftWrapIDs.remove(eventId) != nil
SecureLogger.log("📮 Rejected id=\(eventId.prefix(16))… reason=\(reason)", SecureLogger.log("📮 Rejected id=\(eventId.prefix(16))… reason=\(reason)",
category: SecureLogger.session, level: isGiftWrap ? .warning : .error) category: .session, level: isGiftWrap ? .warning : .error)
} }
case .notice: case .notice:
break break
@@ -452,16 +452,16 @@ final class NostrRelayManager: ObservableObject {
let message = String(data: data, encoding: .utf8) ?? "" let message = String(data: data, encoding: .utf8) ?? ""
SecureLogger.log("📤 Send kind=\(event.kind) id=\(event.id.prefix(16))… relay=\(relayUrl)", SecureLogger.log("📤 Send kind=\(event.kind) id=\(event.id.prefix(16))… relay=\(relayUrl)",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
connection.send(.string(message)) { [weak self] error in connection.send(.string(message)) { [weak self] error in
DispatchQueue.main.async { DispatchQueue.main.async {
if let error = error { if let error = error {
SecureLogger.log("❌ Failed to send event to \(relayUrl): \(error)", SecureLogger.log("❌ Failed to send event to \(relayUrl): \(error)",
category: SecureLogger.session, level: .error) category: .session, level: .error)
} else { } else {
// SecureLogger.log(" Event sent to relay: \(relayUrl)", // SecureLogger.log(" Event sent to relay: \(relayUrl)",
// category: SecureLogger.session, level: .debug) // category: .session, level: .debug)
// Update relay stats // Update relay stats
if let index = self?.relays.firstIndex(where: { $0.url == relayUrl }) { if let index = self?.relays.firstIndex(where: { $0.url == relayUrl }) {
self?.relays[index].messagesSent += 1 self?.relays[index].messagesSent += 1
@@ -470,7 +470,7 @@ final class NostrRelayManager: ObservableObject {
} }
} }
} catch { } catch {
SecureLogger.log("Failed to encode event: \(error)", category: SecureLogger.session, level: .error) SecureLogger.log("Failed to encode event: \(error)", category: .session, level: .error)
} }
} }
@@ -509,7 +509,7 @@ final class NostrRelayManager: ObservableObject {
errorDescription.contains("dns") || errorDescription.contains("dns") ||
(ns.domain == NSURLErrorDomain && ns.code == NSURLErrorBadServerResponse) { (ns.domain == NSURLErrorDomain && ns.code == NSURLErrorBadServerResponse) {
if relays.first(where: { $0.url == relayUrl })?.lastError == nil { if relays.first(where: { $0.url == relayUrl })?.lastError == nil {
SecureLogger.log("Nostr relay permanent failure for \(relayUrl) - not retrying (code=\(ns.code))", category: SecureLogger.session, level: .warning) SecureLogger.log("Nostr relay permanent failure for \(relayUrl) - not retrying (code=\(ns.code))", category: .session, level: .warning)
} }
if let index = relays.firstIndex(where: { $0.url == relayUrl }) { if let index = relays.firstIndex(where: { $0.url == relayUrl }) {
relays[index].lastError = error relays[index].lastError = error
@@ -528,7 +528,7 @@ final class NostrRelayManager: ObservableObject {
// Stop attempting after max attempts // Stop attempting after max attempts
if relays[index].reconnectAttempts >= maxReconnectAttempts { if relays[index].reconnectAttempts >= maxReconnectAttempts {
SecureLogger.log("Max reconnection attempts (\(maxReconnectAttempts)) reached for \(relayUrl)", SecureLogger.log("Max reconnection attempts (\(maxReconnectAttempts)) reached for \(relayUrl)",
category: SecureLogger.session, level: .warning) category: .session, level: .warning)
return return
} }
+94 -94
View File
@@ -231,7 +231,7 @@ final class BLEService: NSObject {
// If single chunk exceeds cap, drop it immediately // If single chunk exceeds cap, drop it immediately
if newSize > capBytes { if newSize > capBytes {
SecureLogger.log("⚠️ Dropping oversized write chunk (\(newSize)B) for peripheral \(uuid)", SecureLogger.log("⚠️ Dropping oversized write chunk (\(newSize)B) for peripheral \(uuid)",
category: SecureLogger.session, level: .warning) category: .session, level: .warning)
} else { } else {
// Append and trim from the front to respect cap // Append and trim from the front to respect cap
var total = queue.reduce(0) { $0 + $1.count } var total = queue.reduce(0) { $0 + $1.count }
@@ -245,7 +245,7 @@ final class BLEService: NSObject {
total -= removed.count total -= removed.count
} }
SecureLogger.log("📉 Trimmed pending write buffer for \(uuid) by \(removedBytes)B to \(total)B", SecureLogger.log("📉 Trimmed pending write buffer for \(uuid) by \(removedBytes)B to \(total)B",
category: SecureLogger.session, level: .warning) category: .session, level: .warning)
} }
self.pendingPeripheralWrites[uuid] = queue.isEmpty ? nil : queue self.pendingPeripheralWrites[uuid] = queue.isEmpty ? nil : queue
} }
@@ -342,7 +342,7 @@ final class BLEService: NSObject {
// This ensures we send pending messages only when session is truly established // This ensures we send pending messages only when session is truly established
noiseService.onPeerAuthenticated = { [weak self] peerID, fingerprint in noiseService.onPeerAuthenticated = { [weak self] peerID, fingerprint in
SecureLogger.log("🔐 Noise session authenticated with \(peerID), fingerprint: \(fingerprint.prefix(16))...", SecureLogger.log("🔐 Noise session authenticated with \(peerID), fingerprint: \(fingerprint.prefix(16))...",
category: SecureLogger.noise, level: .debug) category: .noise, level: .debug)
// Send any messages that were queued during handshake // Send any messages that were queued during handshake
self?.messageQueue.async { [weak self] in self?.messageQueue.async { [weak self] in
self?.sendPendingMessagesAfterHandshake(for: peerID) self?.sendPendingMessagesAfterHandshake(for: peerID)
@@ -588,7 +588,7 @@ final class BLEService: NSObject {
func sendFavoriteNotification(to peerID: String, isFavorite: Bool) { func sendFavoriteNotification(to peerID: String, isFavorite: Bool) {
SecureLogger.log("🔔 sendFavoriteNotification called - peerID: \(peerID), isFavorite: \(isFavorite)", SecureLogger.log("🔔 sendFavoriteNotification called - peerID: \(peerID), isFavorite: \(isFavorite)",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
// Include Nostr public key in the notification // Include Nostr public key in the notification
var content = isFavorite ? "[FAVORITED]" : "[UNFAVORITED]" var content = isFavorite ? "[FAVORITED]" : "[UNFAVORITED]"
@@ -597,11 +597,11 @@ final class BLEService: NSObject {
if let myNostrIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() { if let myNostrIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() {
content += ":" + myNostrIdentity.npub content += ":" + myNostrIdentity.npub
SecureLogger.log("📝 Sending favorite notification with Nostr npub: \(myNostrIdentity.npub)", SecureLogger.log("📝 Sending favorite notification with Nostr npub: \(myNostrIdentity.npub)",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
} }
SecureLogger.log("📤 Sending favorite notification to \(peerID): \(content)", SecureLogger.log("📤 Sending favorite notification to \(peerID): \(content)",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
sendPrivateMessage(content, to: peerID, messageID: UUID().uuidString) sendPrivateMessage(content, to: peerID, messageID: UUID().uuidString)
} }
@@ -612,7 +612,7 @@ final class BLEService: NSObject {
if noiseService.hasEstablishedSession(with: peerID) { if noiseService.hasEstablishedSession(with: peerID) {
SecureLogger.log("📤 Sending READ receipt for message \(receipt.originalMessageID) to \(peerID)", SecureLogger.log("📤 Sending READ receipt for message \(receipt.originalMessageID) to \(peerID)",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
do { do {
let encrypted = try noiseService.encrypt(payload, for: peerID) let encrypted = try noiseService.encrypt(payload, for: peerID)
let packet = BitchatPacket( let packet = BitchatPacket(
@@ -630,7 +630,7 @@ final class BLEService: NSObject {
messageQueue.async { [weak self] in self?.broadcastPacket(packet) } messageQueue.async { [weak self] in self?.broadcastPacket(packet) }
} }
} catch { } catch {
SecureLogger.log("Failed to send read receipt: \(error)", category: SecureLogger.noise, level: .error) SecureLogger.log("Failed to send read receipt: \(error)", category: .noise, level: .error)
} }
} else { } else {
// Queue for after handshake and initiate if needed // Queue for after handshake and initiate if needed
@@ -640,7 +640,7 @@ final class BLEService: NSObject {
} }
if !noiseService.hasSession(with: peerID) { initiateNoiseHandshake(with: peerID) } if !noiseService.hasSession(with: peerID) { initiateNoiseHandshake(with: peerID) }
SecureLogger.log("🕒 Queued READ receipt for \(peerID) until handshake completes", SecureLogger.log("🕒 Queued READ receipt for \(peerID) until handshake completes",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
} }
} }
@@ -682,7 +682,7 @@ final class BLEService: NSObject {
messageQueue.async { [weak self] in self?.broadcastPacket(packet) } messageQueue.async { [weak self] in self?.broadcastPacket(packet) }
} }
} catch { } catch {
SecureLogger.log("Failed to send verification payload: \(error)", category: SecureLogger.noise, level: .error) SecureLogger.log("Failed to send verification payload: \(error)", category: .noise, level: .error)
} }
} }
@@ -748,7 +748,7 @@ final class BLEService: NSObject {
guard let self = self else { return } guard let self = self else { return }
guard content.count <= self.maxMessageLength else { guard content.count <= self.maxMessageLength else {
SecureLogger.log("Message too long: \(content.count) chars", category: SecureLogger.session, level: .error) SecureLogger.log("Message too long: \(content.count) chars", category: .session, level: .error)
return return
} }
@@ -771,7 +771,7 @@ final class BLEService: NSObject {
ttl: self.messageTTL ttl: self.messageTTL
) )
guard let signedPacket = self.noiseService.signPacket(basePacket) else { guard let signedPacket = self.noiseService.signPacket(basePacket) else {
SecureLogger.log("❌ Failed to sign public message", category: SecureLogger.security, level: .error) SecureLogger.log("❌ Failed to sign public message", category: .security, level: .error)
return return
} }
// Pre-mark our own broadcast as processed to avoid handling relayed self copy // Pre-mark our own broadcast as processed to avoid handling relayed self copy
@@ -789,7 +789,7 @@ final class BLEService: NSObject {
// MARK: - Private Message Handling // MARK: - Private Message Handling
private func sendPrivateMessage(_ content: String, to recipientID: String, messageID: String) { private func sendPrivateMessage(_ content: String, to recipientID: String, messageID: String) {
SecureLogger.log("📨 Sending PM to \(recipientID): \(content.prefix(30))...", category: SecureLogger.session, level: .debug) SecureLogger.log("📨 Sending PM to \(recipientID): \(content.prefix(30))...", category: .session, level: .debug)
// Check if we have an established Noise session // Check if we have an established Noise session
if noiseService.hasEstablishedSession(with: recipientID) { if noiseService.hasEstablishedSession(with: recipientID) {
@@ -798,7 +798,7 @@ final class BLEService: NSObject {
// Create TLV-encoded private message // Create TLV-encoded private message
let privateMessage = PrivateMessagePacket(messageID: messageID, content: content) let privateMessage = PrivateMessagePacket(messageID: messageID, content: content)
guard let tlvData = privateMessage.encode() else { guard let tlvData = privateMessage.encode() else {
SecureLogger.log("Failed to encode private message with TLV", category: SecureLogger.noise, level: .error) SecureLogger.log("Failed to encode private message with TLV", category: .noise, level: .error)
return return
} }
@@ -847,11 +847,11 @@ final class BLEService: NSObject {
self?.delegate?.didUpdateMessageDeliveryStatus(messageID, status: .sent) self?.delegate?.didUpdateMessageDeliveryStatus(messageID, status: .sent)
} }
} catch { } catch {
SecureLogger.log("Failed to encrypt message: \(error)", category: SecureLogger.noise, level: .error) SecureLogger.log("Failed to encrypt message: \(error)", category: .noise, level: .error)
} }
} else { } else {
// Queue message for sending after handshake completes // Queue message for sending after handshake completes
SecureLogger.log("🤝 No session with \(recipientID), initiating handshake and queueing message", category: SecureLogger.session, level: .debug) SecureLogger.log("🤝 No session with \(recipientID), initiating handshake and queueing message", category: .session, level: .debug)
// Queue the message (especially important for favorite notifications) // Queue the message (especially important for favorite notifications)
collectionsQueue.sync(flags: .barrier) { collectionsQueue.sync(flags: .barrier) {
@@ -896,7 +896,7 @@ final class BLEService: NSObject {
} }
} }
} catch { } catch {
SecureLogger.log("Failed to initiate handshake: \(error)", category: SecureLogger.noise, level: .error) SecureLogger.log("Failed to initiate handshake: \(error)", category: .noise, level: .error)
} }
} }
@@ -911,7 +911,7 @@ final class BLEService: NSObject {
guard let messages = pendingMessages, !messages.isEmpty else { return } guard let messages = pendingMessages, !messages.isEmpty else { return }
SecureLogger.log("📤 Sending \(messages.count) pending messages after handshake to \(peerID)", SecureLogger.log("📤 Sending \(messages.count) pending messages after handshake to \(peerID)",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
// Send each pending message directly (we know session is established) // Send each pending message directly (we know session is established)
for (content, messageID) in messages { for (content, messageID) in messages {
@@ -919,7 +919,7 @@ final class BLEService: NSObject {
// Use the same TLV format as normal sends to keep receiver decoding consistent // Use the same TLV format as normal sends to keep receiver decoding consistent
let privateMessage = PrivateMessagePacket(messageID: messageID, content: content) let privateMessage = PrivateMessagePacket(messageID: messageID, content: content)
guard let tlvData = privateMessage.encode() else { guard let tlvData = privateMessage.encode() else {
SecureLogger.log("Failed to encode pending private message TLV", category: SecureLogger.noise, level: .error) SecureLogger.log("Failed to encode pending private message TLV", category: .noise, level: .error)
continue continue
} }
@@ -947,10 +947,10 @@ final class BLEService: NSObject {
} }
SecureLogger.log("✅ Sent pending message \(messageID) to \(peerID) after handshake", SecureLogger.log("✅ Sent pending message \(messageID) to \(peerID) after handshake",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
} catch { } catch {
SecureLogger.log("Failed to send pending message after handshake: \(error)", SecureLogger.log("Failed to send pending message after handshake: \(error)",
category: SecureLogger.noise, level: .error) category: .noise, level: .error)
// Notify delegate of failure // Notify delegate of failure
notifyUI { [weak self] in notifyUI { [weak self] in
@@ -966,7 +966,7 @@ final class BLEService: NSObject {
// Encode once using a small per-type padding policy, then delegate by type // Encode once using a small per-type padding policy, then delegate by type
let padForBLE = padPolicy(for: packet.type) let padForBLE = padPolicy(for: packet.type)
guard let data = packet.toBinaryData(padding: padForBLE) else { guard let data = packet.toBinaryData(padding: padForBLE) else {
SecureLogger.log("❌ Failed to convert packet to binary data", category: SecureLogger.session, level: .error) SecureLogger.log("❌ Failed to convert packet to binary data", category: .session, level: .error)
return return
} }
if packet.type == MessageType.noiseEncrypted.rawValue { if packet.type == MessageType.noiseEncrypted.rawValue {
@@ -1037,7 +1037,7 @@ final class BLEService: NSObject {
guard let self = self else { return } guard let self = self else { return }
if self.pendingNotifications.count < TransportConfig.blePendingNotificationsCapCount { if self.pendingNotifications.count < TransportConfig.blePendingNotificationsCapCount {
self.pendingNotifications.append((data: data, centrals: [central])) self.pendingNotifications.append((data: data, centrals: [central]))
SecureLogger.log("📋 Queued encrypted packet for retry (notification queue full)", category: SecureLogger.session, level: .debug) SecureLogger.log("📋 Queued encrypted packet for retry (notification queue full)", category: .session, level: .debug)
} }
} }
} }
@@ -1150,7 +1150,7 @@ final class BLEService: NSObject {
if byMsg[msgID] == nil { if byMsg[msgID] == nil {
byMsg[msgID] = (packet: packet, enqueuedAt: Date()) byMsg[msgID] = (packet: packet, enqueuedAt: Date())
self.pendingDirectedRelays[recipientPeerID] = byMsg self.pendingDirectedRelays[recipientPeerID] = byMsg
SecureLogger.log("🧳 Spooling directed packet for \(recipientPeerID) mid=\(msgID.prefix(8))", category: SecureLogger.session, level: .debug) SecureLogger.log("🧳 Spooling directed packet for \(recipientPeerID) mid=\(msgID.prefix(8))", category: .session, level: .debug)
} }
} }
} }
@@ -1322,7 +1322,7 @@ final class BLEService: NSObject {
if let originalPacket = BinaryProtocol.decode(reassembled) { if let originalPacket = BinaryProtocol.decode(reassembled) {
handleReceivedPacket(originalPacket, from: peerID) handleReceivedPacket(originalPacket, from: peerID)
} else { } else {
SecureLogger.log("❌ Failed to decode reassembled packet (type=\(originalType), total=\(total))", category: SecureLogger.session, level: .error) SecureLogger.log("❌ Failed to decode reassembled packet (type=\(originalType), total=\(total))", category: .session, level: .error)
} }
// Cleanup // Cleanup
@@ -1343,7 +1343,7 @@ final class BLEService: NSObject {
if packet.type != MessageType.announce.rawValue { if packet.type != MessageType.announce.rawValue {
// Log packet details for debugging // Log packet details for debugging
SecureLogger.log("📦 Handling packet type \(packet.type) from \(senderID), messageID: \(messageID)", SecureLogger.log("📦 Handling packet type \(packet.type) from \(senderID), messageID: \(messageID)",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
} }
// Efficient deduplication // Efficient deduplication
@@ -1353,7 +1353,7 @@ final class BLEService: NSObject {
// It's normal to see these as duplicates - don't log them to reduce noise // It's normal to see these as duplicates - don't log them to reduce noise
if packet.type != MessageType.announce.rawValue { if packet.type != MessageType.announce.rawValue {
SecureLogger.log("⚠️ Duplicate packet ignored: \(messageID)", SecureLogger.log("⚠️ Duplicate packet ignored: \(messageID)",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
} }
// In sparse graphs (<=2 neighbors), keep the pending relay to ensure bridging. // In sparse graphs (<=2 neighbors), keep the pending relay to ensure bridging.
// In denser graphs, cancel the pending relay to reduce redundant floods. // In denser graphs, cancel the pending relay to reduce redundant floods.
@@ -1406,7 +1406,7 @@ final class BLEService: NSObject {
handleLeave(packet, from: senderID) handleLeave(packet, from: senderID)
default: default:
SecureLogger.log("⚠️ Unknown message type: \(packet.type)", category: SecureLogger.session, level: .warning) SecureLogger.log("⚠️ Unknown message type: \(packet.type)", category: .session, level: .warning)
break break
} }
@@ -1446,7 +1446,7 @@ final class BLEService: NSObject {
private func handleAnnounce(_ packet: BitchatPacket, from peerID: String) { private func handleAnnounce(_ packet: BitchatPacket, from peerID: String) {
guard let announcement = AnnouncementPacket.decode(from: packet.payload) else { guard let announcement = AnnouncementPacket.decode(from: packet.payload) else {
SecureLogger.log("❌ Failed to decode announce packet from \(peerID)", category: SecureLogger.session, level: .error) SecureLogger.log("❌ Failed to decode announce packet from \(peerID)", category: .session, level: .error)
return return
} }
@@ -1454,7 +1454,7 @@ final class BLEService: NSObject {
// This helps detect relayed or spoofed announces. Only warn in release; assert in debug. // This helps detect relayed or spoofed announces. Only warn in release; assert in debug.
let derivedFromKey = PeerIDUtils.derivePeerID(fromPublicKey: announcement.noisePublicKey) let derivedFromKey = PeerIDUtils.derivePeerID(fromPublicKey: announcement.noisePublicKey)
if derivedFromKey != peerID { if derivedFromKey != peerID {
SecureLogger.log("⚠️ Announce sender mismatch: derived \(derivedFromKey.prefix(8))… vs packet \(peerID.prefix(8))", category: SecureLogger.security, level: .warning) SecureLogger.log("⚠️ Announce sender mismatch: derived \(derivedFromKey.prefix(8))… vs packet \(peerID.prefix(8))", category: .security, level: .warning)
} }
@@ -1471,11 +1471,11 @@ final class BLEService: NSObject {
if packet.signature != nil { if packet.signature != nil {
verifiedAnnounce = noiseService.verifyPacketSignature(packet, publicKey: announcement.signingPublicKey) verifiedAnnounce = noiseService.verifyPacketSignature(packet, publicKey: announcement.signingPublicKey)
if !verifiedAnnounce { if !verifiedAnnounce {
SecureLogger.log("⚠️ Signature verification for announce failed \(peerID.prefix(8))", category: SecureLogger.security, level: .warning) SecureLogger.log("⚠️ Signature verification for announce failed \(peerID.prefix(8))", category: .security, level: .warning)
} }
} }
if let existingKey = existingPeerForVerify?.noisePublicKey, existingKey != announcement.noisePublicKey { if let existingKey = existingPeerForVerify?.noisePublicKey, existingKey != announcement.noisePublicKey {
SecureLogger.log("⚠️ Announce key mismatch for \(peerID.prefix(8))… — keeping unverified", category: SecureLogger.security, level: .warning) SecureLogger.log("⚠️ Announce key mismatch for \(peerID.prefix(8))… — keeping unverified", category: .security, level: .warning)
verifiedAnnounce = false verifiedAnnounce = false
} }
@@ -1508,7 +1508,7 @@ final class BLEService: NSObject {
// Require verified announce; ignore otherwise (no backward compatibility) // Require verified announce; ignore otherwise (no backward compatibility)
if !verified { if !verified {
SecureLogger.log("❌ Ignoring unverified announce from \(peerID.prefix(8))", category: SecureLogger.security, level: .warning) SecureLogger.log("❌ Ignoring unverified announce from \(peerID.prefix(8))", category: .security, level: .warning)
return return
} }
@@ -1541,17 +1541,17 @@ final class BLEService: NSObject {
if isDirectAnnounce || hasPeripheralConnection || hasCentralSubscription { if isDirectAnnounce || hasPeripheralConnection || hasCentralSubscription {
let now = Date() let now = Date()
if existingPeer == nil { if existingPeer == nil {
SecureLogger.log("🆕 New peer: \(announcement.nickname)", category: SecureLogger.session, level: .debug) SecureLogger.log("🆕 New peer: \(announcement.nickname)", category: .session, level: .debug)
} else if wasDisconnected { } else if wasDisconnected {
// Debounce 'reconnected' logs within short window // Debounce 'reconnected' logs within short window
if let last = lastReconnectLogAt[peerID], now.timeIntervalSince(last) < TransportConfig.bleReconnectLogDebounceSeconds { if let last = lastReconnectLogAt[peerID], now.timeIntervalSince(last) < TransportConfig.bleReconnectLogDebounceSeconds {
// Skip duplicate log // Skip duplicate log
} else { } else {
SecureLogger.log("🔄 Peer \(announcement.nickname) reconnected", category: SecureLogger.session, level: .debug) SecureLogger.log("🔄 Peer \(announcement.nickname) reconnected", category: .session, level: .debug)
lastReconnectLogAt[peerID] = now lastReconnectLogAt[peerID] = now
} }
} else if existingPeer?.nickname != announcement.nickname { } else if existingPeer?.nickname != announcement.nickname {
SecureLogger.log("🔄 Peer \(peerID) changed nickname: \(existingPeer?.nickname ?? "Unknown") -> \(announcement.nickname)", category: SecureLogger.session, level: .debug) SecureLogger.log("🔄 Peer \(peerID) changed nickname: \(existingPeer?.nickname ?? "Unknown") -> \(announcement.nickname)", category: .session, level: .debug)
} }
} }
} }
@@ -1661,12 +1661,12 @@ final class BLEService: NSObject {
} }
guard accepted else { guard accepted else {
SecureLogger.log("🚫 Dropping public message from unverified or unknown peer \(peerID.prefix(8))", category: SecureLogger.security, level: .warning) SecureLogger.log("🚫 Dropping public message from unverified or unknown peer \(peerID.prefix(8))", category: .security, level: .warning)
return return
} }
guard let content = String(data: packet.payload, encoding: .utf8) else { guard let content = String(data: packet.payload, encoding: .utf8) else {
SecureLogger.log("❌ Failed to decode message payload as UTF-8", category: SecureLogger.session, level: .error) SecureLogger.log("❌ Failed to decode message payload as UTF-8", category: .session, level: .error)
return return
} }
// Determine if we have a direct link to the sender // Determine if we have a direct link to the sender
@@ -1678,7 +1678,7 @@ final class BLEService: NSObject {
} }
let pathTag = hasDirectLink ? "direct" : "mesh" let pathTag = hasDirectLink ? "direct" : "mesh"
SecureLogger.log("💬 [\(senderNickname)] TTL:\(packet.ttl) (\(pathTag)): \(String(content.prefix(50)))\(content.count > 50 ? "..." : "")", category: SecureLogger.session, level: .debug) SecureLogger.log("💬 [\(senderNickname)] TTL:\(packet.ttl) (\(pathTag)): \(String(content.prefix(50)))\(content.count > 50 ? "..." : "")", category: .session, level: .debug)
let ts = Date(timeIntervalSince1970: Double(packet.timestamp) / 1000) let ts = Date(timeIntervalSince1970: Double(packet.timestamp) / 1000)
notifyUI { [weak self] in notifyUI { [weak self] in
@@ -1710,7 +1710,7 @@ final class BLEService: NSObject {
// Session establishment will trigger onPeerAuthenticated callback // Session establishment will trigger onPeerAuthenticated callback
// which will send any pending messages at the right time // which will send any pending messages at the right time
} catch { } catch {
SecureLogger.log("Failed to process handshake: \(error)", category: SecureLogger.noise, level: .error) SecureLogger.log("Failed to process handshake: \(error)", category: .noise, level: .error)
// Try initiating a new handshake // Try initiating a new handshake
if !noiseService.hasSession(with: peerID) { if !noiseService.hasSession(with: peerID) {
initiateNoiseHandshake(with: peerID) initiateNoiseHandshake(with: peerID)
@@ -1721,16 +1721,16 @@ final class BLEService: NSObject {
private func handleNoiseEncrypted(_ packet: BitchatPacket, from peerID: String) { private func handleNoiseEncrypted(_ packet: BitchatPacket, from peerID: String) {
SecureLogger.log("🔐 handleNoiseEncrypted called for packet from \(peerID)", SecureLogger.log("🔐 handleNoiseEncrypted called for packet from \(peerID)",
category: SecureLogger.noise, level: .debug) category: .noise, level: .debug)
guard let recipientID = packet.recipientID else { guard let recipientID = packet.recipientID else {
SecureLogger.log("⚠️ Encrypted message has no recipient ID", category: SecureLogger.session, level: .warning) SecureLogger.log("⚠️ Encrypted message has no recipient ID", category: .session, level: .warning)
return return
} }
let recipientHex = recipientID.hexEncodedString() let recipientHex = recipientID.hexEncodedString()
if recipientHex != myPeerID { if recipientHex != myPeerID {
SecureLogger.log("🔐 Encrypted message not for me (for \(recipientHex), I am \(myPeerID))", category: SecureLogger.session, level: .debug) SecureLogger.log("🔐 Encrypted message not for me (for \(recipientHex), I am \(myPeerID))", category: .session, level: .debug)
return return
} }
@@ -1772,19 +1772,19 @@ final class BLEService: NSObject {
self?.delegate?.didReceiveNoisePayload(from: peerID, type: .verifyResponse, payload: Data(payloadData), timestamp: ts) self?.delegate?.didReceiveNoisePayload(from: peerID, type: .verifyResponse, payload: Data(payloadData), timestamp: ts)
} }
default: default:
SecureLogger.log("⚠️ Unknown noise payload type: \(payloadType)", category: SecureLogger.noise, level: .warning) SecureLogger.log("⚠️ Unknown noise payload type: \(payloadType)", category: .noise, level: .warning)
} }
} catch NoiseEncryptionError.sessionNotEstablished { } catch NoiseEncryptionError.sessionNotEstablished {
// We received an encrypted message before establishing a session with this peer. // We received an encrypted message before establishing a session with this peer.
// Trigger a handshake so future messages can be decrypted. // Trigger a handshake so future messages can be decrypted.
SecureLogger.log("🔑 Encrypted message from \(peerID) without session; initiating handshake", SecureLogger.log("🔑 Encrypted message from \(peerID) without session; initiating handshake",
category: SecureLogger.noise, level: .debug) category: .noise, level: .debug)
if !noiseService.hasSession(with: peerID) { if !noiseService.hasSession(with: peerID) {
initiateNoiseHandshake(with: peerID) initiateNoiseHandshake(with: peerID)
} }
} catch { } catch {
SecureLogger.log("❌ Failed to decrypt message from \(peerID): \(error)", SecureLogger.log("❌ Failed to decrypt message from \(peerID): \(error)",
category: SecureLogger.noise, level: .error) category: .noise, level: .error)
} }
} }
@@ -1808,7 +1808,7 @@ final class BLEService: NSObject {
// MARK: - Helper Functions // MARK: - Helper Functions
private func sendLeave() { private func sendLeave() {
SecureLogger.log("👋 Sending leave announcement", category: SecureLogger.session, level: .debug) SecureLogger.log("👋 Sending leave announcement", category: .session, level: .debug)
let packet = BitchatPacket( let packet = BitchatPacket(
type: MessageType.leave.rawValue, type: MessageType.leave.rawValue,
ttl: messageTTL, ttl: messageTTL,
@@ -1845,7 +1845,7 @@ final class BLEService: NSObject {
) )
guard let payload = announcement.encode() else { guard let payload = announcement.encode() else {
SecureLogger.log("❌ Failed to encode announce packet", category: SecureLogger.session, level: .error) SecureLogger.log("❌ Failed to encode announce packet", category: .session, level: .error)
return return
} }
@@ -1862,7 +1862,7 @@ final class BLEService: NSObject {
// Sign the packet using the noise private key // Sign the packet using the noise private key
guard let signedPacket = noiseService.signPacket(packet) else { guard let signedPacket = noiseService.signPacket(packet) else {
SecureLogger.log("❌ Failed to sign announce packet", category: SecureLogger.security, level: .error) SecureLogger.log("❌ Failed to sign announce packet", category: .security, level: .error)
return return
} }
@@ -1895,7 +1895,7 @@ final class BLEService: NSObject {
) )
broadcastPacket(packet) broadcastPacket(packet)
} catch { } catch {
SecureLogger.log("Failed to send delivery ACK: \(error)", category: SecureLogger.noise, level: .error) SecureLogger.log("Failed to send delivery ACK: \(error)", category: .noise, level: .error)
} }
} else { } else {
// Queue for after handshake and initiate if needed // Queue for after handshake and initiate if needed
@@ -1905,7 +1905,7 @@ final class BLEService: NSObject {
} }
if !noiseService.hasSession(with: peerID) { initiateNoiseHandshake(with: peerID) } if !noiseService.hasSession(with: peerID) { initiateNoiseHandshake(with: peerID) }
SecureLogger.log("🕒 Queued DELIVERED ack for \(peerID) until handshake completes", SecureLogger.log("🕒 Queued DELIVERED ack for \(peerID) until handshake completes",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
} }
} }
@@ -1917,7 +1917,7 @@ final class BLEService: NSObject {
} }
guard !payloads.isEmpty else { return } guard !payloads.isEmpty else { return }
SecureLogger.log("📤 Sending \(payloads.count) pending noise payloads to \(peerID) after handshake", SecureLogger.log("📤 Sending \(payloads.count) pending noise payloads to \(peerID) after handshake",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
for payload in payloads { for payload in payloads {
do { do {
let encrypted = try noiseService.encrypt(payload, for: peerID) let encrypted = try noiseService.encrypt(payload, for: peerID)
@@ -1933,7 +1933,7 @@ final class BLEService: NSObject {
broadcastPacket(packet) broadcastPacket(packet)
} catch { } catch {
SecureLogger.log("❌ Failed to send pending noise payload to \(peerID): \(error)", SecureLogger.log("❌ Failed to send pending noise payload to \(peerID): \(error)",
category: SecureLogger.noise, level: .error) category: .noise, level: .error)
} }
} }
} }
@@ -2104,7 +2104,7 @@ final class BLEService: NSObject {
if !peer.isConnected { if !peer.isConnected {
if age > retention { if age > retention {
SecureLogger.log("🗑️ Removing stale peer after reachability window: \(peerID) (\(peer.nickname))", SecureLogger.log("🗑️ Removing stale peer after reachability window: \(peerID) (\(peer.nickname))",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
peers.removeValue(forKey: peerID) peers.removeValue(forKey: peerID)
removedOfflineCount += 1 removedOfflineCount += 1
} }
@@ -2396,7 +2396,7 @@ extension BLEService: CBCentralManagerDelegate {
// Connect to the peripheral with options for faster connection // Connect to the peripheral with options for faster connection
SecureLogger.log("📱 Connect: \(advertisedName) [RSSI:\(rssiValue)]", SecureLogger.log("📱 Connect: \(advertisedName) [RSSI:\(rssiValue)]",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
// Use connection options for faster reconnection // Use connection options for faster reconnection
let options: [String: Any] = [ let options: [String: Any] = [
@@ -2416,7 +2416,7 @@ extension BLEService: CBCentralManagerDelegate {
// Connection timed out - cancel it // Connection timed out - cancel it
SecureLogger.log("⏱️ Timeout: \(advertisedName)", SecureLogger.log("⏱️ Timeout: \(advertisedName)",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
central.cancelPeripheralConnection(peripheral) central.cancelPeripheralConnection(peripheral)
self.peripherals[peripheralID] = nil self.peripherals[peripheralID] = nil
self.recentConnectTimeouts[peripheralID] = Date() self.recentConnectTimeouts[peripheralID] = Date()
@@ -2449,7 +2449,7 @@ func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeriph
failureCounts[peripheralID] = 0 failureCounts[peripheralID] = 0
recentConnectTimeouts.removeValue(forKey: peripheralID) recentConnectTimeouts.removeValue(forKey: peripheralID)
SecureLogger.log("✅ Connected: \(peripheral.name ?? "Unknown") [\(peripheralID)]", category: SecureLogger.session, level: .debug) SecureLogger.log("✅ Connected: \(peripheral.name ?? "Unknown") [\(peripheralID)]", category: .session, level: .debug)
// Discover services // Discover services
peripheral.discoverServices([BLEService.serviceUUID]) peripheral.discoverServices([BLEService.serviceUUID])
@@ -2462,7 +2462,7 @@ func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeriph
let peerID = peripherals[peripheralID]?.peerID let peerID = peripherals[peripheralID]?.peerID
SecureLogger.log("📱 Disconnect: \(peerID ?? peripheralID)\(error != nil ? " (\(error!.localizedDescription))" : "")", SecureLogger.log("📱 Disconnect: \(peerID ?? peripheralID)\(error != nil ? " (\(error!.localizedDescription))" : "")",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
// If disconnect carried an error (often timeout), apply short backoff to avoid thrash // If disconnect carried an error (often timeout), apply short backoff to avoid thrash
if error != nil { if error != nil {
@@ -2517,7 +2517,7 @@ func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeriph
// Clean up the references // Clean up the references
peripherals.removeValue(forKey: peripheralID) peripherals.removeValue(forKey: peripheralID)
SecureLogger.log("❌ Failed to connect to peripheral: \(peripheral.name ?? "Unknown") [\(peripheralID)] - Error: \(error?.localizedDescription ?? "Unknown")", category: SecureLogger.session, level: .error) SecureLogger.log("❌ Failed to connect to peripheral: \(peripheral.name ?? "Unknown") [\(peripheralID)] - Error: \(error?.localizedDescription ?? "Unknown")", category: .session, level: .error)
failureCounts[peripheralID, default: 0] += 1 failureCounts[peripheralID, default: 0] += 1
// Try next candidate // Try next candidate
bleQueue.async { [weak self] in self?.tryConnectFromQueue() } bleQueue.async { [weak self] in self?.tryConnectFromQueue() }
@@ -2590,7 +2590,7 @@ extension BLEService {
] ]
central.connect(peripheral, options: options) central.connect(peripheral, options: options)
lastGlobalConnectAttempt = Date() lastGlobalConnectAttempt = Date()
SecureLogger.log("⏩ Queue connect: \(candidate.name) [RSSI:\(candidate.rssi)]", category: SecureLogger.session, level: .debug) SecureLogger.log("⏩ Queue connect: \(candidate.name) [RSSI:\(candidate.rssi)]", category: .session, level: .debug)
} }
} }
@@ -2635,7 +2635,7 @@ extension BLEService {
extension BLEService: CBPeripheralDelegate { extension BLEService: CBPeripheralDelegate {
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
if let error = error { if let error = error {
SecureLogger.log("❌ Error discovering services for \(peripheral.name ?? "Unknown"): \(error.localizedDescription)", category: SecureLogger.session, level: .error) SecureLogger.log("❌ Error discovering services for \(peripheral.name ?? "Unknown"): \(error.localizedDescription)", category: .session, level: .error)
// Retry service discovery after a delay // Retry service discovery after a delay
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
guard peripheral.state == .connected else { return } guard peripheral.state == .connected else { return }
@@ -2645,7 +2645,7 @@ extension BLEService: CBPeripheralDelegate {
} }
guard let services = peripheral.services else { guard let services = peripheral.services else {
SecureLogger.log("⚠️ No services discovered for \(peripheral.name ?? "Unknown")", category: SecureLogger.session, level: .warning) SecureLogger.log("⚠️ No services discovered for \(peripheral.name ?? "Unknown")", category: .session, level: .warning)
return return
} }
@@ -2661,12 +2661,12 @@ extension BLEService: CBPeripheralDelegate {
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
if let error = error { if let error = error {
SecureLogger.log("❌ Error discovering characteristics for \(peripheral.name ?? "Unknown"): \(error.localizedDescription)", category: SecureLogger.session, level: .error) SecureLogger.log("❌ Error discovering characteristics for \(peripheral.name ?? "Unknown"): \(error.localizedDescription)", category: .session, level: .error)
return return
} }
guard let characteristic = service.characteristics?.first(where: { $0.uuid == BLEService.characteristicUUID }) else { guard let characteristic = service.characteristics?.first(where: { $0.uuid == BLEService.characteristicUUID }) else {
SecureLogger.log("⚠️ No matching characteristic found for \(peripheral.name ?? "Unknown")", category: SecureLogger.session, level: .warning) SecureLogger.log("⚠️ No matching characteristic found for \(peripheral.name ?? "Unknown")", category: .session, level: .warning)
return return
} }
@@ -2683,7 +2683,7 @@ extension BLEService: CBPeripheralDelegate {
// Verify characteristic supports reliable writes // Verify characteristic supports reliable writes
if !characteristic.properties.contains(.write) { if !characteristic.properties.contains(.write) {
SecureLogger.log("⚠️ Characteristic doesn't support reliable writes (withResponse)!", category: SecureLogger.session, level: .warning) SecureLogger.log("⚠️ Characteristic doesn't support reliable writes (withResponse)!", category: .session, level: .warning)
} }
// Store characteristic in our consolidated structure // Store characteristic in our consolidated structure
@@ -2696,7 +2696,7 @@ extension BLEService: CBPeripheralDelegate {
// Subscribe for notifications // Subscribe for notifications
if characteristic.properties.contains(.notify) { if characteristic.properties.contains(.notify) {
peripheral.setNotifyValue(true, for: characteristic) peripheral.setNotifyValue(true, for: characteristic)
SecureLogger.log("🔔 Subscribed to notifications from \(peripheral.name ?? "Unknown")", category: SecureLogger.session, level: .debug) SecureLogger.log("🔔 Subscribed to notifications from \(peripheral.name ?? "Unknown")", category: .session, level: .debug)
// Send announce after subscription is confirmed (force send for new connection) // Send announce after subscription is confirmed (force send for new connection)
messageQueue.asyncAfter(deadline: .now() + TransportConfig.blePostSubscribeAnnounceDelaySeconds) { [weak self] in messageQueue.asyncAfter(deadline: .now() + TransportConfig.blePostSubscribeAnnounceDelaySeconds) { [weak self] in
@@ -2707,18 +2707,18 @@ extension BLEService: CBPeripheralDelegate {
self?.rebroadcastRecentAnnounces() self?.rebroadcastRecentAnnounces()
} }
} else { } else {
SecureLogger.log("⚠️ Characteristic does not support notifications", category: SecureLogger.session, level: .warning) SecureLogger.log("⚠️ Characteristic does not support notifications", category: .session, level: .warning)
} }
} }
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
if let error = error { if let error = error {
SecureLogger.log("❌ Error receiving notification: \(error.localizedDescription)", category: SecureLogger.session, level: .error) SecureLogger.log("❌ Error receiving notification: \(error.localizedDescription)", category: .session, level: .error)
return return
} }
guard let data = characteristic.value else { guard let data = characteristic.value else {
SecureLogger.log("⚠️ No data in notification", category: SecureLogger.session, level: .warning) SecureLogger.log("⚠️ No data in notification", category: .session, level: .warning)
return return
} }
@@ -2729,7 +2729,7 @@ extension BLEService: CBPeripheralDelegate {
// Avoid dumping entire payload; log size and short prefix for diagnostics // Avoid dumping entire payload; log size and short prefix for diagnostics
let prefix = data.prefix(16).map { String(format: "%02x", $0) }.joined(separator: " ") let prefix = data.prefix(16).map { String(format: "%02x", $0) }.joined(separator: " ")
SecureLogger.log("❌ Failed to decode notification packet (len=\(data.count), prefix=\(prefix))", SecureLogger.log("❌ Failed to decode notification packet (len=\(data.count), prefix=\(prefix))",
category: SecureLogger.session, level: .error) category: .session, level: .error)
return return
} }
@@ -2737,7 +2737,7 @@ extension BLEService: CBPeripheralDelegate {
let senderID = packet.senderID.hexEncodedString() let senderID = packet.senderID.hexEncodedString()
// Only log non-announce packets // Only log non-announce packets
if packet.type != MessageType.announce.rawValue { if packet.type != MessageType.announce.rawValue {
SecureLogger.log("📦 Decoded notification packet type: \(packet.type) from sender: \(senderID)", category: SecureLogger.session, level: .debug) SecureLogger.log("📦 Decoded notification packet type: \(packet.type) from sender: \(senderID)", category: .session, level: .debug)
} }
let peripheralUUID = peripheral.identifier.uuidString let peripheralUUID = peripheral.identifier.uuidString
@@ -2775,10 +2775,10 @@ extension BLEService: CBPeripheralDelegate {
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
if let error = error { if let error = error {
SecureLogger.log("❌ Write failed to \(peripheral.name ?? peripheral.identifier.uuidString): \(error.localizedDescription)", category: SecureLogger.session, level: .error) SecureLogger.log("❌ Write failed to \(peripheral.name ?? peripheral.identifier.uuidString): \(error.localizedDescription)", category: .session, level: .error)
// Don't retry - just log the error // Don't retry - just log the error
} else { } else {
SecureLogger.log("✅ Write confirmed to \(peripheral.name ?? peripheral.identifier.uuidString)", category: SecureLogger.session, level: .debug) SecureLogger.log("✅ Write confirmed to \(peripheral.name ?? peripheral.identifier.uuidString)", category: .session, level: .debug)
} }
} }
@@ -2788,14 +2788,14 @@ extension BLEService: CBPeripheralDelegate {
} }
func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]) { func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]) {
SecureLogger.log("⚠️ Services modified for \(peripheral.name ?? peripheral.identifier.uuidString)", category: SecureLogger.session, level: .warning) SecureLogger.log("⚠️ Services modified for \(peripheral.name ?? peripheral.identifier.uuidString)", category: .session, level: .warning)
// Check if our service was invalidated (peer app quit) // Check if our service was invalidated (peer app quit)
let hasOurService = peripheral.services?.contains { $0.uuid == BLEService.serviceUUID } ?? false let hasOurService = peripheral.services?.contains { $0.uuid == BLEService.serviceUUID } ?? false
if !hasOurService { if !hasOurService {
// Service is gone - disconnect // Service is gone - disconnect
SecureLogger.log("❌ BitChat service removed - disconnecting from \(peripheral.name ?? peripheral.identifier.uuidString)", category: SecureLogger.session, level: .warning) SecureLogger.log("❌ BitChat service removed - disconnecting from \(peripheral.name ?? peripheral.identifier.uuidString)", category: .session, level: .warning)
centralManager?.cancelPeripheralConnection(peripheral) centralManager?.cancelPeripheralConnection(peripheral)
} else { } else {
// Try to rediscover // Try to rediscover
@@ -2805,9 +2805,9 @@ extension BLEService: CBPeripheralDelegate {
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
if let error = error { if let error = error {
SecureLogger.log("❌ Error updating notification state: \(error.localizedDescription)", category: SecureLogger.session, level: .error) SecureLogger.log("❌ Error updating notification state: \(error.localizedDescription)", category: .session, level: .error)
} else { } else {
SecureLogger.log("🔔 Notification state updated for \(peripheral.name ?? peripheral.identifier.uuidString): \(characteristic.isNotifying ? "ON" : "OFF")", category: SecureLogger.session, level: .debug) SecureLogger.log("🔔 Notification state updated for \(peripheral.name ?? peripheral.identifier.uuidString): \(characteristic.isNotifying ? "ON" : "OFF")", category: .session, level: .debug)
// If notifications are now on, send an announce to ensure this peer knows about us // If notifications are now on, send an announce to ensure this peer knows about us
if characteristic.isNotifying { if characteristic.isNotifying {
@@ -2822,7 +2822,7 @@ extension BLEService: CBPeripheralDelegate {
extension BLEService: CBPeripheralManagerDelegate { extension BLEService: CBPeripheralManagerDelegate {
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) { func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
SecureLogger.log("📡 Peripheral manager state: \(peripheral.state.rawValue)", category: SecureLogger.session, level: .debug) SecureLogger.log("📡 Peripheral manager state: \(peripheral.state.rawValue)", category: .session, level: .debug)
if peripheral.state == .poweredOn { if peripheral.state == .poweredOn {
// Remove all services first to ensure clean state // Remove all services first to ensure clean state
@@ -2841,28 +2841,28 @@ extension BLEService: CBPeripheralManagerDelegate {
service.characteristics = [characteristic!] service.characteristics = [characteristic!]
// Add service (advertising will start in didAdd delegate) // Add service (advertising will start in didAdd delegate)
SecureLogger.log("🔧 Adding BLE service...", category: SecureLogger.session, level: .debug) SecureLogger.log("🔧 Adding BLE service...", category: .session, level: .debug)
peripheral.add(service) peripheral.add(service)
} }
} }
func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) { func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) {
if let error = error { if let error = error {
SecureLogger.log("❌ Failed to add service: \(error.localizedDescription)", category: SecureLogger.session, level: .error) SecureLogger.log("❌ Failed to add service: \(error.localizedDescription)", category: .session, level: .error)
return return
} }
SecureLogger.log("✅ Service added successfully, starting advertising", category: SecureLogger.session, level: .debug) SecureLogger.log("✅ Service added successfully, starting advertising", category: .session, level: .debug)
// Start advertising after service is confirmed added // Start advertising after service is confirmed added
let adData = buildAdvertisementData() let adData = buildAdvertisementData()
peripheral.startAdvertising(adData) peripheral.startAdvertising(adData)
SecureLogger.log("📡 Started advertising (LocalName: \((adData[CBAdvertisementDataLocalNameKey] as? String) != nil ? "on" : "off"), ID: \(myPeerID.prefix(8))…)", category: SecureLogger.session, level: .debug) SecureLogger.log("📡 Started advertising (LocalName: \((adData[CBAdvertisementDataLocalNameKey] as? String) != nil ? "on" : "off"), ID: \(myPeerID.prefix(8))…)", category: .session, level: .debug)
} }
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) { func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
SecureLogger.log("📥 Central subscribed: \(central.identifier.uuidString)", category: SecureLogger.session, level: .debug) SecureLogger.log("📥 Central subscribed: \(central.identifier.uuidString)", category: .session, level: .debug)
subscribedCentrals.append(central) subscribedCentrals.append(central)
// Send announce to the newly subscribed central after a small delay to avoid overwhelming // Send announce to the newly subscribed central after a small delay to avoid overwhelming
messageQueue.asyncAfter(deadline: .now() + TransportConfig.blePostAnnounceDelaySeconds) { [weak self] in messageQueue.asyncAfter(deadline: .now() + TransportConfig.blePostAnnounceDelaySeconds) { [weak self] in
@@ -2875,12 +2875,12 @@ extension BLEService: CBPeripheralManagerDelegate {
} }
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) { func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) {
SecureLogger.log("📤 Central unsubscribed: \(central.identifier.uuidString)", category: SecureLogger.session, level: .debug) SecureLogger.log("📤 Central unsubscribed: \(central.identifier.uuidString)", category: .session, level: .debug)
subscribedCentrals.removeAll { $0.identifier == central.identifier } subscribedCentrals.removeAll { $0.identifier == central.identifier }
// Ensure we're still advertising for other devices to find us // Ensure we're still advertising for other devices to find us
if peripheral.isAdvertising == false { if peripheral.isAdvertising == false {
SecureLogger.log("📡 Restarting advertising after central unsubscribed", category: SecureLogger.session, level: .debug) SecureLogger.log("📡 Restarting advertising after central unsubscribed", category: .session, level: .debug)
peripheral.startAdvertising(buildAdvertisementData()) peripheral.startAdvertising(buildAdvertisementData())
} }
@@ -2914,7 +2914,7 @@ extension BLEService: CBPeripheralManagerDelegate {
} }
func peripheralManagerIsReady(toUpdateSubscribers peripheral: CBPeripheralManager) { func peripheralManagerIsReady(toUpdateSubscribers peripheral: CBPeripheralManager) {
SecureLogger.log("📤 Peripheral manager ready to send more notifications", category: SecureLogger.session, level: .debug) SecureLogger.log("📤 Peripheral manager ready to send more notifications", category: .session, level: .debug)
// Retry pending notifications now that queue has space // Retry pending notifications now that queue has space
collectionsQueue.async(flags: .barrier) { [weak self] in collectionsQueue.async(flags: .barrier) { [weak self] in
@@ -2934,11 +2934,11 @@ extension BLEService: CBPeripheralManagerDelegate {
// Still full, re-queue // Still full, re-queue
self.pendingNotifications.append((data: data, centrals: centrals)) self.pendingNotifications.append((data: data, centrals: centrals))
SecureLogger.log("⚠️ Notification queue still full, re-queuing", SecureLogger.log("⚠️ Notification queue still full, re-queuing",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
break // Stop trying, wait for next ready callback break // Stop trying, wait for next ready callback
} else { } else {
SecureLogger.log("✅ Sent pending notification from retry queue", SecureLogger.log("✅ Sent pending notification from retry queue",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
} }
} else { } else {
// Broadcast to all // Broadcast to all
@@ -2953,7 +2953,7 @@ extension BLEService: CBPeripheralManagerDelegate {
if !self.pendingNotifications.isEmpty { if !self.pendingNotifications.isEmpty {
SecureLogger.log("📋 Still have \(self.pendingNotifications.count) pending notifications", SecureLogger.log("📋 Still have \(self.pendingNotifications.count) pending notifications",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
} }
} }
} }
@@ -2961,7 +2961,7 @@ extension BLEService: CBPeripheralManagerDelegate {
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) { func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
// Suppress logs for single write requests to reduce noise // Suppress logs for single write requests to reduce noise
if requests.count > 1 { if requests.count > 1 {
SecureLogger.log("📥 Received \(requests.count) write requests from central", category: SecureLogger.session, level: .debug) SecureLogger.log("📥 Received \(requests.count) write requests from central", category: .session, level: .debug)
} }
// IMPORTANT: Respond immediately to prevent timeouts! // IMPORTANT: Respond immediately to prevent timeouts!
@@ -3000,7 +3000,7 @@ extension BLEService: CBPeripheralManagerDelegate {
if combined.count >= 2 { if combined.count >= 2 {
let peekType = combined[1] let peekType = combined[1]
if peekType != MessageType.announce.rawValue { if peekType != MessageType.announce.rawValue {
SecureLogger.log("📥 Accumulated write from central \(centralUUID): size=\(combined.count) (+\(appendedBytes)) bytes (type=\(peekType)), offsets=\(offsets)", category: SecureLogger.session, level: .debug) SecureLogger.log("📥 Accumulated write from central \(centralUUID): size=\(combined.count) (+\(appendedBytes)) bytes (type=\(peekType)), offsets=\(offsets)", category: .session, level: .debug)
} }
} }
@@ -3010,7 +3010,7 @@ extension BLEService: CBPeripheralManagerDelegate {
pendingWriteBuffers.removeValue(forKey: centralUUID) pendingWriteBuffers.removeValue(forKey: centralUUID)
let senderID = packet.senderID.hexEncodedString() let senderID = packet.senderID.hexEncodedString()
if packet.type != MessageType.announce.rawValue { if packet.type != MessageType.announce.rawValue {
SecureLogger.log("📦 Decoded (combined) packet type: \(packet.type) from sender: \(senderID)", category: SecureLogger.session, level: .debug) SecureLogger.log("📦 Decoded (combined) packet type: \(packet.type) from sender: \(senderID)", category: .session, level: .debug)
} }
if !subscribedCentrals.contains(sorted[0].central) { if !subscribedCentrals.contains(sorted[0].central) {
subscribedCentrals.append(sorted[0].central) subscribedCentrals.append(sorted[0].central)
@@ -3035,12 +3035,12 @@ extension BLEService: CBPeripheralManagerDelegate {
// If buffer grows suspiciously large, reset to avoid memory leak // If buffer grows suspiciously large, reset to avoid memory leak
if combined.count > TransportConfig.blePendingWriteBufferCapBytes { // cap for safety if combined.count > TransportConfig.blePendingWriteBufferCapBytes { // cap for safety
pendingWriteBuffers.removeValue(forKey: centralUUID) pendingWriteBuffers.removeValue(forKey: centralUUID)
SecureLogger.log("⚠️ Dropping oversized pending write buffer (\(combined.count) bytes) for central \(centralUUID)", category: SecureLogger.session, level: .warning) SecureLogger.log("⚠️ Dropping oversized pending write buffer (\(combined.count) bytes) for central \(centralUUID)", category: .session, level: .warning)
} }
// If this was a single short write and still failed, log the raw chunk for debugging // If this was a single short write and still failed, log the raw chunk for debugging
if !hasMultiple, let only = sorted.first, let raw = only.value { if !hasMultiple, let only = sorted.first, let raw = only.value {
let prefix = raw.prefix(16).map { String(format: "%02x", $0) }.joined(separator: " ") let prefix = raw.prefix(16).map { String(format: "%02x", $0) }.joined(separator: " ")
SecureLogger.log("❌ Failed to decode packet from central (len=\(raw.count), prefix=\(prefix))", category: SecureLogger.session, level: .error) SecureLogger.log("❌ Failed to decode packet from central (len=\(raw.count), prefix=\(prefix))", category: .session, level: .error)
} }
} }
} }
@@ -48,7 +48,7 @@ final class FavoritesPersistenceService: ObservableObject {
peerNickname: String peerNickname: String
) { ) {
SecureLogger.log("⭐️ Adding favorite: \(peerNickname) (\(peerNoisePublicKey.hexEncodedString()))", SecureLogger.log("⭐️ Adding favorite: \(peerNickname) (\(peerNoisePublicKey.hexEncodedString()))",
category: SecureLogger.session, level: .info) category: .session, level: .info)
let existing = favorites[peerNoisePublicKey] let existing = favorites[peerNoisePublicKey]
@@ -65,7 +65,7 @@ final class FavoritesPersistenceService: ObservableObject {
// Log if this creates a mutual favorite // Log if this creates a mutual favorite
if relationship.isMutual { if relationship.isMutual {
SecureLogger.log("💕 Mutual favorite relationship established with \(peerNickname)!", SecureLogger.log("💕 Mutual favorite relationship established with \(peerNickname)!",
category: SecureLogger.session, level: .info) category: .session, level: .info)
} }
favorites[peerNoisePublicKey] = relationship favorites[peerNoisePublicKey] = relationship
@@ -84,7 +84,7 @@ final class FavoritesPersistenceService: ObservableObject {
guard let existing = favorites[peerNoisePublicKey] else { return } guard let existing = favorites[peerNoisePublicKey] else { return }
SecureLogger.log("⭐️ Removing favorite: \(existing.peerNickname) (\(peerNoisePublicKey.hexEncodedString()))", SecureLogger.log("⭐️ Removing favorite: \(existing.peerNickname) (\(peerNoisePublicKey.hexEncodedString()))",
category: SecureLogger.session, level: .info) category: .session, level: .info)
// If they still favorite us, keep the record but mark us as not favoriting // If they still favorite us, keep the record but mark us as not favoriting
if existing.theyFavoritedUs { if existing.theyFavoritedUs {
@@ -126,7 +126,7 @@ final class FavoritesPersistenceService: ObservableObject {
let displayName = peerNickname ?? existing?.peerNickname ?? "Unknown" let displayName = peerNickname ?? existing?.peerNickname ?? "Unknown"
SecureLogger.log("📨 Received favorite notification: \(displayName) \(favorited ? "favorited" : "unfavorited") us", SecureLogger.log("📨 Received favorite notification: \(displayName) \(favorited ? "favorited" : "unfavorited") us",
category: SecureLogger.session, level: .info) category: .session, level: .info)
let relationship = FavoriteRelationship( let relationship = FavoriteRelationship(
peerNoisePublicKey: peerNoisePublicKey, peerNoisePublicKey: peerNoisePublicKey,
@@ -148,7 +148,7 @@ final class FavoritesPersistenceService: ObservableObject {
// Check if this creates a mutual favorite // Check if this creates a mutual favorite
if relationship.isMutual { if relationship.isMutual {
SecureLogger.log("💕 Mutual favorite relationship established with \(displayName)!", SecureLogger.log("💕 Mutual favorite relationship established with \(displayName)!",
category: SecureLogger.session, level: .info) category: .session, level: .info)
} }
} }
@@ -241,14 +241,14 @@ final class FavoritesPersistenceService: ObservableObject {
func updateNoisePublicKey(from oldKey: Data, to newKey: Data, peerNickname: String) { func updateNoisePublicKey(from oldKey: Data, to newKey: Data, peerNickname: String) {
guard let existing = favorites[oldKey] else { guard let existing = favorites[oldKey] else {
SecureLogger.log("⚠️ Cannot update noise key - no favorite found for \(oldKey.hexEncodedString())", SecureLogger.log("⚠️ Cannot update noise key - no favorite found for \(oldKey.hexEncodedString())",
category: SecureLogger.session, level: .warning) category: .session, level: .warning)
return return
} }
// Check if we already have a favorite with the new key // Check if we already have a favorite with the new key
if favorites[newKey] != nil { if favorites[newKey] != nil {
SecureLogger.log("⚠️ Favorite already exists with new key \(newKey.hexEncodedString()), removing old entry", SecureLogger.log("⚠️ Favorite already exists with new key \(newKey.hexEncodedString()), removing old entry",
category: SecureLogger.session, level: .warning) category: .session, level: .warning)
favorites.removeValue(forKey: oldKey) favorites.removeValue(forKey: oldKey)
saveFavorites() saveFavorites()
return return
@@ -302,7 +302,7 @@ final class FavoritesPersistenceService: ObservableObject {
/// Clear all favorites - used for panic mode /// Clear all favorites - used for panic mode
func clearAllFavorites() { func clearAllFavorites() {
SecureLogger.log("🧹 Clearing all favorites (panic mode)", category: SecureLogger.session, level: .warning) SecureLogger.log("🧹 Clearing all favorites (panic mode)", category: .session, level: .warning)
favorites.removeAll() favorites.removeAll()
saveFavorites() saveFavorites()
@@ -336,7 +336,7 @@ final class FavoritesPersistenceService: ObservableObject {
// Successfully saved favorites // Successfully saved favorites
} catch { } catch {
SecureLogger.log("Failed to save favorites: \(error)", category: SecureLogger.session, level: .error) SecureLogger.log("Failed to save favorites: \(error)", category: .session, level: .error)
} }
} }
@@ -355,13 +355,13 @@ final class FavoritesPersistenceService: ObservableObject {
let relationships = try decoder.decode([FavoriteRelationship].self, from: data) let relationships = try decoder.decode([FavoriteRelationship].self, from: data)
SecureLogger.log("✅ Loaded \(relationships.count) favorite relationships", SecureLogger.log("✅ Loaded \(relationships.count) favorite relationships",
category: SecureLogger.session, level: .info) category: .session, level: .info)
// Log Nostr public key info // Log Nostr public key info
for relationship in relationships { for relationship in relationships {
if relationship.peerNostrPublicKey == nil { if relationship.peerNostrPublicKey == nil {
SecureLogger.log("⚠️ No Nostr public key stored for '\(relationship.peerNickname)'", SecureLogger.log("⚠️ No Nostr public key stored for '\(relationship.peerNickname)'",
category: SecureLogger.session, level: .warning) category: .session, level: .warning)
} }
} }
@@ -373,7 +373,7 @@ final class FavoritesPersistenceService: ObservableObject {
// Check for duplicates by public key (the actual unique identifier) // Check for duplicates by public key (the actual unique identifier)
if let existing = seenPublicKeys[relationship.peerNoisePublicKey] { if let existing = seenPublicKeys[relationship.peerNoisePublicKey] {
SecureLogger.log("⚠️ Duplicate favorite found for public key \(relationship.peerNoisePublicKey.hexEncodedString()) - nicknames: '\(existing.peerNickname)' vs '\(relationship.peerNickname)'", SecureLogger.log("⚠️ Duplicate favorite found for public key \(relationship.peerNoisePublicKey.hexEncodedString()) - nicknames: '\(existing.peerNickname)' vs '\(relationship.peerNickname)'",
category: SecureLogger.session, level: .warning) category: .session, level: .warning)
// Keep the most recent or most complete relationship // Keep the most recent or most complete relationship
if relationship.lastUpdated > existing.lastUpdated || if relationship.lastUpdated > existing.lastUpdated ||
@@ -414,7 +414,7 @@ final class FavoritesPersistenceService: ObservableObject {
// Log loaded relationships // Log loaded relationships
// Loaded relationships successfully // Loaded relationships successfully
} catch { } catch {
SecureLogger.log("Failed to load favorites: \(error)", category: SecureLogger.session, level: .error) SecureLogger.log("Failed to load favorites: \(error)", category: .session, level: .error)
} }
} }
} }
+6 -6
View File
@@ -113,9 +113,9 @@ final class KeychainManager {
if status == errSecSuccess { return true } if status == errSecSuccess { return true }
if status == -34018 && !triedWithoutGroup { if status == -34018 && !triedWithoutGroup {
SecureLogger.logError(NSError(domain: "Keychain", code: -34018), context: "Missing keychain entitlement", category: SecureLogger.keychain) SecureLogger.logError(NSError(domain: "Keychain", code: -34018), context: "Missing keychain entitlement", category: .keychain)
} else if status != errSecDuplicateItem { } else if status != errSecDuplicateItem {
SecureLogger.logError(NSError(domain: "Keychain", code: Int(status)), context: "Error saving to keychain", category: SecureLogger.keychain) SecureLogger.logError(NSError(domain: "Keychain", code: Int(status)), context: "Error saving to keychain", category: .keychain)
} }
return false return false
} }
@@ -151,7 +151,7 @@ final class KeychainManager {
if status == errSecSuccess { return result as? Data } if status == errSecSuccess { return result as? Data }
if status == -34018 { if status == -34018 {
SecureLogger.logError(NSError(domain: "Keychain", code: -34018), context: "Missing keychain entitlement", category: SecureLogger.keychain) SecureLogger.logError(NSError(domain: "Keychain", code: -34018), context: "Missing keychain entitlement", category: .keychain)
} }
return nil return nil
} }
@@ -198,7 +198,7 @@ final class KeychainManager {
// Delete ALL keychain data for panic mode // Delete ALL keychain data for panic mode
func deleteAllKeychainData() -> Bool { func deleteAllKeychainData() -> Bool {
SecureLogger.log("Panic mode - deleting all keychain data", category: SecureLogger.security, level: .warning) SecureLogger.log("Panic mode - deleting all keychain data", category: .security, level: .warning)
var totalDeleted = 0 var totalDeleted = 0
@@ -261,7 +261,7 @@ final class KeychainManager {
let deleteStatus = SecItemDelete(deleteQuery as CFDictionary) let deleteStatus = SecItemDelete(deleteQuery as CFDictionary)
if deleteStatus == errSecSuccess { if deleteStatus == errSecSuccess {
totalDeleted += 1 totalDeleted += 1
SecureLogger.log("Deleted keychain item: \(account) from \(service)", category: SecureLogger.keychain, level: .info) SecureLogger.log("Deleted keychain item: \(account) from \(service)", category: .keychain, level: .info)
} }
} }
} }
@@ -303,7 +303,7 @@ final class KeychainManager {
totalDeleted += 1 totalDeleted += 1
} }
SecureLogger.log("Panic mode cleanup completed. Total items deleted: \(totalDeleted)", category: SecureLogger.keychain, level: .warning) SecureLogger.log("Panic mode cleanup completed. Total items deleted: \(totalDeleted)", category: .keychain, level: .warning)
return totalDeleted > 0 return totalDeleted > 0
} }
@@ -198,7 +198,7 @@ final class LocationChannelManager: NSObject, CLLocationManagerDelegate, Observa
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
// Surface as denied/restricted if relevant; otherwise keep previous state // Surface as denied/restricted if relevant; otherwise keep previous state
SecureLogger.log("LocationChannelManager: location error: \(error.localizedDescription)", SecureLogger.log("LocationChannelManager: location error: \(error.localizedDescription)",
category: SecureLogger.session, level: .error) category: .session, level: .error)
} }
// MARK: - Helpers // MARK: - Helpers
+9 -9
View File
@@ -40,19 +40,19 @@ final class MessageRouter {
let reachableMesh = mesh.isPeerReachable(peerID) let reachableMesh = mesh.isPeerReachable(peerID)
if reachableMesh { if reachableMesh {
SecureLogger.log("Routing PM via mesh (reachable) to \(peerID.prefix(8))… id=\(messageID.prefix(8))", SecureLogger.log("Routing PM via mesh (reachable) to \(peerID.prefix(8))… id=\(messageID.prefix(8))",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
// BLEService will initiate a handshake if needed and queue the message // BLEService will initiate a handshake if needed and queue the message
mesh.sendPrivateMessage(content, to: peerID, recipientNickname: recipientNickname, messageID: messageID) mesh.sendPrivateMessage(content, to: peerID, recipientNickname: recipientNickname, messageID: messageID)
} else if canSendViaNostr(peerID: peerID) { } else if canSendViaNostr(peerID: peerID) {
SecureLogger.log("Routing PM via Nostr to \(peerID.prefix(8))… id=\(messageID.prefix(8))", SecureLogger.log("Routing PM via Nostr to \(peerID.prefix(8))… id=\(messageID.prefix(8))",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
nostr.sendPrivateMessage(content, to: peerID, recipientNickname: recipientNickname, messageID: messageID) nostr.sendPrivateMessage(content, to: peerID, recipientNickname: recipientNickname, messageID: messageID)
} else { } else {
// Queue for later (when mesh connects or Nostr mapping appears) // Queue for later (when mesh connects or Nostr mapping appears)
if outbox[peerID] == nil { outbox[peerID] = [] } if outbox[peerID] == nil { outbox[peerID] = [] }
outbox[peerID]?.append((content, recipientNickname, messageID)) outbox[peerID]?.append((content, recipientNickname, messageID))
SecureLogger.log("Queued PM for \(peerID.prefix(8))… (no mesh, no Nostr mapping) id=\(messageID.prefix(8))", SecureLogger.log("Queued PM for \(peerID.prefix(8))… (no mesh, no Nostr mapping) id=\(messageID.prefix(8))",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
} }
} }
@@ -60,11 +60,11 @@ final class MessageRouter {
// Prefer mesh for reachable peers; BLE will queue if handshake is needed // Prefer mesh for reachable peers; BLE will queue if handshake is needed
if mesh.isPeerReachable(peerID) { if mesh.isPeerReachable(peerID) {
SecureLogger.log("Routing READ ack via mesh (reachable) to \(peerID.prefix(8))… id=\(receipt.originalMessageID.prefix(8))", SecureLogger.log("Routing READ ack via mesh (reachable) to \(peerID.prefix(8))… id=\(receipt.originalMessageID.prefix(8))",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
mesh.sendReadReceipt(receipt, to: peerID) mesh.sendReadReceipt(receipt, to: peerID)
} else { } else {
SecureLogger.log("Routing READ ack via Nostr to \(peerID.prefix(8))… id=\(receipt.originalMessageID.prefix(8))", SecureLogger.log("Routing READ ack via Nostr to \(peerID.prefix(8))… id=\(receipt.originalMessageID.prefix(8))",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
nostr.sendReadReceipt(receipt, to: peerID) nostr.sendReadReceipt(receipt, to: peerID)
} }
} }
@@ -72,7 +72,7 @@ final class MessageRouter {
func sendDeliveryAck(_ messageID: String, to peerID: String) { func sendDeliveryAck(_ messageID: String, to peerID: String) {
if mesh.isPeerReachable(peerID) { if mesh.isPeerReachable(peerID) {
SecureLogger.log("Routing DELIVERED ack via mesh (reachable) to \(peerID.prefix(8))… id=\(messageID.prefix(8))", SecureLogger.log("Routing DELIVERED ack via mesh (reachable) to \(peerID.prefix(8))… id=\(messageID.prefix(8))",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
mesh.sendDeliveryAck(for: messageID, to: peerID) mesh.sendDeliveryAck(for: messageID, to: peerID)
} else { } else {
nostr.sendDeliveryAck(for: messageID, to: peerID) nostr.sendDeliveryAck(for: messageID, to: peerID)
@@ -109,17 +109,17 @@ final class MessageRouter {
func flushOutbox(for peerID: String) { func flushOutbox(for peerID: String) {
guard let queued = outbox[peerID], !queued.isEmpty else { return } guard let queued = outbox[peerID], !queued.isEmpty else { return }
SecureLogger.log("Flushing outbox for \(peerID.prefix(8))… count=\(queued.count)", SecureLogger.log("Flushing outbox for \(peerID.prefix(8))… count=\(queued.count)",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
var remaining: [(content: String, nickname: String, messageID: String)] = [] var remaining: [(content: String, nickname: String, messageID: String)] = []
// Prefer mesh if connected; else try Nostr if mapping exists // Prefer mesh if connected; else try Nostr if mapping exists
for (content, nickname, messageID) in queued { for (content, nickname, messageID) in queued {
if mesh.isPeerReachable(peerID) { if mesh.isPeerReachable(peerID) {
SecureLogger.log("Outbox -> mesh for \(peerID.prefix(8))… id=\(messageID.prefix(8))", SecureLogger.log("Outbox -> mesh for \(peerID.prefix(8))… id=\(messageID.prefix(8))",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
mesh.sendPrivateMessage(content, to: peerID, recipientNickname: nickname, messageID: messageID) mesh.sendPrivateMessage(content, to: peerID, recipientNickname: nickname, messageID: messageID)
} else if canSendViaNostr(peerID: peerID) { } else if canSendViaNostr(peerID: peerID) {
SecureLogger.log("Outbox -> Nostr for \(peerID.prefix(8))… id=\(messageID.prefix(8))", SecureLogger.log("Outbox -> Nostr for \(peerID.prefix(8))… id=\(messageID.prefix(8))",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
nostr.sendPrivateMessage(content, to: peerID, recipientNickname: nickname, messageID: messageID) nostr.sendPrivateMessage(content, to: peerID, recipientNickname: nickname, messageID: messageID)
} else { } else {
// Keep unsent items queued // Keep unsent items queued
@@ -270,7 +270,7 @@ final class NoiseEncryptionService {
let deletedStatic = KeychainManager.shared.deleteIdentityKey(forKey: "noiseStaticKey") let deletedStatic = KeychainManager.shared.deleteIdentityKey(forKey: "noiseStaticKey")
let deletedSigning = KeychainManager.shared.deleteIdentityKey(forKey: "ed25519SigningKey") let deletedSigning = KeychainManager.shared.deleteIdentityKey(forKey: "ed25519SigningKey")
SecureLogger.logKeyOperation("delete", keyType: "identity keys", success: deletedStatic && deletedSigning) SecureLogger.logKeyOperation("delete", keyType: "identity keys", success: deletedStatic && deletedSigning)
SecureLogger.log("Panic mode activated - identity cleared", category: SecureLogger.security, level: .warning) SecureLogger.log("Panic mode activated - identity cleared", category: .security, level: .warning)
// Stop rekey timer // Stop rekey timer
stopRekeyTimer() stopRekeyTimer()
} }
@@ -281,7 +281,7 @@ final class NoiseEncryptionService {
let signature = try signingKey.signature(for: data) let signature = try signingKey.signature(for: data)
return signature return signature
} catch { } catch {
SecureLogger.logError(error, context: "Failed to sign data", category: SecureLogger.noise) SecureLogger.logError(error, context: "Failed to sign data", category: .noise)
return nil return nil
} }
} }
@@ -292,7 +292,7 @@ final class NoiseEncryptionService {
let signingPublicKey = try Curve25519.Signing.PublicKey(rawRepresentation: publicKey) let signingPublicKey = try Curve25519.Signing.PublicKey(rawRepresentation: publicKey)
return signingPublicKey.isValidSignature(signature, for: data) return signingPublicKey.isValidSignature(signature, for: data)
} catch { } catch {
SecureLogger.logError(error, context: "Failed to verify signature", category: SecureLogger.noise) SecureLogger.logError(error, context: "Failed to verify signature", category: .noise)
return false return false
} }
} }
@@ -573,12 +573,12 @@ final class NoiseEncryptionService {
// Attempt to rekey the session // Attempt to rekey the session
do { do {
try sessionManager.initiateRekey(for: peerID) try sessionManager.initiateRekey(for: peerID)
SecureLogger.log("Key rotation initiated for peer: \(peerID)", category: SecureLogger.security, level: .debug) SecureLogger.log("Key rotation initiated for peer: \(peerID)", category: .security, level: .debug)
// Signal that handshake is needed // Signal that handshake is needed
onHandshakeRequired?(peerID) onHandshakeRequired?(peerID)
} catch { } catch {
SecureLogger.logError(error, context: "Failed to initiate rekey for peer: \(peerID)", category: SecureLogger.session) SecureLogger.logError(error, context: "Failed to initiate rekey for peer: \(peerID)", category: .session)
} }
} }
} }
+24 -24
View File
@@ -52,30 +52,30 @@ final class NostrTransport: Transport {
guard let recipientNpub = resolveRecipientNpub(for: peerID) else { return } guard let recipientNpub = resolveRecipientNpub(for: peerID) else { return }
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return } guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return }
SecureLogger.log("NostrTransport: preparing PM to \(recipientNpub.prefix(16))… for peerID \(peerID.prefix(8))… id=\(messageID.prefix(8))", SecureLogger.log("NostrTransport: preparing PM to \(recipientNpub.prefix(16))… for peerID \(peerID.prefix(8))… id=\(messageID.prefix(8))",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
// Convert recipient npub -> hex (x-only) // Convert recipient npub -> hex (x-only)
let recipientHex: String let recipientHex: String
do { do {
let (hrp, data) = try Bech32.decode(recipientNpub) let (hrp, data) = try Bech32.decode(recipientNpub)
guard hrp == "npub" else { guard hrp == "npub" else {
SecureLogger.log("NostrTransport: recipient key not npub (hrp=\(hrp))", category: SecureLogger.session, level: .error) SecureLogger.log("NostrTransport: recipient key not npub (hrp=\(hrp))", category: .session, level: .error)
return return
} }
recipientHex = data.hexEncodedString() recipientHex = data.hexEncodedString()
} catch { } catch {
SecureLogger.log("NostrTransport: failed to decode npub -> hex: \(error)", category: SecureLogger.session, level: .error) SecureLogger.log("NostrTransport: failed to decode npub -> hex: \(error)", category: .session, level: .error)
return return
} }
guard let embedded = NostrEmbeddedBitChat.encodePMForNostr(content: content, messageID: messageID, recipientPeerID: peerID, senderPeerID: senderPeerID) else { guard let embedded = NostrEmbeddedBitChat.encodePMForNostr(content: content, messageID: messageID, recipientPeerID: peerID, senderPeerID: senderPeerID) else {
SecureLogger.log("NostrTransport: failed to embed PM packet", category: SecureLogger.session, level: .error) SecureLogger.log("NostrTransport: failed to embed PM packet", category: .session, level: .error)
return return
} }
guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: senderIdentity) else { guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: senderIdentity) else {
SecureLogger.log("NostrTransport: failed to build Nostr event for PM", category: SecureLogger.session, level: .error) SecureLogger.log("NostrTransport: failed to build Nostr event for PM", category: .session, level: .error)
return return
} }
SecureLogger.log("NostrTransport: sending PM giftWrap id=\(event.id.prefix(16))", SecureLogger.log("NostrTransport: sending PM giftWrap id=\(event.id.prefix(16))",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
NostrRelayManager.shared.sendEvent(event) NostrRelayManager.shared.sendEvent(event)
} }
} }
@@ -100,7 +100,7 @@ final class NostrTransport: Transport {
guard let recipientNpub = resolveRecipientNpub(for: item.peerID) else { scheduleNextReadAck(); return } guard let recipientNpub = resolveRecipientNpub(for: item.peerID) else { scheduleNextReadAck(); return }
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { scheduleNextReadAck(); return } guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { scheduleNextReadAck(); return }
SecureLogger.log("NostrTransport: preparing READ ack for id=\(item.receipt.originalMessageID.prefix(8))… to \(recipientNpub.prefix(16))", SecureLogger.log("NostrTransport: preparing READ ack for id=\(item.receipt.originalMessageID.prefix(8))… to \(recipientNpub.prefix(16))",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
// Convert recipient npub -> hex // Convert recipient npub -> hex
let recipientHex: String let recipientHex: String
do { do {
@@ -109,15 +109,15 @@ final class NostrTransport: Transport {
recipientHex = data.hexEncodedString() recipientHex = data.hexEncodedString()
} catch { scheduleNextReadAck(); return } } catch { scheduleNextReadAck(); return }
guard let ack = NostrEmbeddedBitChat.encodeAckForNostr(type: .readReceipt, messageID: item.receipt.originalMessageID, recipientPeerID: item.peerID, senderPeerID: senderPeerID) else { guard let ack = NostrEmbeddedBitChat.encodeAckForNostr(type: .readReceipt, messageID: item.receipt.originalMessageID, recipientPeerID: item.peerID, senderPeerID: senderPeerID) else {
SecureLogger.log("NostrTransport: failed to embed READ ack", category: SecureLogger.session, level: .error) SecureLogger.log("NostrTransport: failed to embed READ ack", category: .session, level: .error)
scheduleNextReadAck(); return scheduleNextReadAck(); return
} }
guard let event = try? NostrProtocol.createPrivateMessage(content: ack, recipientPubkey: recipientHex, senderIdentity: senderIdentity) else { guard let event = try? NostrProtocol.createPrivateMessage(content: ack, recipientPubkey: recipientHex, senderIdentity: senderIdentity) else {
SecureLogger.log("NostrTransport: failed to build Nostr event for READ ack", category: SecureLogger.session, level: .error) SecureLogger.log("NostrTransport: failed to build Nostr event for READ ack", category: .session, level: .error)
scheduleNextReadAck(); return scheduleNextReadAck(); return
} }
SecureLogger.log("NostrTransport: sending READ ack giftWrap id=\(event.id.prefix(16))", SecureLogger.log("NostrTransport: sending READ ack giftWrap id=\(event.id.prefix(16))",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
NostrRelayManager.shared.sendEvent(event) NostrRelayManager.shared.sendEvent(event)
scheduleNextReadAck() scheduleNextReadAck()
} }
@@ -137,7 +137,7 @@ final class NostrTransport: Transport {
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return } guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return }
let content = isFavorite ? "[FAVORITED]:\(senderIdentity.npub)" : "[UNFAVORITED]:\(senderIdentity.npub)" let content = isFavorite ? "[FAVORITED]:\(senderIdentity.npub)" : "[UNFAVORITED]:\(senderIdentity.npub)"
SecureLogger.log("NostrTransport: preparing FAVORITE(\(isFavorite)) to \(recipientNpub.prefix(16))", SecureLogger.log("NostrTransport: preparing FAVORITE(\(isFavorite)) to \(recipientNpub.prefix(16))",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
// Convert recipient npub -> hex // Convert recipient npub -> hex
let recipientHex: String let recipientHex: String
do { do {
@@ -146,15 +146,15 @@ final class NostrTransport: Transport {
recipientHex = data.hexEncodedString() recipientHex = data.hexEncodedString()
} catch { return } } catch { return }
guard let embedded = NostrEmbeddedBitChat.encodePMForNostr(content: content, messageID: UUID().uuidString, recipientPeerID: peerID, senderPeerID: senderPeerID) else { guard let embedded = NostrEmbeddedBitChat.encodePMForNostr(content: content, messageID: UUID().uuidString, recipientPeerID: peerID, senderPeerID: senderPeerID) else {
SecureLogger.log("NostrTransport: failed to embed favorite notification", category: SecureLogger.session, level: .error) SecureLogger.log("NostrTransport: failed to embed favorite notification", category: .session, level: .error)
return return
} }
guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: senderIdentity) else { guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: senderIdentity) else {
SecureLogger.log("NostrTransport: failed to build Nostr event for favorite notification", category: SecureLogger.session, level: .error) SecureLogger.log("NostrTransport: failed to build Nostr event for favorite notification", category: .session, level: .error)
return return
} }
SecureLogger.log("NostrTransport: sending favorite giftWrap id=\(event.id.prefix(16))", SecureLogger.log("NostrTransport: sending favorite giftWrap id=\(event.id.prefix(16))",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
NostrRelayManager.shared.sendEvent(event) NostrRelayManager.shared.sendEvent(event)
} }
} }
@@ -181,7 +181,7 @@ final class NostrTransport: Transport {
guard let recipientNpub = resolveRecipientNpub(for: peerID) else { return } guard let recipientNpub = resolveRecipientNpub(for: peerID) else { return }
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return } guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return }
SecureLogger.log("NostrTransport: preparing DELIVERED ack for id=\(messageID.prefix(8))… to \(recipientNpub.prefix(16))", SecureLogger.log("NostrTransport: preparing DELIVERED ack for id=\(messageID.prefix(8))… to \(recipientNpub.prefix(16))",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
let recipientHex: String let recipientHex: String
do { do {
let (hrp, data) = try Bech32.decode(recipientNpub) let (hrp, data) = try Bech32.decode(recipientNpub)
@@ -189,15 +189,15 @@ final class NostrTransport: Transport {
recipientHex = data.hexEncodedString() recipientHex = data.hexEncodedString()
} catch { return } } catch { return }
guard let ack = NostrEmbeddedBitChat.encodeAckForNostr(type: .delivered, messageID: messageID, recipientPeerID: peerID, senderPeerID: senderPeerID) else { guard let ack = NostrEmbeddedBitChat.encodeAckForNostr(type: .delivered, messageID: messageID, recipientPeerID: peerID, senderPeerID: senderPeerID) else {
SecureLogger.log("NostrTransport: failed to embed DELIVERED ack", category: SecureLogger.session, level: .error) SecureLogger.log("NostrTransport: failed to embed DELIVERED ack", category: .session, level: .error)
return return
} }
guard let event = try? NostrProtocol.createPrivateMessage(content: ack, recipientPubkey: recipientHex, senderIdentity: senderIdentity) else { guard let event = try? NostrProtocol.createPrivateMessage(content: ack, recipientPubkey: recipientHex, senderIdentity: senderIdentity) else {
SecureLogger.log("NostrTransport: failed to build Nostr event for DELIVERED ack", category: SecureLogger.session, level: .error) SecureLogger.log("NostrTransport: failed to build Nostr event for DELIVERED ack", category: .session, level: .error)
return return
} }
SecureLogger.log("NostrTransport: sending DELIVERED ack giftWrap id=\(event.id.prefix(16))", SecureLogger.log("NostrTransport: sending DELIVERED ack giftWrap id=\(event.id.prefix(16))",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
NostrRelayManager.shared.sendEvent(event) NostrRelayManager.shared.sendEvent(event)
} }
} }
@@ -206,7 +206,7 @@ final class NostrTransport: Transport {
func sendDeliveryAckGeohash(for messageID: String, toRecipientHex recipientHex: String, from identity: NostrIdentity) { func sendDeliveryAckGeohash(for messageID: String, toRecipientHex recipientHex: String, from identity: NostrIdentity) {
Task { @MainActor in Task { @MainActor in
SecureLogger.log("GeoDM: send DELIVERED -> recip=\(recipientHex.prefix(8))… mid=\(messageID.prefix(8))… from=\(identity.publicKeyHex.prefix(8))", SecureLogger.log("GeoDM: send DELIVERED -> recip=\(recipientHex.prefix(8))… mid=\(messageID.prefix(8))… from=\(identity.publicKeyHex.prefix(8))",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
guard let embedded = NostrEmbeddedBitChat.encodeAckForNostrNoRecipient(type: .delivered, messageID: messageID, senderPeerID: senderPeerID) else { return } guard let embedded = NostrEmbeddedBitChat.encodeAckForNostrNoRecipient(type: .delivered, messageID: messageID, senderPeerID: senderPeerID) else { return }
guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: identity) else { return } guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: identity) else { return }
NostrRelayManager.registerPendingGiftWrap(id: event.id) NostrRelayManager.registerPendingGiftWrap(id: event.id)
@@ -217,7 +217,7 @@ final class NostrTransport: Transport {
func sendReadReceiptGeohash(_ messageID: String, toRecipientHex recipientHex: String, from identity: NostrIdentity) { func sendReadReceiptGeohash(_ messageID: String, toRecipientHex recipientHex: String, from identity: NostrIdentity) {
Task { @MainActor in Task { @MainActor in
SecureLogger.log("GeoDM: send READ -> recip=\(recipientHex.prefix(8))… mid=\(messageID.prefix(8))… from=\(identity.publicKeyHex.prefix(8))", SecureLogger.log("GeoDM: send READ -> recip=\(recipientHex.prefix(8))… mid=\(messageID.prefix(8))… from=\(identity.publicKeyHex.prefix(8))",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
guard let embedded = NostrEmbeddedBitChat.encodeAckForNostrNoRecipient(type: .readReceipt, messageID: messageID, senderPeerID: senderPeerID) else { return } guard let embedded = NostrEmbeddedBitChat.encodeAckForNostrNoRecipient(type: .readReceipt, messageID: messageID, senderPeerID: senderPeerID) else { return }
guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: identity) else { return } guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: identity) else { return }
NostrRelayManager.registerPendingGiftWrap(id: event.id) NostrRelayManager.registerPendingGiftWrap(id: event.id)
@@ -230,18 +230,18 @@ final class NostrTransport: Transport {
Task { @MainActor in Task { @MainActor in
guard !recipientHex.isEmpty else { return } guard !recipientHex.isEmpty else { return }
SecureLogger.log("GeoDM: send PM -> recip=\(recipientHex.prefix(8))… mid=\(messageID.prefix(8))… from=\(identity.publicKeyHex.prefix(8))", SecureLogger.log("GeoDM: send PM -> recip=\(recipientHex.prefix(8))… mid=\(messageID.prefix(8))… from=\(identity.publicKeyHex.prefix(8))",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
// Build embedded BitChat packet without recipient peer ID // Build embedded BitChat packet without recipient peer ID
guard let embedded = NostrEmbeddedBitChat.encodePMForNostrNoRecipient(content: content, messageID: messageID, senderPeerID: senderPeerID) else { guard let embedded = NostrEmbeddedBitChat.encodePMForNostrNoRecipient(content: content, messageID: messageID, senderPeerID: senderPeerID) else {
SecureLogger.log("NostrTransport: failed to embed geohash PM packet", category: SecureLogger.session, level: .error) SecureLogger.log("NostrTransport: failed to embed geohash PM packet", category: .session, level: .error)
return return
} }
guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: identity) else { guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: identity) else {
SecureLogger.log("NostrTransport: failed to build Nostr event for geohash PM", category: SecureLogger.session, level: .error) SecureLogger.log("NostrTransport: failed to build Nostr event for geohash PM", category: .session, level: .error)
return return
} }
SecureLogger.log("NostrTransport: sending geohash PM giftWrap id=\(event.id.prefix(16))", SecureLogger.log("NostrTransport: sending geohash PM giftWrap id=\(event.id.prefix(16))",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
NostrRelayManager.registerPendingGiftWrap(id: event.id) NostrRelayManager.registerPendingGiftWrap(id: event.id)
NostrRelayManager.shared.sendEvent(event) NostrRelayManager.shared.sendEvent(event)
} }
+1 -1
View File
@@ -229,7 +229,7 @@ final class PrivateChatManager: ObservableObject {
// Route via MessageRouter to avoid handshakeRequired spam when session isn't established // Route via MessageRouter to avoid handshakeRequired spam when session isn't established
if let router = messageRouter { if let router = messageRouter {
SecureLogger.log("PrivateChatManager: sending READ ack for \(message.id.prefix(8))… to \(senderPeerID.prefix(8))… via router", SecureLogger.log("PrivateChatManager: sending READ ack for \(message.id.prefix(8))… to \(senderPeerID.prefix(8))… via router",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
Task { @MainActor in Task { @MainActor in
router.sendReadReceipt(receipt, to: senderPeerID) router.sendReadReceipt(receipt, to: senderPeerID)
} }
+13 -13
View File
@@ -189,7 +189,7 @@ final class TorManager: ObservableObject {
var started = false var started = false
// If already running (per C glue), treat as started // If already running (per C glue), treat as started
if tor_host_is_running() != 0 { if tor_host_is_running() != 0 {
SecureLogger.log("TorManager: embed reports already running", category: SecureLogger.session, level: .info) SecureLogger.log("TorManager: embed reports already running", category: .session, level: .info)
return true return true
} }
dir.withCString { dptr in dir.withCString { dptr in
@@ -198,9 +198,9 @@ final class TorManager: ObservableObject {
let rc = tor_host_start(dptr, sptr, cptr, 1) let rc = tor_host_start(dptr, sptr, cptr, 1)
started = (rc == 0) started = (rc == 0)
if rc != 0 { if rc != 0 {
SecureLogger.log("TorManager: tor_host_start failed rc=\(rc)", category: SecureLogger.session, level: .error) SecureLogger.log("TorManager: tor_host_start failed rc=\(rc)", category: .session, level: .error)
} else { } else {
SecureLogger.log("TorManager: tor_host_start OK (\(socks), control \(control))", category: SecureLogger.session, level: .info) SecureLogger.log("TorManager: tor_host_start OK (\(socks), control \(control))", category: .session, level: .info)
} }
} }
} }
@@ -215,10 +215,10 @@ final class TorManager: ObservableObject {
await MainActor.run { await MainActor.run {
self.socksReady = ready self.socksReady = ready
if ready { if ready {
SecureLogger.log("TorManager: SOCKS ready at \(self.socksHost):\(self.socksPort) [embed]", category: SecureLogger.session, level: .info) SecureLogger.log("TorManager: SOCKS ready at \(self.socksHost):\(self.socksPort) [embed]", category: .session, level: .info)
} else { } else {
self.lastError = NSError(domain: "TorManager", code: -14, userInfo: [NSLocalizedDescriptionKey: "Tor SOCKS not reachable after embed start"]) self.lastError = NSError(domain: "TorManager", code: -14, userInfo: [NSLocalizedDescriptionKey: "Tor SOCKS not reachable after embed start"])
SecureLogger.log("TorManager: SOCKS not reachable (timeout) [embed]", category: SecureLogger.session, level: .error) SecureLogger.log("TorManager: SOCKS not reachable (timeout) [embed]", category: .session, level: .error)
} }
} }
} }
@@ -282,13 +282,13 @@ final class TorManager: ObservableObject {
/// Returns true if the attempt started and port probing was scheduled. /// Returns true if the attempt started and port probing was scheduled.
private func startTorViaDlopen() -> Bool { private func startTorViaDlopen() -> Bool {
guard let fwURL = frameworkBinaryURL() else { guard let fwURL = frameworkBinaryURL() else {
SecureLogger.log("TorManager: no embedded tor framework found", category: SecureLogger.session, level: .warning) SecureLogger.log("TorManager: no embedded tor framework found", category: .session, level: .warning)
return false return false
} }
// Load the library // Load the library
let mode = RTLD_NOW | RTLD_LOCAL let mode = RTLD_NOW | RTLD_LOCAL
SecureLogger.log("TorManager: dlopen(\(fwURL.lastPathComponent))…", category: SecureLogger.session, level: .info) SecureLogger.log("TorManager: dlopen(\(fwURL.lastPathComponent))…", category: .session, level: .info)
guard let handle = dlopen(fwURL.path, mode) else { guard let handle = dlopen(fwURL.path, mode) else {
let err = String(cString: dlerror()) let err = String(cString: dlerror())
self.lastError = NSError(domain: "TorManager", code: -10, userInfo: [NSLocalizedDescriptionKey: "dlopen failed: \(err)"]) self.lastError = NSError(domain: "TorManager", code: -10, userInfo: [NSLocalizedDescriptionKey: "dlopen failed: \(err)"])
@@ -314,7 +314,7 @@ final class TorManager: ObservableObject {
argv.append(contentsOf: ["-f", torrc]) argv.append(contentsOf: ["-f", torrc])
} }
// Run Tor on a background thread to avoid blocking the main actor // Run Tor on a background thread to avoid blocking the main actor
SecureLogger.log("TorManager: launching tor_main with torrc", category: SecureLogger.session, level: .info) SecureLogger.log("TorManager: launching tor_main with torrc", category: .session, level: .info)
let argc = Int32(argv.count) let argc = Int32(argv.count)
DispatchQueue.global(qos: .utility).async { DispatchQueue.global(qos: .utility).async {
// Build stable C argv in this thread // Build stable C argv in this thread
@@ -339,9 +339,9 @@ final class TorManager: ObservableObject {
self.socksReady = ready self.socksReady = ready
if !ready { if !ready {
self.lastError = NSError(domain: "TorManager", code: -12, userInfo: [NSLocalizedDescriptionKey: "Tor SOCKS not reachable after dlopen start"]) self.lastError = NSError(domain: "TorManager", code: -12, userInfo: [NSLocalizedDescriptionKey: "Tor SOCKS not reachable after dlopen start"])
SecureLogger.log("TorManager: SOCKS not reachable (timeout)", category: SecureLogger.session, level: .error) SecureLogger.log("TorManager: SOCKS not reachable (timeout)", category: .session, level: .error)
} else { } else {
SecureLogger.log("TorManager: SOCKS ready at \(self.socksHost):\(self.socksPort)", category: SecureLogger.session, level: .info) SecureLogger.log("TorManager: SOCKS ready at \(self.socksHost):\(self.socksPort)", category: .session, level: .info)
} }
// isStarting will be cleared when bootstrap reaches 100% // isStarting will be cleared when bootstrap reaches 100%
} }
@@ -385,7 +385,7 @@ final class TorManager: ObservableObject {
var argv: [String] = ["tor"] var argv: [String] = ["tor"]
if let torrc = torrcURL()?.path { argv.append(contentsOf: ["-f", torrc]) } if let torrc = torrcURL()?.path { argv.append(contentsOf: ["-f", torrc]) }
SecureLogger.log("TorManager: starting tor_main (static)", category: SecureLogger.session, level: .info) SecureLogger.log("TorManager: starting tor_main (static)", category: .session, level: .info)
let argc = Int32(argv.count) let argc = Int32(argv.count)
DispatchQueue.global(qos: .utility).async { DispatchQueue.global(qos: .utility).async {
// Build stable C argv in this thread // Build stable C argv in this thread
@@ -409,10 +409,10 @@ final class TorManager: ObservableObject {
await MainActor.run { await MainActor.run {
self.socksReady = ready self.socksReady = ready
if ready { if ready {
SecureLogger.log("TorManager: SOCKS ready at \(self.socksHost):\(self.socksPort)", category: SecureLogger.session, level: .info) SecureLogger.log("TorManager: SOCKS ready at \(self.socksHost):\(self.socksPort)", category: .session, level: .info)
} else { } else {
self.lastError = NSError(domain: "TorManager", code: -13, userInfo: [NSLocalizedDescriptionKey: "Tor SOCKS not reachable after static start"]) self.lastError = NSError(domain: "TorManager", code: -13, userInfo: [NSLocalizedDescriptionKey: "Tor SOCKS not reachable after static start"])
SecureLogger.log("TorManager: SOCKS not reachable (timeout)", category: SecureLogger.session, level: .error) SecureLogger.log("TorManager: SOCKS not reachable (timeout)", category: .session, level: .error)
} }
// isStarting will be cleared when bootstrap reaches 100% // isStarting will be cleared when bootstrap reaches 100%
} }
+5 -5
View File
@@ -205,7 +205,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
let noiseKey = peerInfo.noisePublicKey { let noiseKey = peerInfo.noisePublicKey {
SecureLogger.log( SecureLogger.log(
"🔄 Found favorite for '\(peerInfo.nickname)' by nickname, updating noise key", "🔄 Found favorite for '\(peerInfo.nickname)' by nickname, updating noise key",
category: SecureLogger.session, category: .session,
level: .debug level: .debug
) )
@@ -283,7 +283,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
func toggleFavorite(_ peerID: String) { func toggleFavorite(_ peerID: String) {
guard let peer = getPeer(by: peerID) else { guard let peer = getPeer(by: peerID) else {
SecureLogger.log("⚠️ Cannot toggle favorite - peer not found: \(peerID)", SecureLogger.log("⚠️ Cannot toggle favorite - peer not found: \(peerID)",
category: SecureLogger.session, level: .warning) category: .session, level: .warning)
return return
} }
@@ -294,14 +294,14 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
// Debug logging to understand the issue // Debug logging to understand the issue
SecureLogger.log("🔍 Toggle favorite - peer.nickname: '\(peer.nickname)', peer.displayName: '\(peer.displayName)', peerID: \(peerID)", SecureLogger.log("🔍 Toggle favorite - peer.nickname: '\(peer.nickname)', peer.displayName: '\(peer.displayName)', peerID: \(peerID)",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
if actualNickname.isEmpty { if actualNickname.isEmpty {
// Try to get from mesh service's current peer list // Try to get from mesh service's current peer list
if let meshPeerNickname = meshService.peerNickname(peerID: peerID) { if let meshPeerNickname = meshService.peerNickname(peerID: peerID) {
actualNickname = meshPeerNickname actualNickname = meshPeerNickname
SecureLogger.log("🔍 Got nickname from mesh service: '\(actualNickname)'", SecureLogger.log("🔍 Got nickname from mesh service: '\(actualNickname)'",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
} }
} }
@@ -329,7 +329,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
// Log the final nickname being saved // Log the final nickname being saved
SecureLogger.log("⭐️ Toggled favorite for '\(finalNickname)' (peerID: \(peerID), was: \(wasFavorite), now: \(!wasFavorite))", SecureLogger.log("⭐️ Toggled favorite for '\(finalNickname)' (peerID: \(peerID), was: \(wasFavorite), now: \(!wasFavorite))",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
// Send favorite notification to the peer via router (mesh or Nostr) // Send favorite notification to the peer via router (mesh or Nostr)
if let router = messageRouter { if let router = messageRouter {
+20
View File
@@ -0,0 +1,20 @@
//
// OSLog+Categories.swift
// bitchat
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import os.log
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")
static let session = OSLog(subsystem: subsystem, category: "session")
static let security = OSLog(subsystem: subsystem, category: "security")
static let handshake = OSLog(subsystem: subsystem, category: "handshake")
}
+8 -19
View File
@@ -13,17 +13,6 @@ import os.log
/// Provides safe logging that filters sensitive data and security events /// Provides safe logging that filters sensitive data and security events
final class SecureLogger { final class SecureLogger {
// MARK: - Log Categories
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")
static let session = OSLog(subsystem: subsystem, category: "session")
static let security = OSLog(subsystem: subsystem, category: "security")
static let handshake = OSLog(subsystem: subsystem, category: "handshake")
// MARK: - Timestamp Formatter // MARK: - Timestamp Formatter
private static let timestampFormatter: DateFormatter = { private static let timestampFormatter: DateFormatter = {
@@ -132,15 +121,15 @@ final class SecureLogger {
let message = "\(location) \(event.message)" let message = "\(location) \(event.message)"
#if DEBUG #if DEBUG
os_log("%{public}@", log: security, type: level.osLogType, message) os_log("%{public}@", log: .security, type: level.osLogType, message)
#else #else
// In release, use private logging to prevent sensitive data exposure // In release, use private logging to prevent sensitive data exposure
os_log("%{private}@", log: security, type: level.osLogType, message) os_log("%{private}@", log: .security, type: level.osLogType, message)
#endif #endif
} }
/// Log general messages with automatic sensitive data filtering /// Log general messages with automatic sensitive data filtering
static func log(_ message: @autoclosure () -> String, category: OSLog = noise, level: LogLevel = .debug, static func log(_ message: @autoclosure () -> String, category: OSLog = .noise, level: LogLevel = .debug,
file: String = #file, line: Int = #line, function: String = #function) { file: String = #file, line: Int = #line, function: String = #function) {
guard shouldLog(level) else { return } guard shouldLog(level) else { return }
let location = formatLocation(file: file, line: line, function: function) let location = formatLocation(file: file, line: line, function: function)
@@ -157,7 +146,7 @@ final class SecureLogger {
} }
/// Log errors with context /// Log errors with context
static func logError(_ error: Error, context: @autoclosure () -> String, category: OSLog = noise, static func logError(_ error: Error, context: @autoclosure () -> String, category: OSLog = .noise,
file: String = #file, line: Int = #line, function: String = #function) { file: String = #file, line: Int = #line, function: String = #function) {
let location = formatLocation(file: file, line: line, function: function) let location = formatLocation(file: file, line: line, function: function)
let sanitized = sanitize(context()) let sanitized = sanitize(context())
@@ -240,10 +229,10 @@ extension SecureLogger {
static func logHandshake(_ phase: String, peerID: String, success: Bool = true, static func logHandshake(_ phase: String, peerID: String, success: Bool = true,
file: String = #file, line: Int = #line, function: String = #function) { file: String = #file, line: Int = #line, function: String = #function) {
if success { if success {
log("Handshake \(phase) with peer: \(peerID)", category: session, level: .info, log("Handshake \(phase) with peer: \(peerID)", category: .session, level: .info,
file: file, line: line, function: function) file: file, line: line, function: function)
} else { } else {
log("Handshake \(phase) failed with peer: \(peerID)", category: session, level: .warning, log("Handshake \(phase) failed with peer: \(peerID)", category: .session, level: .warning,
file: file, line: line, function: function) file: file, line: line, function: function)
} }
} }
@@ -253,7 +242,7 @@ extension SecureLogger {
file: String = #file, line: Int = #line, function: String = #function) { file: String = #file, line: Int = #line, function: String = #function) {
let level: LogLevel = success ? .debug : .error let level: LogLevel = success ? .debug : .error
log("Encryption operation '\(operation)' \(success ? "succeeded" : "failed")", log("Encryption operation '\(operation)' \(success ? "succeeded" : "failed")",
category: encryption, level: level, file: file, line: line, function: function) category: .encryption, level: level, file: file, line: line, function: function)
} }
/// Log key management operations /// Log key management operations
@@ -261,7 +250,7 @@ extension SecureLogger {
file: String = #file, line: Int = #line, function: String = #function) { file: String = #file, line: Int = #line, function: String = #function) {
let level: LogLevel = success ? .debug : .error let level: LogLevel = success ? .debug : .error
log("Key operation '\(operation)' for \(keyType) \(success ? "succeeded" : "failed")", log("Key operation '\(operation)' for \(keyType) \(success ? "succeeded" : "failed")",
category: keychain, level: level, file: file, line: line, function: function) category: .keychain, level: level, file: file, line: line, function: function)
} }
} }
+87 -87
View File
@@ -463,7 +463,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
UserDefaults.standard.set(data, forKey: "sentReadReceipts") UserDefaults.standard.set(data, forKey: "sentReadReceipts")
} else { } else {
SecureLogger.log("❌ Failed to encode read receipts for persistence", SecureLogger.log("❌ Failed to encode read receipts for persistence",
category: SecureLogger.session, level: .error) category: .session, level: .error)
} }
} }
} }
@@ -562,11 +562,11 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Wait for Tor readiness before Nostr init // Wait for Tor readiness before Nostr init
let ready = await TorManager.shared.awaitReady(timeout: 60) let ready = await TorManager.shared.awaitReady(timeout: 60)
guard ready else { guard ready else {
SecureLogger.log("Nostr init skipped: Tor not ready", category: SecureLogger.session, level: .error) SecureLogger.log("Nostr init skipped: Tor not ready", category: .session, level: .error)
return return
} }
nostrRelayManager = NostrRelayManager.shared nostrRelayManager = NostrRelayManager.shared
SecureLogger.log("Initializing Nostr relay connections", category: SecureLogger.session, level: .debug) SecureLogger.log("Initializing Nostr relay connections", category: .session, level: .debug)
// Connect is managed centrally on scene activation; avoid duplicate connects here // Connect is managed centrally on scene activation; avoid duplicate connects here
// Attempt to flush any queued outbox after Nostr comes online // Attempt to flush any queued outbox after Nostr comes online
@@ -597,7 +597,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
uniquePeers[peer.id] = peer uniquePeers[peer.id] = peer
} else { } else {
SecureLogger.log("⚠️ Duplicate peer ID detected: \(peer.id) (\(peer.displayName))", SecureLogger.log("⚠️ Duplicate peer ID detected: \(peer.id) (\(peer.displayName))",
category: SecureLogger.session, level: .warning) category: .session, level: .warning)
} }
} }
self.peerIndex = uniquePeers self.peerIndex = uniquePeers
@@ -1036,10 +1036,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
self.privateChats[convKey]?[idx].deliveryStatus = .delivered(to: self.displayNameForNostrPubkey(senderPubkey), at: Date()) self.privateChats[convKey]?[idx].deliveryStatus = .delivered(to: self.displayNameForNostrPubkey(senderPubkey), at: Date())
self.objectWillChange.send() self.objectWillChange.send()
SecureLogger.log("GeoDM: recv DELIVERED for mid=\(messageID.prefix(8))… from=\(senderPubkey.prefix(8))", SecureLogger.log("GeoDM: recv DELIVERED for mid=\(messageID.prefix(8))… from=\(senderPubkey.prefix(8))",
category: SecureLogger.session, level: .info) category: .session, level: .info)
} else { } else {
SecureLogger.log("GeoDM: delivered ack for unknown mid=\(messageID.prefix(8))… conv=\(convKey)", SecureLogger.log("GeoDM: delivered ack for unknown mid=\(messageID.prefix(8))… conv=\(convKey)",
category: SecureLogger.session, level: .warning) category: .session, level: .warning)
} }
} }
case .readReceipt: case .readReceipt:
@@ -1048,10 +1048,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
self.privateChats[convKey]?[idx].deliveryStatus = .read(by: self.displayNameForNostrPubkey(senderPubkey), at: Date()) self.privateChats[convKey]?[idx].deliveryStatus = .read(by: self.displayNameForNostrPubkey(senderPubkey), at: Date())
self.objectWillChange.send() self.objectWillChange.send()
SecureLogger.log("GeoDM: recv READ for mid=\(messageID.prefix(8))… from=\(senderPubkey.prefix(8))", SecureLogger.log("GeoDM: recv READ for mid=\(messageID.prefix(8))… from=\(senderPubkey.prefix(8))",
category: SecureLogger.session, level: .info) category: .session, level: .info)
} else { } else {
SecureLogger.log("GeoDM: read ack for unknown mid=\(messageID.prefix(8))… conv=\(convKey)", SecureLogger.log("GeoDM: read ack for unknown mid=\(messageID.prefix(8))… conv=\(convKey)",
category: SecureLogger.session, level: .warning) category: .session, level: .warning)
} }
} }
case .verifyChallenge, .verifyResponse: case .verifyChallenge, .verifyResponse:
@@ -1464,14 +1464,14 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
count: TransportConfig.nostrGeoRelayCount count: TransportConfig.nostrGeoRelayCount
) )
if targetRelays.isEmpty { if targetRelays.isEmpty {
SecureLogger.log("Geo: no geohash relays available for \(ch.geohash); not sending", category: SecureLogger.session, level: .warning) SecureLogger.log("Geo: no geohash relays available for \(ch.geohash); not sending", category: .session, level: .warning)
} else { } else {
NostrRelayManager.shared.sendEvent(event, to: targetRelays) NostrRelayManager.shared.sendEvent(event, to: targetRelays)
} }
// Track ourselves as active participant // Track ourselves as active participant
self.recordGeoParticipant(pubkeyHex: identity.publicKeyHex) self.recordGeoParticipant(pubkeyHex: identity.publicKeyHex)
SecureLogger.log("GeoTeleport: sent geo message pub=\(identity.publicKeyHex.prefix(8))… teleported=\(LocationChannelManager.shared.teleported)", SecureLogger.log("GeoTeleport: sent geo message pub=\(identity.publicKeyHex.prefix(8))… teleported=\(LocationChannelManager.shared.teleported)",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
// If we tagged this as teleported, also mark our pubkey in teleportedGeo for UI // If we tagged this as teleported, also mark our pubkey in teleportedGeo for UI
// Only when not in our regional set (and regional list is known) // Only when not in our regional set (and regional list is known)
let hasRegional = !LocationChannelManager.shared.availableChannels.isEmpty let hasRegional = !LocationChannelManager.shared.availableChannels.isEmpty
@@ -1480,10 +1480,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
let key = identity.publicKeyHex.lowercased() let key = identity.publicKeyHex.lowercased()
self.teleportedGeo = self.teleportedGeo.union([key]) self.teleportedGeo = self.teleportedGeo.union([key])
SecureLogger.log("GeoTeleport: mark self teleported key=\(key.prefix(8))… total=\(self.teleportedGeo.count)", SecureLogger.log("GeoTeleport: mark self teleported key=\(key.prefix(8))… total=\(self.teleportedGeo.count)",
category: SecureLogger.session, level: .info) category: .session, level: .info)
} }
} catch { } catch {
SecureLogger.log("❌ Failed to send geohash message: \(error)", category: SecureLogger.session, level: .error) SecureLogger.log("❌ Failed to send geohash message: \(error)", category: .session, level: .error)
self.addSystemMessage("failed to send to location channel") self.addSystemMessage("failed to send to location channel")
} }
} }
@@ -1510,7 +1510,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Debug: log if any empty messages are present // Debug: log if any empty messages are present
let emptyMesh = messages.filter { $0.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }.count let emptyMesh = messages.filter { $0.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }.count
if emptyMesh > 0 { if emptyMesh > 0 {
SecureLogger.log("RenderGuard: mesh timeline contains \(emptyMesh) empty messages", category: SecureLogger.session, level: .debug) SecureLogger.log("RenderGuard: mesh timeline contains \(emptyMesh) empty messages", category: .session, level: .debug)
} }
stopGeoParticipantsTimer() stopGeoParticipantsTimer()
geohashPeople = [] geohashPeople = []
@@ -1537,7 +1537,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Debug: log if any empty messages are present post-sanitize // Debug: log if any empty messages are present post-sanitize
let emptyGeo = messages.filter { $0.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }.count let emptyGeo = messages.filter { $0.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }.count
if emptyGeo > 0 { if emptyGeo > 0 {
SecureLogger.log("RenderGuard: geohash \(ch.geohash) timeline has \(emptyGeo) empty messages after sanitize", category: SecureLogger.session, level: .debug) SecureLogger.log("RenderGuard: geohash \(ch.geohash) timeline has \(emptyGeo) empty messages after sanitize", category: .session, level: .debug)
} }
} }
// If switching to a location channel, flush any pending geohash-only system messages // If switching to a location channel, flush any pending geohash-only system messages
@@ -1570,7 +1570,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
if LocationChannelManager.shared.teleported && hasRegional && !inRegional { if LocationChannelManager.shared.teleported && hasRegional && !inRegional {
teleportedGeo = teleportedGeo.union([key]) teleportedGeo = teleportedGeo.union([key])
SecureLogger.log("GeoTeleport: channel switch mark self teleported key=\(key.prefix(8))… total=\(teleportedGeo.count)", SecureLogger.log("GeoTeleport: channel switch mark self teleported key=\(key.prefix(8))… total=\(teleportedGeo.count)",
category: SecureLogger.session, level: .info) category: .session, level: .info)
} else { } else {
teleportedGeo.remove(key) teleportedGeo.remove(key)
} }
@@ -1594,7 +1594,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Log incoming tags for diagnostics // Log incoming tags for diagnostics
let tagSummary = event.tags.map { "[" + $0.joined(separator: ",") + "]" }.joined(separator: ",") let tagSummary = event.tags.map { "[" + $0.joined(separator: ",") + "]" }.joined(separator: ",")
SecureLogger.log("GeoTeleport: recv pub=\(event.pubkey.prefix(8))… tags=\(tagSummary)", SecureLogger.log("GeoTeleport: recv pub=\(event.pubkey.prefix(8))… tags=\(tagSummary)",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
// Track teleport tag for participants only our format ["t", "teleport"] // Track teleport tag for participants only our format ["t", "teleport"]
let hasTeleportTag: Bool = event.tags.contains(where: { tag in let hasTeleportTag: Bool = event.tags.contains(where: { tag in
tag.count >= 2 && tag[0].lowercased() == "t" && tag[1].lowercased() == "teleport" tag.count >= 2 && tag[0].lowercased() == "t" && tag[1].lowercased() == "teleport"
@@ -1612,7 +1612,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
Task { @MainActor in Task { @MainActor in
self.teleportedGeo = self.teleportedGeo.union([key]) self.teleportedGeo = self.teleportedGeo.union([key])
SecureLogger.log("GeoTeleport: mark peer teleported key=\(key.prefix(8))… total=\(self.teleportedGeo.count)", SecureLogger.log("GeoTeleport: mark peer teleported key=\(key.prefix(8))… total=\(self.teleportedGeo.count)",
category: SecureLogger.session, level: .info) category: .session, level: .info)
} }
} }
} }
@@ -1677,7 +1677,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Log GeoDM subscribe only when Tor is ready to avoid early noise // Log GeoDM subscribe only when Tor is ready to avoid early noise
if TorManager.shared.isReady { if TorManager.shared.isReady {
SecureLogger.log("GeoDM: subscribing DMs pub=\(id.publicKeyHex.prefix(8))… sub=\(dmSub)", SecureLogger.log("GeoDM: subscribing DMs pub=\(id.publicKeyHex.prefix(8))… sub=\(dmSub)",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
} }
let dmFilter = NostrFilter.giftWrapsFor(pubkey: id.publicKeyHex, since: Date().addingTimeInterval(-TransportConfig.nostrDMSubscribeLookbackSeconds)) let dmFilter = NostrFilter.giftWrapsFor(pubkey: id.publicKeyHex, since: Date().addingTimeInterval(-TransportConfig.nostrDMSubscribeLookbackSeconds))
NostrRelayManager.shared.subscribe(filter: dmFilter, id: dmSub) { [weak self] giftWrap in NostrRelayManager.shared.subscribe(filter: dmFilter, id: dmSub) { [weak self] giftWrap in
@@ -1688,11 +1688,11 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Decrypt with per-geohash identity // Decrypt with per-geohash identity
guard let (content, senderPubkey, rumorTs) = try? NostrProtocol.decryptPrivateMessage(giftWrap: giftWrap, recipientIdentity: id) else { guard let (content, senderPubkey, rumorTs) = try? NostrProtocol.decryptPrivateMessage(giftWrap: giftWrap, recipientIdentity: id) else {
SecureLogger.log("GeoDM: failed decrypt giftWrap id=\(giftWrap.id.prefix(8))", SecureLogger.log("GeoDM: failed decrypt giftWrap id=\(giftWrap.id.prefix(8))",
category: SecureLogger.session, level: .warning) category: .session, level: .warning)
return return
} }
SecureLogger.log("GeoDM: decrypted gift-wrap id=\(giftWrap.id.prefix(16))... from=\(senderPubkey.prefix(8))...", SecureLogger.log("GeoDM: decrypted gift-wrap id=\(giftWrap.id.prefix(16))... from=\(senderPubkey.prefix(8))...",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
guard content.hasPrefix("bitchat1:") else { return } guard content.hasPrefix("bitchat1:") else { return }
guard let packetData = Self.base64URLDecode(String(content.dropFirst("bitchat1:".count))), guard let packetData = Self.base64URLDecode(String(content.dropFirst("bitchat1:".count))),
let packet = BitchatPacket.from(packetData) else { return } let packet = BitchatPacket.from(packetData) else { return }
@@ -1706,7 +1706,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
guard let pm = PrivateMessagePacket.decode(from: noisePayload.data) else { return } guard let pm = PrivateMessagePacket.decode(from: noisePayload.data) else { return }
let messageId = pm.messageID let messageId = pm.messageID
SecureLogger.log("GeoDM: recv PM <- sender=\(senderPubkey.prefix(8))… mid=\(messageId.prefix(8))", SecureLogger.log("GeoDM: recv PM <- sender=\(senderPubkey.prefix(8))… mid=\(messageId.prefix(8))",
category: SecureLogger.session, level: .info) category: .session, level: .info)
// Send delivery ACK immediately (even if duplicate), once per messageID // Send delivery ACK immediately (even if duplicate), once per messageID
if !self.sentGeoDeliveryAcks.contains(messageId) { if !self.sentGeoDeliveryAcks.contains(messageId) {
let nostrTransport = NostrTransport() let nostrTransport = NostrTransport()
@@ -1770,10 +1770,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
self.privateChats[convKey]?[idx].deliveryStatus = .delivered(to: self.displayNameForNostrPubkey(senderPubkey), at: Date()) self.privateChats[convKey]?[idx].deliveryStatus = .delivered(to: self.displayNameForNostrPubkey(senderPubkey), at: Date())
self.objectWillChange.send() self.objectWillChange.send()
SecureLogger.log("GeoDM: recv DELIVERED for mid=\(messageID.prefix(8))… from=\(senderPubkey.prefix(8))", SecureLogger.log("GeoDM: recv DELIVERED for mid=\(messageID.prefix(8))… from=\(senderPubkey.prefix(8))",
category: SecureLogger.session, level: .info) category: .session, level: .info)
} else { } else {
SecureLogger.log("GeoDM: delivered ack for unknown mid=\(messageID.prefix(8))… conv=\(convKey)", SecureLogger.log("GeoDM: delivered ack for unknown mid=\(messageID.prefix(8))… conv=\(convKey)",
category: SecureLogger.session, level: .warning) category: .session, level: .warning)
} }
} }
case .readReceipt: case .readReceipt:
@@ -1782,10 +1782,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
self.privateChats[convKey]?[idx].deliveryStatus = .read(by: self.displayNameForNostrPubkey(senderPubkey), at: Date()) self.privateChats[convKey]?[idx].deliveryStatus = .read(by: self.displayNameForNostrPubkey(senderPubkey), at: Date())
self.objectWillChange.send() self.objectWillChange.send()
SecureLogger.log("GeoDM: recv READ for mid=\(messageID.prefix(8))… from=\(senderPubkey.prefix(8))", SecureLogger.log("GeoDM: recv READ for mid=\(messageID.prefix(8))… from=\(senderPubkey.prefix(8))",
category: SecureLogger.session, level: .info) category: .session, level: .info)
} else { } else {
SecureLogger.log("GeoDM: read ack for unknown mid=\(messageID.prefix(8))… conv=\(convKey)", SecureLogger.log("GeoDM: read ack for unknown mid=\(messageID.prefix(8))… conv=\(convKey)",
category: SecureLogger.session, level: .warning) category: .session, level: .warning)
} }
} }
default: default:
@@ -2153,7 +2153,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
return return
} }
SecureLogger.log("GeoDM: local send mid=\(messageID.prefix(8))… to=\(recipientHex.prefix(8))… conv=\(peerID)", SecureLogger.log("GeoDM: local send mid=\(messageID.prefix(8))… to=\(recipientHex.prefix(8))… conv=\(peerID)",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
let nostrTransport = NostrTransport() let nostrTransport = NostrTransport()
nostrTransport.senderPeerID = meshService.myPeerID nostrTransport.senderPeerID = meshService.myPeerID
nostrTransport.sendPrivateMessageGeohash(content: content, toRecipientHex: recipientHex, from: id, messageID: messageID) nostrTransport.sendPrivateMessageGeohash(content: content, toRecipientHex: recipientHex, from: id, messageID: messageID)
@@ -2474,7 +2474,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
if hadUnreadTemp { if hadUnreadTemp {
unreadPrivateMessages.insert(peerID) unreadPrivateMessages.insert(peerID)
SecureLogger.log("📬 Transferred unread status from temp peer IDs to \(peerID)", SecureLogger.log("📬 Transferred unread status from temp peer IDs to \(peerID)",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
} }
if consolidatedCount > 0 { if consolidatedCount > 0 {
@@ -2482,7 +2482,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
privateChats[peerID]?.sort { $0.timestamp < $1.timestamp } privateChats[peerID]?.sort { $0.timestamp < $1.timestamp }
SecureLogger.log("📥 Consolidated \(consolidatedCount) Nostr messages from temporary peer IDs to \(peerNickname)", SecureLogger.log("📥 Consolidated \(consolidatedCount) Nostr messages from temporary peer IDs to \(peerNickname)",
category: SecureLogger.session, level: .info) category: .session, level: .info)
} }
} }
@@ -2497,7 +2497,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
} }
} else { } else {
SecureLogger.log("GeoDM: skipping mesh handshake for virtual peerID=\(peerID)", SecureLogger.log("GeoDM: skipping mesh handshake for virtual peerID=\(peerID)",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
} }
// Delegate to private chat manager but add already-acked messages first // Delegate to private chat manager but add already-acked messages first
@@ -2578,7 +2578,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
guard let receipt = notification.userInfo?["receipt"] as? ReadReceipt else { return } guard let receipt = notification.userInfo?["receipt"] as? ReadReceipt else { return }
SecureLogger.log("📖 Handling read receipt for message \(receipt.originalMessageID) from Nostr", SecureLogger.log("📖 Handling read receipt for message \(receipt.originalMessageID) from Nostr",
category: SecureLogger.session, level: .info) category: .session, level: .info)
// Process the read receipt through the same flow as Bluetooth read receipts // Process the read receipt through the same flow as Bluetooth read receipts
didReceiveReadReceipt(receipt) didReceiveReadReceipt(receipt)
@@ -2604,7 +2604,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// If we have a private chat open with the old peer ID, update it to the new one // If we have a private chat open with the old peer ID, update it to the new one
if selectedPrivateChatPeer == oldPeerID { if selectedPrivateChatPeer == oldPeerID {
SecureLogger.log("📱 Updating private chat peer ID due to key change: \(oldPeerID) -> \(newPeerID)", SecureLogger.log("📱 Updating private chat peer ID due to key change: \(oldPeerID) -> \(newPeerID)",
category: SecureLogger.session, level: .info) category: .session, level: .info)
// Transfer private chat messages to new peer ID // Transfer private chat messages to new peer ID
if let messages = privateChats[oldPeerID] { if let messages = privateChats[oldPeerID] {
@@ -2636,7 +2636,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Even if the chat isn't open, migrate any existing private chat data // Even if the chat isn't open, migrate any existing private chat data
if let messages = privateChats[oldPeerID] { if let messages = privateChats[oldPeerID] {
SecureLogger.log("📱 Migrating private chat messages from \(oldPeerID) to \(newPeerID)", SecureLogger.log("📱 Migrating private chat messages from \(oldPeerID) to \(newPeerID)",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
var chats = privateChats var chats = privateChats
chats[newPeerID] = messages chats[newPeerID] = messages
chats.removeValue(forKey: oldPeerID) chats.removeValue(forKey: oldPeerID)
@@ -2742,7 +2742,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
messageRouter.sendPrivate(screenshotMessage, to: peerID, recipientNickname: peerNickname, messageID: UUID().uuidString) messageRouter.sendPrivate(screenshotMessage, to: peerID, recipientNickname: peerNickname, messageID: UUID().uuidString)
default: default:
// Don't send screenshot notification if no session exists // Don't send screenshot notification if no session exists
SecureLogger.log("Skipping screenshot notification to \(peerID) - no established session", category: SecureLogger.security, level: .debug) SecureLogger.log("Skipping screenshot notification to \(peerID) - no established session", category: .security, level: .debug)
} }
} }
@@ -2783,14 +2783,14 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
) )
let targetRelays = GeoRelayDirectory.shared.closestRelays(toGeohash: ch.geohash, count: 5) let targetRelays = GeoRelayDirectory.shared.closestRelays(toGeohash: ch.geohash, count: 5)
if targetRelays.isEmpty { if targetRelays.isEmpty {
SecureLogger.log("Geo: no geohash relays available for \(ch.geohash); not sending", category: SecureLogger.session, level: .warning) SecureLogger.log("Geo: no geohash relays available for \(ch.geohash); not sending", category: .session, level: .warning)
} else { } else {
NostrRelayManager.shared.sendEvent(event, to: targetRelays) NostrRelayManager.shared.sendEvent(event, to: targetRelays)
} }
// Track ourselves as active participant // Track ourselves as active participant
self.recordGeoParticipant(pubkeyHex: identity.publicKeyHex) self.recordGeoParticipant(pubkeyHex: identity.publicKeyHex)
} catch { } catch {
SecureLogger.log("❌ Failed to send geohash screenshot message: \(error)", category: SecureLogger.session, level: .error) SecureLogger.log("❌ Failed to send geohash screenshot message: \(error)", category: .session, level: .error)
self.addSystemMessage("failed to send to location channel") self.addSystemMessage("failed to send to location channel")
} }
} }
@@ -2849,7 +2849,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
for (currentPeerID, currentNickname) in meshService.getPeerNicknames() { for (currentPeerID, currentNickname) in meshService.getPeerNicknames() {
if currentNickname == peerNickname { if currentNickname == peerNickname {
SecureLogger.log("📖 Resolved updated peer ID for read receipt: \(peerID) -> \(currentPeerID)", SecureLogger.log("📖 Resolved updated peer ID for read receipt: \(peerID) -> \(currentPeerID)",
category: SecureLogger.session, level: .info) category: .session, level: .info)
actualPeerID = currentPeerID actualPeerID = currentPeerID
break break
} }
@@ -2878,7 +2878,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
for message in messages where message.senderPeerID == peerID && !message.isRelay { for message in messages where message.senderPeerID == peerID && !message.isRelay {
if !sentReadReceipts.contains(message.id) { if !sentReadReceipts.contains(message.id) {
SecureLogger.log("GeoDM: sending READ for mid=\(message.id.prefix(8))… to=\(recipientHex.prefix(8))", SecureLogger.log("GeoDM: sending READ for mid=\(message.id.prefix(8))… to=\(recipientHex.prefix(8))",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
let nostrTransport = NostrTransport() let nostrTransport = NostrTransport()
nostrTransport.senderPeerID = meshService.myPeerID nostrTransport.senderPeerID = meshService.myPeerID
nostrTransport.sendReadReceiptGeohash(message.id, toRecipientHex: recipientHex, from: id) nostrTransport.sendReadReceiptGeohash(message.id, toRecipientHex: recipientHex, from: id)
@@ -4322,14 +4322,14 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
verifiedFingerprints = SecureIdentityStateManager.shared.getVerifiedFingerprints() verifiedFingerprints = SecureIdentityStateManager.shared.getVerifiedFingerprints()
// Log snapshot for debugging persistence // Log snapshot for debugging persistence
let sample = Array(verifiedFingerprints.prefix(TransportConfig.uiFingerprintSampleCount)).map { $0.prefix(8) }.joined(separator: ", ") let sample = Array(verifiedFingerprints.prefix(TransportConfig.uiFingerprintSampleCount)).map { $0.prefix(8) }.joined(separator: ", ")
SecureLogger.log("🔐 Verified loaded: \(verifiedFingerprints.count) [\(sample)]", category: SecureLogger.security, level: .info) SecureLogger.log("🔐 Verified loaded: \(verifiedFingerprints.count) [\(sample)]", category: .security, level: .info)
// Also log any offline favorites and whether we consider them verified // Also log any offline favorites and whether we consider them verified
let offlineFavorites = unifiedPeerService.favorites.filter { !$0.isConnected } let offlineFavorites = unifiedPeerService.favorites.filter { !$0.isConnected }
for fav in offlineFavorites { for fav in offlineFavorites {
let fp = unifiedPeerService.getFingerprint(for: fav.id) let fp = unifiedPeerService.getFingerprint(for: fav.id)
let isVer = fp.flatMap { verifiedFingerprints.contains($0) } ?? false let isVer = fp.flatMap { verifiedFingerprints.contains($0) } ?? false
let fpShort = fp?.prefix(8) ?? "nil" let fpShort = fp?.prefix(8) ?? "nil"
SecureLogger.log("⭐️ Favorite offline: \(fav.nickname) fp=\(fpShort) verified=\(isVer)", category: SecureLogger.security, level: .info) SecureLogger.log("⭐️ Favorite offline: \(fav.nickname) fp=\(fpShort) verified=\(isVer)", category: .security, level: .info)
} }
// Invalidate cached encryption statuses so offline favorites can show verified badges immediately // Invalidate cached encryption statuses so offline favorites can show verified badges immediately
invalidateEncryptionCache() invalidateEncryptionCache()
@@ -4345,7 +4345,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
DispatchQueue.main.async { DispatchQueue.main.async {
guard let self = self else { return } guard let self = self else { return }
SecureLogger.log("🔐 Authenticated: \(peerID)", category: SecureLogger.security, level: .debug) SecureLogger.log("🔐 Authenticated: \(peerID)", category: .security, level: .debug)
// Update encryption status // Update encryption status
if self.verifiedFingerprints.contains(fingerprint) { if self.verifiedFingerprints.contains(fingerprint) {
@@ -4365,7 +4365,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
let stable = keyData.hexEncodedString() let stable = keyData.hexEncodedString()
self.shortIDToNoiseKey[peerID] = stable self.shortIDToNoiseKey[peerID] = stable
SecureLogger.log("🗺️ Mapped short peerID to Noise key for header continuity: \(peerID) -> \(stable.prefix(8))", SecureLogger.log("🗺️ Mapped short peerID to Noise key for header continuity: \(peerID) -> \(stable.prefix(8))",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
} }
// If a QR verification is pending but not sent yet, send it now that session is authenticated // If a QR verification is pending but not sent yet, send it now that session is authenticated
@@ -4373,7 +4373,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
self.meshService.sendVerifyChallenge(to: peerID, noiseKeyHex: pending.noiseKeyHex, nonceA: pending.nonceA) self.meshService.sendVerifyChallenge(to: peerID, noiseKeyHex: pending.noiseKeyHex, nonceA: pending.nonceA)
pending.sent = true pending.sent = true
self.pendingQRVerifications[peerID] = pending self.pendingQRVerifications[peerID] = pending
SecureLogger.log("📤 Sent deferred verify challenge to \(peerID) after handshake", category: SecureLogger.security, level: .debug) SecureLogger.log("📤 Sent deferred verify challenge to \(peerID) after handshake", category: .security, level: .debug)
} }
// Schedule UI update // Schedule UI update
@@ -4519,7 +4519,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
pendingQRVerifications.removeValue(forKey: peerID) pendingQRVerifications.removeValue(forKey: peerID)
if let fp = getFingerprint(for: peerID) { if let fp = getFingerprint(for: peerID) {
let short = fp.prefix(8) let short = fp.prefix(8)
SecureLogger.log("🔐 Marking verified fingerprint: \(short)", category: SecureLogger.security, level: .info) SecureLogger.log("🔐 Marking verified fingerprint: \(short)", category: .security, level: .info)
SecureIdentityStateManager.shared.setVerified(fingerprint: fp, verified: true) SecureIdentityStateManager.shared.setVerified(fingerprint: fp, verified: true)
SecureIdentityStateManager.shared.forceSave() SecureIdentityStateManager.shared.forceSave()
verifiedFingerprints.insert(fp) verifiedFingerprints.insert(fp)
@@ -4605,7 +4605,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// MARK: - Peer Connection Events // MARK: - Peer Connection Events
func didConnectToPeer(_ peerID: String) { func didConnectToPeer(_ peerID: String) {
SecureLogger.log("🤝 Peer connected: \(peerID)", category: SecureLogger.session, level: .debug) SecureLogger.log("🤝 Peer connected: \(peerID)", category: .session, level: .debug)
// Handle all main actor work async // Handle all main actor work async
Task { @MainActor in Task { @MainActor in
@@ -4623,7 +4623,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
try? await Task.sleep(nanoseconds: TransportConfig.uiAsyncMediumSleepNs) // 0.5 seconds try? await Task.sleep(nanoseconds: TransportConfig.uiAsyncMediumSleepNs) // 0.5 seconds
meshService.sendFavoriteNotification(to: peerID, isFavorite: true) meshService.sendFavoriteNotification(to: peerID, isFavorite: true)
SecureLogger.log("📤 Resent favorite notification to reconnected peer \(peerID)", SecureLogger.log("📤 Resent favorite notification to reconnected peer \(peerID)",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
} }
// Force UI refresh // Force UI refresh
@@ -4643,7 +4643,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
} }
func didDisconnectFromPeer(_ peerID: String) { func didDisconnectFromPeer(_ peerID: String) {
SecureLogger.log("👋 Peer disconnected: \(peerID)", category: SecureLogger.session, level: .debug) SecureLogger.log("👋 Peer disconnected: \(peerID)", category: .session, level: .debug)
// Remove ephemeral session from identity manager // Remove ephemeral session from identity manager
SecureIdentityStateManager.shared.removeEphemeralSession(peerID: peerID) SecureIdentityStateManager.shared.removeEphemeralSession(peerID: peerID)
@@ -4739,7 +4739,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
self.recentlySeenPeers = currentPeerSet self.recentlySeenPeers = currentPeerSet
NotificationService.shared.sendNetworkAvailableNotification(peerCount: meshPeers.count) NotificationService.shared.sendNetworkAvailableNotification(peerCount: meshPeers.count)
SecureLogger.log("👥 Sent bitchatters nearby notification for \(meshPeers.count) mesh peers", SecureLogger.log("👥 Sent bitchatters nearby notification for \(meshPeers.count) mesh peers",
category: SecureLogger.session, level: .info) category: .session, level: .info)
} }
} else { } else {
// No peers immediately reset to allow next rising-edge to notify // No peers immediately reset to allow next rising-edge to notify
@@ -4749,7 +4749,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
self.networkResetTimer?.invalidate() self.networkResetTimer?.invalidate()
self.networkResetTimer = nil self.networkResetTimer = nil
} }
SecureLogger.log("⏳ Mesh empty — reset network notification state", category: SecureLogger.session, level: .debug) SecureLogger.log("⏳ Mesh empty — reset network notification state", category: .session, level: .debug)
} }
// Register ephemeral sessions for all connected peers // Register ephemeral sessions for all connected peers
@@ -4812,7 +4812,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
if !idsToRemove.isEmpty { if !idsToRemove.isEmpty {
SecureLogger.log("🧹 Cleaned up \(idsToRemove.count) stale unread peer IDs", SecureLogger.log("🧹 Cleaned up \(idsToRemove.count) stale unread peer IDs",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
} }
} }
@@ -4843,7 +4843,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
let removedCount = oldCount - sentReadReceipts.count let removedCount = oldCount - sentReadReceipts.count
if removedCount > 0 { if removedCount > 0 {
SecureLogger.log("🧹 Cleaned up \(removedCount) old read receipts", SecureLogger.log("🧹 Cleaned up \(removedCount) old read receipts",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
} }
} }
@@ -4891,7 +4891,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
nostrPublicKey = data.hexEncodedString() nostrPublicKey = data.hexEncodedString()
} }
} catch { } catch {
SecureLogger.log("Failed to decode Nostr npub: \(error)", category: SecureLogger.session, level: .error) SecureLogger.log("Failed to decode Nostr npub: \(error)", category: .session, level: .error)
} }
} }
@@ -5032,7 +5032,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
NostrRelayManager.shared.sendEvent(event, to: targetRelays) NostrRelayManager.shared.sendEvent(event, to: targetRelays)
} }
} catch { } catch {
SecureLogger.log("❌ Failed to send geohash raw message: \(error)", category: SecureLogger.session, level: .error) SecureLogger.log("❌ Failed to send geohash raw message: \(error)", category: .session, level: .error)
} }
} }
return return
@@ -5048,12 +5048,12 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
@MainActor @MainActor
private func setupNostrMessageHandling() { private func setupNostrMessageHandling() {
guard let currentIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { guard let currentIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else {
SecureLogger.log("⚠️ No Nostr identity available for message handling", category: SecureLogger.session, level: .warning) SecureLogger.log("⚠️ No Nostr identity available for message handling", category: .session, level: .warning)
return return
} }
SecureLogger.log("🔑 Setting up Nostr subscription for pubkey: \(currentIdentity.publicKeyHex.prefix(16))...", SecureLogger.log("🔑 Setting up Nostr subscription for pubkey: \(currentIdentity.publicKeyHex.prefix(16))...",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
// Subscribe to Nostr messages // Subscribe to Nostr messages
let filter = NostrFilter.giftWrapsFor( let filter = NostrFilter.giftWrapsFor(
@@ -5093,19 +5093,19 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Expect embedded BitChat packet content // Expect embedded BitChat packet content
guard content.hasPrefix("bitchat1:") else { guard content.hasPrefix("bitchat1:") else {
SecureLogger.log("Ignoring non-embedded Nostr DM content", category: SecureLogger.session, level: .debug) SecureLogger.log("Ignoring non-embedded Nostr DM content", category: .session, level: .debug)
return return
} }
guard let packetData = Self.base64URLDecode(String(content.dropFirst("bitchat1:".count))), guard let packetData = Self.base64URLDecode(String(content.dropFirst("bitchat1:".count))),
let packet = BitchatPacket.from(packetData) else { let packet = BitchatPacket.from(packetData) else {
SecureLogger.log("Failed to decode embedded BitChat packet from Nostr DM", category: SecureLogger.session, level: .error) SecureLogger.log("Failed to decode embedded BitChat packet from Nostr DM", category: .session, level: .error)
return return
} }
// Only process typed noiseEncrypted envelope for private messages/receipts // Only process typed noiseEncrypted envelope for private messages/receipts
guard packet.type == MessageType.noiseEncrypted.rawValue else { guard packet.type == MessageType.noiseEncrypted.rawValue else {
SecureLogger.log("Unsupported embedded packet type: \(packet.type)", category: SecureLogger.session, level: .warning) SecureLogger.log("Unsupported embedded packet type: \(packet.type)", category: .session, level: .warning)
return return
} }
@@ -5119,7 +5119,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Parse plaintext typed payload // Parse plaintext typed payload
guard let noisePayload = NoisePayload.decode(packet.payload) else { guard let noisePayload = NoisePayload.decode(packet.payload) else {
SecureLogger.log("Failed to parse embedded NoisePayload", category: SecureLogger.session, level: .error) SecureLogger.log("Failed to parse embedded NoisePayload", category: .session, level: .error)
return return
} }
@@ -5215,14 +5215,14 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Send delivery ack via Nostr embedded // Send delivery ack via Nostr embedded
if !wasReadBefore { if !wasReadBefore {
if let key = actualSenderNoiseKey { if let key = actualSenderNoiseKey {
SecureLogger.log("Sending DELIVERED ack for \(messageId.prefix(8))… via router", category: SecureLogger.session, level: .debug) SecureLogger.log("Sending DELIVERED ack for \(messageId.prefix(8))… via router", category: .session, level: .debug)
messageRouter.sendDeliveryAck(messageId, to: key.hexEncodedString()) messageRouter.sendDeliveryAck(messageId, to: key.hexEncodedString())
} else if let id = try? NostrIdentityBridge.getCurrentNostrIdentity() { } else if let id = try? NostrIdentityBridge.getCurrentNostrIdentity() {
// Fallback: no Noise mapping yet send directly to sender's Nostr pubkey // Fallback: no Noise mapping yet send directly to sender's Nostr pubkey
let nt = NostrTransport() let nt = NostrTransport()
nt.senderPeerID = meshService.myPeerID nt.senderPeerID = meshService.myPeerID
nt.sendDeliveryAckGeohash(for: messageId, toRecipientHex: senderPubkey, from: id) nt.sendDeliveryAckGeohash(for: messageId, toRecipientHex: senderPubkey, from: id)
SecureLogger.log("Sent DELIVERED ack directly to Nostr pub=\(senderPubkey.prefix(8))… for mid=\(messageId.prefix(8))", category: SecureLogger.session, level: .debug) SecureLogger.log("Sent DELIVERED ack directly to Nostr pub=\(senderPubkey.prefix(8))… for mid=\(messageId.prefix(8))", category: .session, level: .debug)
} }
} }
@@ -5237,7 +5237,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
if !sentReadReceipts.contains(messageId) { if !sentReadReceipts.contains(messageId) {
if let key = actualSenderNoiseKey { if let key = actualSenderNoiseKey {
let receipt = ReadReceipt(originalMessageID: messageId, readerID: meshService.myPeerID, readerNickname: nickname) let receipt = ReadReceipt(originalMessageID: messageId, readerID: meshService.myPeerID, readerNickname: nickname)
SecureLogger.log("Viewing chat; sending READ ack for \(messageId.prefix(8))… via router", category: SecureLogger.session, level: .debug) SecureLogger.log("Viewing chat; sending READ ack for \(messageId.prefix(8))… via router", category: .session, level: .debug)
messageRouter.sendReadReceipt(receipt, to: key.hexEncodedString()) messageRouter.sendReadReceipt(receipt, to: key.hexEncodedString())
sentReadReceipts.insert(messageId) sentReadReceipts.insert(messageId)
} else if let id = try? NostrIdentityBridge.getCurrentNostrIdentity() { } else if let id = try? NostrIdentityBridge.getCurrentNostrIdentity() {
@@ -5245,7 +5245,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
nt.senderPeerID = meshService.myPeerID nt.senderPeerID = meshService.myPeerID
nt.sendReadReceiptGeohash(messageId, toRecipientHex: senderPubkey, from: id) nt.sendReadReceiptGeohash(messageId, toRecipientHex: senderPubkey, from: id)
sentReadReceipts.insert(messageId) sentReadReceipts.insert(messageId)
SecureLogger.log("Viewing chat; sent READ ack directly to Nostr pub=\(senderPubkey.prefix(8))… for mid=\(messageId.prefix(8))", category: SecureLogger.session, level: .debug) SecureLogger.log("Viewing chat; sent READ ack directly to Nostr pub=\(senderPubkey.prefix(8))… for mid=\(messageId.prefix(8))", category: .session, level: .debug)
} }
} }
} else { } else {
@@ -5290,7 +5290,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
} }
} catch { } catch {
SecureLogger.log("Failed to decrypt Nostr message: \(error)", category: SecureLogger.session, level: .error) SecureLogger.log("Failed to decrypt Nostr message: \(error)", category: .session, level: .error)
} }
} }
@@ -5311,7 +5311,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Parse ACK format: "ACK:TYPE:MESSAGE_ID" // Parse ACK format: "ACK:TYPE:MESSAGE_ID"
let parts = content.split(separator: ":", maxSplits: 2) let parts = content.split(separator: ":", maxSplits: 2)
guard parts.count >= 3 else { guard parts.count >= 3 else {
SecureLogger.log("⚠️ Invalid ACK format: \(content)", category: SecureLogger.session, level: .warning) SecureLogger.log("⚠️ Invalid ACK format: \(content)", category: .session, level: .warning)
return return
} }
@@ -5327,7 +5327,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
processedNostrAcks.insert(ackKey) processedNostrAcks.insert(ackKey)
SecureLogger.log("📨 Received \(ackType) ACK for message \(messageId.prefix(16))... from \(senderPubkey.prefix(16))...", SecureLogger.log("📨 Received \(ackType) ACK for message \(messageId.prefix(16))... from \(senderPubkey.prefix(16))...",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
// Verify the sender has a valid Noise key // Verify the sender has a valid Noise key
guard findNoiseKey(for: senderPubkey) != nil else { guard findNoiseKey(for: senderPubkey) != nil else {
@@ -5346,12 +5346,12 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
case "READ": case "READ":
privateChats[chatPeerID]?[index].deliveryStatus = .read(by: "recipient", at: Date()) privateChats[chatPeerID]?[index].deliveryStatus = .read(by: "recipient", at: Date())
default: default:
SecureLogger.log("⚠️ Unknown ACK type: \(ackType)", category: SecureLogger.session, level: .warning) SecureLogger.log("⚠️ Unknown ACK type: \(ackType)", category: .session, level: .warning)
} }
messageFound = true messageFound = true
SecureLogger.log("✅ Updated message \(messageId.prefix(16))... status to \(ackType) in chat \(chatPeerID.prefix(16))...", SecureLogger.log("✅ Updated message \(messageId.prefix(16))... status to \(ackType) in chat \(chatPeerID.prefix(16))...",
category: SecureLogger.session, level: .info) category: .session, level: .info)
// Don't break - continue to update in all chats where this message exists // Don't break - continue to update in all chats where this message exists
} }
} }
@@ -5360,7 +5360,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
objectWillChange.send() objectWillChange.send()
} else { } else {
SecureLogger.log("⚠️ Could not find message \(messageId) to update status from ACK", SecureLogger.log("⚠️ Could not find message \(messageId) to update status from ACK",
category: SecureLogger.session, level: .warning) category: .session, level: .warning)
} }
} }
@@ -5387,7 +5387,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
if parts.count > 1 { if parts.count > 1 {
nostrPubkey = String(parts[1]) nostrPubkey = String(parts[1])
SecureLogger.log("📝 Received Nostr npub in favorite notification: \(nostrPubkey ?? "none")", SecureLogger.log("📝 Received Nostr npub in favorite notification: \(nostrPubkey ?? "none")",
category: SecureLogger.session, level: .info) category: .session, level: .info)
} }
// Get the noise public key for this peer // Get the noise public key for this peer
@@ -5406,7 +5406,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
guard let finalNoiseKey = noiseKey else { guard let finalNoiseKey = noiseKey else {
SecureLogger.log("⚠️ Cannot get Noise key for peer \(peerID)", SecureLogger.log("⚠️ Cannot get Noise key for peer \(peerID)",
category: SecureLogger.session, level: .warning) category: .session, level: .warning)
return return
} }
@@ -5421,7 +5421,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// If they favorited us and provided their Nostr key, ensure it's stored // If they favorited us and provided their Nostr key, ensure it's stored
if isFavorite && nostrPubkey != nil { if isFavorite && nostrPubkey != nil {
SecureLogger.log("💾 Storing Nostr key association for \(senderNickname): \(nostrPubkey!.prefix(16))...", SecureLogger.log("💾 Storing Nostr key association for \(senderNickname): \(nostrPubkey!.prefix(16))...",
category: SecureLogger.session, level: .info) category: .session, level: .info)
} }
// Show system message // Show system message
@@ -5533,7 +5533,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
} }
SecureLogger.log("📬 Stored Nostr message from unknown sender \(finalSenderNickname) in temporary peer \(tempPeerID)", SecureLogger.log("📬 Stored Nostr message from unknown sender \(finalSenderNickname) in temporary peer \(tempPeerID)",
category: SecureLogger.session, level: .info) category: .session, level: .info)
} }
@MainActor @MainActor
@@ -5546,7 +5546,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Try to convert hex to npub // Try to convert hex to npub
guard let pubkeyData = Data(hexString: nostrPubkey) else { guard let pubkeyData = Data(hexString: nostrPubkey) else {
SecureLogger.log("⚠️ Invalid hex public key format: \(nostrPubkey.prefix(16))...", SecureLogger.log("⚠️ Invalid hex public key format: \(nostrPubkey.prefix(16))...",
category: SecureLogger.session, level: .warning) category: .session, level: .warning)
return nil return nil
} }
@@ -5554,7 +5554,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
npubToMatch = try Bech32.encode(hrp: "npub", data: pubkeyData) npubToMatch = try Bech32.encode(hrp: "npub", data: pubkeyData)
} catch { } catch {
SecureLogger.log("⚠️ Failed to convert hex to npub: \(error)", SecureLogger.log("⚠️ Failed to convert hex to npub: \(error)",
category: SecureLogger.session, level: .warning) category: .session, level: .warning)
return nil return nil
} }
} }
@@ -5565,21 +5565,21 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Compare npub format // Compare npub format
if storedNostrKey == npubToMatch { if storedNostrKey == npubToMatch {
// SecureLogger.log(" Found Noise key for Nostr sender (npub match)", // SecureLogger.log(" Found Noise key for Nostr sender (npub match)",
// category: SecureLogger.session, level: .debug) // category: .session, level: .debug)
return noiseKey return noiseKey
} }
// Also try hex comparison if stored value is hex // Also try hex comparison if stored value is hex
if !storedNostrKey.hasPrefix("npub") && storedNostrKey == nostrPubkey { if !storedNostrKey.hasPrefix("npub") && storedNostrKey == nostrPubkey {
SecureLogger.log("✅ Found Noise key for Nostr sender (hex match)", SecureLogger.log("✅ Found Noise key for Nostr sender (hex match)",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
return noiseKey return noiseKey
} }
} }
} }
SecureLogger.log("⚠️ No matching Noise key found for Nostr pubkey: \(nostrPubkey.prefix(16))... (tried npub: \(npubToMatch.prefix(16))...)", SecureLogger.log("⚠️ No matching Noise key found for Nostr pubkey: \(nostrPubkey.prefix(16))... (tried npub: \(npubToMatch.prefix(16))...)",
category: SecureLogger.session, level: .debug) category: .session, level: .debug)
return nil return nil
} }
@@ -5607,13 +5607,13 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Try mesh first for connected peers // Try mesh first for connected peers
if meshService.isPeerConnected(peerID) { if meshService.isPeerConnected(peerID) {
messageRouter.sendFavoriteNotification(to: peerID, isFavorite: isFavorite) messageRouter.sendFavoriteNotification(to: peerID, isFavorite: isFavorite)
SecureLogger.log("📤 Sent favorite notification via BLE to \(peerID)", category: SecureLogger.session, level: .debug) SecureLogger.log("📤 Sent favorite notification via BLE to \(peerID)", category: .session, level: .debug)
} else if let key = noiseKey { } else if let key = noiseKey {
// Send via Nostr for offline peers (using router) // Send via Nostr for offline peers (using router)
let recipientPeerID = key.hexEncodedString() let recipientPeerID = key.hexEncodedString()
messageRouter.sendFavoriteNotification(to: recipientPeerID, isFavorite: isFavorite) messageRouter.sendFavoriteNotification(to: recipientPeerID, isFavorite: isFavorite)
} else { } else {
SecureLogger.log("⚠️ Cannot send favorite notification - peer not connected and no Nostr pubkey", category: SecureLogger.session, level: .warning) SecureLogger.log("⚠️ Cannot send favorite notification - peer not connected and no Nostr pubkey", category: .session, level: .warning)
} }
} }
@@ -5704,11 +5704,11 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
} else { } else {
// Keep old messages in original location but don't show in UI // Keep old messages in original location but don't show in UI
SecureLogger.log("📦 Partially migrating \(recentMessages.count) of \(messages.count) messages from \(oldPeerID)", SecureLogger.log("📦 Partially migrating \(recentMessages.count) of \(messages.count) messages from \(oldPeerID)",
category: SecureLogger.session, level: .info) category: .session, level: .info)
} }
SecureLogger.log("📦 Migrating \(recentMessages.count) recent messages from old peer ID \(oldPeerID) to \(peerID) (fingerprint match)", SecureLogger.log("📦 Migrating \(recentMessages.count) recent messages from old peer ID \(oldPeerID) to \(peerID) (fingerprint match)",
category: SecureLogger.session, level: .info) category: .session, level: .info)
} else if currentFingerprint == nil || oldFingerprint == nil { } else if currentFingerprint == nil || oldFingerprint == nil {
// Check if this chat contains messages with this sender by nickname // Check if this chat contains messages with this sender by nickname
let isRelevantChat = recentMessages.contains { msg in let isRelevantChat = recentMessages.contains { msg in
@@ -5725,7 +5725,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
} }
SecureLogger.log("📦 Migrating \(recentMessages.count) recent messages from old peer ID \(oldPeerID) to \(peerID) (nickname match)", SecureLogger.log("📦 Migrating \(recentMessages.count) recent messages from old peer ID \(oldPeerID) to \(peerID) (nickname match)",
category: SecureLogger.session, level: .warning) category: .session, level: .warning)
} }
} }
} }
@@ -5765,7 +5765,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
if needsSelectedUpdate { if needsSelectedUpdate {
selectedPrivateChatPeer = peerID selectedPrivateChatPeer = peerID
SecureLogger.log("📱 Updated selectedPrivateChatPeer from old ID to \(peerID) during migration", SecureLogger.log("📱 Updated selectedPrivateChatPeer from old ID to \(peerID) during migration",
category: SecureLogger.session, level: .info) category: .session, level: .info)
} }
} }
} }
@@ -5774,11 +5774,11 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
/// Handle incoming private message /// Handle incoming private message
@MainActor @MainActor
private func handlePrivateMessage(_ message: BitchatMessage) { private func handlePrivateMessage(_ message: BitchatMessage) {
SecureLogger.log("📥 handlePrivateMessage called for message from \(message.sender)", category: SecureLogger.session, level: .debug) SecureLogger.log("📥 handlePrivateMessage called for message from \(message.sender)", category: .session, level: .debug)
let senderPeerID = message.senderPeerID ?? getPeerIDForNickname(message.sender) let senderPeerID = message.senderPeerID ?? getPeerIDForNickname(message.sender)
guard let peerID = senderPeerID else { guard let peerID = senderPeerID else {
SecureLogger.log("⚠️ Could not get peer ID for sender \(message.sender)", category: SecureLogger.session, level: .warning) SecureLogger.log("⚠️ Could not get peer ID for sender \(message.sender)", category: .session, level: .warning)
return return
} }
@@ -5819,7 +5819,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
privateChats.removeValue(forKey: stableKeyHex) privateChats.removeValue(forKey: stableKeyHex)
SecureLogger.log("📥 Consolidated \(nostrMessages.count) Nostr messages from stable key to ephemeral peer \(peerID)", SecureLogger.log("📥 Consolidated \(nostrMessages.count) Nostr messages from stable key to ephemeral peer \(peerID)",
category: SecureLogger.session, level: .info) category: .session, level: .info)
} }
} }
} }
@@ -6087,7 +6087,7 @@ private func checkForMentions(_ message: BitchatMessage) {
if isMentioned && message.sender != nickname { if isMentioned && message.sender != nickname {
SecureLogger.log("🔔 Mention from \(message.sender)", SecureLogger.log("🔔 Mention from \(message.sender)",
category: SecureLogger.session, level: .info) category: .session, level: .info)
NotificationService.shared.sendMentionNotification(from: message.sender, message: message.content) NotificationService.shared.sendMentionNotification(from: message.sender, message: message.content)
} }
} }