mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 21:25:19 +00:00
Implement Noise Protocol Framework and peer ID rotation for enhanced security and privacy
This major update replaces the basic encryption with the Noise Protocol Framework and adds ephemeral peer ID rotation for enhanced privacy. Key Changes: Security Infrastructure: - Implemented Noise Protocol Framework (XX handshake pattern) - End-to-end encryption with forward secrecy and identity hiding - Session management with automatic rekey support - Channel encryption with password-derived keys Privacy Enhancements: - Ephemeral peer ID rotation (5-15 minute random intervals) - Persistent identity through public key fingerprints - Favorites and verification persist across ID rotations - Block list based on fingerprints, not ephemeral IDs Core Components Added: - NoiseEncryptionService: Main encryption service - NoiseSession: Individual peer session management - NoiseChannelEncryption: Password-protected channel support - SecureIdentityStateManager: Persistent identity storage - FingerprintView: Visual fingerprint verification UI Bug Fixes: - Fixed handshake storm with tie-breaker mechanism - Fixed missing connect messages during peer rotation - Fixed delivery ACK compression issues - Fixed race conditions in message queue - Fixed nickname resolution for rotated peer IDs Testing: - Comprehensive test suite for Noise implementation - Security validator tests - Channel encryption tests - Identity persistence tests - Rate limiter tests Documentation: - BRING_THE_NOISE.md: Technical implementation details - Updated WHITEPAPER.md: Simplified and focused on core innovations - Removed temporary debug documentation The implementation maintains backward compatibility while significantly improving security and privacy. All existing features (channels, private messages, favorites, blocking) work seamlessly with the new system.
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
//
|
||||
// LRUCache.swift
|
||||
// bitchat
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Thread-safe LRU (Least Recently Used) cache implementation
|
||||
final class LRUCache<Key: Hashable, Value> {
|
||||
private class Node {
|
||||
var key: Key
|
||||
var value: Value
|
||||
var prev: Node?
|
||||
var next: Node?
|
||||
|
||||
init(key: Key, value: Value) {
|
||||
self.key = key
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
||||
private let maxSize: Int
|
||||
private var cache: [Key: Node] = [:]
|
||||
private var head: Node?
|
||||
private var tail: Node?
|
||||
private let queue = DispatchQueue(label: "bitchat.lrucache", attributes: .concurrent)
|
||||
|
||||
init(maxSize: Int) {
|
||||
self.maxSize = maxSize
|
||||
}
|
||||
|
||||
func set(_ key: Key, value: Value) {
|
||||
queue.sync(flags: .barrier) {
|
||||
if let node = cache[key] {
|
||||
// Update existing value and move to front
|
||||
node.value = value
|
||||
moveToFront(node)
|
||||
} else {
|
||||
// Add new node
|
||||
let newNode = Node(key: key, value: value)
|
||||
cache[key] = newNode
|
||||
addToFront(newNode)
|
||||
|
||||
// Remove oldest if over capacity
|
||||
if cache.count > maxSize {
|
||||
if let tailNode = tail {
|
||||
removeNode(tailNode)
|
||||
cache.removeValue(forKey: tailNode.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func get(_ key: Key) -> Value? {
|
||||
return queue.sync(flags: .barrier) {
|
||||
guard let node = cache[key] else { return nil }
|
||||
moveToFront(node)
|
||||
return node.value
|
||||
}
|
||||
}
|
||||
|
||||
func contains(_ key: Key) -> Bool {
|
||||
return queue.sync {
|
||||
return cache[key] != nil
|
||||
}
|
||||
}
|
||||
|
||||
func remove(_ key: Key) {
|
||||
queue.sync(flags: .barrier) {
|
||||
if let node = cache[key] {
|
||||
removeNode(node)
|
||||
cache.removeValue(forKey: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeAll() {
|
||||
queue.sync(flags: .barrier) {
|
||||
cache.removeAll()
|
||||
head = nil
|
||||
tail = nil
|
||||
}
|
||||
}
|
||||
|
||||
var count: Int {
|
||||
return queue.sync {
|
||||
return cache.count
|
||||
}
|
||||
}
|
||||
|
||||
var keys: [Key] {
|
||||
return queue.sync {
|
||||
return Array(cache.keys)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private Helpers
|
||||
|
||||
private func addToFront(_ node: Node) {
|
||||
node.next = head
|
||||
node.prev = nil
|
||||
|
||||
if let head = head {
|
||||
head.prev = node
|
||||
}
|
||||
|
||||
head = node
|
||||
|
||||
if tail == nil {
|
||||
tail = node
|
||||
}
|
||||
}
|
||||
|
||||
private func removeNode(_ node: Node) {
|
||||
if let prev = node.prev {
|
||||
prev.next = node.next
|
||||
} else {
|
||||
head = node.next
|
||||
}
|
||||
|
||||
if let next = node.next {
|
||||
next.prev = node.prev
|
||||
} else {
|
||||
tail = node.prev
|
||||
}
|
||||
|
||||
node.prev = nil
|
||||
node.next = nil
|
||||
}
|
||||
|
||||
private func moveToFront(_ node: Node) {
|
||||
guard node !== head else { return }
|
||||
removeNode(node)
|
||||
addToFront(node)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Bounded Set
|
||||
|
||||
/// Thread-safe set with maximum size using LRU eviction
|
||||
final class BoundedSet<Element: Hashable> {
|
||||
private let cache: LRUCache<Element, Bool>
|
||||
|
||||
init(maxSize: Int) {
|
||||
self.cache = LRUCache(maxSize: maxSize)
|
||||
}
|
||||
|
||||
func insert(_ element: Element) {
|
||||
cache.set(element, value: true)
|
||||
}
|
||||
|
||||
func contains(_ element: Element) -> Bool {
|
||||
return cache.get(element) != nil
|
||||
}
|
||||
|
||||
func remove(_ element: Element) {
|
||||
cache.remove(element)
|
||||
}
|
||||
|
||||
func removeAll() {
|
||||
cache.removeAll()
|
||||
}
|
||||
|
||||
var count: Int {
|
||||
return cache.count
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
//
|
||||
// NoiseTestingHelper.swift
|
||||
// bitchat
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - Encryption Status Enum
|
||||
public enum EncryptionStatus {
|
||||
case noiseVerified // Noise + fingerprint verified
|
||||
case noiseSecured // Noise established
|
||||
case noiseHandshaking // Noise in progress
|
||||
case none // No encryption
|
||||
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .noiseVerified:
|
||||
return "checkmark.shield.fill" // Verified secure
|
||||
case .noiseSecured:
|
||||
return "lock.fill" // Secure
|
||||
case .noiseHandshaking:
|
||||
return "lock.rotation" // In progress
|
||||
// Legacy case removed
|
||||
case .none:
|
||||
return "lock.slash" // Not secure
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .noiseVerified:
|
||||
return "Verified Secure"
|
||||
case .noiseSecured:
|
||||
return "Secure (Noise)"
|
||||
case .noiseHandshaking:
|
||||
return "Securing..."
|
||||
// Legacy case removed
|
||||
case .none:
|
||||
return "Not Encrypted"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Testing Helper for Noise Protocol Migration
|
||||
|
||||
#if DEBUG
|
||||
class NoiseTestingHelper {
|
||||
static let shared = NoiseTestingHelper()
|
||||
|
||||
// Test Scenarios Checklist
|
||||
struct TestScenario {
|
||||
let name: String
|
||||
let steps: [String]
|
||||
var passed: Bool = false
|
||||
}
|
||||
|
||||
private var testScenarios: [TestScenario] = [
|
||||
TestScenario(
|
||||
name: "Basic Handshake",
|
||||
steps: [
|
||||
"1. Connect two devices via Bluetooth",
|
||||
"2. Verify Noise handshake completes (check logs)",
|
||||
"3. Confirm lock icon appears next to peer name",
|
||||
"4. Send a message and verify delivery"
|
||||
]
|
||||
),
|
||||
TestScenario(
|
||||
name: "Legacy Fallback",
|
||||
steps: [
|
||||
"1. Connect old app version to new version",
|
||||
"2. Verify legacy encryption still works",
|
||||
"3. Check for warning icon (not fully secure)",
|
||||
"4. Messages should still deliver"
|
||||
]
|
||||
),
|
||||
TestScenario(
|
||||
name: "Fingerprint Verification",
|
||||
steps: [
|
||||
"1. Long-press on peer name to see fingerprint",
|
||||
"2. Compare fingerprints on both devices",
|
||||
"3. Mark as verified",
|
||||
"4. Check for verified checkmark"
|
||||
]
|
||||
),
|
||||
TestScenario(
|
||||
name: "Channel Encryption",
|
||||
steps: [
|
||||
"1. Create password-protected channel",
|
||||
"2. Join from another device",
|
||||
"3. Send messages to channel",
|
||||
"4. Verify only members can decrypt"
|
||||
]
|
||||
),
|
||||
TestScenario(
|
||||
name: "Session Recovery",
|
||||
steps: [
|
||||
"1. Establish Noise session",
|
||||
"2. Force quit app",
|
||||
"3. Reopen and reconnect",
|
||||
"4. Verify session re-establishes automatically"
|
||||
]
|
||||
),
|
||||
TestScenario(
|
||||
name: "Rate Limiting",
|
||||
steps: [
|
||||
"1. Send many messages rapidly",
|
||||
"2. Verify rate limit kicks in after 100 msgs/sec",
|
||||
"3. Wait and verify messaging resumes",
|
||||
"4. Check no messages lost"
|
||||
]
|
||||
),
|
||||
TestScenario(
|
||||
name: "Panic Mode",
|
||||
steps: [
|
||||
"1. Establish sessions with peers",
|
||||
"2. Trigger panic mode (shake device)",
|
||||
"3. Verify all keys cleared",
|
||||
"4. Check new identity generated on restart"
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
// Debug logging for Noise events
|
||||
func logNoiseEvent(_ event: String, details: Any? = nil) {
|
||||
// Logging removed - keeping method signature for compatibility
|
||||
}
|
||||
|
||||
// Get encryption status for peer
|
||||
func getEncryptionStatus(for peerID: String, noiseService: NoiseEncryptionService) -> EncryptionStatus {
|
||||
if noiseService.hasEstablishedSession(with: peerID) {
|
||||
// Check if fingerprint is verified
|
||||
if let fingerprint = noiseService.getPeerFingerprint(peerID),
|
||||
isFingerprinted(peerID: peerID, fingerprint: fingerprint) {
|
||||
return .noiseVerified
|
||||
}
|
||||
return .noiseSecured
|
||||
} else {
|
||||
// Always use Noise - no legacy encryption
|
||||
return .noiseHandshaking
|
||||
}
|
||||
}
|
||||
|
||||
// Store verified fingerprints (in production, use Keychain)
|
||||
private var verifiedFingerprints: [String: String] = [:]
|
||||
|
||||
func verifyFingerprint(peerID: String, fingerprint: String) {
|
||||
verifiedFingerprints[peerID] = fingerprint
|
||||
}
|
||||
|
||||
func isFingerprinted(peerID: String, fingerprint: String) -> Bool {
|
||||
return verifiedFingerprints[peerID] == fingerprint
|
||||
}
|
||||
|
||||
// Format fingerprint for display
|
||||
func formatFingerprint(_ fingerprint: String) -> String {
|
||||
// Convert to uppercase and format into 2 lines (8 groups of 4 on each line)
|
||||
let uppercased = fingerprint.uppercased()
|
||||
var formatted = ""
|
||||
|
||||
for (index, char) in uppercased.enumerated() {
|
||||
// Add space every 4 characters (but not at the start)
|
||||
if index > 0 && index % 4 == 0 {
|
||||
// Add newline after 32 characters (8 groups of 4)
|
||||
if index == 32 {
|
||||
formatted += "\n"
|
||||
} else {
|
||||
formatted += " "
|
||||
}
|
||||
}
|
||||
formatted += String(char)
|
||||
}
|
||||
|
||||
return formatted
|
||||
}
|
||||
|
||||
// Get test scenario checklist
|
||||
func getTestChecklist() -> String {
|
||||
var checklist = "NOISE PROTOCOL TEST CHECKLIST\n"
|
||||
checklist += "=" .repeated(30) + "\n\n"
|
||||
|
||||
for scenario in testScenarios {
|
||||
checklist += "□ \(scenario.name)\n"
|
||||
for step in scenario.steps {
|
||||
checklist += " \(step)\n"
|
||||
}
|
||||
checklist += "\n"
|
||||
}
|
||||
|
||||
return checklist
|
||||
}
|
||||
}
|
||||
|
||||
// String extension for repeating
|
||||
extension String {
|
||||
func repeated(_ count: Int) -> String {
|
||||
return String(repeating: self, count: count)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,196 @@
|
||||
//
|
||||
// SecurityLogger.swift
|
||||
// bitchat
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
/// Centralized security-aware logging framework
|
||||
/// Provides safe logging that filters sensitive data and security events
|
||||
class SecurityLogger {
|
||||
|
||||
// MARK: - Log Categories
|
||||
|
||||
private static let subsystem = "chat.bitchat"
|
||||
|
||||
static let noise = OSLog(subsystem: subsystem, category: "noise")
|
||||
static let encryption = OSLog(subsystem: subsystem, category: "encryption")
|
||||
static let keychain = OSLog(subsystem: subsystem, category: "keychain")
|
||||
static let session = OSLog(subsystem: subsystem, category: "session")
|
||||
static let security = OSLog(subsystem: subsystem, category: "security")
|
||||
|
||||
// MARK: - Log Levels
|
||||
|
||||
enum LogLevel {
|
||||
case debug
|
||||
case info
|
||||
case warning
|
||||
case error
|
||||
case fault
|
||||
|
||||
var osLogType: OSLogType {
|
||||
switch self {
|
||||
case .debug: return .debug
|
||||
case .info: return .info
|
||||
case .warning: return .default
|
||||
case .error: return .error
|
||||
case .fault: return .fault
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Security Event Types
|
||||
|
||||
enum SecurityEvent {
|
||||
case handshakeStarted(peerID: String)
|
||||
case handshakeCompleted(peerID: String)
|
||||
case handshakeFailed(peerID: String, error: String)
|
||||
case sessionExpired(peerID: String)
|
||||
case keyRotation(channel: String)
|
||||
case invalidKey(reason: String)
|
||||
case replayAttackDetected(channel: String)
|
||||
case authenticationFailed(peerID: String)
|
||||
|
||||
var message: String {
|
||||
switch self {
|
||||
case .handshakeStarted(let peerID):
|
||||
return "Handshake started with peer: \(sanitize(peerID))"
|
||||
case .handshakeCompleted(let peerID):
|
||||
return "Handshake completed with peer: \(sanitize(peerID))"
|
||||
case .handshakeFailed(let peerID, let error):
|
||||
return "Handshake failed with peer: \(sanitize(peerID)), error: \(error)"
|
||||
case .sessionExpired(let peerID):
|
||||
return "Session expired for peer: \(sanitize(peerID))"
|
||||
case .keyRotation(let channel):
|
||||
return "Key rotation performed for channel: \(sanitize(channel))"
|
||||
case .invalidKey(let reason):
|
||||
return "Invalid key detected: \(reason)"
|
||||
case .replayAttackDetected(let channel):
|
||||
return "Replay attack detected on channel: \(sanitize(channel))"
|
||||
case .authenticationFailed(let peerID):
|
||||
return "Authentication failed for peer: \(sanitize(peerID))"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public Logging Methods
|
||||
|
||||
/// Log a security event
|
||||
static func logSecurityEvent(_ event: SecurityEvent, level: LogLevel = .info) {
|
||||
#if DEBUG
|
||||
os_log("%{public}@", log: security, type: level.osLogType, event.message)
|
||||
#else
|
||||
// In release, use private logging to prevent sensitive data exposure
|
||||
os_log("%{private}@", log: security, type: level.osLogType, event.message)
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Log general messages with automatic sensitive data filtering
|
||||
static func log(_ message: String, category: OSLog = noise, level: LogLevel = .debug) {
|
||||
let sanitized = sanitize(message)
|
||||
|
||||
#if DEBUG
|
||||
os_log("%{public}@", log: category, type: level.osLogType, sanitized)
|
||||
#else
|
||||
// In release builds, only log non-debug messages
|
||||
if level != .debug {
|
||||
os_log("%{private}@", log: category, type: level.osLogType, sanitized)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Log errors with context
|
||||
static func logError(_ error: Error, context: String, category: OSLog = noise) {
|
||||
let sanitized = sanitize(context)
|
||||
let errorDesc = sanitize(error.localizedDescription)
|
||||
|
||||
#if DEBUG
|
||||
os_log("Error in %{public}@: %{public}@", log: category, type: .error, sanitized, errorDesc)
|
||||
#else
|
||||
os_log("Error in %{private}@: %{private}@", log: category, type: .error, sanitized, errorDesc)
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - Private Helpers
|
||||
|
||||
/// Sanitize strings to remove potentially sensitive data
|
||||
private static func sanitize(_ input: String) -> String {
|
||||
var sanitized = input
|
||||
|
||||
// Remove full fingerprints (keep first 8 chars for debugging)
|
||||
let fingerprintPattern = #/[a-fA-F0-9]{64}/#
|
||||
sanitized = sanitized.replacing(fingerprintPattern) { match in
|
||||
let fingerprint = String(match.output)
|
||||
return String(fingerprint.prefix(8)) + "..."
|
||||
}
|
||||
|
||||
// Remove base64 encoded data that might be keys
|
||||
let base64Pattern = #/[A-Za-z0-9+/]{40,}={0,2}/#
|
||||
sanitized = sanitized.replacing(base64Pattern) { _ in
|
||||
"<base64-data>"
|
||||
}
|
||||
|
||||
// Remove potential passwords (assuming they're in quotes or after "password:")
|
||||
let passwordPattern = #/password["\s:=]+["']?[^"'\s]+["']?/#
|
||||
sanitized = sanitized.replacing(passwordPattern) { _ in
|
||||
"password: <redacted>"
|
||||
}
|
||||
|
||||
// Truncate peer IDs to first 8 characters
|
||||
let peerIDPattern = #/peerID: ([a-zA-Z0-9]{8})[a-zA-Z0-9]+/#
|
||||
sanitized = sanitized.replacing(peerIDPattern) { match in
|
||||
"peerID: \(match.1)..."
|
||||
}
|
||||
|
||||
return sanitized
|
||||
}
|
||||
|
||||
/// Sanitize individual values
|
||||
private static func sanitize<T>(_ value: T) -> String {
|
||||
let stringValue = String(describing: value)
|
||||
return sanitize(stringValue)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience Extensions
|
||||
|
||||
extension SecurityLogger {
|
||||
|
||||
/// Log handshake events
|
||||
static func logHandshake(_ phase: String, peerID: String, success: Bool = true) {
|
||||
if success {
|
||||
log("Handshake \(phase) with peer: \(peerID)", category: session, level: .info)
|
||||
} else {
|
||||
log("Handshake \(phase) failed with peer: \(peerID)", category: session, level: .warning)
|
||||
}
|
||||
}
|
||||
|
||||
/// Log encryption operations
|
||||
static func logEncryption(_ operation: String, success: Bool = true) {
|
||||
let level: LogLevel = success ? .debug : .error
|
||||
log("Encryption operation '\(operation)' \(success ? "succeeded" : "failed")",
|
||||
category: encryption, level: level)
|
||||
}
|
||||
|
||||
/// Log key management operations
|
||||
static func logKeyOperation(_ operation: String, keyType: String, success: Bool = true) {
|
||||
let level: LogLevel = success ? .info : .error
|
||||
log("Key operation '\(operation)' for \(keyType) \(success ? "succeeded" : "failed")",
|
||||
category: keychain, level: level)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Migration Helper
|
||||
|
||||
/// Helper to migrate from print statements to SecurityLogger
|
||||
/// Usage: Replace print(...) with secureLog(...)
|
||||
func secureLog(_ items: Any..., separator: String = " ", terminator: String = "\n") {
|
||||
#if DEBUG
|
||||
let message = items.map { String(describing: $0) }.joined(separator: separator)
|
||||
SecurityLogger.log(message, level: .debug)
|
||||
#endif
|
||||
}
|
||||
Reference in New Issue
Block a user