Feature/nostr embedded bitchat (#448)

* UI: prefer mesh radio icon when connected; map short peer ID to full Noise key; rename ephemeral mapping to shortIDToNoiseKey; ensure header flips to purple globe on disconnect and keeps name.

* chore: stop tracking build artifacts; ignore .DerivedData and .Result*

* logging: add global threshold via BITCHAT_LOG_LEVEL and demote chatty logs to debug; keep critical errors/warnings and key state transitions

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
jack
2025-08-17 11:25:42 +02:00
committed by GitHub
co-authored by jack
parent fb1988ac27
commit 3ebfa85e90
9 changed files with 510 additions and 308 deletions
@@ -485,7 +485,7 @@ class NoiseEncryptionService {
// Attempt to rekey the session
do {
try sessionManager.initiateRekey(for: peerID)
SecureLogger.log("Key rotation initiated for peer: \(peerID)", category: SecureLogger.security, level: .info)
SecureLogger.log("Key rotation initiated for peer: \(peerID)", category: SecureLogger.security, level: .debug)
// Signal that handshake is needed
onHandshakeRequired?(peerID)
+120 -30
View File
@@ -152,7 +152,7 @@ final class SimplifiedBluetoothService: NSObject {
// This ensures we send pending messages only when session is truly established
noiseService.onPeerAuthenticated = { [weak self] peerID, fingerprint in
SecureLogger.log("🔐 Noise session authenticated with \(peerID), fingerprint: \(fingerprint.prefix(16))...",
category: SecureLogger.noise, level: .info)
category: SecureLogger.noise, level: .debug)
// Send any messages that were queued during handshake
self?.messageQueue.async { [weak self] in
self?.sendPendingMessagesAfterHandshake(for: peerID)
@@ -341,7 +341,7 @@ final class SimplifiedBluetoothService: NSObject {
func sendFavoriteNotification(to peerID: String, isFavorite: Bool) {
SecureLogger.log("🔔 sendFavoriteNotification called - peerID: \(peerID), isFavorite: \(isFavorite)",
category: SecureLogger.session, level: .info)
category: SecureLogger.session, level: .debug)
// Include Nostr public key in the notification
var content = isFavorite ? "[FAVORITED]" : "[UNFAVORITED]"
@@ -350,11 +350,11 @@ final class SimplifiedBluetoothService: NSObject {
if let myNostrIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() {
content += ":" + myNostrIdentity.npub
SecureLogger.log("📝 Sending favorite notification with Nostr npub: \(myNostrIdentity.npub)",
category: SecureLogger.session, level: .info)
category: SecureLogger.session, level: .debug)
}
SecureLogger.log("📤 Sending favorite notification to \(peerID): \(content)",
category: SecureLogger.session, level: .info)
category: SecureLogger.session, level: .debug)
sendPrivateMessage(content, to: peerID, messageID: UUID().uuidString)
}
@@ -366,7 +366,7 @@ final class SimplifiedBluetoothService: NSObject {
}
SecureLogger.log("📤 Sending READ receipt for message \(receipt.originalMessageID) to \(peerID)",
category: SecureLogger.session, level: .info)
category: SecureLogger.session, level: .debug)
// Create read receipt payload: [type byte] + [message ID]
var receiptPayload = Data([NoisePayloadType.readReceipt.rawValue])
@@ -496,7 +496,7 @@ final class SimplifiedBluetoothService: NSObject {
// MARK: - Private Message Handling
private func sendPrivateMessage(_ content: String, to recipientID: String, messageID: String) {
SecureLogger.log("📨 Sending PM to \(recipientID): \(content.prefix(30))...", category: SecureLogger.session, level: .info)
SecureLogger.log("📨 Sending PM to \(recipientID): \(content.prefix(30))...", category: SecureLogger.session, level: .debug)
// Check if we have an established Noise session
if noiseService.hasEstablishedSession(with: recipientID) {
@@ -558,7 +558,7 @@ final class SimplifiedBluetoothService: NSObject {
}
} else {
// Queue message for sending after handshake completes
SecureLogger.log("🤝 No session with \(recipientID), initiating handshake and queueing message", category: SecureLogger.session, level: .info)
SecureLogger.log("🤝 No session with \(recipientID), initiating handshake and queueing message", category: SecureLogger.session, level: .debug)
// Queue the message (especially important for favorite notifications)
collectionsQueue.sync(flags: .barrier) {
@@ -618,7 +618,7 @@ final class SimplifiedBluetoothService: NSObject {
guard let messages = pendingMessages, !messages.isEmpty else { return }
SecureLogger.log("📤 Sending \(messages.count) pending messages after handshake to \(peerID)",
category: SecureLogger.session, level: .info)
category: SecureLogger.session, level: .debug)
// Send each pending message directly (we know session is established)
for (content, messageID) in messages {
@@ -650,7 +650,7 @@ final class SimplifiedBluetoothService: NSObject {
}
SecureLogger.log("✅ Sent pending message \(messageID) to \(peerID) after handshake",
category: SecureLogger.session, level: .info)
category: SecureLogger.session, level: .debug)
} catch {
SecureLogger.log("Failed to send pending message after handshake: \(error)",
category: SecureLogger.noise, level: .error)
@@ -675,7 +675,7 @@ final class SimplifiedBluetoothService: NSObject {
// Log encrypted and relayed packets for debugging
if packet.type == MessageType.noiseEncrypted.rawValue {
SecureLogger.log("📡 Encrypted packet to \(packet.recipientID?.hexEncodedString() ?? "unknown")",
category: SecureLogger.session, level: .info)
category: SecureLogger.session, level: .debug)
} else if packet.ttl < messageTTL {
// Relayed packet
}
@@ -1042,11 +1042,11 @@ final class SimplifiedBluetoothService: NSObject {
// Log connection status
if existingPeer == nil {
SecureLogger.log("🆕 New peer: \(announcement.nickname)", category: SecureLogger.session, level: .info)
SecureLogger.log("🆕 New peer: \(announcement.nickname)", category: SecureLogger.session, level: .debug)
} else if wasDisconnected {
SecureLogger.log("🔄 Peer \(announcement.nickname) reconnected", category: SecureLogger.session, level: .info)
SecureLogger.log("🔄 Peer \(announcement.nickname) reconnected", category: SecureLogger.session, level: .debug)
} else if existingPeer?.nickname != announcement.nickname {
SecureLogger.log("🔄 Peer \(peerID) changed nickname: \(existingPeer?.nickname ?? "Unknown") -> \(announcement.nickname)", category: SecureLogger.session, level: .info)
SecureLogger.log("🔄 Peer \(peerID) changed nickname: \(existingPeer?.nickname ?? "Unknown") -> \(announcement.nickname)", category: SecureLogger.session, level: .debug)
}
}
@@ -1117,7 +1117,7 @@ final class SimplifiedBluetoothService: NSObject {
}
let senderNickname = peers[peerID]?.nickname ?? "Unknown"
SecureLogger.log("💬 [\(senderNickname)] TTL:\(packet.ttl): \(String(content.prefix(50)))\(content.count > 50 ? "..." : "")", category: SecureLogger.session, level: .info)
SecureLogger.log("💬 [\(senderNickname)] TTL:\(packet.ttl): \(String(content.prefix(50)))\(content.count > 50 ? "..." : "")", category: SecureLogger.session, level: .debug)
// Parse mentions from the message content
let mentions = parseMentions(from: content)
@@ -1231,7 +1231,7 @@ final class SimplifiedBluetoothService: NSObject {
mentions: mentions.isEmpty ? nil : mentions
)
SecureLogger.log("🔓 Decrypted TLV PM from \(message.sender): \(messageContent.prefix(30))...", category: SecureLogger.session, level: .info)
SecureLogger.log("🔓 Decrypted TLV PM from \(message.sender): \(messageContent.prefix(30))...", category: SecureLogger.session, level: .debug)
// Send on main thread
notifyUI { [weak self] in
@@ -1262,7 +1262,7 @@ final class SimplifiedBluetoothService: NSObject {
guard let messageID = String(data: payloadData, encoding: .utf8) else { return }
SecureLogger.log("📖 Received READ receipt for message \(messageID) from \(peerID)",
category: SecureLogger.session, level: .info)
category: SecureLogger.session, level: .debug)
// Update read status
notifyUI { [weak self] in
@@ -1277,7 +1277,7 @@ final class SimplifiedBluetoothService: NSObject {
// We received an encrypted message before establishing a session with this peer.
// Trigger a handshake so future messages can be decrypted.
SecureLogger.log("🔑 Encrypted message from \(peerID) without session; initiating handshake",
category: SecureLogger.noise, level: .info)
category: SecureLogger.noise, level: .debug)
if !noiseService.hasSession(with: peerID) {
initiateNoiseHandshake(with: peerID)
}
@@ -1308,7 +1308,7 @@ final class SimplifiedBluetoothService: NSObject {
// MARK: - Helper Functions
private func sendLeave() {
SecureLogger.log("👋 Sending leave announcement", category: SecureLogger.session, level: .info)
SecureLogger.log("👋 Sending leave announcement", category: SecureLogger.session, level: .debug)
let packet = BitchatPacket(
type: MessageType.leave.rawValue,
ttl: messageTTL,
@@ -1460,7 +1460,7 @@ final class SimplifiedBluetoothService: NSObject {
if !hasPeripheralConnection && !hasCentralConnection {
// Remove the peer completely (they'll be re-added when they reconnect)
SecureLogger.log("⏱️ Peer timed out (no packets for 20s): \(peerID) (\(peer.nickname))",
category: SecureLogger.session, level: .info)
category: SecureLogger.session, level: .debug)
peers.removeValue(forKey: peerID)
disconnectedPeers.append(peerID)
}
@@ -1584,7 +1584,7 @@ extension SimplifiedBluetoothService: CBCentralManagerDelegate {
// Connect to the peripheral with options for faster connection
SecureLogger.log("📱 Connect: \(advertisedName) [RSSI:\(rssiValue)]",
category: SecureLogger.session, level: .info)
category: SecureLogger.session, level: .debug)
// Use connection options for faster reconnection
let options: [String: Any] = [
@@ -1627,7 +1627,7 @@ extension SimplifiedBluetoothService: CBCentralManagerDelegate {
)
}
SecureLogger.log("✅ Connected: \(peripheral.name ?? "Unknown") [\(peripheralID)]", category: SecureLogger.session, level: .info)
SecureLogger.log("✅ Connected: \(peripheral.name ?? "Unknown") [\(peripheralID)]", category: SecureLogger.session, level: .debug)
// Discover services
peripheral.discoverServices([SimplifiedBluetoothService.serviceUUID])
@@ -1640,7 +1640,7 @@ extension SimplifiedBluetoothService: CBCentralManagerDelegate {
let peerID = peripherals[peripheralID]?.peerID
SecureLogger.log("📱 Disconnect: \(peerID ?? peripheralID)\(error != nil ? " (\(error!.localizedDescription))" : "")",
category: SecureLogger.session, level: .info)
category: SecureLogger.session, level: .debug)
// Clean up references
peripherals.removeValue(forKey: peripheralID)
@@ -1895,13 +1895,13 @@ extension SimplifiedBluetoothService: CBPeripheralManagerDelegate {
return
}
SecureLogger.log("✅ Service added successfully, starting advertising", category: SecureLogger.session, level: .info)
SecureLogger.log("✅ Service added successfully, starting advertising", category: SecureLogger.session, level: .debug)
// Start advertising after service is confirmed added
let adData = buildAdvertisementData()
peripheral.startAdvertising(adData)
SecureLogger.log("📡 Started advertising (LocalName: \((adData[CBAdvertisementDataLocalNameKey] as? String) != nil ? "on" : "off"), ID: \(myPeerID.prefix(8))…)", category: SecureLogger.session, level: .info)
SecureLogger.log("📡 Started advertising (LocalName: \((adData[CBAdvertisementDataLocalNameKey] as? String) != nil ? "on" : "off"), ID: \(myPeerID.prefix(8))…)", category: SecureLogger.session, level: .debug)
}
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
@@ -1913,12 +1913,12 @@ extension SimplifiedBluetoothService: CBPeripheralManagerDelegate {
}
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) {
SecureLogger.log("📤 Central unsubscribed: \(central.identifier.uuidString)", category: SecureLogger.session, level: .info)
SecureLogger.log("📤 Central unsubscribed: \(central.identifier.uuidString)", category: SecureLogger.session, level: .debug)
subscribedCentrals.removeAll { $0.identifier == central.identifier }
// Ensure we're still advertising for other devices to find us
if peripheral.isAdvertising == false {
SecureLogger.log("📡 Restarting advertising after central unsubscribed", category: SecureLogger.session, level: .info)
SecureLogger.log("📡 Restarting advertising after central unsubscribed", category: SecureLogger.session, level: .debug)
peripheral.startAdvertising(buildAdvertisementData())
}
@@ -1948,7 +1948,7 @@ extension SimplifiedBluetoothService: CBPeripheralManagerDelegate {
}
func peripheralManagerIsReady(toUpdateSubscribers peripheral: CBPeripheralManager) {
SecureLogger.log("📤 Peripheral manager ready to send more notifications", category: SecureLogger.session, level: .info)
SecureLogger.log("📤 Peripheral manager ready to send more notifications", category: SecureLogger.session, level: .debug)
// Retry pending notifications now that queue has space
collectionsQueue.async(flags: .barrier) { [weak self] in
@@ -1995,7 +1995,7 @@ extension SimplifiedBluetoothService: CBPeripheralManagerDelegate {
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
// Suppress logs for single write requests to reduce noise
if requests.count > 1 {
SecureLogger.log("📥 Received \(requests.count) write requests from central", category: SecureLogger.session, level: .info)
SecureLogger.log("📥 Received \(requests.count) write requests from central", category: SecureLogger.session, level: .debug)
}
// IMPORTANT: Respond immediately to prevent timeouts!
@@ -2022,7 +2022,7 @@ extension SimplifiedBluetoothService: CBPeripheralManagerDelegate {
let senderID = dataToHexString(packet.senderID)
// Only log non-announce packets
if packet.type != MessageType.announce.rawValue {
SecureLogger.log("📦 Decoded packet type: \(packet.type) from sender: \(senderID)", category: SecureLogger.session, level: .info)
SecureLogger.log("📦 Decoded packet type: \(packet.type) from sender: \(senderID)", category: SecureLogger.session, level: .debug)
}
// Store central in our list if not already there
@@ -2089,7 +2089,7 @@ extension SimplifiedBluetoothService {
}
private func buildAdvertisementData() -> [String: Any] {
var data: [String: Any] = [
let data: [String: Any] = [
CBAdvertisementDataServiceUUIDsKey: [SimplifiedBluetoothService.serviceUUID]
]
// No Local Name for privacy
@@ -2099,6 +2099,96 @@ extension SimplifiedBluetoothService {
// No alias rotation or advertising restarts required.
}
// MARK: - Nostr Embedding Helpers
extension SimplifiedBluetoothService {
/// Build a `bitchat1:` base64url-encoded BitChat packet carrying a private message
/// for transport over Nostr DMs. The payload is a plaintext typed NoisePayload
/// (no inner Noise encryption; NIP-17 provides transport-layer E2E).
func buildNostrEmbeddedPrivateMessageContent(content: String, to recipientPeerID: String, messageID: String) -> String? {
// TLV-encode the private message
let pm = PrivateMessagePacket(messageID: messageID, content: content)
guard let tlv = pm.encode() else { return nil }
// Prefix with NoisePayloadType
var payload = Data([NoisePayloadType.privateMessage.rawValue])
payload.append(tlv)
// Build BitChat packet (noiseEncrypted type used as a typed envelope)
// Determine correct 8-byte recipient ID (peerID) to embed
let recipientIDHex: String = {
if let maybeData = Data(hexString: recipientPeerID) {
if maybeData.count == 32 {
// Treat as Noise static public key; derive peerID from fingerprint
return Self.derivePeerID(fromPublicKey: maybeData)
} else if maybeData.count == 8 {
// Already an 8-byte peer ID
return recipientPeerID
}
}
// Fallback (should not happen): use myPeerID to avoid dropping
return recipientPeerID.count == 16 ? recipientPeerID : myPeerID
}()
let packet = BitchatPacket(
type: MessageType.noiseEncrypted.rawValue,
senderID: hexStringToData(myPeerID),
recipientID: hexStringToData(recipientIDHex),
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: payload,
signature: nil,
ttl: messageTTL
)
guard let data = packet.toBinaryData() else { return nil }
return "bitchat1:" + Self.base64URLEncode(data)
}
/// Build a `bitchat1:` base64url-encoded BitChat packet carrying a delivery/read ack
/// for transport over Nostr DMs. Payload is plaintext typed NoisePayload.
func buildNostrEmbeddedAckContent(type: NoisePayloadType, messageID: String, to recipientPeerID: String) -> String? {
guard type == .delivered || type == .readReceipt else { return nil }
var payload = Data([type.rawValue])
payload.append(Data(messageID.utf8))
// Determine correct 8-byte recipient ID (peerID) to embed
let recipientIDHex: String = {
if let maybeData = Data(hexString: recipientPeerID) {
if maybeData.count == 32 {
return Self.derivePeerID(fromPublicKey: maybeData)
} else if maybeData.count == 8 {
return recipientPeerID
}
}
return recipientPeerID.count == 16 ? recipientPeerID : myPeerID
}()
let packet = BitchatPacket(
type: MessageType.noiseEncrypted.rawValue,
senderID: hexStringToData(myPeerID),
recipientID: hexStringToData(recipientIDHex),
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: payload,
signature: nil,
ttl: messageTTL
)
guard let data = packet.toBinaryData() else { return nil }
return "bitchat1:" + Self.base64URLEncode(data)
}
/// Base64url encode without padding
private static func base64URLEncode(_ data: Data) -> String {
let b64 = data.base64EncodedString()
let urlSafe = b64
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
return urlSafe
}
}
// MARK: - Message Deduplicator
/// Efficient message deduplication with time-based cleanup
+4 -4
View File
@@ -190,7 +190,7 @@ class UnifiedPeerService: ObservableObject {
SecureLogger.log(
"🔄 Found favorite for '\(peerInfo.nickname)' by nickname, updating noise key",
category: SecureLogger.session,
level: .info
level: .debug
)
// Update the favorite's key in persistence
@@ -277,7 +277,7 @@ class UnifiedPeerService: ObservableObject {
// Debug logging to understand the issue
SecureLogger.log("🔍 Toggle favorite - peer.nickname: '\(peer.nickname)', peer.displayName: '\(peer.displayName)', peerID: \(peerID)",
category: SecureLogger.session, level: .info)
category: SecureLogger.session, level: .debug)
if actualNickname.isEmpty {
// Try to get from mesh service's current peer list
@@ -312,7 +312,7 @@ class UnifiedPeerService: ObservableObject {
// Log the final nickname being saved
SecureLogger.log("⭐️ Toggled favorite for '\(finalNickname)' (peerID: \(peerID), was: \(wasFavorite), now: \(!wasFavorite))",
category: SecureLogger.session, level: .info)
category: SecureLogger.session, level: .debug)
// Send favorite notification to the peer
meshService.sendFavoriteNotification(to: peerID, isFavorite: !wasFavorite)
@@ -402,4 +402,4 @@ extension Data {
let hash = SHA256.hash(data: self)
return hash.map { String(format: "%02x", $0) }.joined()
}
}
}