Compare commits

...
9 Commits
19 changed files with 252 additions and 258 deletions
+1 -1
View File
@@ -87,7 +87,7 @@ import Foundation
/// Represents the ephemeral layer of identity - short-lived peer IDs that provide network privacy.
/// These IDs rotate periodically to prevent tracking while maintaining cryptographic relationships.
struct EphemeralIdentity {
let peerID: String // 8 random bytes
let peer: Peer // 8 random bytes
let sessionStart: Date
var handshakeState: HandshakeState
}
@@ -103,7 +103,7 @@ protocol SecureIdentityStateManagerProtocol {
// MARK: Cryptographic Identities
func upsertCryptographicIdentity(fingerprint: String, noisePublicKey: Data, signingPublicKey: Data?, claimedNickname: String?)
func getCryptoIdentitiesByPeerIDPrefix(_ peerID: String) -> [CryptographicIdentity]
func getCryptoIdentitiesByPeerIDPrefix(_ peer: Peer) -> [CryptographicIdentity]
func updateSocialIdentity(_ identity: SocialIdentity)
// MARK: Favorites Management
@@ -121,12 +121,12 @@ protocol SecureIdentityStateManagerProtocol {
func getBlockedNostrPubkeys() -> Set<String>
// MARK: Ephemeral Session Management
func registerEphemeralSession(peerID: String, handshakeState: HandshakeState)
func updateHandshakeState(peerID: String, state: HandshakeState)
func registerEphemeralSession(peer: Peer, handshakeState: HandshakeState)
func updateHandshakeState(peer: Peer, state: HandshakeState)
// MARK: Cleanup
func clearAllIdentityData()
func removeEphemeralSession(peerID: String)
func removeEphemeralSession(peer: Peer)
// MARK: Verification
func setVerified(fingerprint: String, verified: Bool)
@@ -143,7 +143,7 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
private let encryptionKeyName = "identityCacheEncryptionKey"
// In-memory state
private var ephemeralSessions: [String: EphemeralIdentity] = [:]
private var ephemeralSessions: [Peer: EphemeralIdentity] = [:]
private var cryptographicIdentities: [String: CryptographicIdentity] = [:]
private var cache: IdentityCache = IdentityCache()
@@ -321,11 +321,11 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
}
/// Find cryptographic identities whose fingerprint prefix matches a peerID (16-hex) short ID
func getCryptoIdentitiesByPeerIDPrefix(_ peerID: String) -> [CryptographicIdentity] {
func getCryptoIdentitiesByPeerIDPrefix(_ peer: Peer) -> [CryptographicIdentity] {
queue.sync {
// Defensive: ensure hex and correct length
guard peerID.count == 16, peerID.allSatisfy({ $0.isHexDigit }) else { return [] }
return cryptographicIdentities.values.filter { $0.fingerprint.hasPrefix(peerID) }
guard peer.isShort, peer.id.allSatisfy({ $0.isHexDigit }) else { return [] }
return cryptographicIdentities.values.filter { $0.fingerprint.hasPrefix(peer.id) }
}
}
@@ -455,19 +455,19 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
// MARK: - Ephemeral Session Management
func registerEphemeralSession(peerID: String, handshakeState: HandshakeState = .none) {
func registerEphemeralSession(peer: Peer, handshakeState: HandshakeState = .none) {
queue.async(flags: .barrier) {
self.ephemeralSessions[peerID] = EphemeralIdentity(
peerID: peerID,
self.ephemeralSessions[peer] = EphemeralIdentity(
peer: peer,
sessionStart: Date(),
handshakeState: handshakeState
)
}
}
func updateHandshakeState(peerID: String, state: HandshakeState) {
func updateHandshakeState(peer: Peer, state: HandshakeState) {
queue.async(flags: .barrier) {
self.ephemeralSessions[peerID]?.handshakeState = state
self.ephemeralSessions[peer]?.handshakeState = state
// If handshake completed, update last interaction
if case .completed(let fingerprint) = state {
@@ -493,9 +493,9 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
}
}
func removeEphemeralSession(peerID: String) {
func removeEphemeralSession(peer: Peer) {
queue.async(flags: .barrier) {
self.ephemeralSessions.removeValue(forKey: peerID)
self.ephemeralSessions.removeValue(forKey: peer)
}
}
+64
View File
@@ -7,6 +7,7 @@
//
import Foundation
import struct CryptoKit.SHA256
struct Peer: Equatable, Hashable {
let id: String
@@ -26,6 +27,50 @@ extension Peer {
}
}
// MARK: - Validation
extension Peer {
private enum Constants {
static let maxIDLength = 64
static let hexIDLength = 16 // 8 bytes = 16 hex chars
}
/// Validates a peer ID from any source (short 16-hex, full 64-hex, or internal alnum/-/_ up to 64)
var isValid: Bool {
// Accept short routing IDs (exact 16-hex) or Full Noise key hex (exact 64-hex)
if isShort || isNoiseKeyHex {
return true
}
// If length equals short or full but isn't valid hex, reject
if id.count == Constants.hexIDLength || id.count == Constants.maxIDLength {
return false
}
// Internal format: alphanumeric + dash/underscore up to 63 (not 16 or 64)
let validCharset = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-_"))
return !id.isEmpty &&
id.count < Constants.maxIDLength &&
id.rangeOfCharacter(from: validCharset.inverted) == nil
}
/// Short routing IDs (exact 16-hex)
var isShort: Bool {
id.count == Constants.hexIDLength && Data(hexString: id) != nil
}
/// Full Noise key hex (exact 64-hex)
var isNoiseKeyHex: Bool {
noiseKey != nil
}
/// Full Noise key (exact 64-hex) as Data
var noiseKey: Data? {
guard id.count == Constants.maxIDLength else { return nil }
return Data(hexString: id)
}
}
// MARK: - ExpressibleByStringLiteral
extension Peer: ExpressibleByStringLiteral {
@@ -73,3 +118,22 @@ extension Peer {
self.init(str: str)
}
}
// MARK: - Noise Public Key Helpers
extension Peer {
/// Derive the stable 16-hex peer ID from a Noise static public key
init(publicKey: Data) {
let digest = SHA256.hash(data: publicKey)
let hex = digest.map { String(format: "%02x", $0) }.joined()
self.init(str: hex.prefix(16))
}
/// Returns a 16-hex short peer ID derived from a 64-hex Noise public key if needed
func toShort() -> Peer {
if id.count == Constants.maxIDLength, let data = Data(hexString: id) {
return Peer(publicKey: data)
}
return self
}
}
+1 -1
View File
@@ -79,7 +79,7 @@ struct ReadReceipt: Codable {
guard let readerIDData = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil }
let readerID = readerIDData.hexEncodedString()
guard InputValidator.validatePeerID(readerID) else { return nil }
guard Peer(str: readerID).isValid else { return nil }
guard let timestamp = dataCopy.readDate(at: &offset),
InputValidator.validateTimestamp(timestamp),
+13 -18
View File
@@ -53,11 +53,6 @@ struct NoiseSecurityValidator {
static func validateHandshakeMessageSize(_ data: Data) -> Bool {
return data.count <= NoiseSecurityConstants.maxHandshakeMessageSize
}
/// Validate peer ID format using unified validator
static func validatePeerID(_ peerID: String) -> Bool {
return InputValidator.validatePeerID(peerID)
}
}
// MARK: - Enhanced Noise Session with Security
@@ -137,8 +132,8 @@ final class SecureNoiseSession: NoiseSession {
// MARK: - Rate Limiter
final class NoiseRateLimiter {
private var handshakeTimestamps: [String: [Date]] = [:] // peerID -> timestamps
private var messageTimestamps: [String: [Date]] = [:] // peerID -> timestamps
private var handshakeTimestamps: [Peer: [Date]] = [:] // Peer -> timestamps
private var messageTimestamps: [Peer: [Date]] = [:] // Peer -> timestamps
// Global rate limiting
private var globalHandshakeTimestamps: [Date] = []
@@ -146,7 +141,7 @@ final class NoiseRateLimiter {
private let queue = DispatchQueue(label: "chat.bitchat.noise.ratelimit", attributes: .concurrent)
func allowHandshake(from peerID: String) -> Bool {
func allowHandshake(from peer: Peer) -> Bool {
return queue.sync(flags: .barrier) {
let now = Date()
let oneMinuteAgo = now.addingTimeInterval(-60)
@@ -159,23 +154,23 @@ final class NoiseRateLimiter {
}
// Check per-peer rate limit
var timestamps = handshakeTimestamps[peerID] ?? []
var timestamps = handshakeTimestamps[peer] ?? []
timestamps = timestamps.filter { $0 > oneMinuteAgo }
if timestamps.count >= NoiseSecurityConstants.maxHandshakesPerMinute {
SecureLogger.warning("Per-peer handshake rate limit exceeded for \(peerID): \(timestamps.count)/\(NoiseSecurityConstants.maxHandshakesPerMinute) per minute", category: .security)
SecureLogger.warning("Per-peer handshake rate limit exceeded for \(peer.id): \(timestamps.count)/\(NoiseSecurityConstants.maxHandshakesPerMinute) per minute", category: .security)
return false
}
// Record new handshake
timestamps.append(now)
handshakeTimestamps[peerID] = timestamps
handshakeTimestamps[peer] = timestamps
globalHandshakeTimestamps.append(now)
return true
}
}
func allowMessage(from peerID: String) -> Bool {
func allowMessage(from peer: Peer) -> Bool {
return queue.sync(flags: .barrier) {
let now = Date()
let oneSecondAgo = now.addingTimeInterval(-1)
@@ -188,26 +183,26 @@ final class NoiseRateLimiter {
}
// Check per-peer rate limit
var timestamps = messageTimestamps[peerID] ?? []
var timestamps = messageTimestamps[peer] ?? []
timestamps = timestamps.filter { $0 > oneSecondAgo }
if timestamps.count >= NoiseSecurityConstants.maxMessagesPerSecond {
SecureLogger.warning("Per-peer message rate limit exceeded for \(peerID): \(timestamps.count)/\(NoiseSecurityConstants.maxMessagesPerSecond) per second", category: .security)
SecureLogger.warning("Per-peer message rate limit exceeded for \(peer.id): \(timestamps.count)/\(NoiseSecurityConstants.maxMessagesPerSecond) per second", category: .security)
return false
}
// Record new message
timestamps.append(now)
messageTimestamps[peerID] = timestamps
messageTimestamps[peer] = timestamps
globalMessageTimestamps.append(now)
return true
}
}
func reset(for peerID: String) {
func reset(for peer: Peer) {
queue.async(flags: .barrier) {
self.handshakeTimestamps.removeValue(forKey: peerID)
self.messageTimestamps.removeValue(forKey: peerID)
self.handshakeTimestamps.removeValue(forKey: peer)
self.messageTimestamps.removeValue(forKey: peer)
}
}
+1 -1
View File
@@ -100,7 +100,7 @@ struct NostrEmbeddedBitChat {
if let maybeData = Data(hexString: recipientPeerID) {
if maybeData.count == 32 {
// Treat as Noise static public key; derive peerID from fingerprint
return PeerIDUtils.derivePeerID(fromPublicKey: maybeData)
return Peer(publicKey: maybeData).id
} else if maybeData.count == 8 {
// Already an 8-byte peer ID
return recipientPeerID
-14
View File
@@ -1,14 +0,0 @@
import Foundation
import CryptoKit
// MARK: - Peer ID Utilities
struct PeerIDUtils {
/// Derive the stable 16-hex peer ID from a Noise static public key
static func derivePeerID(fromPublicKey publicKey: Data) -> String {
let digest = SHA256.hash(data: publicKey)
let hex = digest.map { String(format: "%02x", $0) }.joined()
return String(hex.prefix(16))
}
}
+25 -34
View File
@@ -556,23 +556,13 @@ final class BLEService: NSObject {
func isPeerConnected(_ peerID: String) -> Bool {
// Accept both 16-hex short IDs and 64-hex Noise keys
let shortID: String = {
if peerID.count == 64, let key = Data(hexString: peerID) {
return PeerIDUtils.derivePeerID(fromPublicKey: key)
}
return peerID
}()
let shortID = Peer(str: peerID).toShort().id
return collectionsQueue.sync { peers[shortID]?.isConnected ?? false }
}
func isPeerReachable(_ peerID: String) -> Bool {
// Accept both 16-hex short IDs and 64-hex Noise keys
let shortID: String = {
if peerID.count == 64, let key = Data(hexString: peerID) {
return PeerIDUtils.derivePeerID(fromPublicKey: key)
}
return peerID
}()
let shortID = Peer(str: peerID).toShort().id
return collectionsQueue.sync {
// Must be mesh-attached: at least one live direct link to the mesh
let meshAttached = peers.values.contains { $0.isConnected }
@@ -626,10 +616,11 @@ final class BLEService: NSObject {
var payload = Data([NoisePayloadType.readReceipt.rawValue])
payload.append(contentsOf: receipt.originalMessageID.utf8)
if noiseService.hasEstablishedSession(with: peerID) {
let peer = Peer(str: peerID)
if noiseService.hasEstablishedSession(with: peer) {
SecureLogger.debug("📤 Sending READ receipt for message \(receipt.originalMessageID) to \(peerID)", category: .session)
do {
let encrypted = try noiseService.encrypt(payload, for: peerID)
let encrypted = try noiseService.encrypt(payload, for: peer)
let packet = BitchatPacket(
type: MessageType.noiseEncrypted.rawValue,
senderID: myPeerIDData,
@@ -653,7 +644,7 @@ final class BLEService: NSObject {
guard let self = self else { return }
self.pendingNoisePayloadsAfterHandshake[peerID, default: []].append(payload)
}
if !noiseService.hasSession(with: peerID) { initiateNoiseHandshake(with: peerID) }
if !noiseService.hasSession(with: peer) { initiateNoiseHandshake(with: peerID) }
SecureLogger.debug("🕒 Queued READ receipt for \(peerID) until handshake completes", category: .session)
}
}
@@ -674,13 +665,13 @@ final class BLEService: NSObject {
}
private func sendNoisePayload(_ typedPayload: Data, to peerID: String) {
guard noiseService.hasSession(with: peerID) else {
guard noiseService.hasSession(with: Peer(str: peerID)) else {
// Lazy-handshake path: queue? For now, initiate handshake and drop
initiateNoiseHandshake(with: peerID)
return
}
do {
let encrypted = try noiseService.encrypt(typedPayload, for: peerID)
let encrypted = try noiseService.encrypt(typedPayload, for: Peer(str: peerID))
let packet = BitchatPacket(
type: MessageType.noiseEncrypted.rawValue,
senderID: myPeerIDData,
@@ -715,9 +706,9 @@ final class BLEService: NSObject {
func getNoiseSessionState(for peerID: String) -> LazyHandshakeState {
if noiseService.hasEstablishedSession(with: peerID) {
if noiseService.hasEstablishedSession(with: Peer(str: peerID)) {
return .established
} else if noiseService.hasSession(with: peerID) {
} else if noiseService.hasSession(with: Peer(str: peerID)) {
return .handshaking
} else {
return .none
@@ -848,7 +839,7 @@ final class BLEService: NSObject {
SecureLogger.debug("📨 Sending PM to \(recipientID): \(content.prefix(30))...", category: .session)
// Check if we have an established Noise session
if noiseService.hasEstablishedSession(with: recipientID) {
if noiseService.hasEstablishedSession(with: Peer(str: recipientID)) {
// Encrypt and send
do {
// Create TLV-encoded private message
@@ -862,7 +853,7 @@ final class BLEService: NSObject {
var messagePayload = Data([NoisePayloadType.privateMessage.rawValue])
messagePayload.append(tlvData)
let encrypted = try noiseService.encrypt(messagePayload, for: recipientID)
let encrypted = try noiseService.encrypt(messagePayload, for: Peer(str: recipientID))
// Convert recipientID to Data (assuming it's a hex string)
var recipientData = Data()
@@ -928,10 +919,10 @@ final class BLEService: NSObject {
private func initiateNoiseHandshake(with peerID: String) {
// Use NoiseEncryptionService for handshake
guard !noiseService.hasSession(with: peerID) else { return }
guard !noiseService.hasSession(with: Peer(str: peerID)) else { return }
do {
let handshakeData = try noiseService.initiateHandshake(with: peerID)
let handshakeData = try noiseService.initiateHandshake(with: Peer(str: peerID))
// Send handshake init
let packet = BitchatPacket(
@@ -981,7 +972,7 @@ final class BLEService: NSObject {
var messagePayload = Data([NoisePayloadType.privateMessage.rawValue])
messagePayload.append(tlvData)
let encrypted = try noiseService.encrypt(messagePayload, for: peerID)
let encrypted = try noiseService.encrypt(messagePayload, for: Peer(str: peerID))
let packet = BitchatPacket(
type: MessageType.noiseEncrypted.rawValue,
@@ -1518,7 +1509,7 @@ final class BLEService: NSObject {
// Verify that the sender's derived ID from the announced noise public key matches the packet senderID
// This helps detect relayed or spoofed announces. Only warn in release; assert in debug.
let derivedFromKey = PeerIDUtils.derivePeerID(fromPublicKey: announcement.noisePublicKey)
let derivedFromKey = Peer(publicKey: announcement.noisePublicKey).id
if derivedFromKey != peerID {
SecureLogger.warning("⚠️ Announce sender mismatch: derived \(derivedFromKey.prefix(8))… vs packet \(peerID.prefix(8))", category: .security)
@@ -1730,7 +1721,7 @@ final class BLEService: NSObject {
// Fallback: verify signature using persisted signing key for this peerID's fingerprint prefix
if let signature = packet.signature, let packetData = packet.toBinaryDataForSigning() {
// Find candidate identities by peerID prefix (16 hex)
let candidates = identityManager.getCryptoIdentitiesByPeerIDPrefix(peerID)
let candidates = identityManager.getCryptoIdentitiesByPeerIDPrefix(Peer(str: peerID))
for candidate in candidates {
if let signingKey = candidate.signingPublicKey,
noiseService.verifySignature(signature, for: packetData, publicKey: signingKey) {
@@ -1795,7 +1786,7 @@ final class BLEService: NSObject {
recipientID.hexEncodedString() == myPeerID {
// Handshake is for us
do {
if let response = try noiseService.processHandshakeMessage(from: peerID, message: packet.payload) {
if let response = try noiseService.processHandshakeMessage(from: Peer(str: peerID), message: packet.payload) {
// Send response
let responsePacket = BitchatPacket(
type: MessageType.noiseHandshake.rawValue,
@@ -1815,7 +1806,7 @@ final class BLEService: NSObject {
} catch {
SecureLogger.error("Failed to process handshake: \(error)")
// Try initiating a new handshake
if !noiseService.hasSession(with: peerID) {
if !noiseService.hasSession(with: Peer(str: peerID)) {
initiateNoiseHandshake(with: peerID)
}
}
@@ -1840,7 +1831,7 @@ final class BLEService: NSObject {
updatePeerLastSeen(peerID)
do {
let decrypted = try noiseService.decrypt(packet.payload, from: peerID)
let decrypted = try noiseService.decrypt(packet.payload, from: Peer(str: peerID))
guard decrypted.count > 0 else { return }
// First byte indicates the payload type
@@ -1880,7 +1871,7 @@ final class BLEService: NSObject {
// We received an encrypted message before establishing a session with this peer.
// Trigger a handshake so future messages can be decrypted.
SecureLogger.debug("🔑 Encrypted message from \(peerID) without session; initiating handshake")
if !noiseService.hasSession(with: peerID) {
if !noiseService.hasSession(with: Peer(str: peerID)) {
initiateNoiseHandshake(with: peerID)
}
} catch {
@@ -1985,9 +1976,9 @@ final class BLEService: NSObject {
var payload = Data([NoisePayloadType.delivered.rawValue])
payload.append(contentsOf: messageID.utf8)
if noiseService.hasEstablishedSession(with: peerID) {
if noiseService.hasEstablishedSession(with: Peer(str: peerID)) {
do {
let encrypted = try noiseService.encrypt(payload, for: peerID)
let encrypted = try noiseService.encrypt(payload, for: Peer(str: peerID))
let packet = BitchatPacket(
type: MessageType.noiseEncrypted.rawValue,
senderID: myPeerIDData,
@@ -2007,7 +1998,7 @@ final class BLEService: NSObject {
guard let self = self else { return }
self.pendingNoisePayloadsAfterHandshake[peerID, default: []].append(payload)
}
if !noiseService.hasSession(with: peerID) { initiateNoiseHandshake(with: peerID) }
if !noiseService.hasSession(with: Peer(str: peerID)) { initiateNoiseHandshake(with: peerID) }
SecureLogger.debug("🕒 Queued DELIVERED ack for \(peerID) until handshake completes", category: .session)
}
}
@@ -2022,7 +2013,7 @@ final class BLEService: NSObject {
SecureLogger.debug("📤 Sending \(payloads.count) pending noise payloads to \(peerID) after handshake", category: .session)
for payload in payloads {
do {
let encrypted = try noiseService.encrypt(payload, for: peerID)
let encrypted = try noiseService.encrypt(payload, for: Peer(str: peerID))
let packet = BitchatPacket(
type: MessageType.noiseEncrypted.rawValue,
senderID: myPeerIDData,
@@ -179,12 +179,11 @@ final class FavoritesPersistenceService: ObservableObject {
/// Resolve favorite status by short peer ID (16-hex derived from Noise pubkey)
/// Falls back to scanning favorites and matching on derived peer ID.
func getFavoriteStatus(forPeerID peerID: String) -> FavoriteRelationship? {
// Quick sanity: peerID should be 16 hex chars (8 bytes)
guard peerID.count == 16 else { return nil }
for (pubkey, rel) in favorites {
let derived = PeerIDUtils.derivePeerID(fromPublicKey: pubkey)
if derived == peerID { return rel }
func getFavoriteStatus(for peer: Peer) -> FavoriteRelationship? {
// Quick sanity: peer.id should be 16 hex chars (8 bytes)
guard peer.isShort else { return nil }
for (pubkey, rel) in favorites where Peer(publicKey: pubkey) == peer {
return rel
}
return nil
}
+45 -45
View File
@@ -6,7 +6,7 @@ import Foundation
final class MessageRouter {
private let mesh: Transport
private let nostr: NostrTransport
private var outbox: [String: [(content: String, nickname: String, messageID: String)]] = [:] // peerID -> queued messages
private var outbox: [Peer: [(content: String, nickname: String, messageID: String)]] = [:] // Peer -> queued messages
init(mesh: Transport, nostr: NostrTransport) {
self.mesh = mesh
@@ -21,80 +21,80 @@ final class MessageRouter {
) { [weak self] note in
guard let self = self else { return }
if let data = note.userInfo?["peerPublicKey"] as? Data {
let peerID = PeerIDUtils.derivePeerID(fromPublicKey: data)
let peer = Peer(publicKey: data)
Task { @MainActor in
self.flushOutbox(for: peerID)
self.flushOutbox(for: peer)
}
}
// Handle key updates
if let newKey = note.userInfo?["peerPublicKey"] as? Data,
let _ = note.userInfo?["isKeyUpdate"] as? Bool {
let peerID = PeerIDUtils.derivePeerID(fromPublicKey: newKey)
let peer = Peer(publicKey: newKey)
Task { @MainActor in
self.flushOutbox(for: peerID)
self.flushOutbox(for: peer)
}
}
}
}
func sendPrivate(_ content: String, to peerID: String, recipientNickname: String, messageID: String) {
let reachableMesh = mesh.isPeerReachable(peerID)
func sendPrivate(_ content: String, to peer: Peer, recipientNickname: String, messageID: String) {
let reachableMesh = mesh.isPeerReachable(peer.id)
if reachableMesh {
SecureLogger.debug("Routing PM via mesh (reachable) to \(peerID.prefix(8))… id=\(messageID.prefix(8))", category: .session)
SecureLogger.debug("Routing PM via mesh (reachable) to \(peer.id.prefix(8))… id=\(messageID.prefix(8))", category: .session)
// BLEService will initiate a handshake if needed and queue the message
mesh.sendPrivateMessage(content, to: peerID, recipientNickname: recipientNickname, messageID: messageID)
} else if canSendViaNostr(peerID: peerID) {
SecureLogger.debug("Routing PM via Nostr to \(peerID.prefix(8))… id=\(messageID.prefix(8))", category: .session)
nostr.sendPrivateMessage(content, to: peerID, recipientNickname: recipientNickname, messageID: messageID)
mesh.sendPrivateMessage(content, to: peer.id, recipientNickname: recipientNickname, messageID: messageID)
} else if canSendViaNostr(peer: peer) {
SecureLogger.debug("Routing PM via Nostr to \(peer.id.prefix(8))… id=\(messageID.prefix(8))", category: .session)
nostr.sendPrivateMessage(content, to: peer.id, recipientNickname: recipientNickname, messageID: messageID)
} else {
// Queue for later (when mesh connects or Nostr mapping appears)
if outbox[peerID] == nil { outbox[peerID] = [] }
outbox[peerID]?.append((content, recipientNickname, messageID))
SecureLogger.debug("Queued PM for \(peerID.prefix(8))… (no mesh, no Nostr mapping) id=\(messageID.prefix(8))", category: .session)
if outbox[peer] == nil { outbox[peer] = [] }
outbox[peer]?.append((content, recipientNickname, messageID))
SecureLogger.debug("Queued PM for \(peer.id.prefix(8))… (no mesh, no Nostr mapping) id=\(messageID.prefix(8))", category: .session)
}
}
func sendReadReceipt(_ receipt: ReadReceipt, to peerID: String) {
func sendReadReceipt(_ receipt: ReadReceipt, to peer: Peer) {
// Prefer mesh for reachable peers; BLE will queue if handshake is needed
if mesh.isPeerReachable(peerID) {
SecureLogger.debug("Routing READ ack via mesh (reachable) to \(peerID.prefix(8))… id=\(receipt.originalMessageID.prefix(8))", category: .session)
mesh.sendReadReceipt(receipt, to: peerID)
if mesh.isPeerReachable(peer.id) {
SecureLogger.debug("Routing READ ack via mesh (reachable) to \(peer.id.prefix(8))… id=\(receipt.originalMessageID.prefix(8))", category: .session)
mesh.sendReadReceipt(receipt, to: peer.id)
} else {
SecureLogger.debug("Routing READ ack via Nostr to \(peerID.prefix(8))… id=\(receipt.originalMessageID.prefix(8))", category: .session)
nostr.sendReadReceipt(receipt, to: peerID)
SecureLogger.debug("Routing READ ack via Nostr to \(peer.id.prefix(8))… id=\(receipt.originalMessageID.prefix(8))", category: .session)
nostr.sendReadReceipt(receipt, to: peer.id)
}
}
func sendDeliveryAck(_ messageID: String, to peerID: String) {
if mesh.isPeerReachable(peerID) {
SecureLogger.debug("Routing DELIVERED ack via mesh (reachable) to \(peerID.prefix(8))… id=\(messageID.prefix(8))", category: .session)
mesh.sendDeliveryAck(for: messageID, to: peerID)
func sendDeliveryAck(_ messageID: String, to peer: Peer) {
if mesh.isPeerReachable(peer.id) {
SecureLogger.debug("Routing DELIVERED ack via mesh (reachable) to \(peer.id.prefix(8))… id=\(messageID.prefix(8))", category: .session)
mesh.sendDeliveryAck(for: messageID, to: peer.id)
} else {
nostr.sendDeliveryAck(for: messageID, to: peerID)
nostr.sendDeliveryAck(for: messageID, to: peer.id)
}
}
func sendFavoriteNotification(to peerID: String, isFavorite: Bool) {
func sendFavoriteNotification(to peer: Peer, isFavorite: Bool) {
// Route via mesh when connected; else use Nostr
if mesh.isPeerConnected(peerID) {
mesh.sendFavoriteNotification(to: peerID, isFavorite: isFavorite)
if mesh.isPeerConnected(peer.id) {
mesh.sendFavoriteNotification(to: peer.id, isFavorite: isFavorite)
} else {
nostr.sendFavoriteNotification(to: peerID, isFavorite: isFavorite)
nostr.sendFavoriteNotification(to: peer.id, isFavorite: isFavorite)
}
}
// MARK: - Outbox Management
private func canSendViaNostr(peerID: String) -> Bool {
private func canSendViaNostr(peer: Peer) -> Bool {
// Two forms are supported:
// - 64-hex Noise public key (32 bytes)
// - 16-hex short peer ID (derived from Noise pubkey)
if peerID.count == 64, let noiseKey = Data(hexString: peerID) {
if let noiseKey = peer.noiseKey {
if let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey),
fav.peerNostrPublicKey != nil {
return true
}
} else if peerID.count == 16 {
if let fav = FavoritesPersistenceService.shared.getFavoriteStatus(forPeerID: peerID),
} else if peer.isShort {
if let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: peer),
fav.peerNostrPublicKey != nil {
return true
}
@@ -102,18 +102,18 @@ final class MessageRouter {
return false
}
func flushOutbox(for peerID: String) {
guard let queued = outbox[peerID], !queued.isEmpty else { return }
SecureLogger.debug("Flushing outbox for \(peerID.prefix(8))… count=\(queued.count)", category: .session)
func flushOutbox(for peer: Peer) {
guard let queued = outbox[peer], !queued.isEmpty else { return }
SecureLogger.debug("Flushing outbox for \(peer.id.prefix(8))… count=\(queued.count)", category: .session)
var remaining: [(content: String, nickname: String, messageID: String)] = []
// Prefer mesh if connected; else try Nostr if mapping exists
for (content, nickname, messageID) in queued {
if mesh.isPeerReachable(peerID) {
SecureLogger.debug("Outbox -> mesh for \(peerID.prefix(8))… id=\(messageID.prefix(8))", category: .session)
mesh.sendPrivateMessage(content, to: peerID, recipientNickname: nickname, messageID: messageID)
} else if canSendViaNostr(peerID: peerID) {
SecureLogger.debug("Outbox -> Nostr for \(peerID.prefix(8))… id=\(messageID.prefix(8))", category: .session)
nostr.sendPrivateMessage(content, to: peerID, recipientNickname: nickname, messageID: messageID)
if mesh.isPeerReachable(peer.id) {
SecureLogger.debug("Outbox -> mesh for \(peer.id.prefix(8))… id=\(messageID.prefix(8))", category: .session)
mesh.sendPrivateMessage(content, to: peer.id, recipientNickname: nickname, messageID: messageID)
} else if canSendViaNostr(peer: peer) {
SecureLogger.debug("Outbox -> Nostr for \(peer.id.prefix(8))… id=\(messageID.prefix(8))", category: .session)
nostr.sendPrivateMessage(content, to: peer.id, recipientNickname: nickname, messageID: messageID)
} else {
// Keep unsent items queued
remaining.append((content, nickname, messageID))
@@ -121,9 +121,9 @@ final class MessageRouter {
}
// Persist only items we could not send
if remaining.isEmpty {
outbox.removeValue(forKey: peerID)
outbox.removeValue(forKey: peer)
} else {
outbox[peerID] = remaining
outbox[peer] = remaining
}
}
+48 -48
View File
@@ -148,8 +148,8 @@ final class NoiseEncryptionService {
private let sessionManager: NoiseSessionManager
// Peer fingerprints (SHA256 hash of static public key)
private var peerFingerprints: [String: String] = [:] // peerID -> fingerprint
private var fingerprintToPeerID: [String: String] = [:] // fingerprint -> peerID
private var peerFingerprints: [Peer: String] = [:] // Peer -> fingerprint
private var fingerprintToPeer: [String: Peer] = [:] // fingerprint -> Peer
// Thread safety
private let serviceQueue = DispatchQueue(label: "chat.bitchat.noise.service", attributes: .concurrent)
@@ -237,7 +237,7 @@ final class NoiseEncryptionService {
// Set up session callbacks
sessionManager.onSessionEstablished = { [weak self] peerID, remoteStaticKey in
self?.handleSessionEstablished(peerID: peerID, remoteStaticKey: remoteStaticKey)
self?.handleSessionEstablished(peer: Peer(str: peerID), remoteStaticKey: remoteStaticKey)
}
// Start session maintenance timer
@@ -263,8 +263,8 @@ final class NoiseEncryptionService {
}
/// Get peer's public key data
func getPeerPublicKeyData(_ peerID: String) -> Data? {
return sessionManager.getRemoteStaticKey(for: peerID)?.rawRepresentation
func getPeerPublicKeyData(_ peer: Peer) -> Data? {
return sessionManager.getRemoteStaticKey(for: peer.id)?.rawRepresentation
}
/// Clear persistent identity (for panic mode)
@@ -391,52 +391,52 @@ final class NoiseEncryptionService {
// MARK: - Handshake Management
/// Initiate a Noise handshake with a peer
func initiateHandshake(with peerID: String) throws -> Data {
func initiateHandshake(with peer: Peer) throws -> Data {
// Validate peer ID
guard NoiseSecurityValidator.validatePeerID(peerID) else {
SecureLogger.warning(.authenticationFailed(peerID: peerID))
guard peer.isValid else {
SecureLogger.warning(.authenticationFailed(peerID: peer.id))
throw NoiseSecurityError.invalidPeerID
}
// Check rate limit
guard rateLimiter.allowHandshake(from: peerID) else {
SecureLogger.warning(.authenticationFailed(peerID: "Rate limited: \(peerID)"))
guard rateLimiter.allowHandshake(from: peer) else {
SecureLogger.warning(.authenticationFailed(peerID: "Rate limited: \(peer.id)"))
throw NoiseSecurityError.rateLimitExceeded
}
SecureLogger.info(.handshakeStarted(peerID: peerID))
SecureLogger.info(.handshakeStarted(peerID: peer.id))
// Return raw handshake data without wrapper
// The Noise protocol handles its own message format
let handshakeData = try sessionManager.initiateHandshake(with: peerID)
let handshakeData = try sessionManager.initiateHandshake(with: peer.id)
return handshakeData
}
/// Process an incoming handshake message
func processHandshakeMessage(from peerID: String, message: Data) throws -> Data? {
func processHandshakeMessage(from peer: Peer, message: Data) throws -> Data? {
// Validate peer ID
guard NoiseSecurityValidator.validatePeerID(peerID) else {
SecureLogger.warning(.authenticationFailed(peerID: peerID))
guard peer.isValid else {
SecureLogger.warning(.authenticationFailed(peerID: peer.id))
throw NoiseSecurityError.invalidPeerID
}
// Validate message size
guard NoiseSecurityValidator.validateHandshakeMessageSize(message) else {
SecureLogger.warning(.handshakeFailed(peerID: peerID, error: "Message too large"))
SecureLogger.warning(.handshakeFailed(peerID: peer.id, error: "Message too large"))
throw NoiseSecurityError.messageTooLarge
}
// Check rate limit
guard rateLimiter.allowHandshake(from: peerID) else {
SecureLogger.warning(.authenticationFailed(peerID: "Rate limited: \(peerID)"))
guard rateLimiter.allowHandshake(from: peer) else {
SecureLogger.warning(.authenticationFailed(peerID: "Rate limited: \(peer.id)"))
throw NoiseSecurityError.rateLimitExceeded
}
// For handshakes, we process the raw data directly without NoiseMessage wrapper
// The Noise protocol handles its own message format
let responsePayload = try sessionManager.handleIncomingHandshake(from: peerID, message: message)
let responsePayload = try sessionManager.handleIncomingHandshake(from: peer.id, message: message)
// Return raw response without wrapper
@@ -444,117 +444,117 @@ final class NoiseEncryptionService {
}
/// Check if we have an established session with a peer
func hasEstablishedSession(with peerID: String) -> Bool {
return sessionManager.getSession(for: peerID)?.isEstablished() ?? false
func hasEstablishedSession(with peer: Peer) -> Bool {
return sessionManager.getSession(for: peer.id)?.isEstablished() ?? false
}
/// Check if we have a session (established or handshaking) with a peer
func hasSession(with peerID: String) -> Bool {
return sessionManager.getSession(for: peerID) != nil
func hasSession(with peer: Peer) -> Bool {
return sessionManager.getSession(for: peer.id) != nil
}
// MARK: - Encryption/Decryption
/// Encrypt data for a specific peer
func encrypt(_ data: Data, for peerID: String) throws -> Data {
func encrypt(_ data: Data, for peer: Peer) throws -> Data {
// Validate message size
guard NoiseSecurityValidator.validateMessageSize(data) else {
throw NoiseSecurityError.messageTooLarge
}
// Check rate limit
guard rateLimiter.allowMessage(from: peerID) else {
guard rateLimiter.allowMessage(from: peer) else {
throw NoiseSecurityError.rateLimitExceeded
}
// Check if we have an established session
guard hasEstablishedSession(with: peerID) else {
guard hasEstablishedSession(with: peer) else {
// Signal that handshake is needed
onHandshakeRequired?(peerID)
onHandshakeRequired?(peer.id)
throw NoiseEncryptionError.handshakeRequired
}
return try sessionManager.encrypt(data, for: peerID)
return try sessionManager.encrypt(data, for: peer.id)
}
/// Decrypt data from a specific peer
func decrypt(_ data: Data, from peerID: String) throws -> Data {
func decrypt(_ data: Data, from peer: Peer) throws -> Data {
// Validate message size
guard NoiseSecurityValidator.validateMessageSize(data) else {
throw NoiseSecurityError.messageTooLarge
}
// Check rate limit
guard rateLimiter.allowMessage(from: peerID) else {
guard rateLimiter.allowMessage(from: peer) else {
throw NoiseSecurityError.rateLimitExceeded
}
// Check if we have an established session
guard hasEstablishedSession(with: peerID) else {
guard hasEstablishedSession(with: peer) else {
throw NoiseEncryptionError.sessionNotEstablished
}
return try sessionManager.decrypt(data, from: peerID)
return try sessionManager.decrypt(data, from: peer.id)
}
// MARK: - Peer Management
/// Get fingerprint for a peer
func getPeerFingerprint(_ peerID: String) -> String? {
func getPeerFingerprint(_ peer: Peer) -> String? {
return serviceQueue.sync {
return peerFingerprints[peerID]
return peerFingerprints[peer]
}
}
/// Get peer ID for a fingerprint
func getPeerID(for fingerprint: String) -> String? {
func getPeer(for fingerprint: String) -> Peer? {
return serviceQueue.sync {
return fingerprintToPeerID[fingerprint]
return fingerprintToPeer[fingerprint]
}
}
/// Remove a peer session
func removePeer(_ peerID: String) {
sessionManager.removeSession(for: peerID)
func removePeer(_ peer: Peer) {
sessionManager.removeSession(for: peer.id)
serviceQueue.sync(flags: .barrier) {
if let fingerprint = peerFingerprints[peerID] {
fingerprintToPeerID.removeValue(forKey: fingerprint)
if let fingerprint = peerFingerprints[peer] {
fingerprintToPeer.removeValue(forKey: fingerprint)
}
peerFingerprints.removeValue(forKey: peerID)
peerFingerprints.removeValue(forKey: peer)
}
SecureLogger.info(.sessionExpired(peerID: peerID))
SecureLogger.info(.sessionExpired(peerID: peer.id))
}
func clearEphemeralStateForPanic() {
sessionManager.removeAllSessions()
serviceQueue.sync(flags: .barrier) {
peerFingerprints.removeAll()
fingerprintToPeerID.removeAll()
fingerprintToPeer.removeAll()
}
rateLimiter.resetAll()
}
// MARK: - Private Helpers
private func handleSessionEstablished(peerID: String, remoteStaticKey: Curve25519.KeyAgreement.PublicKey) {
private func handleSessionEstablished(peer: Peer, remoteStaticKey: Curve25519.KeyAgreement.PublicKey) {
// Calculate fingerprint
let fingerprint = calculateFingerprint(for: remoteStaticKey)
// Store fingerprint mapping
serviceQueue.sync(flags: .barrier) {
peerFingerprints[peerID] = fingerprint
fingerprintToPeerID[fingerprint] = peerID
peerFingerprints[peer] = fingerprint
fingerprintToPeer[fingerprint] = peer
}
// Log security event
SecureLogger.info(.handshakeCompleted(peerID: peerID))
SecureLogger.info(.handshakeCompleted(peerID: peer.id))
// Notify all handlers about authentication
serviceQueue.async { [weak self] in
self?.onPeerAuthenticatedHandlers.forEach { handler in
handler(peerID, fingerprint)
handler(peer.id, fingerprint)
}
}
}
+1 -1
View File
@@ -174,7 +174,7 @@ final class NostrTransport: Transport {
return npub
}
if peerID.count == 16,
let fav = FavoritesPersistenceService.shared.getFavoriteStatus(forPeerID: peerID),
let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: Peer(str: peerID)),
let npub = fav.peerNostrPublicKey {
return npub
}
+2 -2
View File
@@ -61,11 +61,11 @@ final class NotificationService {
sendLocalNotification(title: title, body: body, identifier: identifier)
}
func sendPrivateMessageNotification(from sender: String, message: String, peerID: String) {
func sendPrivateMessageNotification(from sender: String, message: String, peer: Peer) {
let title = "🔒 DM from \(sender)"
let body = message
let identifier = "private-\(UUID().uuidString)"
let userInfo = ["peerID": peerID, "senderName": sender]
let userInfo = ["peerID": peer.id, "senderName": sender]
sendLocalNotification(title: title, body: body, identifier: identifier, userInfo: userInfo)
}
+2 -2
View File
@@ -126,7 +126,7 @@ final class PrivateChatManager: ObservableObject {
NotificationService.shared.sendPrivateMessageNotification(
from: message.sender,
message: message.content,
peerID: senderPeerID
peer: Peer(str: senderPeerID)
)
}
} else {
@@ -231,7 +231,7 @@ final class PrivateChatManager: ObservableObject {
if let router = messageRouter {
SecureLogger.debug("PrivateChatManager: sending READ ack for \(message.id.prefix(8))… to \(senderPeerID.prefix(8))… via router", category: .session)
Task { @MainActor in
router.sendReadReceipt(receipt, to: senderPeerID)
router.sendReadReceipt(receipt, to: Peer(str: senderPeerID))
}
} else {
// Fallback: preserve previous behavior
+1 -1
View File
@@ -307,7 +307,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
// Send favorite notification to the peer via router (mesh or Nostr)
if let router = messageRouter {
router.sendFavoriteNotification(to: peerID, isFavorite: !wasFavorite)
router.sendFavoriteNotification(to: Peer(str: peerID), isFavorite: !wasFavorite)
} else {
// Fallback to mesh-only if router not yet wired
meshService.sendFavoriteNotification(to: peerID, isFavorite: !wasFavorite)
-21
View File
@@ -10,27 +10,6 @@ struct InputValidator {
static let maxNicknameLength = 50
static let maxMessageLength = 10_000
static let maxReasonLength = 200
static let maxPeerIDLength = 64
static let hexPeerIDLength = 16 // 8 bytes = 16 hex chars
}
// MARK: - Peer ID Validation
/// Validates a peer ID from any source (short 16-hex, full 64-hex, or internal alnum/-/_ up to 64)
static func validatePeerID(_ peerID: String) -> Bool {
// Accept short routing IDs (exact 16-hex)
if PeerIDResolver.isShortID(peerID) { return true }
// If length equals short-hex length but isn't valid hex, reject
if peerID.count == Limits.hexPeerIDLength { return false }
// Accept full Noise key hex (exact 64-hex)
if PeerIDResolver.isNoiseKeyHex(peerID) { return true }
// If length equals full key length but isn't valid hex, reject
if peerID.count == Limits.maxPeerIDLength { return false }
// Internal format: alphanumeric + dash/underscore up to 63 (not 16 or 64)
let validCharset = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-_"))
return !peerID.isEmpty &&
peerID.count < Limits.maxPeerIDLength &&
peerID.rangeOfCharacter(from: validCharset.inverted) == nil
}
// MARK: - String Content Validation
-20
View File
@@ -1,20 +0,0 @@
import Foundation
struct PeerIDResolver {
/// Returns a 16-hex short peer ID derived from a 64-hex Noise public key if needed
static func toShortID(_ id: String) -> String {
if id.count == 64, let data = Data(hexString: id) {
return PeerIDUtils.derivePeerID(fromPublicKey: data)
}
return id
}
static func isShortID(_ id: String) -> Bool {
return id.count == 16 && Data(hexString: id) != nil
}
static func isNoiseKeyHex(_ id: String) -> Bool {
return id.count == 64 && Data(hexString: id) != nil
}
}
+27 -27
View File
@@ -310,7 +310,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
if let mapped = shortIDToNoiseKey[shortPeerID] { return mapped }
// Fallback: derive from active Noise session if available
if shortPeerID.count == 16,
let key = meshService.getNoiseService().getPeerPublicKeyData(shortPeerID) {
let key = meshService.getNoiseService().getPeerPublicKeyData(Peer(str: shortPeerID)) {
let stable = key.hexEncodedString()
shortIDToNoiseKey[shortPeerID] = stable
return stable
@@ -1096,7 +1096,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
NotificationService.shared.sendPrivateMessageNotification(
from: senderName,
message: pm.content,
peerID: convKey
peer: Peer(str: convKey)
)
}
}
@@ -1298,7 +1298,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
}
// Update identity state manager with handshake completion
identityManager.updateHandshakeState(peerID: peerID, state: .completed(fingerprint: fingerprintStr))
identityManager.updateHandshakeState(peer: Peer(str: peerID), state: .completed(fingerprint: fingerprintStr))
// Update encryption status now that we have the fingerprint
updateEncryptionStatus(for: peerID)
@@ -1850,7 +1850,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
NotificationService.shared.sendPrivateMessageNotification(
from: senderName,
message: pm.content,
peerID: convKey
peer: Peer(str: convKey)
)
}
@@ -2284,7 +2284,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Send via appropriate transport (BLE if connected/reachable, else Nostr when possible)
if isConnected || isReachable || (isMutualFavorite && hasNostrKey) {
messageRouter.sendPrivate(content, to: peerID, recipientNickname: recipientNickname ?? "user", messageID: messageID)
messageRouter.sendPrivate(content, to: Peer(str: peerID), recipientNickname: recipientNickname ?? "user", messageID: messageID)
// Optimistically mark as sent for both transports; delivery/read will update subsequently
if let idx = privateChats[peerID]?.firstIndex(where: { $0.id == messageID }) {
privateChats[peerID]?[idx].deliveryStatus = .sent
@@ -2866,7 +2866,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
switch sessionState {
case .established:
// Send the message directly without going through sendPrivateMessage to avoid local echo
messageRouter.sendPrivate(screenshotMessage, to: peerID, recipientNickname: peerNickname, messageID: UUID().uuidString)
messageRouter.sendPrivate(screenshotMessage, to: Peer(str: peerID), recipientNickname: peerNickname, messageID: UUID().uuidString)
default:
// Don't send screenshot notification if no session exists
SecureLogger.debug("Skipping screenshot notification to \(peerID) - no established session", category: .security)
@@ -2988,7 +2988,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
return
}
// Use router to decide (mesh if reachable, else Nostr if available)
messageRouter.sendReadReceipt(receipt, to: actualPeerID)
messageRouter.sendReadReceipt(receipt, to: Peer(str: actualPeerID))
}
@MainActor
@@ -3049,7 +3049,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Use stable Noise key hex if available; else fall back to peerID
let recipPeer = (Data(hexString: peerID) != nil) ? peerID : (unifiedPeerService.getPeer(by: peerID)?.noisePublicKey.hexEncodedString() ?? peerID)
let receipt = ReadReceipt(originalMessageID: message.id, readerID: meshService.myPeerID, readerNickname: nickname)
messageRouter.sendReadReceipt(receipt, to: recipPeer)
messageRouter.sendReadReceipt(receipt, to: Peer(str: recipPeer))
sentReadReceipts.insert(message.id)
}
}
@@ -3730,7 +3730,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
func updateEncryptionStatusForPeer(_ peerID: String) {
let noiseService = meshService.getNoiseService()
if noiseService.hasEstablishedSession(with: peerID) {
if noiseService.hasEstablishedSession(with: Peer(str: peerID)) {
// Check if fingerprint is verified using our persisted data
if let fingerprint = getFingerprint(for: peerID),
verifiedFingerprints.contains(fingerprint) {
@@ -3738,7 +3738,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
} else {
peerEncryptionStatus[peerID] = .noiseSecured
}
} else if noiseService.hasSession(with: peerID) {
} else if noiseService.hasSession(with: Peer(str: peerID)) {
// Session exists but not established - handshaking
peerEncryptionStatus[peerID] = .noiseHandshaking
} else {
@@ -4196,7 +4196,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
private func updateEncryptionStatus(for peerID: String) {
let noiseService = meshService.getNoiseService()
if noiseService.hasEstablishedSession(with: peerID) {
if noiseService.hasEstablishedSession(with: Peer(str: peerID)) {
if let fingerprint = getFingerprint(for: peerID) {
if verifiedFingerprints.contains(fingerprint) {
peerEncryptionStatus[peerID] = .noiseVerified
@@ -4207,7 +4207,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Session established but no fingerprint yet
peerEncryptionStatus[peerID] = .noiseSecured
}
} else if noiseService.hasSession(with: peerID) {
} else if noiseService.hasSession(with: Peer(str: peerID)) {
peerEncryptionStatus[peerID] = .noiseHandshaking
} else {
peerEncryptionStatus[peerID] = Optional.none
@@ -4358,7 +4358,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Cache shortID -> full Noise key mapping as soon as session authenticates
if self.shortIDToNoiseKey[peerID] == nil,
let keyData = self.meshService.getNoiseService().getPeerPublicKeyData(peerID) {
let keyData = self.meshService.getNoiseService().getPeerPublicKeyData(Peer(str: peerID)) {
let stable = keyData.hexEncodedString()
self.shortIDToNoiseKey[peerID] = stable
SecureLogger.debug("🗺️ Mapped short peerID to Noise key for header continuity: \(peerID) -> \(stable.prefix(8))", category: .session)
@@ -4587,7 +4587,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
pendingQRVerifications[peerID] = pending
// If Noise session is established, send immediately; otherwise trigger handshake and send on auth
let noise = meshService.getNoiseService()
if noise.hasEstablishedSession(with: peerID) {
if noise.hasEstablishedSession(with: Peer(str: peerID)) {
meshService.sendVerifyChallenge(to: peerID, noiseKeyHex: qr.noiseKeyHex, nonceA: nonce)
pending.sent = true
pendingQRVerifications[peerID] = pending
@@ -4608,7 +4608,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
isConnected = true
// Register ephemeral session with identity manager
identityManager.registerEphemeralSession(peerID: peerID, handshakeState: .none)
identityManager.registerEphemeralSession(peer: Peer(str: peerID), handshakeState: .none)
// Intentionally do not resend favorites on reconnect.
// We only send our npub when a favorite is toggled on, or if our npub changes.
@@ -4623,7 +4623,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
}
// Flush any queued messages for this peer via router
messageRouter.flushOutbox(for: peerID)
messageRouter.flushOutbox(for: Peer(str: peerID))
}
//
@@ -4633,12 +4633,12 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
SecureLogger.debug("👋 Peer disconnected: \(peerID)", category: .session)
// Remove ephemeral session from identity manager
identityManager.removeEphemeralSession(peerID: peerID)
identityManager.removeEphemeralSession(peer: Peer(str: peerID))
// If the open PM is tied to this short peer ID, switch UI context to the full Noise key (offline favorite)
var derivedStableKeyHex: String? = shortIDToNoiseKey[peerID]
if derivedStableKeyHex == nil,
let key = meshService.getNoiseService().getPeerPublicKeyData(peerID) {
let key = meshService.getNoiseService().getPeerPublicKeyData(Peer(str: peerID)) {
derivedStableKeyHex = key.hexEncodedString()
shortIDToNoiseKey[peerID] = derivedStableKeyHex
}
@@ -4740,7 +4740,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Register ephemeral sessions for all connected peers
for peerID in peers {
self.identityManager.registerEphemeralSession(peerID: peerID, handshakeState: .none)
self.identityManager.registerEphemeralSession(peer: Peer(str: peerID), handshakeState: .none)
}
// Schedule UI refresh to ensure offline favorites are shown
@@ -5312,7 +5312,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
if let key {
SecureLogger.debug("Sending DELIVERED ack for \(message.id.prefix(8))… via router", category: .session)
messageRouter.sendDeliveryAck(message.id, to: key.hexEncodedString())
messageRouter.sendDeliveryAck(message.id, to: Peer(str: key.hexEncodedString()))
} else if let id = try? NostrIdentityBridge.getCurrentNostrIdentity() {
// Fallback: no Noise mapping yet send directly to sender's Nostr pubkey
let nt = NostrTransport(keychain: keychain)
@@ -5333,7 +5333,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
if let key {
let receipt = ReadReceipt(originalMessageID: message.id, readerID: meshService.myPeerID, readerNickname: nickname)
SecureLogger.debug("Viewing chat; sending READ ack for \(message.id.prefix(8))… via router", category: .session)
messageRouter.sendReadReceipt(receipt, to: key.hexEncodedString())
messageRouter.sendReadReceipt(receipt, to: Peer(str: key.hexEncodedString()))
sentReadReceipts.insert(message.id)
} else if let id = try? NostrIdentityBridge.getCurrentNostrIdentity() {
let nt = NostrTransport(keychain: keychain)
@@ -5366,7 +5366,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
NotificationService.shared.sendPrivateMessageNotification(
from: senderNickname,
message: messageContent,
peerID: targetPeerID
peer: Peer(str: targetPeerID)
)
}
}
@@ -5598,7 +5598,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
NotificationService.shared.sendPrivateMessageNotification(
from: finalSenderNickname,
message: content,
peerID: tempPeerID
peer: Peer(str: tempPeerID)
)
} else {
// Not notifying for old message
@@ -5655,7 +5655,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
@MainActor
private func sendFavoriteNotificationViaNostr(noisePublicKey: Data, isFavorite: Bool) {
let peerIDHex = noisePublicKey.hexEncodedString()
messageRouter.sendFavoriteNotification(to: peerIDHex, isFavorite: isFavorite)
messageRouter.sendFavoriteNotification(to: Peer(str: peerIDHex), isFavorite: isFavorite)
}
@MainActor
@@ -5675,12 +5675,12 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Try mesh first for connected peers
if meshService.isPeerConnected(peerID) {
messageRouter.sendFavoriteNotification(to: peerID, isFavorite: isFavorite)
messageRouter.sendFavoriteNotification(to: Peer(str: peerID), isFavorite: isFavorite)
SecureLogger.debug("📤 Sent favorite notification via BLE to \(peerID)", category: .session)
} else if let key = noiseKey {
// Send via Nostr for offline peers (using router)
let recipientPeerID = key.hexEncodedString()
messageRouter.sendFavoriteNotification(to: recipientPeerID, isFavorite: isFavorite)
messageRouter.sendFavoriteNotification(to: Peer(str: recipientPeerID), isFavorite: isFavorite)
} else {
SecureLogger.warning("⚠️ Cannot send favorite notification - peer not connected and no Nostr pubkey", category: .session)
}
@@ -5932,7 +5932,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
NotificationService.shared.sendPrivateMessageNotification(
from: message.sender,
message: message.content,
peerID: peerID
peer: Peer(str: peerID)
)
}
} else {
+1 -1
View File
@@ -1372,7 +1372,7 @@ struct ContentView: View {
!fav.peerNickname.isEmpty { return fav.peerNickname }
// Fallback: resolve from persisted social identity via fingerprint mapping
if headerPeerID.count == 16 {
let candidates = viewModel.identityManager.getCryptoIdentitiesByPeerIDPrefix(headerPeerID)
let candidates = viewModel.identityManager.getCryptoIdentitiesByPeerIDPrefix(Peer(str: headerPeerID))
if let id = candidates.first,
let social = viewModel.identityManager.getSocialIdentity(for: id.fingerprint) {
if let pet = social.localPetname, !pet.isEmpty { return pet }