mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 17:25:20 +00:00
Sign public broadcasts; verify relayed messages via persisted signing keys; keep scheduled relays in sparse graphs and speed their jitter; persist announce signing key for offline auth; add short backoff after disconnect errors to reduce reconnect thrash
This commit is contained in:
@@ -106,6 +106,8 @@ enum HandshakeState {
|
|||||||
struct CryptographicIdentity: Codable {
|
struct CryptographicIdentity: Codable {
|
||||||
let fingerprint: String // SHA256 of public key
|
let fingerprint: String // SHA256 of public key
|
||||||
let publicKey: Data // Noise static public key
|
let publicKey: Data // Noise static public key
|
||||||
|
// Optional Ed25519 signing public key (used to authenticate public messages)
|
||||||
|
var signingPublicKey: Data? = nil
|
||||||
let firstSeen: Date
|
let firstSeen: Date
|
||||||
let lastHandshake: Date?
|
let lastHandshake: Date?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -230,6 +230,91 @@ class SecureIdentityStateManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Cryptographic Identities
|
||||||
|
|
||||||
|
/// Insert or update a cryptographic identity and optionally persist its signing key and claimed nickname.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - fingerprint: SHA-256 hex of the Noise static public key
|
||||||
|
/// - noisePublicKey: Noise static public key data
|
||||||
|
/// - signingPublicKey: Optional Ed25519 signing public key for authenticating public messages
|
||||||
|
/// - claimedNickname: Optional latest claimed nickname to persist into social identity
|
||||||
|
func upsertCryptographicIdentity(fingerprint: String, noisePublicKey: Data, signingPublicKey: Data?, claimedNickname: String? = nil) {
|
||||||
|
queue.async(flags: .barrier) {
|
||||||
|
let now = Date()
|
||||||
|
if var existing = self.cryptographicIdentities[fingerprint] {
|
||||||
|
// Update keys if changed
|
||||||
|
if existing.publicKey != noisePublicKey {
|
||||||
|
existing = CryptographicIdentity(
|
||||||
|
fingerprint: fingerprint,
|
||||||
|
publicKey: noisePublicKey,
|
||||||
|
signingPublicKey: signingPublicKey ?? existing.signingPublicKey,
|
||||||
|
firstSeen: existing.firstSeen,
|
||||||
|
lastHandshake: now
|
||||||
|
)
|
||||||
|
self.cryptographicIdentities[fingerprint] = existing
|
||||||
|
} else {
|
||||||
|
// Update signing key and lastHandshake
|
||||||
|
existing.signingPublicKey = signingPublicKey ?? existing.signingPublicKey
|
||||||
|
let updated = CryptographicIdentity(
|
||||||
|
fingerprint: existing.fingerprint,
|
||||||
|
publicKey: existing.publicKey,
|
||||||
|
signingPublicKey: existing.signingPublicKey,
|
||||||
|
firstSeen: existing.firstSeen,
|
||||||
|
lastHandshake: now
|
||||||
|
)
|
||||||
|
self.cryptographicIdentities[fingerprint] = updated
|
||||||
|
}
|
||||||
|
// Persist updated state (already assigned in branches above)
|
||||||
|
} else {
|
||||||
|
// New entry
|
||||||
|
let entry = CryptographicIdentity(
|
||||||
|
fingerprint: fingerprint,
|
||||||
|
publicKey: noisePublicKey,
|
||||||
|
signingPublicKey: signingPublicKey,
|
||||||
|
firstSeen: now,
|
||||||
|
lastHandshake: now
|
||||||
|
)
|
||||||
|
self.cryptographicIdentities[fingerprint] = entry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optionally persist claimed nickname into social identity
|
||||||
|
if let claimed = claimedNickname {
|
||||||
|
var identity = self.cache.socialIdentities[fingerprint] ?? SocialIdentity(
|
||||||
|
fingerprint: fingerprint,
|
||||||
|
localPetname: nil,
|
||||||
|
claimedNickname: claimed,
|
||||||
|
trustLevel: .unknown,
|
||||||
|
isFavorite: false,
|
||||||
|
isBlocked: false,
|
||||||
|
notes: nil
|
||||||
|
)
|
||||||
|
// Update claimed nickname if changed
|
||||||
|
if identity.claimedNickname != claimed {
|
||||||
|
identity.claimedNickname = claimed
|
||||||
|
self.cache.socialIdentities[fingerprint] = identity
|
||||||
|
} else if self.cache.socialIdentities[fingerprint] == nil {
|
||||||
|
self.cache.socialIdentities[fingerprint] = identity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.saveIdentityCache()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve cryptographic identity by fingerprint
|
||||||
|
func getCryptographicIdentity(for fingerprint: String) -> CryptographicIdentity? {
|
||||||
|
queue.sync { cryptographicIdentities[fingerprint] }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find cryptographic identities whose fingerprint prefix matches a peerID (16-hex) short ID
|
||||||
|
func getCryptoIdentitiesByPeerIDPrefix(_ peerID: String) -> [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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getAllSocialIdentities() -> [SocialIdentity] {
|
func getAllSocialIdentities() -> [SocialIdentity] {
|
||||||
queue.sync {
|
queue.sync {
|
||||||
return Array(cache.socialIdentities.values)
|
return Array(cache.socialIdentities.values)
|
||||||
|
|||||||
@@ -697,19 +697,26 @@ final class BLEService: NSObject {
|
|||||||
self.sendPrivateMessage(content, to: recipientID, messageID: finalMessageID)
|
self.sendPrivateMessage(content, to: recipientID, messageID: finalMessageID)
|
||||||
} else {
|
} else {
|
||||||
// Public broadcast
|
// Public broadcast
|
||||||
// Public message - logged at relay point for mesh debugging
|
// Create packet with explicit fields so we can sign it
|
||||||
let packet = BitchatPacket(
|
let basePacket = BitchatPacket(
|
||||||
type: MessageType.message.rawValue,
|
type: MessageType.message.rawValue,
|
||||||
ttl: self.messageTTL,
|
senderID: Data(hexString: self.myPeerID) ?? Data(),
|
||||||
senderID: self.myPeerID,
|
recipientID: nil,
|
||||||
payload: Data(content.utf8)
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
|
payload: Data(content.utf8),
|
||||||
|
signature: nil,
|
||||||
|
ttl: self.messageTTL
|
||||||
)
|
)
|
||||||
|
guard let signedPacket = self.noiseService.signPacket(basePacket) else {
|
||||||
|
SecureLogger.log("❌ Failed to sign public message", category: SecureLogger.security, level: .error)
|
||||||
|
return
|
||||||
|
}
|
||||||
// Pre-mark our own broadcast as processed to avoid handling relayed self copy
|
// Pre-mark our own broadcast as processed to avoid handling relayed self copy
|
||||||
let senderHex = packet.senderID.hexEncodedString()
|
let senderHex = signedPacket.senderID.hexEncodedString()
|
||||||
let dedupID = "\(senderHex)-\(packet.timestamp)-\(packet.type)"
|
let dedupID = "\(senderHex)-\(signedPacket.timestamp)-\(signedPacket.type)"
|
||||||
self.messageDeduplicator.markProcessed(dedupID)
|
self.messageDeduplicator.markProcessed(dedupID)
|
||||||
// Call synchronously since we're already on background queue
|
// Call synchronously since we're already on background queue
|
||||||
self.broadcastPacket(packet)
|
self.broadcastPacket(signedPacket)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1222,10 +1229,14 @@ final class BLEService: NSObject {
|
|||||||
SecureLogger.log("⚠️ Duplicate packet ignored: \(messageID)",
|
SecureLogger.log("⚠️ Duplicate packet ignored: \(messageID)",
|
||||||
category: SecureLogger.session, level: .debug)
|
category: SecureLogger.session, level: .debug)
|
||||||
}
|
}
|
||||||
// Cancel any pending relay for this message (arrived via another neighbor)
|
// In sparse graphs (<=2 neighbors), keep the pending relay to ensure bridging.
|
||||||
collectionsQueue.async(flags: .barrier) { [weak self] in
|
// In denser graphs, cancel the pending relay to reduce redundant floods.
|
||||||
if let task = self?.scheduledRelays.removeValue(forKey: messageID) {
|
let connectedCount = collectionsQueue.sync { peers.values.filter { $0.isConnected }.count }
|
||||||
task.cancel()
|
if connectedCount > 2 {
|
||||||
|
collectionsQueue.async(flags: .barrier) { [weak self] in
|
||||||
|
if let task = self?.scheduledRelays.removeValue(forKey: messageID) {
|
||||||
|
task.cancel()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return // Duplicate ignored
|
return // Duplicate ignored
|
||||||
@@ -1405,6 +1416,19 @@ final class BLEService: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Persist cryptographic identity and signing key for robust offline verification
|
||||||
|
do {
|
||||||
|
// Derive fingerprint from Noise public key
|
||||||
|
let hash = SHA256.hash(data: announcement.noisePublicKey)
|
||||||
|
let fingerprint = hash.map { String(format: "%02x", $0) }.joined()
|
||||||
|
SecureIdentityStateManager.shared.upsertCryptographicIdentity(
|
||||||
|
fingerprint: fingerprint,
|
||||||
|
noisePublicKey: announcement.noisePublicKey,
|
||||||
|
signingPublicKey: announcement.signingPublicKey,
|
||||||
|
claimedNickname: announcement.nickname
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Notify UI on main thread
|
// Notify UI on main thread
|
||||||
notifyUI { [weak self] in
|
notifyUI { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
@@ -1441,8 +1465,40 @@ final class BLEService: NSObject {
|
|||||||
// Ignore self-origin public messages that may be seen again via relay
|
// Ignore self-origin public messages that may be seen again via relay
|
||||||
if peerID == myPeerID { return }
|
if peerID == myPeerID { return }
|
||||||
|
|
||||||
// Enforce: only accept public messages from verified peers we know
|
var accepted = false
|
||||||
guard let info = peers[peerID], info.isVerifiedNickname else {
|
var senderNickname: String = ""
|
||||||
|
|
||||||
|
if let info = peers[peerID], info.isVerifiedNickname {
|
||||||
|
// Known verified peer path
|
||||||
|
accepted = true
|
||||||
|
senderNickname = info.nickname
|
||||||
|
// Handle nickname collisions
|
||||||
|
let hasCollision = peers.values.contains { $0.isConnected && $0.nickname == info.nickname && $0.id != peerID } || (myNickname == info.nickname)
|
||||||
|
if hasCollision {
|
||||||
|
senderNickname += "#" + String(peerID.prefix(4))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 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 = SecureIdentityStateManager.shared.getCryptoIdentitiesByPeerIDPrefix(peerID)
|
||||||
|
for candidate in candidates {
|
||||||
|
if let signingKey = candidate.signingPublicKey,
|
||||||
|
noiseService.verifySignature(signature, for: packetData, publicKey: signingKey) {
|
||||||
|
accepted = true
|
||||||
|
// Prefer persisted social petname or claimed nickname
|
||||||
|
if let social = SecureIdentityStateManager.shared.getSocialIdentity(for: candidate.fingerprint) {
|
||||||
|
senderNickname = social.localPetname ?? social.claimedNickname
|
||||||
|
} else {
|
||||||
|
senderNickname = "anon" + String(peerID.prefix(4))
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard accepted else {
|
||||||
SecureLogger.log("🚫 Dropping public message from unverified or unknown peer \(peerID.prefix(8))…", category: SecureLogger.security, level: .warning)
|
SecureLogger.log("🚫 Dropping public message from unverified or unknown peer \(peerID.prefix(8))…", category: SecureLogger.security, level: .warning)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1452,14 +1508,6 @@ final class BLEService: NSObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve display nickname; if collisions exist, append short peerID suffix
|
|
||||||
var senderNickname = info.nickname
|
|
||||||
// Treat a collision if another connected peer shares the nickname OR our own nickname matches
|
|
||||||
let hasCollision = peers.values.contains { $0.isConnected && $0.nickname == info.nickname && $0.id != peerID } || (myNickname == info.nickname)
|
|
||||||
if hasCollision {
|
|
||||||
senderNickname += "#" + String(peerID.prefix(4))
|
|
||||||
}
|
|
||||||
|
|
||||||
SecureLogger.log("💬 [\(senderNickname)] TTL:\(packet.ttl): \(String(content.prefix(50)))\(content.count > 50 ? "..." : "")", category: SecureLogger.session, level: .debug)
|
SecureLogger.log("💬 [\(senderNickname)] TTL:\(packet.ttl): \(String(content.prefix(50)))\(content.count > 50 ? "..." : "")", category: SecureLogger.session, level: .debug)
|
||||||
|
|
||||||
let ts = Date(timeIntervalSince1970: Double(packet.timestamp) / 1000)
|
let ts = Date(timeIntervalSince1970: Double(packet.timestamp) / 1000)
|
||||||
@@ -2138,6 +2186,11 @@ func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeriph
|
|||||||
SecureLogger.log("📱 Disconnect: \(peerID ?? peripheralID)\(error != nil ? " (\(error!.localizedDescription))" : "")",
|
SecureLogger.log("📱 Disconnect: \(peerID ?? peripheralID)\(error != nil ? " (\(error!.localizedDescription))" : "")",
|
||||||
category: SecureLogger.session, level: .debug)
|
category: SecureLogger.session, level: .debug)
|
||||||
|
|
||||||
|
// If disconnect carried an error (often timeout), apply short backoff to avoid thrash
|
||||||
|
if error != nil {
|
||||||
|
recentConnectTimeouts[peripheralID] = Date()
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up references
|
// Clean up references
|
||||||
peripherals.removeValue(forKey: peripheralID)
|
peripherals.removeValue(forKey: peripheralID)
|
||||||
|
|
||||||
|
|||||||
@@ -48,9 +48,10 @@ struct RelayController {
|
|||||||
let newTTL = clamped &- 1
|
let newTTL = clamped &- 1
|
||||||
|
|
||||||
// Wider jitter window to allow duplicate suppression to win more often
|
// Wider jitter window to allow duplicate suppression to win more often
|
||||||
|
// For sparse graphs (<=2), relay quickly to avoid cancellation races
|
||||||
let delayMs: Int
|
let delayMs: Int
|
||||||
switch degree {
|
switch degree {
|
||||||
case 0...2: delayMs = Int.random(in: 40...100)
|
case 0...2: delayMs = Int.random(in: 10...40)
|
||||||
case 3...5: delayMs = Int.random(in: 60...150)
|
case 3...5: delayMs = Int.random(in: 60...150)
|
||||||
case 6...9: delayMs = Int.random(in: 80...180)
|
case 6...9: delayMs = Int.random(in: 80...180)
|
||||||
default: delayMs = Int.random(in: 100...220)
|
default: delayMs = Int.random(in: 100...220)
|
||||||
|
|||||||
Reference in New Issue
Block a user