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:
jack
2025-07-15 13:15:31 +02:00
parent 6d39222ea0
commit 3070a4d307
42 changed files with 10952 additions and 2233 deletions
+313 -49
View File
@@ -8,14 +8,91 @@
import Foundation
import Security
import os.log
class KeychainManager {
static let shared = KeychainManager()
private let service = "com.bitchat.passwords"
private let accessGroup: String? = nil // Set this if using app groups
// Use consistent service name for all keychain items
private let service = "chat.bitchat"
private let appGroup = "group.chat.bitchat"
private init() {}
private init() {
// Clean up legacy keychain items on first run
cleanupLegacyKeychainItems()
}
private func cleanupLegacyKeychainItems() {
// Check if we've already done cleanup
let cleanupKey = "bitchat.keychain.cleanup.v2"
if UserDefaults.standard.bool(forKey: cleanupKey) {
return
}
// List of old service names to migrate from
let legacyServices = [
"com.bitchat.passwords",
"com.bitchat.deviceidentity",
"com.bitchat.noise.identity",
"chat.bitchat.passwords",
"bitchat.keychain"
]
var migratedItems = 0
// Try to migrate identity keys
for oldService in legacyServices {
// Check for noise identity key
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: oldService,
kSecAttrAccount as String: "identity_noiseStaticKey",
kSecReturnData as String: true
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
if status == errSecSuccess, let data = result as? Data {
// Save to new service
if saveIdentityKey(data, forKey: "noiseStaticKey") {
migratedItems += 1
}
// Delete from old service
let deleteQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: oldService,
kSecAttrAccount as String: "identity_noiseStaticKey"
]
SecItemDelete(deleteQuery as CFDictionary)
}
}
// Clean up all other legacy items
for oldService in legacyServices {
let deleteQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: oldService
]
SecItemDelete(deleteQuery as CFDictionary)
}
// Mark cleanup as done
UserDefaults.standard.set(true, forKey: cleanupKey)
}
private func isSandboxed() -> Bool {
#if os(macOS)
let environment = ProcessInfo.processInfo.environment
return environment["APP_SANDBOX_CONTAINER_ID"] != nil
#else
return false
#endif
}
// MARK: - Channel Passwords
@@ -37,17 +114,17 @@ class KeychainManager {
func getAllChannelPasswords() -> [String: String] {
var passwords: [String: String] = [:]
// Query all items
// Build query without kSecReturnData to avoid error -50
var query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecMatchLimit as String: kSecMatchLimitAll,
kSecReturnAttributes as String: true,
kSecReturnData as String: true
kSecReturnAttributes as String: true
]
if let accessGroup = accessGroup {
query[kSecAttrAccessGroup as String] = accessGroup
// For sandboxed apps, use the app group
if isSandboxed() {
query[kSecAttrAccessGroup as String] = appGroup
}
var result: AnyObject?
@@ -56,11 +133,12 @@ class KeychainManager {
if status == errSecSuccess, let items = result as? [[String: Any]] {
for item in items {
if let account = item[kSecAttrAccount as String] as? String,
account.hasPrefix("channel_"),
let data = item[kSecValueData as String] as? Data,
let password = String(data: data, encoding: .utf8) {
account.hasPrefix("channel_") {
// Now retrieve the actual password data for this specific item
let channel = String(account.dropFirst(8)) // Remove "channel_" prefix
passwords[channel] = password
if let password = getChannelPassword(for: channel) {
passwords[channel] = password
}
}
}
}
@@ -71,11 +149,17 @@ class KeychainManager {
// MARK: - Identity Keys
func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool {
return saveData(keyData, forKey: "identity_\(key)")
let fullKey = "identity_\(key)"
return saveData(keyData, forKey: fullKey)
}
func getIdentityKey(forKey key: String) -> Data? {
return retrieveData(forKey: "identity_\(key)")
let fullKey = "identity_\(key)"
return retrieveData(forKey: fullKey)
}
func deleteIdentityKey(forKey key: String) -> Bool {
return delete(forKey: "identity_\(key)")
}
// MARK: - Generic Operations
@@ -86,35 +170,43 @@ class KeychainManager {
}
private func saveData(_ data: Data, forKey key: String) -> Bool {
// First try to update existing
let updateQuery: [String: Any] = [
// Delete any existing item first to ensure clean state
_ = delete(forKey: key)
// Build query with all necessary attributes for sandboxed apps
var query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key
]
var mutableUpdateQuery = updateQuery
if let accessGroup = accessGroup {
mutableUpdateQuery[kSecAttrAccessGroup as String] = accessGroup
}
let updateAttributes: [String: Any] = [
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
kSecAttrService as String: service,
// Important for sandboxed apps: make it accessible when unlocked
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked
]
var status = SecItemUpdate(mutableUpdateQuery as CFDictionary, updateAttributes as CFDictionary)
// Add a label for easier debugging
query[kSecAttrLabel as String] = "bitchat-\(key)"
if status == errSecItemNotFound {
// Item doesn't exist, create it
var createQuery = mutableUpdateQuery
createQuery[kSecValueData as String] = data
createQuery[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly
status = SecItemAdd(createQuery as CFDictionary, nil)
// For sandboxed apps, use the app group for sharing between app instances
if isSandboxed() {
query[kSecAttrAccessGroup as String] = appGroup
}
return status == errSecSuccess
// For sandboxed macOS apps, we need to ensure the item is NOT synchronized
#if os(macOS)
query[kSecAttrSynchronizable as String] = false
#endif
let status = SecItemAdd(query as CFDictionary, nil)
if status == errSecSuccess {
return true
} else if status == -34018 {
SecurityLogger.logError(NSError(domain: "Keychain", code: -34018), context: "Missing keychain entitlement", category: SecurityLogger.keychain)
} else if status != errSecDuplicateItem {
SecurityLogger.logError(NSError(domain: "Keychain", code: Int(status)), context: "Error saving to keychain", category: SecurityLogger.keychain)
}
return false
}
private func retrieve(forKey key: String) -> String? {
@@ -125,35 +217,40 @@ class KeychainManager {
private func retrieveData(forKey key: String) -> Data? {
var query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
kSecAttrService as String: service,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
if let accessGroup = accessGroup {
query[kSecAttrAccessGroup as String] = accessGroup
// For sandboxed apps, use the app group
if isSandboxed() {
query[kSecAttrAccessGroup as String] = appGroup
}
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
if status == errSecSuccess, let data = result as? Data {
return data
if status == errSecSuccess {
return result as? Data
} else if status == -34018 {
SecurityLogger.logError(NSError(domain: "Keychain", code: -34018), context: "Missing keychain entitlement", category: SecurityLogger.keychain)
}
return nil
}
private func delete(forKey key: String) -> Bool {
// Build basic query
var query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key
kSecAttrAccount as String: key,
kSecAttrService as String: service
]
if let accessGroup = accessGroup {
query[kSecAttrAccessGroup as String] = accessGroup
// For sandboxed apps, use the app group
if isSandboxed() {
query[kSecAttrAccessGroup as String] = appGroup
}
let status = SecItemDelete(query as CFDictionary)
@@ -164,15 +261,182 @@ class KeychainManager {
func deleteAllPasswords() -> Bool {
var query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service
kSecClass as String: kSecClassGenericPassword
]
if let accessGroup = accessGroup {
query[kSecAttrAccessGroup as String] = accessGroup
// Add service if not empty
if !service.isEmpty {
query[kSecAttrService as String] = service
}
let status = SecItemDelete(query as CFDictionary)
return status == errSecSuccess || status == errSecItemNotFound
}
// Force cleanup to run again (for development/testing)
func resetCleanupFlag() {
UserDefaults.standard.removeObject(forKey: "bitchat.keychain.cleanup.v2")
}
// Delete ALL keychain data for panic mode
func deleteAllKeychainData() -> Bool {
var totalDeleted = 0
// Search without service restriction to catch all items
let searchQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecMatchLimit as String: kSecMatchLimitAll,
kSecReturnAttributes as String: true
]
var result: AnyObject?
let searchStatus = SecItemCopyMatching(searchQuery as CFDictionary, &result)
if searchStatus == errSecSuccess, let items = result as? [[String: Any]] {
for item in items {
var shouldDelete = false
let account = item[kSecAttrAccount as String] as? String ?? ""
let service = item[kSecAttrService as String] as? String ?? ""
// ONLY delete if service name contains "bitchat"
// This is the safest approach - we only touch items we know are ours
if service.lowercased().contains("bitchat") {
shouldDelete = true
}
if shouldDelete {
// Build delete query with all available attributes for precise deletion
var deleteQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword
]
if !account.isEmpty {
deleteQuery[kSecAttrAccount as String] = account
}
if !service.isEmpty {
deleteQuery[kSecAttrService as String] = service
}
// Add access group if present
if let accessGroup = item[kSecAttrAccessGroup as String] as? String,
!accessGroup.isEmpty && accessGroup != "test" {
deleteQuery[kSecAttrAccessGroup as String] = accessGroup
}
let deleteStatus = SecItemDelete(deleteQuery as CFDictionary)
if deleteStatus == errSecSuccess {
totalDeleted += 1
}
}
}
}
// Also try to delete by known service names (in case we missed any)
let knownServices = [
"chat.bitchat",
"com.bitchat.passwords",
"com.bitchat.deviceidentity",
"com.bitchat.noise.identity",
"chat.bitchat.passwords",
"bitchat.keychain",
"bitchat",
"com.bitchat"
]
for serviceName in knownServices {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: serviceName
]
let status = SecItemDelete(query as CFDictionary)
if status == errSecSuccess {
totalDeleted += 1
}
}
return totalDeleted > 0
}
// MARK: - Debug
func verifyIdentityKeyExists() -> Bool {
let key = "identity_noiseStaticKey"
return retrieveData(forKey: key) != nil
}
// Aggressive cleanup for legacy items - can be called manually
func aggressiveCleanupLegacyItems() -> Int {
var deletedCount = 0
// List of KNOWN bitchat service names from our development history
let knownBitchatServices = [
"com.bitchat.passwords",
"com.bitchat.deviceidentity",
"com.bitchat.noise.identity",
"chat.bitchat.passwords",
"bitchat.keychain",
"Bitchat",
"BitChat"
]
// First, delete all items from known legacy services
for legacyService in knownBitchatServices {
let deleteQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: legacyService
]
let status = SecItemDelete(deleteQuery as CFDictionary)
if status == errSecSuccess {
deletedCount += 1
}
}
// Now search for items that have our specific account patterns with bitchat service names
let searchQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecMatchLimit as String: kSecMatchLimitAll,
kSecReturnAttributes as String: true
]
var result: AnyObject?
let status = SecItemCopyMatching(searchQuery as CFDictionary, &result)
if status == errSecSuccess, let items = result as? [[String: Any]] {
for item in items {
let account = item[kSecAttrAccount as String] as? String ?? ""
let service = item[kSecAttrService as String] as? String ?? ""
// ONLY delete if service name contains "bitchat" somewhere
// This ensures we never touch other apps' keychain items
var shouldDelete = false
// Check if service contains "bitchat" (case insensitive) but NOT our current service
let serviceLower = service.lowercased()
if service != self.service && serviceLower.contains("bitchat") {
shouldDelete = true
}
if shouldDelete {
// Build precise delete query
let deleteQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account
]
let deleteStatus = SecItemDelete(deleteQuery as CFDictionary)
if deleteStatus == errSecSuccess {
deletedCount += 1
}
}
}
}
return deletedCount
}
}