mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 19:25:20 +00:00
Compare commits
9
Commits
peer-old
...
peer-id-10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8daacb2dce | ||
|
|
2d8d45a4d7 | ||
|
|
7267fb6404 | ||
|
|
a6d013bee9 | ||
|
|
7b20bdf821 | ||
|
|
9be05f98e9 | ||
|
|
b97241978d | ||
|
|
a454a85d86 | ||
|
|
9ecd346197 |
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user