mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 22:05:21 +00:00
Merge branch 'pr/575'
This commit is contained in:
@@ -129,7 +129,7 @@ final class SecureIdentityStateManager {
|
|||||||
// Try to load from keychain
|
// Try to load from keychain
|
||||||
if let keyData = keychain.getIdentityKey(forKey: encryptionKeyName) {
|
if let keyData = keychain.getIdentityKey(forKey: encryptionKeyName) {
|
||||||
loadedKey = SymmetricKey(data: keyData)
|
loadedKey = SymmetricKey(data: keyData)
|
||||||
SecureLogger.logKeyOperation("load", keyType: "identity cache encryption key", success: true)
|
SecureLogger.logKeyOperation(.load, keyType: "identity cache encryption key", success: true)
|
||||||
}
|
}
|
||||||
// Generate new key if needed
|
// Generate new key if needed
|
||||||
else {
|
else {
|
||||||
@@ -137,7 +137,7 @@ final class SecureIdentityStateManager {
|
|||||||
let keyData = loadedKey.withUnsafeBytes { Data($0) }
|
let keyData = loadedKey.withUnsafeBytes { Data($0) }
|
||||||
// Save to keychain
|
// Save to keychain
|
||||||
let saved = keychain.saveIdentityKey(keyData, forKey: encryptionKeyName)
|
let saved = keychain.saveIdentityKey(keyData, forKey: encryptionKeyName)
|
||||||
SecureLogger.logKeyOperation("generate", keyType: "identity cache encryption key", success: saved)
|
SecureLogger.logKeyOperation(.generate, keyType: "identity cache encryption key", success: saved)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.encryptionKey = loadedKey
|
self.encryptionKey = loadedKey
|
||||||
@@ -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.error(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.debug("Identity cache saved to keychain", category: .security)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
SecureLogger.logError(error, context: "Failed to save identity cache", category: SecureLogger.security)
|
SecureLogger.error(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.info("User \(isBlocked ? "blocked" : "unblocked"): \(fingerprint)", category: .security)
|
||||||
|
|
||||||
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.warning("Clearing all identity data", category: .security)
|
||||||
|
|
||||||
queue.async(flags: .barrier) {
|
queue.async(flags: .barrier) {
|
||||||
self.cache = IdentityCache()
|
self.cache = IdentityCache()
|
||||||
@@ -529,7 +529,7 @@ final class SecureIdentityStateManager {
|
|||||||
|
|
||||||
// Delete from keychain
|
// Delete from keychain
|
||||||
let deleted = self.keychain.deleteIdentityKey(forKey: self.cacheKey)
|
let deleted = self.keychain.deleteIdentityKey(forKey: self.cacheKey)
|
||||||
SecureLogger.logKeyOperation("delete", keyType: "identity cache", success: deleted)
|
SecureLogger.logKeyOperation(.delete, keyType: "identity cache", success: deleted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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.info("Fingerprint \(verified ? "verified" : "unverified"): \(fingerprint)", category: .security)
|
||||||
|
|
||||||
queue.async(flags: .barrier) {
|
queue.async(flags: .barrier) {
|
||||||
if verified {
|
if verified {
|
||||||
|
|||||||
@@ -68,8 +68,7 @@ final class NoiseHandshakeCoordinator {
|
|||||||
switch state {
|
switch state {
|
||||||
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.warning("Forcing new handshake with \(remotePeerID) - previous stuck in initiating", category: .handshake)
|
||||||
category: SecureLogger.handshake, level: .warning)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -77,8 +76,7 @@ final class NoiseHandshakeCoordinator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SecureLogger.log("Already in active handshake with \(remotePeerID), state: \(state)",
|
SecureLogger.debug("Already in active handshake with \(remotePeerID), state: \(state)", category: .handshake)
|
||||||
category: SecureLogger.handshake, level: .debug)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,8 +105,7 @@ final class NoiseHandshakeCoordinator {
|
|||||||
handshakeQueue.async(flags: .barrier) {
|
handshakeQueue.async(flags: .barrier) {
|
||||||
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.info("Recording handshake initiation with \(peerID), attempt \(attempt)", category: .handshake)
|
||||||
category: SecureLogger.handshake, level: .info)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,8 +113,7 @@ final class NoiseHandshakeCoordinator {
|
|||||||
func recordHandshakeResponse(peerID: String) {
|
func recordHandshakeResponse(peerID: String) {
|
||||||
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.info("Recording handshake response to \(peerID)", category: .handshake)
|
||||||
category: SecureLogger.handshake, level: .info)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,8 +121,7 @@ final class NoiseHandshakeCoordinator {
|
|||||||
func recordHandshakeSuccess(peerID: String) {
|
func recordHandshakeSuccess(peerID: String) {
|
||||||
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.info("Handshake successfully established with \(peerID)", category: .handshake)
|
||||||
category: SecureLogger.handshake, level: .info)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,8 +131,7 @@ final class NoiseHandshakeCoordinator {
|
|||||||
let attempts = self.getCurrentAttempt(for: peerID)
|
let attempts = self.getCurrentAttempt(for: peerID)
|
||||||
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.warning("Handshake failed with \(peerID): \(reason), canRetry: \(canRetry)", category: .handshake)
|
||||||
category: SecureLogger.handshake, level: .warning)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,8 +140,7 @@ final class NoiseHandshakeCoordinator {
|
|||||||
return handshakeQueue.sync {
|
return handshakeQueue.sync {
|
||||||
// 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.debug("Rejecting handshake from \(remotePeerID) - already established", category: .handshake)
|
||||||
category: SecureLogger.handshake, level: .debug)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,8 +150,7 @@ final class NoiseHandshakeCoordinator {
|
|||||||
if role == .initiator {
|
if role == .initiator {
|
||||||
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.warning("Accepting handshake from \(remotePeerID) despite being initiator (race condition recovery)", category: .handshake)
|
||||||
category: SecureLogger.handshake, level: .warning)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -215,8 +207,7 @@ final class NoiseHandshakeCoordinator {
|
|||||||
func resetHandshakeState(for peerID: String) {
|
func resetHandshakeState(for peerID: String) {
|
||||||
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.debug("Reset handshake state for \(peerID)", category: .handshake)
|
||||||
category: SecureLogger.handshake, level: .debug)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,8 +247,7 @@ final class NoiseHandshakeCoordinator {
|
|||||||
|
|
||||||
if isStale {
|
if isStale {
|
||||||
stalePeerIDs.append(peerID)
|
stalePeerIDs.append(peerID)
|
||||||
SecureLogger.log("Found stale handshake state for \(peerID): \(state)",
|
SecureLogger.warning("Found stale handshake state for \(peerID): \(state)", category: .handshake)
|
||||||
category: SecureLogger.handshake, level: .warning)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,8 +260,7 @@ final class NoiseHandshakeCoordinator {
|
|||||||
for i in 0..<sessionsToRemove {
|
for i in 0..<sessionsToRemove {
|
||||||
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.info("Removing old established session for \(peerID) to maintain session limit", category: .handshake)
|
||||||
category: SecureLogger.handshake, level: .info)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,8 +270,7 @@ final class NoiseHandshakeCoordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !stalePeerIDs.isEmpty {
|
if !stalePeerIDs.isEmpty {
|
||||||
SecureLogger.log("Cleaned up \(stalePeerIDs.count) stale handshake states",
|
SecureLogger.info("Cleaned up \(stalePeerIDs.count) stale handshake states", category: .handshake)
|
||||||
category: SecureLogger.handshake, level: .info)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return stalePeerIDs
|
return stalePeerIDs
|
||||||
@@ -333,7 +321,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.debug("=== Handshake States ===", category: .handshake)
|
||||||
for (peerID, state) in handshakeStates {
|
for (peerID, state) in handshakeStates {
|
||||||
let stateDesc: String
|
let stateDesc: String
|
||||||
switch state {
|
switch state {
|
||||||
@@ -352,16 +340,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.debug(" \(peerID): \(stateDesc)", category: .handshake)
|
||||||
}
|
}
|
||||||
SecureLogger.log("========================", category: SecureLogger.handshake, level: .debug)
|
SecureLogger.debug("========================", category: .handshake)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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.warning("Clearing all handshake states for panic mode", category: .handshake)
|
||||||
self.handshakeStates.removeAll()
|
self.handshakeStates.removeAll()
|
||||||
self.processedHandshakeMessages.removeAll()
|
self.processedHandshakeMessages.removeAll()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,6 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
import os.log
|
|
||||||
|
|
||||||
// Core Noise Protocol implementation
|
// Core Noise Protocol implementation
|
||||||
// Based on the Noise Protocol Framework specification
|
// Based on the Noise Protocol Framework specification
|
||||||
@@ -285,7 +284,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.warning("High nonce value detected: \(currentNonce) - consider rekeying", category: .encryption)
|
||||||
}
|
}
|
||||||
|
|
||||||
return combinedPayload
|
return combinedPayload
|
||||||
@@ -307,13 +306,13 @@ final class NoiseCipherState {
|
|||||||
if useExtractedNonce {
|
if useExtractedNonce {
|
||||||
// Extract nonce and ciphertext from combined payload
|
// Extract nonce and ciphertext from combined payload
|
||||||
guard let (extractedNonce, actualCiphertext) = try extractNonceFromCiphertextPayload(ciphertext) else {
|
guard let (extractedNonce, actualCiphertext) = try extractNonceFromCiphertextPayload(ciphertext) else {
|
||||||
SecureLogger.log("Decrypt failed: Could not extract nonce from payload")
|
SecureLogger.debug("Decrypt failed: Could not extract nonce from payload")
|
||||||
throw NoiseError.invalidCiphertext
|
throw NoiseError.invalidCiphertext
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate nonce with sliding window replay protection
|
// Validate nonce with sliding window replay protection
|
||||||
guard isValidNonce(extractedNonce) else {
|
guard isValidNonce(extractedNonce) else {
|
||||||
SecureLogger.log("Replay attack detected: nonce \(extractedNonce) rejected")
|
SecureLogger.debug("Replay attack detected: nonce \(extractedNonce) rejected")
|
||||||
throw NoiseError.replayDetected
|
throw NoiseError.replayDetected
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,7 +341,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.warning("High nonce value detected: \(decryptionNonce) - consider rekeying", category: .encryption)
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
@@ -355,9 +354,9 @@ final class NoiseCipherState {
|
|||||||
nonce += 1
|
nonce += 1
|
||||||
return plaintext
|
return plaintext
|
||||||
} catch {
|
} catch {
|
||||||
SecureLogger.log("Decrypt failed: \(error) for nonce \(decryptionNonce)")
|
SecureLogger.debug("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.error("Decryption failed at nonce \(decryptionNonce)", category: .encryption)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -661,7 +660,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.warning("Invalid ephemeral public key received", category: .security)
|
||||||
throw NoiseError.invalidMessage
|
throw NoiseError.invalidMessage
|
||||||
}
|
}
|
||||||
symmetricState.mixHash(ephemeralData)
|
symmetricState.mixHash(ephemeralData)
|
||||||
@@ -678,7 +677,7 @@ final class NoiseHandshakeState {
|
|||||||
let decrypted = try symmetricState.decryptAndHash(staticData)
|
let decrypted = try symmetricState.decryptAndHash(staticData)
|
||||||
remoteStaticPublic = try NoiseHandshakeState.validatePublicKey(decrypted)
|
remoteStaticPublic = try NoiseHandshakeState.validatePublicKey(decrypted)
|
||||||
} catch {
|
} catch {
|
||||||
SecureLogger.logSecurityEvent(.authenticationFailed(peerID: "Unknown - handshake"), level: .error)
|
SecureLogger.error(.authenticationFailed(peerID: "Unknown - handshake"))
|
||||||
throw NoiseError.authenticationFailure
|
throw NoiseError.authenticationFailure
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -877,7 +876,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.warning("Low-order point detected", category: .security)
|
||||||
throw NoiseError.invalidPublicKey
|
throw NoiseError.invalidPublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -887,7 +886,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.warning("CryptoKit validation failed", category: .security)
|
||||||
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.warning("Global handshake rate limit exceeded: \(globalHandshakeTimestamps.count)/\(NoiseSecurityConstants.maxGlobalHandshakesPerMinute) per minute", category: .security)
|
||||||
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.warning("Per-peer handshake rate limit exceeded for \(peerID): \(timestamps.count)/\(NoiseSecurityConstants.maxHandshakesPerMinute) per minute", category: .security)
|
||||||
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.warning("Global message rate limit exceeded: \(globalMessageTimestamps.count)/\(NoiseSecurityConstants.maxGlobalMessagesPerSecond) per second", category: .security)
|
||||||
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.warning("Per-peer message rate limit exceeded for \(peerID): \(timestamps.count)/\(NoiseSecurityConstants.maxMessagesPerSecond) per second", category: .security)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
import os.log
|
|
||||||
|
|
||||||
// MARK: - Noise Session State
|
// MARK: - Noise Session State
|
||||||
|
|
||||||
@@ -92,7 +91,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.debug("NoiseSession[\(peerID)]: Processing handshake message, current state: \(state), role: \(role)")
|
||||||
|
|
||||||
// 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 +102,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.debug("NoiseSession[\(peerID)]: Initialized handshake state for responder")
|
||||||
}
|
}
|
||||||
|
|
||||||
guard case .handshaking = state, let handshake = handshakeState else {
|
guard case .handshaking = state, let handshake = handshakeState else {
|
||||||
@@ -112,7 +111,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.debug("NoiseSession[\(peerID)]: Read handshake message, checking if complete")
|
||||||
|
|
||||||
// Check if handshake is complete
|
// Check if handshake is complete
|
||||||
if handshake.isHandshakeComplete() {
|
if handshake.isHandshakeComplete() {
|
||||||
@@ -130,15 +129,15 @@ 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.debug("NoiseSession[\(peerID)]: Handshake complete (no response needed), transitioning to established")
|
||||||
SecureLogger.logSecurityEvent(.handshakeCompleted(peerID: peerID))
|
SecureLogger.info(.handshakeCompleted(peerID: peerID))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
// 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.debug("NoiseSession[\(peerID)]: Generated handshake response of size \(response.count)")
|
||||||
|
|
||||||
// Check if handshake is complete after writing
|
// Check if handshake is complete after writing
|
||||||
if handshake.isHandshakeComplete() {
|
if handshake.isHandshakeComplete() {
|
||||||
@@ -156,8 +155,8 @@ 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.debug("NoiseSession[\(peerID)]: Handshake complete after writing response, transitioning to established")
|
||||||
SecureLogger.logSecurityEvent(.handshakeCompleted(peerID: peerID))
|
SecureLogger.info(.handshakeCompleted(peerID: peerID))
|
||||||
}
|
}
|
||||||
|
|
||||||
return response
|
return response
|
||||||
@@ -242,7 +241,7 @@ class NoiseSession {
|
|||||||
handshakeHash = nil
|
handshakeHash = nil
|
||||||
|
|
||||||
if wasEstablished {
|
if wasEstablished {
|
||||||
SecureLogger.logSecurityEvent(.sessionExpired(peerID: peerID))
|
SecureLogger.info(.sessionExpired(peerID: peerID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -287,7 +286,7 @@ final class NoiseSessionManager {
|
|||||||
managerQueue.sync(flags: .barrier) {
|
managerQueue.sync(flags: .barrier) {
|
||||||
if let session = sessions[peerID] {
|
if let session = sessions[peerID] {
|
||||||
if session.isEstablished() {
|
if session.isEstablished() {
|
||||||
SecureLogger.logSecurityEvent(.sessionExpired(peerID: peerID))
|
SecureLogger.info(.sessionExpired(peerID: peerID))
|
||||||
}
|
}
|
||||||
// Clear sensitive data before removing
|
// Clear sensitive data before removing
|
||||||
session.reset()
|
session.reset()
|
||||||
@@ -331,7 +330,7 @@ final class NoiseSessionManager {
|
|||||||
} catch {
|
} catch {
|
||||||
// Clean up failed session
|
// Clean up failed session
|
||||||
_ = sessions.removeValue(forKey: peerID)
|
_ = sessions.removeValue(forKey: peerID)
|
||||||
SecureLogger.logSecurityEvent(.handshakeFailed(peerID: peerID, error: error.localizedDescription), level: .error)
|
SecureLogger.error(.handshakeFailed(peerID: peerID, error: error.localizedDescription))
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -348,8 +347,7 @@ final class NoiseSessionManager {
|
|||||||
// for a good reason (e.g., decryption failure, restart, etc.)
|
// for a good reason (e.g., decryption failure, restart, etc.)
|
||||||
// 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.info("Accepting handshake from \(peerID) despite existing session - peer likely cleared their session", category: .session)
|
||||||
category: SecureLogger.session, level: .info)
|
|
||||||
_ = sessions.removeValue(forKey: peerID)
|
_ = sessions.removeValue(forKey: peerID)
|
||||||
shouldCreateNew = true
|
shouldCreateNew = true
|
||||||
} else {
|
} else {
|
||||||
@@ -404,7 +402,7 @@ final class NoiseSessionManager {
|
|||||||
self?.onSessionFailed?(peerID, error)
|
self?.onSessionFailed?(peerID, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
SecureLogger.logSecurityEvent(.handshakeFailed(peerID: peerID, error: error.localizedDescription), level: .error)
|
SecureLogger.error(.handshakeFailed(peerID: peerID, error: error.localizedDescription))
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.warning("GeoRelayDirectory: Tor not ready; skipping remote fetch (fail-closed)", category: .session)
|
||||||
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.info("GeoRelayDirectory: refreshed \(parsed.count) relays from remote", category: .session)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SecureLogger.log("GeoRelayDirectory: remote fetch failed; keeping local entries", category: SecureLogger.session, level: .warning)
|
SecureLogger.warning("GeoRelayDirectory: remote fetch failed; keeping local entries", category: .session)
|
||||||
}
|
}
|
||||||
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.warning("GeoRelayDirectory: failed to write cache: \(error)", category: .session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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.warning("GeoRelayDirectory: no local CSV found; entries empty", category: .session)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,8 +78,7 @@ struct NostrProtocol {
|
|||||||
)
|
)
|
||||||
// Successfully unwrapped gift wrap
|
// Successfully unwrapped gift wrap
|
||||||
} catch {
|
} catch {
|
||||||
SecureLogger.log("❌ Failed to unwrap gift wrap: \(error)",
|
SecureLogger.error("❌ Failed to unwrap gift wrap: \(error)", category: .session)
|
||||||
category: SecureLogger.session, level: .error)
|
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,8 +91,7 @@ struct NostrProtocol {
|
|||||||
)
|
)
|
||||||
// Successfully opened seal
|
// Successfully opened seal
|
||||||
} catch {
|
} catch {
|
||||||
SecureLogger.log("❌ Failed to open seal: \(error)",
|
SecureLogger.error("❌ Failed to open seal: \(error)", category: .session)
|
||||||
category: SecureLogger.session, level: .error)
|
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.error("❌ Tor not ready; aborting relay connections (fail-closed)", category: .session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
SecureLogger.log("🌐 Connecting to \(self.relays.count) Nostr relays (via Tor)", category: SecureLogger.session, level: .debug)
|
SecureLogger.debug("🌐 Connecting to \(self.relays.count) Nostr relays (via Tor)", category: .session)
|
||||||
for relay in self.relays {
|
for relay in self.relays {
|
||||||
self.connectToRelay(relay.url)
|
self.connectToRelay(relay.url)
|
||||||
}
|
}
|
||||||
@@ -231,12 +231,11 @@ 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.error("❌ Failed to encode subscription request", category: .session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// SecureLogger.log("📋 Subscription filter JSON: \(messageString.prefix(200))...",
|
// SecureLogger.debug("📋 Subscription filter JSON: \(messageString.prefix(200))...", category: .session)
|
||||||
// category: SecureLogger.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
|
||||||
@@ -251,8 +250,7 @@ final class NostrRelayManager: ObservableObject {
|
|||||||
map[id] = messageString
|
map[id] = messageString
|
||||||
self.pendingSubscriptions[url] = map
|
self.pendingSubscriptions[url] = map
|
||||||
}
|
}
|
||||||
SecureLogger.log("📋 Queued subscription id=\(id) for \(urls.count) relay(s)",
|
SecureLogger.debug("📋 Queued subscription id=\(id) for \(urls.count) relay(s)", category: .session)
|
||||||
category: SecureLogger.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
|
||||||
@@ -262,8 +260,7 @@ final class NostrRelayManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
SecureLogger.log("❌ Failed to encode subscription request: \(error)",
|
SecureLogger.error("❌ Failed to encode subscription request: \(error)", category: .session)
|
||||||
category: SecureLogger.session, level: .error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,7 +292,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.warning("Invalid relay URL: \(urlString)", category: .session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,7 +318,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.error("❌ Tor not ready; skipping connection to \(urlString)", category: .session) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -340,14 +337,12 @@ final class NostrRelayManager: ObservableObject {
|
|||||||
task.sendPing { [weak self] error in
|
task.sendPing { [weak self] error in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if error == nil {
|
if error == nil {
|
||||||
SecureLogger.log("✅ Connected to Nostr relay: \(urlString)",
|
SecureLogger.debug("✅ Connected to Nostr relay: \(urlString)", category: .session)
|
||||||
category: SecureLogger.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.error("❌ Failed to connect to Nostr relay \(urlString): \(error?.localizedDescription ?? "Unknown error")", category: .session)
|
||||||
category: SecureLogger.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))
|
||||||
@@ -364,8 +359,7 @@ final class NostrRelayManager: ObservableObject {
|
|||||||
if self.subscriptions[relayUrl]?.contains(id) == true { continue }
|
if self.subscriptions[relayUrl]?.contains(id) == true { continue }
|
||||||
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.error("❌ Failed to send pending subscription to \(relayUrl): \(error)", category: .session)
|
||||||
category: SecureLogger.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>()
|
||||||
@@ -414,8 +408,7 @@ final class NostrRelayManager: ObservableObject {
|
|||||||
switch parsed {
|
switch parsed {
|
||||||
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.debug("📥 Event kind=\(event.kind) id=\(event.id.prefix(16))… relay=\(relayUrl)", category: .session)
|
||||||
category: SecureLogger.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
|
||||||
@@ -423,8 +416,7 @@ final class NostrRelayManager: ObservableObject {
|
|||||||
if let handler = self.messageHandlers[subId] {
|
if let handler = self.messageHandlers[subId] {
|
||||||
handler(event)
|
handler(event)
|
||||||
} else {
|
} else {
|
||||||
SecureLogger.log("⚠️ No handler for subscription \(subId)",
|
SecureLogger.warning("⚠️ No handler for subscription \(subId)", category: .session)
|
||||||
category: SecureLogger.session, level: .warning)
|
|
||||||
}
|
}
|
||||||
case .eose:
|
case .eose:
|
||||||
// No-op for now
|
// No-op for now
|
||||||
@@ -432,12 +424,14 @@ final class NostrRelayManager: ObservableObject {
|
|||||||
case .ok(let eventId, let success, let reason):
|
case .ok(let eventId, let success, let reason):
|
||||||
if success {
|
if success {
|
||||||
_ = Self.pendingGiftWrapIDs.remove(eventId)
|
_ = Self.pendingGiftWrapIDs.remove(eventId)
|
||||||
SecureLogger.log("✅ Accepted id=\(eventId.prefix(16))… relay=\(relayUrl)",
|
SecureLogger.debug("✅ Accepted id=\(eventId.prefix(16))… relay=\(relayUrl)", category: .session)
|
||||||
category: SecureLogger.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)",
|
if isGiftWrap {
|
||||||
category: SecureLogger.session, level: isGiftWrap ? .warning : .error)
|
SecureLogger.warning("📮 Rejected id=\(eventId.prefix(16))… reason=\(reason)", category: .session)
|
||||||
|
} else {
|
||||||
|
SecureLogger.error("📮 Rejected id=\(eventId.prefix(16))… reason=\(reason)", category: .session)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case .notice:
|
case .notice:
|
||||||
break
|
break
|
||||||
@@ -451,17 +445,14 @@ final class NostrRelayManager: ObservableObject {
|
|||||||
let data = try encoder.encode(req)
|
let data = try encoder.encode(req)
|
||||||
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.debug("📤 Send kind=\(event.kind) id=\(event.id.prefix(16))… relay=\(relayUrl)", category: .session)
|
||||||
category: SecureLogger.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.error("❌ Failed to send event to \(relayUrl): \(error)", category: .session)
|
||||||
category: SecureLogger.session, level: .error)
|
|
||||||
} else {
|
} else {
|
||||||
// SecureLogger.log("✅ Event sent to relay: \(relayUrl)",
|
// SecureLogger.debug("✅ Event sent to relay: \(relayUrl)", category: .session)
|
||||||
// category: SecureLogger.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 +461,7 @@ final class NostrRelayManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
SecureLogger.log("Failed to encode event: \(error)", category: SecureLogger.session, level: .error)
|
SecureLogger.error("Failed to encode event: \(error)", category: .session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,7 +500,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.warning("Nostr relay permanent failure for \(relayUrl) - not retrying (code=\(ns.code))", category: .session)
|
||||||
}
|
}
|
||||||
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
|
||||||
@@ -527,8 +518,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.warning("Max reconnection attempts (\(maxReconnectAttempts)) reached for \(relayUrl)", category: .session)
|
||||||
category: SecureLogger.session, level: .warning)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -230,8 +230,7 @@ final class BLEService: NSObject {
|
|||||||
let newSize = data.count
|
let newSize = data.count
|
||||||
// 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.warning("⚠️ Dropping oversized write chunk (\(newSize)B) for peripheral \(uuid)", category: .session)
|
||||||
category: SecureLogger.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 }
|
||||||
@@ -244,8 +243,7 @@ final class BLEService: NSObject {
|
|||||||
removedBytes += removed.count
|
removedBytes += removed.count
|
||||||
total -= removed.count
|
total -= removed.count
|
||||||
}
|
}
|
||||||
SecureLogger.log("📉 Trimmed pending write buffer for \(uuid) by \(removedBytes)B to \(total)B",
|
SecureLogger.warning("📉 Trimmed pending write buffer for \(uuid) by \(removedBytes)B to \(total)B", category: .session)
|
||||||
category: SecureLogger.session, level: .warning)
|
|
||||||
}
|
}
|
||||||
self.pendingPeripheralWrites[uuid] = queue.isEmpty ? nil : queue
|
self.pendingPeripheralWrites[uuid] = queue.isEmpty ? nil : queue
|
||||||
}
|
}
|
||||||
@@ -341,8 +339,7 @@ final class BLEService: NSObject {
|
|||||||
// Set up Noise session establishment callback
|
// Set up Noise session establishment callback
|
||||||
// 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.debug("🔐 Noise session authenticated with \(peerID), fingerprint: \(fingerprint.prefix(16))...")
|
||||||
category: SecureLogger.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)
|
||||||
@@ -587,8 +584,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.debug("🔔 sendFavoriteNotification called - peerID: \(peerID), isFavorite: \(isFavorite)", category: .session)
|
||||||
category: SecureLogger.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]"
|
||||||
@@ -596,12 +592,10 @@ final class BLEService: NSObject {
|
|||||||
// Add our Nostr public key if available
|
// Add our Nostr public key if available
|
||||||
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.debug("📝 Sending favorite notification with Nostr npub: \(myNostrIdentity.npub)", category: .session)
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SecureLogger.log("📤 Sending favorite notification to \(peerID): \(content)",
|
SecureLogger.debug("📤 Sending favorite notification to \(peerID): \(content)", category: .session)
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
sendPrivateMessage(content, to: peerID, messageID: UUID().uuidString)
|
sendPrivateMessage(content, to: peerID, messageID: UUID().uuidString)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,8 +605,7 @@ final class BLEService: NSObject {
|
|||||||
payload.append(contentsOf: receipt.originalMessageID.utf8)
|
payload.append(contentsOf: receipt.originalMessageID.utf8)
|
||||||
|
|
||||||
if noiseService.hasEstablishedSession(with: peerID) {
|
if noiseService.hasEstablishedSession(with: peerID) {
|
||||||
SecureLogger.log("📤 Sending READ receipt for message \(receipt.originalMessageID) to \(peerID)",
|
SecureLogger.debug("📤 Sending READ receipt for message \(receipt.originalMessageID) to \(peerID)", category: .session)
|
||||||
category: SecureLogger.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 +623,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.error("Failed to send read receipt: \(error)")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Queue for after handshake and initiate if needed
|
// Queue for after handshake and initiate if needed
|
||||||
@@ -639,8 +632,7 @@ final class BLEService: NSObject {
|
|||||||
self.pendingNoisePayloadsAfterHandshake[peerID, default: []].append(payload)
|
self.pendingNoisePayloadsAfterHandshake[peerID, default: []].append(payload)
|
||||||
}
|
}
|
||||||
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.debug("🕒 Queued READ receipt for \(peerID) until handshake completes", category: .session)
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -682,7 +674,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.error("Failed to send verification payload: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -748,7 +740,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.error("Message too long: \(content.count) chars", category: .session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -771,7 +763,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.error("❌ Failed to sign public message", category: .security)
|
||||||
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 +781,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.debug("📨 Sending PM to \(recipientID): \(content.prefix(30))...", category: .session)
|
||||||
|
|
||||||
// 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 +790,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.error("Failed to encode private message with TLV")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -847,11 +839,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.error("Failed to encrypt message: \(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.debug("🤝 No session with \(recipientID), initiating handshake and queueing message", category: .session)
|
||||||
|
|
||||||
// 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 +888,7 @@ final class BLEService: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
SecureLogger.log("Failed to initiate handshake: \(error)", category: SecureLogger.noise, level: .error)
|
SecureLogger.error("Failed to initiate handshake: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -910,8 +902,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.debug("📤 Sending \(messages.count) pending messages after handshake to \(peerID)", category: .session)
|
||||||
category: SecureLogger.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 +910,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.error("Failed to encode pending private message TLV")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -946,11 +937,9 @@ final class BLEService: NSObject {
|
|||||||
self?.delegate?.didUpdateMessageDeliveryStatus(messageID, status: .sent)
|
self?.delegate?.didUpdateMessageDeliveryStatus(messageID, status: .sent)
|
||||||
}
|
}
|
||||||
|
|
||||||
SecureLogger.log("✅ Sent pending message \(messageID) to \(peerID) after handshake",
|
SecureLogger.debug("✅ Sent pending message \(messageID) to \(peerID) after handshake", category: .session)
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
} catch {
|
} catch {
|
||||||
SecureLogger.log("Failed to send pending message after handshake: \(error)",
|
SecureLogger.error("Failed to send pending message after handshake: \(error)")
|
||||||
category: SecureLogger.noise, level: .error)
|
|
||||||
|
|
||||||
// Notify delegate of failure
|
// Notify delegate of failure
|
||||||
notifyUI { [weak self] in
|
notifyUI { [weak self] in
|
||||||
@@ -966,7 +955,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.error("❌ Failed to convert packet to binary data", category: .session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if packet.type == MessageType.noiseEncrypted.rawValue {
|
if packet.type == MessageType.noiseEncrypted.rawValue {
|
||||||
@@ -1037,7 +1026,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.debug("📋 Queued encrypted packet for retry (notification queue full)", category: .session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1150,7 +1139,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.debug("🧳 Spooling directed packet for \(recipientPeerID) mid=\(msgID.prefix(8))…", category: .session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1322,7 +1311,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.error("❌ Failed to decode reassembled packet (type=\(originalType), total=\(total))", category: .session)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
@@ -1342,8 +1331,7 @@ final class BLEService: NSObject {
|
|||||||
// Only log non-announce packets to reduce noise
|
// Only log non-announce packets to reduce noise
|
||||||
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.debug("📦 Handling packet type \(packet.type) from \(senderID), messageID: \(messageID)", category: .session)
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Efficient deduplication
|
// Efficient deduplication
|
||||||
@@ -1352,8 +1340,7 @@ final class BLEService: NSObject {
|
|||||||
// Announce packets (type 1) are sent every 10 seconds for peer discovery
|
// Announce packets (type 1) are sent every 10 seconds for peer discovery
|
||||||
// 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.debug("⚠️ Duplicate packet ignored: \(messageID)", category: .session)
|
||||||
category: SecureLogger.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 +1393,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.warning("⚠️ Unknown message type: \(packet.type)", category: .session)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1446,7 +1433,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.error("❌ Failed to decode announce packet from \(peerID)", category: .session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1454,7 +1441,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.warning("⚠️ Announce sender mismatch: derived \(derivedFromKey.prefix(8))… vs packet \(peerID.prefix(8))…", category: .security)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1471,11 +1458,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.warning("⚠️ Signature verification for announce failed \(peerID.prefix(8))", category: .security)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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.warning("⚠️ Announce key mismatch for \(peerID.prefix(8))… — keeping unverified", category: .security)
|
||||||
verifiedAnnounce = false
|
verifiedAnnounce = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1508,7 +1495,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.warning("❌ Ignoring unverified announce from \(peerID.prefix(8))…", category: .security)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1541,17 +1528,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.debug("🆕 New peer: \(announcement.nickname)", category: .session)
|
||||||
} 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.debug("🔄 Peer \(announcement.nickname) reconnected", category: .session)
|
||||||
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.debug("🔄 Peer \(peerID) changed nickname: \(existingPeer?.nickname ?? "Unknown") -> \(announcement.nickname)", category: .session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1661,12 +1648,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.warning("🚫 Dropping public message from unverified or unknown peer \(peerID.prefix(8))…", category: .security)
|
||||||
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.error("❌ Failed to decode message payload as UTF-8", category: .session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Determine if we have a direct link to the sender
|
// Determine if we have a direct link to the sender
|
||||||
@@ -1678,7 +1665,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.debug("💬 [\(senderNickname)] TTL:\(packet.ttl) (\(pathTag)): \(String(content.prefix(50)))\(content.count > 50 ? "..." : "")", category: .session)
|
||||||
|
|
||||||
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 +1697,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.error("Failed to process handshake: \(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)
|
||||||
@@ -1720,17 +1707,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.debug("🔐 handleNoiseEncrypted called for packet from \(peerID)")
|
||||||
category: SecureLogger.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.warning("⚠️ Encrypted message has no recipient ID", category: .session)
|
||||||
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.debug("🔐 Encrypted message not for me (for \(recipientHex), I am \(myPeerID))", category: .session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1772,19 +1758,17 @@ 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.warning("⚠️ Unknown noise payload type: \(payloadType)")
|
||||||
}
|
}
|
||||||
} 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.debug("🔑 Encrypted message from \(peerID) without session; initiating handshake")
|
||||||
category: SecureLogger.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.error("❌ Failed to decrypt message from \(peerID): \(error)")
|
||||||
category: SecureLogger.noise, level: .error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1808,7 +1792,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.debug("👋 Sending leave announcement", category: .session)
|
||||||
let packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.leave.rawValue,
|
type: MessageType.leave.rawValue,
|
||||||
ttl: messageTTL,
|
ttl: messageTTL,
|
||||||
@@ -1845,7 +1829,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.error("❌ Failed to encode announce packet", category: .session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1862,7 +1846,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.error("❌ Failed to sign announce packet", category: .security)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1895,7 +1879,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.error("Failed to send delivery ACK: \(error)")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Queue for after handshake and initiate if needed
|
// Queue for after handshake and initiate if needed
|
||||||
@@ -1904,8 +1888,7 @@ final class BLEService: NSObject {
|
|||||||
self.pendingNoisePayloadsAfterHandshake[peerID, default: []].append(payload)
|
self.pendingNoisePayloadsAfterHandshake[peerID, default: []].append(payload)
|
||||||
}
|
}
|
||||||
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.debug("🕒 Queued DELIVERED ack for \(peerID) until handshake completes", category: .session)
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1916,8 +1899,7 @@ final class BLEService: NSObject {
|
|||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
guard !payloads.isEmpty else { return }
|
guard !payloads.isEmpty else { return }
|
||||||
SecureLogger.log("📤 Sending \(payloads.count) pending noise payloads to \(peerID) after handshake",
|
SecureLogger.debug("📤 Sending \(payloads.count) pending noise payloads to \(peerID) after handshake", category: .session)
|
||||||
category: SecureLogger.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)
|
||||||
@@ -1932,8 +1914,7 @@ final class BLEService: NSObject {
|
|||||||
)
|
)
|
||||||
broadcastPacket(packet)
|
broadcastPacket(packet)
|
||||||
} catch {
|
} catch {
|
||||||
SecureLogger.log("❌ Failed to send pending noise payload to \(peerID): \(error)",
|
SecureLogger.error("❌ Failed to send pending noise payload to \(peerID): \(error)")
|
||||||
category: SecureLogger.noise, level: .error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2103,8 +2084,7 @@ final class BLEService: NSObject {
|
|||||||
// Cleanup: remove peers that are not connected and past reachability retention
|
// Cleanup: remove peers that are not connected and past reachability retention
|
||||||
if !peer.isConnected {
|
if !peer.isConnected {
|
||||||
if age > retention {
|
if age > retention {
|
||||||
SecureLogger.log("🗑️ Removing stale peer after reachability window: \(peerID) (\(peer.nickname))",
|
SecureLogger.debug("🗑️ Removing stale peer after reachability window: \(peerID) (\(peer.nickname))", category: .session)
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
peers.removeValue(forKey: peerID)
|
peers.removeValue(forKey: peerID)
|
||||||
removedOfflineCount += 1
|
removedOfflineCount += 1
|
||||||
}
|
}
|
||||||
@@ -2395,8 +2375,7 @@ extension BLEService: CBCentralManagerDelegate {
|
|||||||
peripheral.delegate = self
|
peripheral.delegate = self
|
||||||
|
|
||||||
// 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.debug("📱 Connect: \(advertisedName) [RSSI:\(rssiValue)]", category: .session)
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
|
|
||||||
// Use connection options for faster reconnection
|
// Use connection options for faster reconnection
|
||||||
let options: [String: Any] = [
|
let options: [String: Any] = [
|
||||||
@@ -2415,8 +2394,7 @@ extension BLEService: CBCentralManagerDelegate {
|
|||||||
state.isConnecting && !state.isConnected else { return }
|
state.isConnecting && !state.isConnected else { return }
|
||||||
|
|
||||||
// Connection timed out - cancel it
|
// Connection timed out - cancel it
|
||||||
SecureLogger.log("⏱️ Timeout: \(advertisedName)",
|
SecureLogger.debug("⏱️ Timeout: \(advertisedName)", category: .session)
|
||||||
category: SecureLogger.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 +2427,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.debug("✅ Connected: \(peripheral.name ?? "Unknown") [\(peripheralID)]", category: .session)
|
||||||
|
|
||||||
// Discover services
|
// Discover services
|
||||||
peripheral.discoverServices([BLEService.serviceUUID])
|
peripheral.discoverServices([BLEService.serviceUUID])
|
||||||
@@ -2461,8 +2439,7 @@ func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeriph
|
|||||||
// Find the peer ID if we have it
|
// Find the peer ID if we have it
|
||||||
let peerID = peripherals[peripheralID]?.peerID
|
let peerID = peripherals[peripheralID]?.peerID
|
||||||
|
|
||||||
SecureLogger.log("📱 Disconnect: \(peerID ?? peripheralID)\(error != nil ? " (\(error!.localizedDescription))" : "")",
|
SecureLogger.debug("📱 Disconnect: \(peerID ?? peripheralID)\(error != nil ? " (\(error!.localizedDescription))" : "")", category: .session)
|
||||||
category: SecureLogger.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 +2494,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.error("❌ Failed to connect to peripheral: \(peripheral.name ?? "Unknown") [\(peripheralID)] - Error: \(error?.localizedDescription ?? "Unknown")", category: .session)
|
||||||
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 +2567,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.debug("⏩ Queue connect: \(candidate.name) [RSSI:\(candidate.rssi)]", category: .session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2635,7 +2612,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.error("❌ Error discovering services for \(peripheral.name ?? "Unknown"): \(error.localizedDescription)", category: .session)
|
||||||
// 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 +2622,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.warning("⚠️ No services discovered for \(peripheral.name ?? "Unknown")", category: .session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2661,12 +2638,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.error("❌ Error discovering characteristics for \(peripheral.name ?? "Unknown"): \(error.localizedDescription)", category: .session)
|
||||||
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.warning("⚠️ No matching characteristic found for \(peripheral.name ?? "Unknown")", category: .session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2683,7 +2660,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.warning("⚠️ Characteristic doesn't support reliable writes (withResponse)!", category: .session)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store characteristic in our consolidated structure
|
// Store characteristic in our consolidated structure
|
||||||
@@ -2696,7 +2673,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.debug("🔔 Subscribed to notifications from \(peripheral.name ?? "Unknown")", category: .session)
|
||||||
|
|
||||||
// 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 +2684,18 @@ extension BLEService: CBPeripheralDelegate {
|
|||||||
self?.rebroadcastRecentAnnounces()
|
self?.rebroadcastRecentAnnounces()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
SecureLogger.log("⚠️ Characteristic does not support notifications", category: SecureLogger.session, level: .warning)
|
SecureLogger.warning("⚠️ Characteristic does not support notifications", category: .session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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.error("❌ Error receiving notification: \(error.localizedDescription)", category: .session)
|
||||||
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.warning("⚠️ No data in notification", category: .session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2728,8 +2705,7 @@ extension BLEService: CBPeripheralDelegate {
|
|||||||
guard let packet = BinaryProtocol.decode(data) else {
|
guard let packet = BinaryProtocol.decode(data) else {
|
||||||
// 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.error("❌ Failed to decode notification packet (len=\(data.count), prefix=\(prefix))", category: .session)
|
||||||
category: SecureLogger.session, level: .error)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2737,7 +2713,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.debug("📦 Decoded notification packet type: \(packet.type) from sender: \(senderID)", category: .session)
|
||||||
}
|
}
|
||||||
|
|
||||||
let peripheralUUID = peripheral.identifier.uuidString
|
let peripheralUUID = peripheral.identifier.uuidString
|
||||||
@@ -2775,10 +2751,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.error("❌ Write failed to \(peripheral.name ?? peripheral.identifier.uuidString): \(error.localizedDescription)", category: .session)
|
||||||
// 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.debug("✅ Write confirmed to \(peripheral.name ?? peripheral.identifier.uuidString)", category: .session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2788,14 +2764,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.warning("⚠️ Services modified for \(peripheral.name ?? peripheral.identifier.uuidString)", category: .session)
|
||||||
|
|
||||||
// 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.warning("❌ BitChat service removed - disconnecting from \(peripheral.name ?? peripheral.identifier.uuidString)", category: .session)
|
||||||
centralManager?.cancelPeripheralConnection(peripheral)
|
centralManager?.cancelPeripheralConnection(peripheral)
|
||||||
} else {
|
} else {
|
||||||
// Try to rediscover
|
// Try to rediscover
|
||||||
@@ -2805,9 +2781,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.error("❌ Error updating notification state: \(error.localizedDescription)", category: .session)
|
||||||
} else {
|
} else {
|
||||||
SecureLogger.log("🔔 Notification state updated for \(peripheral.name ?? peripheral.identifier.uuidString): \(characteristic.isNotifying ? "ON" : "OFF")", category: SecureLogger.session, level: .debug)
|
SecureLogger.debug("🔔 Notification state updated for \(peripheral.name ?? peripheral.identifier.uuidString): \(characteristic.isNotifying ? "ON" : "OFF")", category: .session)
|
||||||
|
|
||||||
// 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 +2798,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.debug("📡 Peripheral manager state: \(peripheral.state.rawValue)", category: .session)
|
||||||
|
|
||||||
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 +2817,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.debug("🔧 Adding BLE service...", category: .session)
|
||||||
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.error("❌ Failed to add service: \(error.localizedDescription)", category: .session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
SecureLogger.log("✅ Service added successfully, starting advertising", category: SecureLogger.session, level: .debug)
|
SecureLogger.debug("✅ Service added successfully, starting advertising", category: .session)
|
||||||
|
|
||||||
// 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.debug("📡 Started advertising (LocalName: \((adData[CBAdvertisementDataLocalNameKey] as? String) != nil ? "on" : "off"), ID: \(myPeerID.prefix(8))…)", category: .session)
|
||||||
}
|
}
|
||||||
|
|
||||||
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.debug("📥 Central subscribed: \(central.identifier.uuidString)", category: .session)
|
||||||
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 +2851,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.debug("📤 Central unsubscribed: \(central.identifier.uuidString)", category: .session)
|
||||||
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.debug("📡 Restarting advertising after central unsubscribed", category: .session)
|
||||||
peripheral.startAdvertising(buildAdvertisementData())
|
peripheral.startAdvertising(buildAdvertisementData())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2914,7 +2890,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.debug("📤 Peripheral manager ready to send more notifications", category: .session)
|
||||||
|
|
||||||
// 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
|
||||||
@@ -2933,12 +2909,10 @@ extension BLEService: CBPeripheralManagerDelegate {
|
|||||||
if !success {
|
if !success {
|
||||||
// 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.debug("⚠️ Notification queue still full, re-queuing", category: .session)
|
||||||
category: SecureLogger.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.debug("✅ Sent pending notification from retry queue", category: .session)
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Broadcast to all
|
// Broadcast to all
|
||||||
@@ -2952,8 +2926,7 @@ extension BLEService: CBPeripheralManagerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !self.pendingNotifications.isEmpty {
|
if !self.pendingNotifications.isEmpty {
|
||||||
SecureLogger.log("📋 Still have \(self.pendingNotifications.count) pending notifications",
|
SecureLogger.debug("📋 Still have \(self.pendingNotifications.count) pending notifications", category: .session)
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2961,7 +2934,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.debug("📥 Received \(requests.count) write requests from central", category: .session)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IMPORTANT: Respond immediately to prevent timeouts!
|
// IMPORTANT: Respond immediately to prevent timeouts!
|
||||||
@@ -3000,7 +2973,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.debug("📥 Accumulated write from central \(centralUUID): size=\(combined.count) (+\(appendedBytes)) bytes (type=\(peekType)), offsets=\(offsets)", category: .session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3010,7 +2983,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.debug("📦 Decoded (combined) packet type: \(packet.type) from sender: \(senderID)", category: .session)
|
||||||
}
|
}
|
||||||
if !subscribedCentrals.contains(sorted[0].central) {
|
if !subscribedCentrals.contains(sorted[0].central) {
|
||||||
subscribedCentrals.append(sorted[0].central)
|
subscribedCentrals.append(sorted[0].central)
|
||||||
@@ -3035,12 +3008,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.warning("⚠️ Dropping oversized pending write buffer (\(combined.count) bytes) for central \(centralUUID)", category: .session)
|
||||||
}
|
}
|
||||||
// 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.error("❌ Failed to decode packet from central (len=\(raw.count), prefix=\(prefix))", category: .session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,7 @@ final class FavoritesPersistenceService: ObservableObject {
|
|||||||
peerNostrPublicKey: String? = nil,
|
peerNostrPublicKey: String? = nil,
|
||||||
peerNickname: String
|
peerNickname: String
|
||||||
) {
|
) {
|
||||||
SecureLogger.log("⭐️ Adding favorite: \(peerNickname) (\(peerNoisePublicKey.hexEncodedString()))",
|
SecureLogger.info("⭐️ Adding favorite: \(peerNickname) (\(peerNoisePublicKey.hexEncodedString()))", category: .session)
|
||||||
category: SecureLogger.session, level: .info)
|
|
||||||
|
|
||||||
let existing = favorites[peerNoisePublicKey]
|
let existing = favorites[peerNoisePublicKey]
|
||||||
|
|
||||||
@@ -64,8 +63,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.info("💕 Mutual favorite relationship established with \(peerNickname)!", category: .session)
|
||||||
category: SecureLogger.session, level: .info)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
favorites[peerNoisePublicKey] = relationship
|
favorites[peerNoisePublicKey] = relationship
|
||||||
@@ -83,8 +81,7 @@ final class FavoritesPersistenceService: ObservableObject {
|
|||||||
func removeFavorite(peerNoisePublicKey: Data) {
|
func removeFavorite(peerNoisePublicKey: Data) {
|
||||||
guard let existing = favorites[peerNoisePublicKey] else { return }
|
guard let existing = favorites[peerNoisePublicKey] else { return }
|
||||||
|
|
||||||
SecureLogger.log("⭐️ Removing favorite: \(existing.peerNickname) (\(peerNoisePublicKey.hexEncodedString()))",
|
SecureLogger.info("⭐️ Removing favorite: \(existing.peerNickname) (\(peerNoisePublicKey.hexEncodedString()))", category: .session)
|
||||||
category: SecureLogger.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 {
|
||||||
@@ -125,8 +122,7 @@ final class FavoritesPersistenceService: ObservableObject {
|
|||||||
let existing = favorites[peerNoisePublicKey]
|
let existing = favorites[peerNoisePublicKey]
|
||||||
let displayName = peerNickname ?? existing?.peerNickname ?? "Unknown"
|
let displayName = peerNickname ?? existing?.peerNickname ?? "Unknown"
|
||||||
|
|
||||||
SecureLogger.log("📨 Received favorite notification: \(displayName) \(favorited ? "favorited" : "unfavorited") us",
|
SecureLogger.info("📨 Received favorite notification: \(displayName) \(favorited ? "favorited" : "unfavorited") us", category: .session)
|
||||||
category: SecureLogger.session, level: .info)
|
|
||||||
|
|
||||||
let relationship = FavoriteRelationship(
|
let relationship = FavoriteRelationship(
|
||||||
peerNoisePublicKey: peerNoisePublicKey,
|
peerNoisePublicKey: peerNoisePublicKey,
|
||||||
@@ -147,8 +143,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.info("💕 Mutual favorite relationship established with \(displayName)!", category: .session)
|
||||||
category: SecureLogger.session, level: .info)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,15 +235,13 @@ final class FavoritesPersistenceService: ObservableObject {
|
|||||||
/// Update noise public key when peer reconnects with new ID
|
/// Update noise public key when peer reconnects with new ID
|
||||||
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.warning("⚠️ Cannot update noise key - no favorite found for \(oldKey.hexEncodedString())", category: .session)
|
||||||
category: SecureLogger.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.warning("⚠️ Favorite already exists with new key \(newKey.hexEncodedString()), removing old entry", category: .session)
|
||||||
category: SecureLogger.session, level: .warning)
|
|
||||||
favorites.removeValue(forKey: oldKey)
|
favorites.removeValue(forKey: oldKey)
|
||||||
saveFavorites()
|
saveFavorites()
|
||||||
return
|
return
|
||||||
@@ -302,7 +295,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.warning("🧹 Clearing all favorites (panic mode)", category: .session)
|
||||||
|
|
||||||
favorites.removeAll()
|
favorites.removeAll()
|
||||||
saveFavorites()
|
saveFavorites()
|
||||||
@@ -336,7 +329,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.error("Failed to save favorites: \(error)", category: .session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,14 +347,12 @@ final class FavoritesPersistenceService: ObservableObject {
|
|||||||
let decoder = JSONDecoder()
|
let decoder = JSONDecoder()
|
||||||
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.info("✅ Loaded \(relationships.count) favorite relationships", category: .session)
|
||||||
category: SecureLogger.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.warning("⚠️ No Nostr public key stored for '\(relationship.peerNickname)'", category: .session)
|
||||||
category: SecureLogger.session, level: .warning)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,8 +363,7 @@ final class FavoritesPersistenceService: ObservableObject {
|
|||||||
for relationship in relationships {
|
for relationship in relationships {
|
||||||
// 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.warning("⚠️ Duplicate favorite found for public key \(relationship.peerNoisePublicKey.hexEncodedString()) - nicknames: '\(existing.peerNickname)' vs '\(relationship.peerNickname)'", category: .session)
|
||||||
category: SecureLogger.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 +404,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.error("Failed to load favorites: \(error)", category: .session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Security
|
import Security
|
||||||
import os.log
|
|
||||||
|
|
||||||
final class KeychainManager {
|
final class KeychainManager {
|
||||||
static let shared = KeychainManager()
|
static let shared = KeychainManager()
|
||||||
@@ -53,7 +52,7 @@ final class KeychainManager {
|
|||||||
func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool {
|
func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool {
|
||||||
let fullKey = "identity_\(key)"
|
let fullKey = "identity_\(key)"
|
||||||
let result = saveData(keyData, forKey: fullKey)
|
let result = saveData(keyData, forKey: fullKey)
|
||||||
SecureLogger.logKeyOperation("save", keyType: key, success: result)
|
SecureLogger.logKeyOperation(.save, keyType: key, success: result)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +63,7 @@ final class KeychainManager {
|
|||||||
|
|
||||||
func deleteIdentityKey(forKey key: String) -> Bool {
|
func deleteIdentityKey(forKey key: String) -> Bool {
|
||||||
let result = delete(forKey: "identity_\(key)")
|
let result = delete(forKey: "identity_\(key)")
|
||||||
SecureLogger.logKeyOperation("delete", keyType: key, success: result)
|
SecureLogger.logKeyOperation(.delete, keyType: key, success: result)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,9 +112,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.error(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.error(NSError(domain: "Keychain", code: Int(status)), context: "Error saving to keychain", category: .keychain)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -151,7 +150,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.error(NSError(domain: "Keychain", code: -34018), context: "Missing keychain entitlement", category: .keychain)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -198,7 +197,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.warning("Panic mode - deleting all keychain data", category: .security)
|
||||||
|
|
||||||
var totalDeleted = 0
|
var totalDeleted = 0
|
||||||
|
|
||||||
@@ -261,7 +260,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.info("Deleted keychain item: \(account) from \(service)", category: .keychain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -303,7 +302,7 @@ final class KeychainManager {
|
|||||||
totalDeleted += 1
|
totalDeleted += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
SecureLogger.log("Panic mode cleanup completed. Total items deleted: \(totalDeleted)", category: SecureLogger.keychain, level: .warning)
|
SecureLogger.warning("Panic mode cleanup completed. Total items deleted: \(totalDeleted)", category: .keychain)
|
||||||
|
|
||||||
return totalDeleted > 0
|
return totalDeleted > 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,8 +197,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.error("LocationChannelManager: location error: \(error.localizedDescription)", category: .session)
|
||||||
category: SecureLogger.session, level: .error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Helpers
|
// MARK: - Helpers
|
||||||
|
|||||||
@@ -39,40 +39,34 @@ final class MessageRouter {
|
|||||||
func sendPrivate(_ content: String, to peerID: String, recipientNickname: String, messageID: String) {
|
func sendPrivate(_ content: String, to peerID: String, recipientNickname: String, messageID: String) {
|
||||||
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.debug("Routing PM via mesh (reachable) to \(peerID.prefix(8))… id=\(messageID.prefix(8))…", category: .session)
|
||||||
category: SecureLogger.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.debug("Routing PM via Nostr to \(peerID.prefix(8))… id=\(messageID.prefix(8))…", category: .session)
|
||||||
category: SecureLogger.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.debug("Queued PM for \(peerID.prefix(8))… (no mesh, no Nostr mapping) id=\(messageID.prefix(8))…", category: .session)
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendReadReceipt(_ receipt: ReadReceipt, to peerID: String) {
|
func sendReadReceipt(_ receipt: ReadReceipt, to peerID: String) {
|
||||||
// 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.debug("Routing READ ack via mesh (reachable) to \(peerID.prefix(8))… id=\(receipt.originalMessageID.prefix(8))…", category: .session)
|
||||||
category: SecureLogger.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.debug("Routing READ ack via Nostr to \(peerID.prefix(8))… id=\(receipt.originalMessageID.prefix(8))…", category: .session)
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
nostr.sendReadReceipt(receipt, to: peerID)
|
nostr.sendReadReceipt(receipt, to: peerID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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.debug("Routing DELIVERED ack via mesh (reachable) to \(peerID.prefix(8))… id=\(messageID.prefix(8))…", category: .session)
|
||||||
category: SecureLogger.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)
|
||||||
@@ -108,18 +102,15 @@ 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.debug("Flushing outbox for \(peerID.prefix(8))… count=\(queued.count)", category: .session)
|
||||||
category: SecureLogger.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.debug("Outbox -> mesh for \(peerID.prefix(8))… id=\(messageID.prefix(8))…", category: .session)
|
||||||
category: SecureLogger.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.debug("Outbox -> Nostr for \(peerID.prefix(8))… id=\(messageID.prefix(8))…", category: .session)
|
||||||
category: SecureLogger.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
|
||||||
|
|||||||
@@ -85,7 +85,6 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
import os.log
|
|
||||||
|
|
||||||
// MARK: - Encryption Status
|
// MARK: - Encryption Status
|
||||||
|
|
||||||
@@ -190,7 +189,7 @@ final class NoiseEncryptionService {
|
|||||||
if let identityData = KeychainManager.shared.getIdentityKey(forKey: "noiseStaticKey"),
|
if let identityData = KeychainManager.shared.getIdentityKey(forKey: "noiseStaticKey"),
|
||||||
let key = try? Curve25519.KeyAgreement.PrivateKey(rawRepresentation: identityData) {
|
let key = try? Curve25519.KeyAgreement.PrivateKey(rawRepresentation: identityData) {
|
||||||
loadedKey = key
|
loadedKey = key
|
||||||
SecureLogger.logKeyOperation("load", keyType: "noiseStaticKey", success: true)
|
SecureLogger.logKeyOperation(.load, keyType: "noiseStaticKey", success: true)
|
||||||
}
|
}
|
||||||
// If no identity exists, create new one
|
// If no identity exists, create new one
|
||||||
else {
|
else {
|
||||||
@@ -199,7 +198,7 @@ final class NoiseEncryptionService {
|
|||||||
|
|
||||||
// Save to keychain
|
// Save to keychain
|
||||||
let saved = KeychainManager.shared.saveIdentityKey(keyData, forKey: "noiseStaticKey")
|
let saved = KeychainManager.shared.saveIdentityKey(keyData, forKey: "noiseStaticKey")
|
||||||
SecureLogger.logKeyOperation("create", keyType: "noiseStaticKey", success: saved)
|
SecureLogger.logKeyOperation(.create, keyType: "noiseStaticKey", success: saved)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now assign the final value
|
// Now assign the final value
|
||||||
@@ -213,7 +212,7 @@ final class NoiseEncryptionService {
|
|||||||
if let signingData = KeychainManager.shared.getIdentityKey(forKey: "ed25519SigningKey"),
|
if let signingData = KeychainManager.shared.getIdentityKey(forKey: "ed25519SigningKey"),
|
||||||
let key = try? Curve25519.Signing.PrivateKey(rawRepresentation: signingData) {
|
let key = try? Curve25519.Signing.PrivateKey(rawRepresentation: signingData) {
|
||||||
loadedSigningKey = key
|
loadedSigningKey = key
|
||||||
SecureLogger.logKeyOperation("load", keyType: "ed25519SigningKey", success: true)
|
SecureLogger.logKeyOperation(.load, keyType: "ed25519SigningKey", success: true)
|
||||||
}
|
}
|
||||||
// If no signing key exists, create new one
|
// If no signing key exists, create new one
|
||||||
else {
|
else {
|
||||||
@@ -222,7 +221,7 @@ final class NoiseEncryptionService {
|
|||||||
|
|
||||||
// Save to keychain
|
// Save to keychain
|
||||||
let saved = KeychainManager.shared.saveIdentityKey(keyData, forKey: "ed25519SigningKey")
|
let saved = KeychainManager.shared.saveIdentityKey(keyData, forKey: "ed25519SigningKey")
|
||||||
SecureLogger.logKeyOperation("create", keyType: "ed25519SigningKey", success: saved)
|
SecureLogger.logKeyOperation(.create, keyType: "ed25519SigningKey", success: saved)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now assign the signing keys
|
// Now assign the signing keys
|
||||||
@@ -269,8 +268,8 @@ final class NoiseEncryptionService {
|
|||||||
// Clear from keychain
|
// Clear from keychain
|
||||||
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.warning("Panic mode activated - identity cleared", category: .security)
|
||||||
// Stop rekey timer
|
// Stop rekey timer
|
||||||
stopRekeyTimer()
|
stopRekeyTimer()
|
||||||
}
|
}
|
||||||
@@ -281,7 +280,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.error(error, context: "Failed to sign data")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -292,7 +291,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.error(error, context: "Failed to verify signature")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -392,17 +391,17 @@ final class NoiseEncryptionService {
|
|||||||
|
|
||||||
// Validate peer ID
|
// Validate peer ID
|
||||||
guard NoiseSecurityValidator.validatePeerID(peerID) else {
|
guard NoiseSecurityValidator.validatePeerID(peerID) else {
|
||||||
SecureLogger.logSecurityEvent(.authenticationFailed(peerID: peerID), level: .warning)
|
SecureLogger.warning(.authenticationFailed(peerID: peerID))
|
||||||
throw NoiseSecurityError.invalidPeerID
|
throw NoiseSecurityError.invalidPeerID
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check rate limit
|
// Check rate limit
|
||||||
guard rateLimiter.allowHandshake(from: peerID) else {
|
guard rateLimiter.allowHandshake(from: peerID) else {
|
||||||
SecureLogger.logSecurityEvent(.authenticationFailed(peerID: "Rate limited: \(peerID)"), level: .warning)
|
SecureLogger.warning(.authenticationFailed(peerID: "Rate limited: \(peerID)"))
|
||||||
throw NoiseSecurityError.rateLimitExceeded
|
throw NoiseSecurityError.rateLimitExceeded
|
||||||
}
|
}
|
||||||
|
|
||||||
SecureLogger.logSecurityEvent(.handshakeStarted(peerID: peerID))
|
SecureLogger.info(.handshakeStarted(peerID: peerID))
|
||||||
|
|
||||||
// Return raw handshake data without wrapper
|
// Return raw handshake data without wrapper
|
||||||
// The Noise protocol handles its own message format
|
// The Noise protocol handles its own message format
|
||||||
@@ -415,19 +414,19 @@ final class NoiseEncryptionService {
|
|||||||
|
|
||||||
// Validate peer ID
|
// Validate peer ID
|
||||||
guard NoiseSecurityValidator.validatePeerID(peerID) else {
|
guard NoiseSecurityValidator.validatePeerID(peerID) else {
|
||||||
SecureLogger.logSecurityEvent(.authenticationFailed(peerID: peerID), level: .warning)
|
SecureLogger.warning(.authenticationFailed(peerID: peerID))
|
||||||
throw NoiseSecurityError.invalidPeerID
|
throw NoiseSecurityError.invalidPeerID
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate message size
|
// Validate message size
|
||||||
guard NoiseSecurityValidator.validateHandshakeMessageSize(message) else {
|
guard NoiseSecurityValidator.validateHandshakeMessageSize(message) else {
|
||||||
SecureLogger.logSecurityEvent(.handshakeFailed(peerID: peerID, error: "Message too large"), level: .warning)
|
SecureLogger.warning(.handshakeFailed(peerID: peerID, error: "Message too large"))
|
||||||
throw NoiseSecurityError.messageTooLarge
|
throw NoiseSecurityError.messageTooLarge
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check rate limit
|
// Check rate limit
|
||||||
guard rateLimiter.allowHandshake(from: peerID) else {
|
guard rateLimiter.allowHandshake(from: peerID) else {
|
||||||
SecureLogger.logSecurityEvent(.authenticationFailed(peerID: "Rate limited: \(peerID)"), level: .warning)
|
SecureLogger.warning(.authenticationFailed(peerID: "Rate limited: \(peerID)"))
|
||||||
throw NoiseSecurityError.rateLimitExceeded
|
throw NoiseSecurityError.rateLimitExceeded
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,7 +520,7 @@ final class NoiseEncryptionService {
|
|||||||
peerFingerprints.removeValue(forKey: peerID)
|
peerFingerprints.removeValue(forKey: peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
SecureLogger.logSecurityEvent(.sessionExpired(peerID: peerID))
|
SecureLogger.info(.sessionExpired(peerID: peerID))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private Helpers
|
// MARK: - Private Helpers
|
||||||
@@ -537,7 +536,7 @@ final class NoiseEncryptionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Log security event
|
// Log security event
|
||||||
SecureLogger.logSecurityEvent(.handshakeCompleted(peerID: peerID))
|
SecureLogger.info(.handshakeCompleted(peerID: peerID))
|
||||||
|
|
||||||
// Notify all handlers about authentication
|
// Notify all handlers about authentication
|
||||||
serviceQueue.async { [weak self] in
|
serviceQueue.async { [weak self] in
|
||||||
@@ -573,12 +572,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.debug("Key rotation initiated for peer: \(peerID)", category: .security)
|
||||||
|
|
||||||
// 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.error(error, context: "Failed to initiate rekey for peer: \(peerID)", category: .session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,31 +51,29 @@ final class NostrTransport: Transport {
|
|||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
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.debug("NostrTransport: preparing PM to \(recipientNpub.prefix(16))… for peerID \(peerID.prefix(8))… id=\(messageID.prefix(8))…", category: .session)
|
||||||
category: SecureLogger.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.error("NostrTransport: recipient key not npub (hrp=\(hrp))", category: .session)
|
||||||
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.error("NostrTransport: failed to decode npub -> hex: \(error)", category: .session)
|
||||||
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.error("NostrTransport: failed to embed PM packet", category: .session)
|
||||||
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.error("NostrTransport: failed to build Nostr event for PM", category: .session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
SecureLogger.log("NostrTransport: sending PM giftWrap id=\(event.id.prefix(16))…",
|
SecureLogger.debug("NostrTransport: sending PM giftWrap id=\(event.id.prefix(16))…", category: .session)
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
NostrRelayManager.shared.sendEvent(event)
|
NostrRelayManager.shared.sendEvent(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,8 +97,7 @@ final class NostrTransport: Transport {
|
|||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
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.debug("NostrTransport: preparing READ ack for id=\(item.receipt.originalMessageID.prefix(8))… to \(recipientNpub.prefix(16))…", category: .session)
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
// Convert recipient npub -> hex
|
// Convert recipient npub -> hex
|
||||||
let recipientHex: String
|
let recipientHex: String
|
||||||
do {
|
do {
|
||||||
@@ -109,15 +106,14 @@ 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.error("NostrTransport: failed to embed READ ack", category: .session)
|
||||||
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.error("NostrTransport: failed to build Nostr event for READ ack", category: .session)
|
||||||
scheduleNextReadAck(); return
|
scheduleNextReadAck(); return
|
||||||
}
|
}
|
||||||
SecureLogger.log("NostrTransport: sending READ ack giftWrap id=\(event.id.prefix(16))…",
|
SecureLogger.debug("NostrTransport: sending READ ack giftWrap id=\(event.id.prefix(16))…", category: .session)
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
NostrRelayManager.shared.sendEvent(event)
|
NostrRelayManager.shared.sendEvent(event)
|
||||||
scheduleNextReadAck()
|
scheduleNextReadAck()
|
||||||
}
|
}
|
||||||
@@ -136,8 +132,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 }
|
||||||
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.debug("NostrTransport: preparing FAVORITE(\(isFavorite)) to \(recipientNpub.prefix(16))…", category: .session)
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
// Convert recipient npub -> hex
|
// Convert recipient npub -> hex
|
||||||
let recipientHex: String
|
let recipientHex: String
|
||||||
do {
|
do {
|
||||||
@@ -146,15 +141,14 @@ 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.error("NostrTransport: failed to embed favorite notification", category: .session)
|
||||||
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.error("NostrTransport: failed to build Nostr event for favorite notification", category: .session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
SecureLogger.log("NostrTransport: sending favorite giftWrap id=\(event.id.prefix(16))…",
|
SecureLogger.debug("NostrTransport: sending favorite giftWrap id=\(event.id.prefix(16))…", category: .session)
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
NostrRelayManager.shared.sendEvent(event)
|
NostrRelayManager.shared.sendEvent(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,8 +174,7 @@ final class NostrTransport: Transport {
|
|||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
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.debug("NostrTransport: preparing DELIVERED ack for id=\(messageID.prefix(8))… to \(recipientNpub.prefix(16))…", category: .session)
|
||||||
category: SecureLogger.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 +182,14 @@ 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.error("NostrTransport: failed to embed DELIVERED ack", category: .session)
|
||||||
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.error("NostrTransport: failed to build Nostr event for DELIVERED ack", category: .session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
SecureLogger.log("NostrTransport: sending DELIVERED ack giftWrap id=\(event.id.prefix(16))…",
|
SecureLogger.debug("NostrTransport: sending DELIVERED ack giftWrap id=\(event.id.prefix(16))…", category: .session)
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
NostrRelayManager.shared.sendEvent(event)
|
NostrRelayManager.shared.sendEvent(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,8 +197,7 @@ final class NostrTransport: Transport {
|
|||||||
// MARK: - Geohash ACK helpers
|
// MARK: - Geohash ACK helpers
|
||||||
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.debug("GeoDM: send DELIVERED -> recip=\(recipientHex.prefix(8))… mid=\(messageID.prefix(8))… from=\(identity.publicKeyHex.prefix(8))…", category: .session)
|
||||||
category: SecureLogger.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)
|
||||||
@@ -216,8 +207,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.debug("GeoDM: send READ -> recip=\(recipientHex.prefix(8))… mid=\(messageID.prefix(8))… from=\(identity.publicKeyHex.prefix(8))…", category: .session)
|
||||||
category: SecureLogger.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)
|
||||||
@@ -229,19 +219,17 @@ final class NostrTransport: Transport {
|
|||||||
func sendPrivateMessageGeohash(content: String, toRecipientHex recipientHex: String, from identity: NostrIdentity, messageID: String) {
|
func sendPrivateMessageGeohash(content: String, toRecipientHex recipientHex: String, from identity: NostrIdentity, messageID: String) {
|
||||||
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.debug("GeoDM: send PM -> recip=\(recipientHex.prefix(8))… mid=\(messageID.prefix(8))… from=\(identity.publicKeyHex.prefix(8))…", category: .session)
|
||||||
category: SecureLogger.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.error("NostrTransport: failed to embed geohash PM packet", category: .session)
|
||||||
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.error("NostrTransport: failed to build Nostr event for geohash PM", category: .session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
SecureLogger.log("NostrTransport: sending geohash PM giftWrap id=\(event.id.prefix(16))…",
|
SecureLogger.debug("NostrTransport: sending geohash PM giftWrap id=\(event.id.prefix(16))…", category: .session)
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
NostrRelayManager.registerPendingGiftWrap(id: event.id)
|
NostrRelayManager.registerPendingGiftWrap(id: event.id)
|
||||||
NostrRelayManager.shared.sendEvent(event)
|
NostrRelayManager.shared.sendEvent(event)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -228,8 +228,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.debug("PrivateChatManager: sending READ ack for \(message.id.prefix(8))… to \(senderPeerID.prefix(8))… via router", category: .session)
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
router.sendReadReceipt(receipt, to: senderPeerID)
|
router.sendReadReceipt(receipt, to: senderPeerID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.info("TorManager: embed reports already running", category: .session)
|
||||||
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.error("TorManager: tor_host_start failed rc=\(rc)", category: .session)
|
||||||
} else {
|
} else {
|
||||||
SecureLogger.log("TorManager: tor_host_start OK (\(socks), control \(control))", category: SecureLogger.session, level: .info)
|
SecureLogger.info("TorManager: tor_host_start OK (\(socks), control \(control))", category: .session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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.info("TorManager: SOCKS ready at \(self.socksHost):\(self.socksPort) [embed]", category: .session)
|
||||||
} 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.error("TorManager: SOCKS not reachable (timeout) [embed]", category: .session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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.warning("TorManager: no embedded tor framework found", category: .session)
|
||||||
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.info("TorManager: dlopen(\(fwURL.lastPathComponent))…", category: .session)
|
||||||
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.info("TorManager: launching tor_main with torrc", category: .session)
|
||||||
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.error("TorManager: SOCKS not reachable (timeout)", category: .session)
|
||||||
} else {
|
} else {
|
||||||
SecureLogger.log("TorManager: SOCKS ready at \(self.socksHost):\(self.socksPort)", category: SecureLogger.session, level: .info)
|
SecureLogger.info("TorManager: SOCKS ready at \(self.socksHost):\(self.socksPort)", category: .session)
|
||||||
}
|
}
|
||||||
// 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.info("TorManager: starting tor_main (static)", category: .session)
|
||||||
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.info("TorManager: SOCKS ready at \(self.socksHost):\(self.socksPort)", category: .session)
|
||||||
} 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.error("TorManager: SOCKS not reachable (timeout)", category: .session)
|
||||||
}
|
}
|
||||||
// isStarting will be cleared when bootstrap reaches 100%
|
// isStarting will be cleared when bootstrap reaches 100%
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,11 +203,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
|||||||
|
|
||||||
if let favorite = favoriteByNickname,
|
if let favorite = favoriteByNickname,
|
||||||
let noiseKey = peerInfo.noisePublicKey {
|
let noiseKey = peerInfo.noisePublicKey {
|
||||||
SecureLogger.log(
|
SecureLogger.debug("🔄 Found favorite for '\(peerInfo.nickname)' by nickname, updating noise key", category: .session)
|
||||||
"🔄 Found favorite for '\(peerInfo.nickname)' by nickname, updating noise key",
|
|
||||||
category: SecureLogger.session,
|
|
||||||
level: .debug
|
|
||||||
)
|
|
||||||
|
|
||||||
// Update the favorite's key in persistence
|
// Update the favorite's key in persistence
|
||||||
favoritesService.updateNoisePublicKey(
|
favoritesService.updateNoisePublicKey(
|
||||||
@@ -282,8 +278,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
|||||||
/// Toggle favorite status
|
/// Toggle favorite status
|
||||||
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.warning("⚠️ Cannot toggle favorite - peer not found: \(peerID)", category: .session)
|
||||||
category: SecureLogger.session, level: .warning)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,15 +288,13 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
|||||||
var actualNickname = peer.nickname
|
var actualNickname = peer.nickname
|
||||||
|
|
||||||
// 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.debug("🔍 Toggle favorite - peer.nickname: '\(peer.nickname)', peer.displayName: '\(peer.displayName)', peerID: \(peerID)", category: .session)
|
||||||
category: SecureLogger.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.debug("🔍 Got nickname from mesh service: '\(actualNickname)'", category: .session)
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,8 +321,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.debug("⭐️ Toggled favorite for '\(finalNickname)' (peerID: \(peerID), was: \(wasFavorite), now: \(!wasFavorite))", category: .session)
|
||||||
category: SecureLogger.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 {
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -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 = {
|
||||||
@@ -124,24 +113,90 @@ final class SecureLogger {
|
|||||||
|
|
||||||
// MARK: - Public Logging Methods
|
// MARK: - Public Logging Methods
|
||||||
|
|
||||||
/// Log a security event
|
static func debug(_ message: @autoclosure () -> String, category: OSLog = .noise,
|
||||||
static func logSecurityEvent(_ event: SecurityEvent, level: LogLevel = .info,
|
|
||||||
file: String = #file, line: Int = #line, function: String = #function) {
|
file: String = #file, line: Int = #line, function: String = #function) {
|
||||||
guard shouldLog(level) else { return }
|
log(message(), category: category, level: .debug, file: file, line: line, function: function)
|
||||||
let location = formatLocation(file: file, line: line, function: function)
|
|
||||||
let message = "\(location) \(event.message)"
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
os_log("%{public}@", log: security, type: level.osLogType, message)
|
|
||||||
#else
|
|
||||||
// In release, use private logging to prevent sensitive data exposure
|
|
||||||
os_log("%{private}@", log: security, type: level.osLogType, message)
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Log general messages with automatic sensitive data filtering
|
static func info(_ message: @autoclosure () -> String, category: OSLog = .noise,
|
||||||
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) {
|
||||||
|
log(message(), category: category, level: .info, file: file, line: line, function: function)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func warning(_ message: @autoclosure () -> String, category: OSLog = .noise,
|
||||||
|
file: String = #file, line: Int = #line, function: String = #function) {
|
||||||
|
log(message(), category: category, level: .warning, file: file, line: line, function: function)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func error(_ message: @autoclosure () -> String, category: OSLog = .noise,
|
||||||
|
file: String = #file, line: Int = #line, function: String = #function) {
|
||||||
|
log(message(), category: category, level: .error, file: file, line: line, function: function)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Security Event Logging
|
||||||
|
|
||||||
|
static func debug(_ event: SecurityEvent, file: String = #file, line: Int = #line, function: String = #function) {
|
||||||
|
logSecurityEvent(event, level: .debug, file: file, line: line, function: function)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func info(_ event: SecurityEvent, file: String = #file, line: Int = #line, function: String = #function) {
|
||||||
|
logSecurityEvent(event, level: .info, file: file, line: line, function: function)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func warning(_ event: SecurityEvent, file: String = #file, line: Int = #line, function: String = #function) {
|
||||||
|
logSecurityEvent(event, level: .warning, file: file, line: line, function: function)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func error(_ event: SecurityEvent, file: String = #file, line: Int = #line, function: String = #function) {
|
||||||
|
logSecurityEvent(event, level: .error, file: file, line: line, function: function)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Log errors with context
|
||||||
|
static func error(_ error: Error, context: @autoclosure () -> String, category: OSLog = .noise,
|
||||||
|
file: String = #file, line: Int = #line, function: String = #function) {
|
||||||
|
let location = formatLocation(file: file, line: line, function: function)
|
||||||
|
let sanitized = sanitize(context())
|
||||||
|
let errorDesc = sanitize(error.localizedDescription)
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
os_log("%{public}@ Error in %{public}@: %{public}@", log: category, type: .error, location, sanitized, errorDesc)
|
||||||
|
#else
|
||||||
|
os_log("%{private}@ Error in %{private}@: %{private}@", log: category, type: .error, location, sanitized, errorDesc)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Convenience Extensions
|
||||||
|
|
||||||
|
extension SecureLogger {
|
||||||
|
|
||||||
|
enum KeyOperation: String, CustomStringConvertible {
|
||||||
|
case load
|
||||||
|
case create
|
||||||
|
case generate
|
||||||
|
case delete
|
||||||
|
case save
|
||||||
|
|
||||||
|
var description: String { rawValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Log key management operations
|
||||||
|
static func logKeyOperation(_ operation: KeyOperation, keyType: String, success: Bool = true,
|
||||||
|
file: String = #file, line: Int = #line, function: String = #function) {
|
||||||
|
if success {
|
||||||
|
debug("Key operation '\(operation)' for \(keyType) succeeded", category: .keychain, file: file, line: line, function: function)
|
||||||
|
} else {
|
||||||
|
error("Key operation '\(operation)' for \(keyType) failed", category: .keychain, file: file, line: line, function: function)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private Helpers
|
||||||
|
|
||||||
|
private extension SecureLogger {
|
||||||
|
/// Log general messages with automatic sensitive data filtering
|
||||||
|
static func log(_ message: @autoclosure () -> String, category: OSLog, level: LogLevel,
|
||||||
|
file: String, line: Int, function: String) {
|
||||||
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)
|
||||||
let sanitized = sanitize("\(location) \(message())")
|
let sanitized = sanitize("\(location) \(message())")
|
||||||
@@ -156,31 +211,30 @@ final class SecureLogger {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Log errors with context
|
/// Log a security event
|
||||||
static func logError(_ error: Error, context: @autoclosure () -> String, category: OSLog = noise,
|
static func logSecurityEvent(_ event: SecurityEvent, level: LogLevel = .info,
|
||||||
file: String = #file, line: Int = #line, function: String = #function) {
|
file: String, line: Int, function: String) {
|
||||||
|
guard shouldLog(level) else { return }
|
||||||
let location = formatLocation(file: file, line: line, function: function)
|
let location = formatLocation(file: file, line: line, function: function)
|
||||||
let sanitized = sanitize(context())
|
let message = "\(location) \(event.message)"
|
||||||
let errorDesc = sanitize(error.localizedDescription)
|
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
os_log("%{public}@ Error in %{public}@: %{public}@", log: category, type: .error, location, sanitized, errorDesc)
|
os_log("%{public}@", log: .security, type: level.osLogType, message)
|
||||||
#else
|
#else
|
||||||
os_log("%{private}@ Error in %{private}@: %{private}@", log: category, type: .error, location, sanitized, errorDesc)
|
// In release, use private logging to prevent sensitive data exposure
|
||||||
|
os_log("%{private}@", log: .security, type: level.osLogType, message)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private Helpers
|
|
||||||
|
|
||||||
/// Format location information for logging
|
/// Format location information for logging
|
||||||
private static func formatLocation(file: String, line: Int, function: String) -> String {
|
static func formatLocation(file: String, line: Int, function: String) -> String {
|
||||||
let fileName = (file as NSString).lastPathComponent
|
let fileName = (file as NSString).lastPathComponent
|
||||||
let timestamp = timestampFormatter.string(from: Date())
|
let timestamp = timestampFormatter.string(from: Date())
|
||||||
return "[\(timestamp)] [\(fileName):\(line) \(function)]"
|
return "[\(timestamp)] [\(fileName):\(line) \(function)]"
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sanitize strings to remove potentially sensitive data
|
/// Sanitize strings to remove potentially sensitive data
|
||||||
private static func sanitize(_ input: String) -> String {
|
static func sanitize(_ input: String) -> String {
|
||||||
let key = input as NSString
|
let key = input as NSString
|
||||||
|
|
||||||
// Check cache first
|
// Check cache first
|
||||||
@@ -226,45 +280,12 @@ final class SecureLogger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Sanitize individual values
|
/// Sanitize individual values
|
||||||
private static func sanitize<T>(_ value: T) -> String {
|
static func sanitize<T>(_ value: T) -> String {
|
||||||
let stringValue = String(describing: value)
|
let stringValue = String(describing: value)
|
||||||
return sanitize(stringValue)
|
return sanitize(stringValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Convenience Extensions
|
|
||||||
|
|
||||||
extension SecureLogger {
|
|
||||||
|
|
||||||
/// Log handshake events
|
|
||||||
static func logHandshake(_ phase: String, peerID: String, success: Bool = true,
|
|
||||||
file: String = #file, line: Int = #line, function: String = #function) {
|
|
||||||
if success {
|
|
||||||
log("Handshake \(phase) with peer: \(peerID)", category: session, level: .info,
|
|
||||||
file: file, line: line, function: function)
|
|
||||||
} else {
|
|
||||||
log("Handshake \(phase) failed with peer: \(peerID)", category: session, level: .warning,
|
|
||||||
file: file, line: line, function: function)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Log encryption operations
|
|
||||||
static func logEncryption(_ operation: String, success: Bool = true,
|
|
||||||
file: String = #file, line: Int = #line, function: String = #function) {
|
|
||||||
let level: LogLevel = success ? .debug : .error
|
|
||||||
log("Encryption operation '\(operation)' \(success ? "succeeded" : "failed")",
|
|
||||||
category: encryption, level: level, file: file, line: line, function: function)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Log key management operations
|
|
||||||
static func logKeyOperation(_ operation: String, keyType: String, success: Bool = true,
|
|
||||||
file: String = #file, line: Int = #line, function: String = #function) {
|
|
||||||
let level: LogLevel = success ? .debug : .error
|
|
||||||
log("Key operation '\(operation)' for \(keyType) \(success ? "succeeded" : "failed")",
|
|
||||||
category: keychain, level: level, file: file, line: line, function: function)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Migration Helper
|
// MARK: - Migration Helper
|
||||||
|
|
||||||
/// Helper to migrate from print statements to SecureLogger
|
/// Helper to migrate from print statements to SecureLogger
|
||||||
@@ -273,6 +294,6 @@ func secureLog(_ items: Any..., separator: String = " ", terminator: String = "\
|
|||||||
file: String = #file, line: Int = #line, function: String = #function) {
|
file: String = #file, line: Int = #line, function: String = #function) {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
let message = items.map { String(describing: $0) }.joined(separator: separator)
|
let message = items.map { String(describing: $0) }.joined(separator: separator)
|
||||||
SecureLogger.log(message, level: .debug, file: file, line: line, function: function)
|
SecureLogger.debug(message, file: file, line: line, function: function)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -462,8 +462,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
if let data = try? JSONEncoder().encode(Array(sentReadReceipts)) {
|
if let data = try? JSONEncoder().encode(Array(sentReadReceipts)) {
|
||||||
UserDefaults.standard.set(data, forKey: "sentReadReceipts")
|
UserDefaults.standard.set(data, forKey: "sentReadReceipts")
|
||||||
} else {
|
} else {
|
||||||
SecureLogger.log("❌ Failed to encode read receipts for persistence",
|
SecureLogger.error("❌ Failed to encode read receipts for persistence", category: .session)
|
||||||
category: SecureLogger.session, level: .error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -562,11 +561,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.error("Nostr init skipped: Tor not ready", category: .session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
nostrRelayManager = NostrRelayManager.shared
|
nostrRelayManager = NostrRelayManager.shared
|
||||||
SecureLogger.log("Initializing Nostr relay connections", category: SecureLogger.session, level: .debug)
|
SecureLogger.debug("Initializing Nostr relay connections", category: .session)
|
||||||
// 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
|
||||||
@@ -596,8 +595,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
if uniquePeers[peer.id] == nil {
|
if uniquePeers[peer.id] == nil {
|
||||||
uniquePeers[peer.id] = peer
|
uniquePeers[peer.id] = peer
|
||||||
} else {
|
} else {
|
||||||
SecureLogger.log("⚠️ Duplicate peer ID detected: \(peer.id) (\(peer.displayName))",
|
SecureLogger.warning("⚠️ Duplicate peer ID detected: \(peer.id) (\(peer.displayName))", category: .session)
|
||||||
category: SecureLogger.session, level: .warning)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.peerIndex = uniquePeers
|
self.peerIndex = uniquePeers
|
||||||
@@ -1035,11 +1033,9 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
if let idx = self.privateChats[convKey]?.firstIndex(where: { $0.id == messageID }) {
|
if let idx = self.privateChats[convKey]?.firstIndex(where: { $0.id == messageID }) {
|
||||||
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.info("GeoDM: recv DELIVERED for mid=\(messageID.prefix(8))… from=\(senderPubkey.prefix(8))…", category: .session)
|
||||||
category: SecureLogger.session, level: .info)
|
|
||||||
} else {
|
} else {
|
||||||
SecureLogger.log("GeoDM: delivered ack for unknown mid=\(messageID.prefix(8))… conv=\(convKey)",
|
SecureLogger.warning("GeoDM: delivered ack for unknown mid=\(messageID.prefix(8))… conv=\(convKey)", category: .session)
|
||||||
category: SecureLogger.session, level: .warning)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case .readReceipt:
|
case .readReceipt:
|
||||||
@@ -1047,11 +1043,9 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
if let idx = self.privateChats[convKey]?.firstIndex(where: { $0.id == messageID }) {
|
if let idx = self.privateChats[convKey]?.firstIndex(where: { $0.id == messageID }) {
|
||||||
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.info("GeoDM: recv READ for mid=\(messageID.prefix(8))… from=\(senderPubkey.prefix(8))…", category: .session)
|
||||||
category: SecureLogger.session, level: .info)
|
|
||||||
} else {
|
} else {
|
||||||
SecureLogger.log("GeoDM: read ack for unknown mid=\(messageID.prefix(8))… conv=\(convKey)",
|
SecureLogger.warning("GeoDM: read ack for unknown mid=\(messageID.prefix(8))… conv=\(convKey)", category: .session)
|
||||||
category: SecureLogger.session, level: .warning)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case .verifyChallenge, .verifyResponse:
|
case .verifyChallenge, .verifyResponse:
|
||||||
@@ -1464,14 +1458,13 @@ 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.warning("Geo: no geohash relays available for \(ch.geohash); not sending", category: .session)
|
||||||
} 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.debug("GeoTeleport: sent geo message pub=\(identity.publicKeyHex.prefix(8))… teleported=\(LocationChannelManager.shared.teleported)", category: .session)
|
||||||
category: SecureLogger.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
|
||||||
@@ -1479,11 +1472,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
if LocationChannelManager.shared.teleported && hasRegional && !inRegional {
|
if LocationChannelManager.shared.teleported && hasRegional && !inRegional {
|
||||||
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.info("GeoTeleport: mark self teleported key=\(key.prefix(8))… total=\(self.teleportedGeo.count)", category: .session)
|
||||||
category: SecureLogger.session, level: .info)
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
SecureLogger.log("❌ Failed to send geohash message: \(error)", category: SecureLogger.session, level: .error)
|
SecureLogger.error("❌ Failed to send geohash message: \(error)", category: .session)
|
||||||
self.addSystemMessage("failed to send to location channel")
|
self.addSystemMessage("failed to send to location channel")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1510,7 +1502,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.debug("RenderGuard: mesh timeline contains \(emptyMesh) empty messages", category: .session)
|
||||||
}
|
}
|
||||||
stopGeoParticipantsTimer()
|
stopGeoParticipantsTimer()
|
||||||
geohashPeople = []
|
geohashPeople = []
|
||||||
@@ -1537,7 +1529,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.debug("RenderGuard: geohash \(ch.geohash) timeline has \(emptyGeo) empty messages after sanitize", category: .session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 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
|
||||||
@@ -1569,8 +1561,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
let key = id.publicKeyHex.lowercased()
|
let key = id.publicKeyHex.lowercased()
|
||||||
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.info("GeoTeleport: channel switch mark self teleported key=\(key.prefix(8))… total=\(teleportedGeo.count)", category: .session)
|
||||||
category: SecureLogger.session, level: .info)
|
|
||||||
} else {
|
} else {
|
||||||
teleportedGeo.remove(key)
|
teleportedGeo.remove(key)
|
||||||
}
|
}
|
||||||
@@ -1593,8 +1584,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
self.recordProcessedEvent(event.id)
|
self.recordProcessedEvent(event.id)
|
||||||
// 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.debug("GeoTeleport: recv pub=\(event.pubkey.prefix(8))… tags=\(tagSummary)", category: .session)
|
||||||
category: SecureLogger.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"
|
||||||
@@ -1611,8 +1601,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
if !isSelf {
|
if !isSelf {
|
||||||
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.info("GeoTeleport: mark peer teleported key=\(key.prefix(8))… total=\(self.teleportedGeo.count)", category: .session)
|
||||||
category: SecureLogger.session, level: .info)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1676,8 +1665,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// pared back logging: subscribe debug only
|
// pared back logging: subscribe debug only
|
||||||
// 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.debug("GeoDM: subscribing DMs pub=\(id.publicKeyHex.prefix(8))… sub=\(dmSub)", category: .session)
|
||||||
category: SecureLogger.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
|
||||||
@@ -1687,12 +1675,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
self.recordProcessedEvent(giftWrap.id)
|
self.recordProcessedEvent(giftWrap.id)
|
||||||
// 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.warning("GeoDM: failed decrypt giftWrap id=\(giftWrap.id.prefix(8))…", category: .session)
|
||||||
category: SecureLogger.session, level: .warning)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
SecureLogger.log("GeoDM: decrypted gift-wrap id=\(giftWrap.id.prefix(16))... from=\(senderPubkey.prefix(8))...",
|
SecureLogger.debug("GeoDM: decrypted gift-wrap id=\(giftWrap.id.prefix(16))... from=\(senderPubkey.prefix(8))...", category: .session)
|
||||||
category: SecureLogger.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 }
|
||||||
@@ -1705,8 +1691,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
case .privateMessage:
|
case .privateMessage:
|
||||||
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.info("GeoDM: recv PM <- sender=\(senderPubkey.prefix(8))… mid=\(messageId.prefix(8))…", category: .session)
|
||||||
category: SecureLogger.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()
|
||||||
@@ -1769,11 +1754,9 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
if let idx = self.privateChats[convKey]?.firstIndex(where: { $0.id == messageID }) {
|
if let idx = self.privateChats[convKey]?.firstIndex(where: { $0.id == messageID }) {
|
||||||
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.info("GeoDM: recv DELIVERED for mid=\(messageID.prefix(8))… from=\(senderPubkey.prefix(8))…", category: .session)
|
||||||
category: SecureLogger.session, level: .info)
|
|
||||||
} else {
|
} else {
|
||||||
SecureLogger.log("GeoDM: delivered ack for unknown mid=\(messageID.prefix(8))… conv=\(convKey)",
|
SecureLogger.warning("GeoDM: delivered ack for unknown mid=\(messageID.prefix(8))… conv=\(convKey)", category: .session)
|
||||||
category: SecureLogger.session, level: .warning)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case .readReceipt:
|
case .readReceipt:
|
||||||
@@ -1781,11 +1764,9 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
if let idx = self.privateChats[convKey]?.firstIndex(where: { $0.id == messageID }) {
|
if let idx = self.privateChats[convKey]?.firstIndex(where: { $0.id == messageID }) {
|
||||||
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.info("GeoDM: recv READ for mid=\(messageID.prefix(8))… from=\(senderPubkey.prefix(8))…", category: .session)
|
||||||
category: SecureLogger.session, level: .info)
|
|
||||||
} else {
|
} else {
|
||||||
SecureLogger.log("GeoDM: read ack for unknown mid=\(messageID.prefix(8))… conv=\(convKey)",
|
SecureLogger.warning("GeoDM: read ack for unknown mid=\(messageID.prefix(8))… conv=\(convKey)", category: .session)
|
||||||
category: SecureLogger.session, level: .warning)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -2152,8 +2133,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
SecureLogger.log("GeoDM: local send mid=\(messageID.prefix(8))… to=\(recipientHex.prefix(8))… conv=\(peerID)",
|
SecureLogger.debug("GeoDM: local send mid=\(messageID.prefix(8))… to=\(recipientHex.prefix(8))… conv=\(peerID)", category: .session)
|
||||||
category: SecureLogger.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)
|
||||||
@@ -2473,16 +2453,14 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// If any temp peer ID had unread messages, mark the consolidated peer as unread
|
// If any temp peer ID had unread messages, mark the consolidated peer as unread
|
||||||
if hadUnreadTemp {
|
if hadUnreadTemp {
|
||||||
unreadPrivateMessages.insert(peerID)
|
unreadPrivateMessages.insert(peerID)
|
||||||
SecureLogger.log("📬 Transferred unread status from temp peer IDs to \(peerID)",
|
SecureLogger.debug("📬 Transferred unread status from temp peer IDs to \(peerID)", category: .session)
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if consolidatedCount > 0 {
|
if consolidatedCount > 0 {
|
||||||
// Sort by timestamp
|
// Sort by timestamp
|
||||||
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.info("📥 Consolidated \(consolidatedCount) Nostr messages from temporary peer IDs to \(peerNickname)", category: .session)
|
||||||
category: SecureLogger.session, level: .info)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2496,8 +2474,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
SecureLogger.log("GeoDM: skipping mesh handshake for virtual peerID=\(peerID)",
|
SecureLogger.debug("GeoDM: skipping mesh handshake for virtual peerID=\(peerID)", category: .session)
|
||||||
category: SecureLogger.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
|
||||||
@@ -2577,8 +2554,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
@objc private func handleNostrReadReceipt(_ notification: Notification) {
|
@objc private func handleNostrReadReceipt(_ notification: Notification) {
|
||||||
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.info("📖 Handling read receipt for message \(receipt.originalMessageID) from Nostr", category: .session)
|
||||||
category: SecureLogger.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)
|
||||||
@@ -2603,8 +2579,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.info("📱 Updating private chat peer ID due to key change: \(oldPeerID) -> \(newPeerID)", category: .session)
|
||||||
category: SecureLogger.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] {
|
||||||
@@ -2635,8 +2610,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
} else {
|
} else {
|
||||||
// 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.debug("📱 Migrating private chat messages from \(oldPeerID) to \(newPeerID)", category: .session)
|
||||||
category: SecureLogger.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 +2716,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.debug("Skipping screenshot notification to \(peerID) - no established session", category: .security)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2783,14 +2757,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.warning("Geo: no geohash relays available for \(ch.geohash); not sending", category: .session)
|
||||||
} 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.error("❌ Failed to send geohash screenshot message: \(error)", category: .session)
|
||||||
self.addSystemMessage("failed to send to location channel")
|
self.addSystemMessage("failed to send to location channel")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2848,8 +2822,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Search for the current peer ID with the same nickname
|
// Search for the current peer ID with the same nickname
|
||||||
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.info("📖 Resolved updated peer ID for read receipt: \(peerID) -> \(currentPeerID)", category: .session)
|
||||||
category: SecureLogger.session, level: .info)
|
|
||||||
actualPeerID = currentPeerID
|
actualPeerID = currentPeerID
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -2877,8 +2850,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
let messages = privateChats[peerID] ?? []
|
let messages = privateChats[peerID] ?? []
|
||||||
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.debug("GeoDM: sending READ for mid=\(message.id.prefix(8))… to=\(recipientHex.prefix(8))…", category: .session)
|
||||||
category: SecureLogger.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 +4294,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.info("🔐 Verified loaded: \(verifiedFingerprints.count) [\(sample)]", category: .security)
|
||||||
// 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.info("⭐️ Favorite offline: \(fav.nickname) fp=\(fpShort) verified=\(isVer)", category: .security)
|
||||||
}
|
}
|
||||||
// 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 +4317,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.debug("🔐 Authenticated: \(peerID)", category: .security)
|
||||||
|
|
||||||
// Update encryption status
|
// Update encryption status
|
||||||
if self.verifiedFingerprints.contains(fingerprint) {
|
if self.verifiedFingerprints.contains(fingerprint) {
|
||||||
@@ -4364,8 +4336,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
let keyData = self.meshService.getNoiseService().getPeerPublicKeyData(peerID) {
|
let keyData = self.meshService.getNoiseService().getPeerPublicKeyData(peerID) {
|
||||||
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.debug("🗺️ Mapped short peerID to Noise key for header continuity: \(peerID) -> \(stable.prefix(8))…", category: .session)
|
||||||
category: SecureLogger.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 +4344,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.debug("📤 Sent deferred verify challenge to \(peerID) after handshake", category: .security)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schedule UI update
|
// Schedule UI update
|
||||||
@@ -4519,7 +4490,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.info("🔐 Marking verified fingerprint: \(short)", category: .security)
|
||||||
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 +4576,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.debug("🤝 Peer connected: \(peerID)", category: .session)
|
||||||
|
|
||||||
// Handle all main actor work async
|
// Handle all main actor work async
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
@@ -4622,8 +4593,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Resend favorite notification with our Nostr key after a short delay
|
// Resend favorite notification with our Nostr key after a short delay
|
||||||
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.debug("📤 Resent favorite notification to reconnected peer \(peerID)", category: .session)
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force UI refresh
|
// Force UI refresh
|
||||||
@@ -4643,7 +4613,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.debug("👋 Peer disconnected: \(peerID)", category: .session)
|
||||||
|
|
||||||
// Remove ephemeral session from identity manager
|
// Remove ephemeral session from identity manager
|
||||||
SecureIdentityStateManager.shared.removeEphemeralSession(peerID: peerID)
|
SecureIdentityStateManager.shared.removeEphemeralSession(peerID: peerID)
|
||||||
@@ -4738,8 +4708,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
self.lastNetworkNotificationTime = Date()
|
self.lastNetworkNotificationTime = Date()
|
||||||
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.info("👥 Sent bitchatters nearby notification for \(meshPeers.count) mesh peers", category: .session)
|
||||||
category: SecureLogger.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 +4718,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.debug("⏳ Mesh empty — reset network notification state", category: .session)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register ephemeral sessions for all connected peers
|
// Register ephemeral sessions for all connected peers
|
||||||
@@ -4811,8 +4780,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !idsToRemove.isEmpty {
|
if !idsToRemove.isEmpty {
|
||||||
SecureLogger.log("🧹 Cleaned up \(idsToRemove.count) stale unread peer IDs",
|
SecureLogger.debug("🧹 Cleaned up \(idsToRemove.count) stale unread peer IDs", category: .session)
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4842,8 +4810,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.debug("🧹 Cleaned up \(removedCount) old read receipts", category: .session)
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4891,7 +4858,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.error("Failed to decode Nostr npub: \(error)", category: .session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5032,7 +4999,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.error("❌ Failed to send geohash raw message: \(error)", category: .session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -5048,12 +5015,11 @@ 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.warning("⚠️ No Nostr identity available for message handling", category: .session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
SecureLogger.log("🔑 Setting up Nostr subscription for pubkey: \(currentIdentity.publicKeyHex.prefix(16))...",
|
SecureLogger.debug("🔑 Setting up Nostr subscription for pubkey: \(currentIdentity.publicKeyHex.prefix(16))...", category: .session)
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
|
|
||||||
// Subscribe to Nostr messages
|
// Subscribe to Nostr messages
|
||||||
let filter = NostrFilter.giftWrapsFor(
|
let filter = NostrFilter.giftWrapsFor(
|
||||||
@@ -5093,19 +5059,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.debug("Ignoring non-embedded Nostr DM content", category: .session)
|
||||||
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.error("Failed to decode embedded BitChat packet from Nostr DM", category: .session)
|
||||||
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.warning("Unsupported embedded packet type: \(packet.type)", category: .session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5119,7 +5085,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.error("Failed to parse embedded NoisePayload", category: .session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5215,14 +5181,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.debug("Sending DELIVERED ack for \(messageId.prefix(8))… via router", category: .session)
|
||||||
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.debug("Sent DELIVERED ack directly to Nostr pub=\(senderPubkey.prefix(8))… for mid=\(messageId.prefix(8))…", category: .session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5237,7 +5203,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.debug("Viewing chat; sending READ ack for \(messageId.prefix(8))… via router", category: .session)
|
||||||
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 +5211,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.debug("Viewing chat; sent READ ack directly to Nostr pub=\(senderPubkey.prefix(8))… for mid=\(messageId.prefix(8))…", category: .session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -5290,7 +5256,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
SecureLogger.log("Failed to decrypt Nostr message: \(error)", category: SecureLogger.session, level: .error)
|
SecureLogger.error("Failed to decrypt Nostr message: \(error)", category: .session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5311,7 +5277,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.warning("⚠️ Invalid ACK format: \(content)", category: .session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5326,8 +5292,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.debug("📨 Received \(ackType) ACK for message \(messageId.prefix(16))... from \(senderPubkey.prefix(16))...", category: .session)
|
||||||
category: SecureLogger.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 +5311,11 @@ 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.warning("⚠️ Unknown ACK type: \(ackType)", category: .session)
|
||||||
}
|
}
|
||||||
|
|
||||||
messageFound = true
|
messageFound = true
|
||||||
SecureLogger.log("✅ Updated message \(messageId.prefix(16))... status to \(ackType) in chat \(chatPeerID.prefix(16))...",
|
SecureLogger.info("✅ Updated message \(messageId.prefix(16))... status to \(ackType) in chat \(chatPeerID.prefix(16))...", category: .session)
|
||||||
category: SecureLogger.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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5359,8 +5323,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
if messageFound {
|
if messageFound {
|
||||||
objectWillChange.send()
|
objectWillChange.send()
|
||||||
} else {
|
} else {
|
||||||
SecureLogger.log("⚠️ Could not find message \(messageId) to update status from ACK",
|
SecureLogger.warning("⚠️ Could not find message \(messageId) to update status from ACK", category: .session)
|
||||||
category: SecureLogger.session, level: .warning)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5386,8 +5349,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
var nostrPubkey: String? = nil
|
var nostrPubkey: String? = nil
|
||||||
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.info("📝 Received Nostr npub in favorite notification: \(nostrPubkey ?? "none")", category: .session)
|
||||||
category: SecureLogger.session, level: .info)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the noise public key for this peer
|
// Get the noise public key for this peer
|
||||||
@@ -5405,8 +5367,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.warning("⚠️ Cannot get Noise key for peer \(peerID)", category: .session)
|
||||||
category: SecureLogger.session, level: .warning)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5420,8 +5381,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.info("💾 Storing Nostr key association for \(senderNickname): \(nostrPubkey!.prefix(16))...", category: .session)
|
||||||
category: SecureLogger.session, level: .info)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show system message
|
// Show system message
|
||||||
@@ -5532,8 +5492,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Not notifying for old message
|
// Not notifying for old message
|
||||||
}
|
}
|
||||||
|
|
||||||
SecureLogger.log("📬 Stored Nostr message from unknown sender \(finalSenderNickname) in temporary peer \(tempPeerID)",
|
SecureLogger.info("📬 Stored Nostr message from unknown sender \(finalSenderNickname) in temporary peer \(tempPeerID)", category: .session)
|
||||||
category: SecureLogger.session, level: .info)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@@ -5545,16 +5504,14 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
} else {
|
} else {
|
||||||
// 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.warning("⚠️ Invalid hex public key format: \(nostrPubkey.prefix(16))...", category: .session)
|
||||||
category: SecureLogger.session, level: .warning)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
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.warning("⚠️ Failed to convert hex to npub: \(error)", category: .session)
|
||||||
category: SecureLogger.session, level: .warning)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5564,22 +5521,19 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
if let storedNostrKey = relationship.peerNostrPublicKey {
|
if let storedNostrKey = relationship.peerNostrPublicKey {
|
||||||
// Compare npub format
|
// Compare npub format
|
||||||
if storedNostrKey == npubToMatch {
|
if storedNostrKey == npubToMatch {
|
||||||
// SecureLogger.log("✅ Found Noise key for Nostr sender (npub match)",
|
// SecureLogger.debug("✅ Found Noise key for Nostr sender (npub match)", category: .session)
|
||||||
// category: SecureLogger.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.debug("✅ Found Noise key for Nostr sender (hex match)", category: .session)
|
||||||
category: SecureLogger.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.debug("⚠️ No matching Noise key found for Nostr pubkey: \(nostrPubkey.prefix(16))... (tried npub: \(npubToMatch.prefix(16))...)", category: .session)
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5607,13 +5561,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.debug("📤 Sent favorite notification via BLE to \(peerID)", category: .session)
|
||||||
} 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.warning("⚠️ Cannot send favorite notification - peer not connected and no Nostr pubkey", category: .session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5703,12 +5657,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
oldPeerIDsToRemove.append(oldPeerID)
|
oldPeerIDsToRemove.append(oldPeerID)
|
||||||
} 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.info("📦 Partially migrating \(recentMessages.count) of \(messages.count) messages from \(oldPeerID)", category: .session)
|
||||||
category: SecureLogger.session, level: .info)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SecureLogger.log("📦 Migrating \(recentMessages.count) recent messages from old peer ID \(oldPeerID) to \(peerID) (fingerprint match)",
|
SecureLogger.info("📦 Migrating \(recentMessages.count) recent messages from old peer ID \(oldPeerID) to \(peerID) (fingerprint match)", category: .session)
|
||||||
category: SecureLogger.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
|
||||||
@@ -5724,8 +5676,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
oldPeerIDsToRemove.append(oldPeerID)
|
oldPeerIDsToRemove.append(oldPeerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
SecureLogger.log("📦 Migrating \(recentMessages.count) recent messages from old peer ID \(oldPeerID) to \(peerID) (nickname match)",
|
SecureLogger.warning("📦 Migrating \(recentMessages.count) recent messages from old peer ID \(oldPeerID) to \(peerID) (nickname match)", category: .session)
|
||||||
category: SecureLogger.session, level: .warning)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5764,8 +5715,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Update selectedPrivateChatPeer if it was pointing to an old ID
|
// Update selectedPrivateChatPeer if it was pointing to an old ID
|
||||||
if needsSelectedUpdate {
|
if needsSelectedUpdate {
|
||||||
selectedPrivateChatPeer = peerID
|
selectedPrivateChatPeer = peerID
|
||||||
SecureLogger.log("📱 Updated selectedPrivateChatPeer from old ID to \(peerID) during migration",
|
SecureLogger.info("📱 Updated selectedPrivateChatPeer from old ID to \(peerID) during migration", category: .session)
|
||||||
category: SecureLogger.session, level: .info)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5774,11 +5724,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.debug("📥 handlePrivateMessage called for message from \(message.sender)", category: .session)
|
||||||
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.warning("⚠️ Could not get peer ID for sender \(message.sender)", category: .session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5818,8 +5768,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Clean up the stable key storage to avoid duplication
|
// Clean up the stable key storage to avoid duplication
|
||||||
privateChats.removeValue(forKey: stableKeyHex)
|
privateChats.removeValue(forKey: stableKeyHex)
|
||||||
|
|
||||||
SecureLogger.log("📥 Consolidated \(nostrMessages.count) Nostr messages from stable key to ephemeral peer \(peerID)",
|
SecureLogger.info("📥 Consolidated \(nostrMessages.count) Nostr messages from stable key to ephemeral peer \(peerID)", category: .session)
|
||||||
category: SecureLogger.session, level: .info)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6086,8 +6035,7 @@ private func checkForMentions(_ message: BitchatMessage) {
|
|||||||
let isMentioned = (message.mentions?.contains { myTokens.contains($0) } ?? false)
|
let isMentioned = (message.mentions?.contains { myTokens.contains($0) } ?? false)
|
||||||
|
|
||||||
if isMentioned && message.sender != nickname {
|
if isMentioned && message.sender != nickname {
|
||||||
SecureLogger.log("🔔 Mention from \(message.sender)",
|
SecureLogger.info("🔔 Mention from \(message.sender)", category: .session)
|
||||||
category: SecureLogger.session, level: .info)
|
|
||||||
NotificationService.shared.sendMentionNotification(from: message.sender, message: message.content)
|
NotificationService.shared.sendMentionNotification(from: message.sender, message: message.content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user