mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 22:05:21 +00:00
Merge pull request #293 from permissionlesstech/enhance-logging-framework
Enhance logging framework and fix build issues
This commit is contained in:
+8
-1
@@ -17,7 +17,14 @@ let package = Package(
|
|||||||
targets: [
|
targets: [
|
||||||
.executableTarget(
|
.executableTarget(
|
||||||
name: "bitchat",
|
name: "bitchat",
|
||||||
path: "bitchat"
|
path: "bitchat",
|
||||||
|
exclude: [
|
||||||
|
"Info.plist",
|
||||||
|
"Assets.xcassets",
|
||||||
|
"bitchat.entitlements",
|
||||||
|
"bitchat-macOS.entitlements",
|
||||||
|
"LaunchScreen.storyboard"
|
||||||
|
]
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -12,6 +12,8 @@
|
|||||||
04636BBB2E2FAA1700FBCFA8 /* BinaryMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BB92E2FAA1700FBCFA8 /* BinaryMessageHandler.swift */; };
|
04636BBB2E2FAA1700FBCFA8 /* BinaryMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BB92E2FAA1700FBCFA8 /* BinaryMessageHandler.swift */; };
|
||||||
04636BBC2E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BB82E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift */; };
|
04636BBC2E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BB82E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift */; };
|
||||||
04636BBD2E2FAA1700FBCFA8 /* BinaryMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BB92E2FAA1700FBCFA8 /* BinaryMessageHandler.swift */; };
|
04636BBD2E2FAA1700FBCFA8 /* BinaryMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BB92E2FAA1700FBCFA8 /* BinaryMessageHandler.swift */; };
|
||||||
|
04636BBF2E2FCA8A00FBCFA8 /* SecureLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BBE2E2FCA8A00FBCFA8 /* SecureLogger.swift */; };
|
||||||
|
04636BC02E2FCA8A00FBCFA8 /* SecureLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BBE2E2FCA8A00FBCFA8 /* SecureLogger.swift */; };
|
||||||
04891CA92E22971E0064A111 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04891CA82E22971E0064A111 /* LRUCache.swift */; };
|
04891CA92E22971E0064A111 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04891CA82E22971E0064A111 /* LRUCache.swift */; };
|
||||||
04891CAA2E22971E0064A111 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04891CA82E22971E0064A111 /* LRUCache.swift */; };
|
04891CAA2E22971E0064A111 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04891CA82E22971E0064A111 /* LRUCache.swift */; };
|
||||||
04AD0B4E2E25B9580002A40A /* IdentityModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2446380E7A44E49A35B664 /* IdentityModels.swift */; };
|
04AD0B4E2E25B9580002A40A /* IdentityModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2446380E7A44E49A35B664 /* IdentityModels.swift */; };
|
||||||
@@ -61,9 +63,7 @@
|
|||||||
04B6BA752E21601B0090FE39 /* KeychainIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA602E21601B0090FE39 /* KeychainIntegrationTests.swift */; };
|
04B6BA752E21601B0090FE39 /* KeychainIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA602E21601B0090FE39 /* KeychainIntegrationTests.swift */; };
|
||||||
04B6BA762E21601B0090FE39 /* NoiseChannelEncryptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA612E21601B0090FE39 /* NoiseChannelEncryptionTests.swift */; };
|
04B6BA762E21601B0090FE39 /* NoiseChannelEncryptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA612E21601B0090FE39 /* NoiseChannelEncryptionTests.swift */; };
|
||||||
04B6BA792E2166A50090FE39 /* NoiseTestingHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA772E2166A50090FE39 /* NoiseTestingHelper.swift */; };
|
04B6BA792E2166A50090FE39 /* NoiseTestingHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA772E2166A50090FE39 /* NoiseTestingHelper.swift */; };
|
||||||
04B6BA7A2E2166A50090FE39 /* SecurityLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA782E2166A50090FE39 /* SecurityLogger.swift */; };
|
|
||||||
04B6BA7B2E2166A50090FE39 /* NoiseTestingHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA772E2166A50090FE39 /* NoiseTestingHelper.swift */; };
|
04B6BA7B2E2166A50090FE39 /* NoiseTestingHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA772E2166A50090FE39 /* NoiseTestingHelper.swift */; };
|
||||||
04B6BA7C2E2166A50090FE39 /* SecurityLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA782E2166A50090FE39 /* SecurityLogger.swift */; };
|
|
||||||
0DAFF1DDE9BA83FF648D5AB3 /* BitchatMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FA8FF26ABDC1C642A8C7AE5 /* BitchatMessageTests.swift */; };
|
0DAFF1DDE9BA83FF648D5AB3 /* BitchatMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FA8FF26ABDC1C642A8C7AE5 /* BitchatMessageTests.swift */; };
|
||||||
0FBC81FF78CF4711B78E092A /* IdentityModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2446380E7A44E49A35B664 /* IdentityModels.swift */; };
|
0FBC81FF78CF4711B78E092A /* IdentityModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2446380E7A44E49A35B664 /* IdentityModels.swift */; };
|
||||||
10E68BB889356219189E38EC /* BitchatApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF625BB3AD919322C01A46B2 /* BitchatApp.swift */; };
|
10E68BB889356219189E38EC /* BitchatApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF625BB3AD919322C01A46B2 /* BitchatApp.swift */; };
|
||||||
@@ -155,6 +155,7 @@
|
|||||||
03C57F452B55FD0FD8F51421 /* bitchatTests_macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = bitchatTests_macOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
03C57F452B55FD0FD8F51421 /* bitchatTests_macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = bitchatTests_macOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
04636BB82E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryEncodingUtils.swift; sourceTree = "<group>"; };
|
04636BB82E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryEncodingUtils.swift; sourceTree = "<group>"; };
|
||||||
04636BB92E2FAA1700FBCFA8 /* BinaryMessageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryMessageHandler.swift; sourceTree = "<group>"; };
|
04636BB92E2FAA1700FBCFA8 /* BinaryMessageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryMessageHandler.swift; sourceTree = "<group>"; };
|
||||||
|
04636BBE2E2FCA8A00FBCFA8 /* SecureLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureLogger.swift; sourceTree = "<group>"; };
|
||||||
04891CA82E22971E0064A111 /* LRUCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LRUCache.swift; sourceTree = "<group>"; };
|
04891CA82E22971E0064A111 /* LRUCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LRUCache.swift; sourceTree = "<group>"; };
|
||||||
04AD0B502E2678220002A40A /* BinaryProtocolVersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocolVersionTests.swift; sourceTree = "<group>"; };
|
04AD0B502E2678220002A40A /* BinaryProtocolVersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocolVersionTests.swift; sourceTree = "<group>"; };
|
||||||
04AD0B512E2678220002A40A /* ProtocolVersionNegotiationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolVersionNegotiationTests.swift; sourceTree = "<group>"; };
|
04AD0B512E2678220002A40A /* ProtocolVersionNegotiationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolVersionNegotiationTests.swift; sourceTree = "<group>"; };
|
||||||
@@ -179,7 +180,6 @@
|
|||||||
04B6BA652E21601B0090FE39 /* NoiseSecurityValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseSecurityValidatorTests.swift; sourceTree = "<group>"; };
|
04B6BA652E21601B0090FE39 /* NoiseSecurityValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseSecurityValidatorTests.swift; sourceTree = "<group>"; };
|
||||||
04B6BA662E21601B0090FE39 /* SecureNoiseSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureNoiseSessionTests.swift; sourceTree = "<group>"; };
|
04B6BA662E21601B0090FE39 /* SecureNoiseSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureNoiseSessionTests.swift; sourceTree = "<group>"; };
|
||||||
04B6BA772E2166A50090FE39 /* NoiseTestingHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseTestingHelper.swift; sourceTree = "<group>"; };
|
04B6BA772E2166A50090FE39 /* NoiseTestingHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseTestingHelper.swift; sourceTree = "<group>"; };
|
||||||
04B6BA782E2166A50090FE39 /* SecurityLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityLogger.swift; sourceTree = "<group>"; };
|
|
||||||
12B9C3EDF3BC73D3BC106DA4 /* DeliveryTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeliveryTracker.swift; sourceTree = "<group>"; };
|
12B9C3EDF3BC73D3BC106DA4 /* DeliveryTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeliveryTracker.swift; sourceTree = "<group>"; };
|
||||||
136696FC4436A02D98CE6A77 /* KeychainManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainManager.swift; sourceTree = "<group>"; };
|
136696FC4436A02D98CE6A77 /* KeychainManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainManager.swift; sourceTree = "<group>"; };
|
||||||
1EB3A8FE16333ED12FCB8ACB /* BloomFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloomFilterTests.swift; sourceTree = "<group>"; };
|
1EB3A8FE16333ED12FCB8ACB /* BloomFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloomFilterTests.swift; sourceTree = "<group>"; };
|
||||||
@@ -279,9 +279,9 @@
|
|||||||
9A78348821A7D3374607D4E3 /* Utils */ = {
|
9A78348821A7D3374607D4E3 /* Utils */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
04636BBE2E2FCA8A00FBCFA8 /* SecureLogger.swift */,
|
||||||
04891CA82E22971E0064A111 /* LRUCache.swift */,
|
04891CA82E22971E0064A111 /* LRUCache.swift */,
|
||||||
04B6BA772E2166A50090FE39 /* NoiseTestingHelper.swift */,
|
04B6BA772E2166A50090FE39 /* NoiseTestingHelper.swift */,
|
||||||
04B6BA782E2166A50090FE39 /* SecurityLogger.swift */,
|
|
||||||
ED176FF3B274E35C2D827894 /* BatteryOptimizer.swift */,
|
ED176FF3B274E35C2D827894 /* BatteryOptimizer.swift */,
|
||||||
32F149C43D1915831B60FE09 /* CompressionUtil.swift */,
|
32F149C43D1915831B60FE09 /* CompressionUtil.swift */,
|
||||||
CB043CA5EEB9AC8B07D61E97 /* OptimizedBloomFilter.swift */,
|
CB043CA5EEB9AC8B07D61E97 /* OptimizedBloomFilter.swift */,
|
||||||
@@ -572,7 +572,6 @@
|
|||||||
04B6BA4F2E2038A70090FE39 /* NoiseEncryptionService.swift in Sources */,
|
04B6BA4F2E2038A70090FE39 /* NoiseEncryptionService.swift in Sources */,
|
||||||
8DCEEA289EF7C49E7CD38B08 /* DeliveryTracker.swift in Sources */,
|
8DCEEA289EF7C49E7CD38B08 /* DeliveryTracker.swift in Sources */,
|
||||||
04B6BA792E2166A50090FE39 /* NoiseTestingHelper.swift in Sources */,
|
04B6BA792E2166A50090FE39 /* NoiseTestingHelper.swift in Sources */,
|
||||||
04B6BA7A2E2166A50090FE39 /* SecurityLogger.swift in Sources */,
|
|
||||||
FB8819B4C84FAFEF5C36B216 /* KeychainManager.swift in Sources */,
|
FB8819B4C84FAFEF5C36B216 /* KeychainManager.swift in Sources */,
|
||||||
04636BBA2E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift in Sources */,
|
04636BBA2E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift in Sources */,
|
||||||
04636BBB2E2FAA1700FBCFA8 /* BinaryMessageHandler.swift in Sources */,
|
04636BBB2E2FAA1700FBCFA8 /* BinaryMessageHandler.swift in Sources */,
|
||||||
@@ -583,6 +582,7 @@
|
|||||||
04B6BA582E203D6C0090FE39 /* NoiseTestingView.swift in Sources */,
|
04B6BA582E203D6C0090FE39 /* NoiseTestingView.swift in Sources */,
|
||||||
C99763A4761567F587D21688 /* MessageRetryService.swift in Sources */,
|
C99763A4761567F587D21688 /* MessageRetryService.swift in Sources */,
|
||||||
749D8CF8A362B6CD0786782D /* NotificationService.swift in Sources */,
|
749D8CF8A362B6CD0786782D /* NotificationService.swift in Sources */,
|
||||||
|
04636BBF2E2FCA8A00FBCFA8 /* SecureLogger.swift in Sources */,
|
||||||
0245710AEAA58AD0A1425234 /* OptimizedBloomFilter.swift in Sources */,
|
0245710AEAA58AD0A1425234 /* OptimizedBloomFilter.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -609,7 +609,6 @@
|
|||||||
04B6BA4E2E2038A70090FE39 /* NoiseEncryptionService.swift in Sources */,
|
04B6BA4E2E2038A70090FE39 /* NoiseEncryptionService.swift in Sources */,
|
||||||
CD0AE423F03AC52BAFC16834 /* DeliveryTracker.swift in Sources */,
|
CD0AE423F03AC52BAFC16834 /* DeliveryTracker.swift in Sources */,
|
||||||
04B6BA7B2E2166A50090FE39 /* NoiseTestingHelper.swift in Sources */,
|
04B6BA7B2E2166A50090FE39 /* NoiseTestingHelper.swift in Sources */,
|
||||||
04B6BA7C2E2166A50090FE39 /* SecurityLogger.swift in Sources */,
|
|
||||||
8F737CE0435792CC2AD65FCB /* KeychainManager.swift in Sources */,
|
8F737CE0435792CC2AD65FCB /* KeychainManager.swift in Sources */,
|
||||||
04636BBC2E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift in Sources */,
|
04636BBC2E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift in Sources */,
|
||||||
04636BBD2E2FAA1700FBCFA8 /* BinaryMessageHandler.swift in Sources */,
|
04636BBD2E2FAA1700FBCFA8 /* BinaryMessageHandler.swift in Sources */,
|
||||||
@@ -620,6 +619,7 @@
|
|||||||
04B6BA562E203D6C0090FE39 /* NoiseTestingView.swift in Sources */,
|
04B6BA562E203D6C0090FE39 /* NoiseTestingView.swift in Sources */,
|
||||||
CEAE115C9C3EB3C4ED82F128 /* MessageRetryService.swift in Sources */,
|
CEAE115C9C3EB3C4ED82F128 /* MessageRetryService.swift in Sources */,
|
||||||
61C81ED5F679D5E973EE0C07 /* NotificationService.swift in Sources */,
|
61C81ED5F679D5E973EE0C07 /* NotificationService.swift in Sources */,
|
||||||
|
04636BC02E2FCA8A00FBCFA8 /* SecureLogger.swift in Sources */,
|
||||||
1F48A8CEEE9399D1EBD08F0C /* OptimizedBloomFilter.swift in Sources */,
|
1F48A8CEEE9399D1EBD08F0C /* OptimizedBloomFilter.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
|||||||
@@ -37,13 +37,15 @@ class SecureIdentityStateManager {
|
|||||||
// Try to load from keychain
|
// Try to load from keychain
|
||||||
if let keyData = keychain.getIdentityKey(forKey: encryptionKeyName) {
|
if let keyData = keychain.getIdentityKey(forKey: encryptionKeyName) {
|
||||||
loadedKey = SymmetricKey(data: keyData)
|
loadedKey = SymmetricKey(data: keyData)
|
||||||
|
SecureLogger.logKeyOperation("load", keyType: "identity cache encryption key", success: true)
|
||||||
}
|
}
|
||||||
// Generate new key if needed
|
// Generate new key if needed
|
||||||
else {
|
else {
|
||||||
loadedKey = SymmetricKey(size: .bits256)
|
loadedKey = SymmetricKey(size: .bits256)
|
||||||
let keyData = loadedKey.withUnsafeBytes { Data($0) }
|
let keyData = loadedKey.withUnsafeBytes { Data($0) }
|
||||||
// Save to keychain
|
// Save to keychain
|
||||||
_ = keychain.saveIdentityKey(keyData, forKey: encryptionKeyName)
|
let saved = keychain.saveIdentityKey(keyData, forKey: encryptionKeyName)
|
||||||
|
SecureLogger.logKeyOperation("generate", keyType: "identity cache encryption key", success: saved)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.encryptionKey = loadedKey
|
self.encryptionKey = loadedKey
|
||||||
@@ -66,7 +68,7 @@ class SecureIdentityStateManager {
|
|||||||
cache = try JSONDecoder().decode(IdentityCache.self, from: decryptedData)
|
cache = try JSONDecoder().decode(IdentityCache.self, from: decryptedData)
|
||||||
} catch {
|
} catch {
|
||||||
// Log error but continue with empty cache
|
// Log error but continue with empty cache
|
||||||
SecurityLogger.log("Failed to load identity cache", category: SecurityLogger.security, level: .error)
|
SecureLogger.logError(error, context: "Failed to load identity cache", category: SecureLogger.security)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +78,7 @@ class SecureIdentityStateManager {
|
|||||||
let sealedBox = try AES.GCM.seal(data, using: encryptionKey)
|
let sealedBox = try AES.GCM.seal(data, using: encryptionKey)
|
||||||
_ = keychain.saveIdentityKey(sealedBox.combined!, forKey: cacheKey)
|
_ = keychain.saveIdentityKey(sealedBox.combined!, forKey: cacheKey)
|
||||||
} catch {
|
} catch {
|
||||||
SecurityLogger.log("Failed to save identity cache", category: SecurityLogger.security, level: .error)
|
SecureLogger.logError(error, context: "Failed to save identity cache", category: SecureLogger.security)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,6 +186,8 @@ class SecureIdentityStateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func setBlocked(_ fingerprint: String, isBlocked: Bool) {
|
func setBlocked(_ fingerprint: String, isBlocked: Bool) {
|
||||||
|
SecureLogger.log("User \(isBlocked ? "blocked" : "unblocked"): \(fingerprint)", category: SecureLogger.security, level: .info)
|
||||||
|
|
||||||
queue.async(flags: .barrier) {
|
queue.async(flags: .barrier) {
|
||||||
if var identity = self.cache.socialIdentities[fingerprint] {
|
if var identity = self.cache.socialIdentities[fingerprint] {
|
||||||
identity.isBlocked = isBlocked
|
identity.isBlocked = isBlocked
|
||||||
@@ -282,6 +286,8 @@ class SecureIdentityStateManager {
|
|||||||
// MARK: - Cleanup
|
// MARK: - Cleanup
|
||||||
|
|
||||||
func clearAllIdentityData() {
|
func clearAllIdentityData() {
|
||||||
|
SecureLogger.logSecurityEvent(.invalidKey(reason: "Clearing all identity data"), level: .warning)
|
||||||
|
|
||||||
queue.async(flags: .barrier) {
|
queue.async(flags: .barrier) {
|
||||||
self.cache = IdentityCache()
|
self.cache = IdentityCache()
|
||||||
self.ephemeralSessions.removeAll()
|
self.ephemeralSessions.removeAll()
|
||||||
@@ -289,7 +295,8 @@ class SecureIdentityStateManager {
|
|||||||
self.pendingActions.removeAll()
|
self.pendingActions.removeAll()
|
||||||
|
|
||||||
// Delete from keychain
|
// Delete from keychain
|
||||||
_ = self.keychain.deleteIdentityKey(forKey: self.cacheKey)
|
let deleted = self.keychain.deleteIdentityKey(forKey: self.cacheKey)
|
||||||
|
SecureLogger.logKeyOperation("delete", keyType: "identity cache", success: deleted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,6 +310,8 @@ class SecureIdentityStateManager {
|
|||||||
// MARK: - Verification
|
// MARK: - Verification
|
||||||
|
|
||||||
func setVerified(fingerprint: String, verified: Bool) {
|
func setVerified(fingerprint: String, verified: Bool) {
|
||||||
|
SecureLogger.log("Fingerprint \(verified ? "verified" : "unverified"): \(fingerprint)", category: SecureLogger.security, level: .info)
|
||||||
|
|
||||||
queue.async(flags: .barrier) {
|
queue.async(flags: .barrier) {
|
||||||
if verified {
|
if verified {
|
||||||
self.cache.verifiedFingerprints.insert(fingerprint)
|
self.cache.verifiedFingerprints.insert(fingerprint)
|
||||||
|
|||||||
@@ -58,7 +58,10 @@ class NoiseChannelEncryption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Store in keychain
|
// Store in keychain
|
||||||
_ = KeychainManager.shared.saveChannelPassword(password, for: channel)
|
let saved = KeychainManager.shared.saveChannelPassword(password, for: channel)
|
||||||
|
if saved {
|
||||||
|
SecureLogger.logKeyOperation("set", keyType: "channel key for \(channel)", success: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get channel key
|
/// Get channel key
|
||||||
@@ -84,7 +87,10 @@ class NoiseChannelEncryption {
|
|||||||
self.channelKeys.removeValue(forKey: channel)
|
self.channelKeys.removeValue(forKey: channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = KeychainManager.shared.deleteChannelPassword(for: channel)
|
let deleted = KeychainManager.shared.deleteChannelPassword(for: channel)
|
||||||
|
if deleted {
|
||||||
|
SecureLogger.logKeyOperation("remove", keyType: "channel key for \(channel)", success: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Replay Protection
|
// MARK: - Replay Protection
|
||||||
@@ -119,6 +125,7 @@ class NoiseChannelEncryption {
|
|||||||
/// Encrypt message for a channel
|
/// Encrypt message for a channel
|
||||||
func encryptChannelMessage(_ message: String, for channel: String) throws -> Data {
|
func encryptChannelMessage(_ message: String, for channel: String) throws -> Data {
|
||||||
guard let key = getChannelKey(for: channel) else {
|
guard let key = getChannelKey(for: channel) else {
|
||||||
|
SecureLogger.log("Channel encryption failed - no key for channel: \(channel)", category: SecureLogger.encryption, level: .error)
|
||||||
throw NoiseChannelError.noChannelKey
|
throw NoiseChannelError.noChannelKey
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +144,7 @@ class NoiseChannelEncryption {
|
|||||||
/// Decrypt channel message
|
/// Decrypt channel message
|
||||||
func decryptChannelMessage(_ encryptedData: Data, for channel: String) throws -> String {
|
func decryptChannelMessage(_ encryptedData: Data, for channel: String) throws -> String {
|
||||||
guard let key = getChannelKey(for: channel) else {
|
guard let key = getChannelKey(for: channel) else {
|
||||||
|
SecureLogger.log("Channel decryption failed - no key for channel: \(channel)", category: SecureLogger.encryption, level: .error)
|
||||||
throw NoiseChannelError.noChannelKey
|
throw NoiseChannelError.noChannelKey
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,6 +165,7 @@ class NoiseChannelEncryption {
|
|||||||
let decryptedData = try ChaChaPoly.open(sealedBox, using: key)
|
let decryptedData = try ChaChaPoly.open(sealedBox, using: key)
|
||||||
|
|
||||||
guard let message = String(data: decryptedData, encoding: .utf8) else {
|
guard let message = String(data: decryptedData, encoding: .utf8) else {
|
||||||
|
SecureLogger.log("Channel decryption failed - invalid UTF8 for channel: \(channel)", category: SecureLogger.encryption, level: .error)
|
||||||
throw NoiseChannelError.decryptionFailed
|
throw NoiseChannelError.decryptionFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,12 +201,15 @@ class NoiseChannelEncryption {
|
|||||||
|
|
||||||
// Verify timestamp is recent (within 5 minutes)
|
// Verify timestamp is recent (within 5 minutes)
|
||||||
let age = Date().timeIntervalSince(packet.timestamp)
|
let age = Date().timeIntervalSince(packet.timestamp)
|
||||||
guard age < 300 else { return nil }
|
guard age < 300 else {
|
||||||
|
SecureLogger.log("Expired channel key packet for channel: \(packet.channel), age: \(age)s", category: SecureLogger.security, level: .warning)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
return keyQueue.sync(flags: .barrier) {
|
return keyQueue.sync(flags: .barrier) {
|
||||||
// Check for replay attack
|
// Check for replay attack
|
||||||
if receivedNonces.contains(packet.nonce) {
|
if receivedNonces.contains(packet.nonce) {
|
||||||
SecurityLogger.logSecurityEvent(.replayAttackDetected(channel: packet.channel), level: .warning)
|
SecureLogger.logSecurityEvent(.replayAttackDetected(channel: packet.channel), level: .warning)
|
||||||
return nil // This nonce was already processed
|
return nil // This nonce was already processed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,8 @@ class NoiseCipherState {
|
|||||||
nonce += 1
|
nonce += 1
|
||||||
|
|
||||||
// Log high nonce values that might indicate issues
|
// Log high nonce values that might indicate issues
|
||||||
if currentNonce > 100 {
|
if currentNonce > 1000000 {
|
||||||
|
SecureLogger.log("High nonce value detected: \(currentNonce) - consider rekeying", category: SecureLogger.encryption, level: .warning)
|
||||||
}
|
}
|
||||||
|
|
||||||
return sealedBox.ciphertext + sealedBox.tag
|
return sealedBox.ciphertext + sealedBox.tag
|
||||||
@@ -122,7 +123,8 @@ class NoiseCipherState {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Log high nonce values that might indicate issues
|
// Log high nonce values that might indicate issues
|
||||||
if currentNonce > 100 {
|
if currentNonce > 1000000 {
|
||||||
|
SecureLogger.log("High nonce value detected: \(currentNonce) - consider rekeying", category: SecureLogger.encryption, level: .warning)
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
@@ -131,6 +133,7 @@ class NoiseCipherState {
|
|||||||
return plaintext
|
return plaintext
|
||||||
} catch {
|
} catch {
|
||||||
// Log authentication failures with nonce info
|
// Log authentication failures with nonce info
|
||||||
|
SecureLogger.log("Decryption failed at nonce \(currentNonce)", category: SecureLogger.encryption, level: .error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -404,6 +407,7 @@ class NoiseHandshakeState {
|
|||||||
do {
|
do {
|
||||||
remoteEphemeralPublic = try NoiseHandshakeState.validatePublicKey(ephemeralData)
|
remoteEphemeralPublic = try NoiseHandshakeState.validatePublicKey(ephemeralData)
|
||||||
} catch {
|
} catch {
|
||||||
|
SecureLogger.log("Invalid ephemeral public key received", category: SecureLogger.security, level: .warning)
|
||||||
throw NoiseError.invalidMessage
|
throw NoiseError.invalidMessage
|
||||||
}
|
}
|
||||||
symmetricState.mixHash(ephemeralData)
|
symmetricState.mixHash(ephemeralData)
|
||||||
@@ -420,6 +424,7 @@ class NoiseHandshakeState {
|
|||||||
let decrypted = try symmetricState.decryptAndHash(staticData)
|
let decrypted = try symmetricState.decryptAndHash(staticData)
|
||||||
remoteStaticPublic = try NoiseHandshakeState.validatePublicKey(decrypted)
|
remoteStaticPublic = try NoiseHandshakeState.validatePublicKey(decrypted)
|
||||||
} catch {
|
} catch {
|
||||||
|
SecureLogger.logSecurityEvent(.authenticationFailed(peerID: "Unknown - handshake"), level: .error)
|
||||||
throw NoiseError.authenticationFailure
|
throw NoiseError.authenticationFailure
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -604,7 +609,7 @@ extension NoiseHandshakeState {
|
|||||||
|
|
||||||
// Check against known bad points
|
// Check against known bad points
|
||||||
if lowOrderPoints.contains(keyData) {
|
if lowOrderPoints.contains(keyData) {
|
||||||
SecurityLogger.logSecurityEvent(.invalidKey(reason: "Low-order point detected"), level: .warning)
|
SecureLogger.logSecurityEvent(.invalidKey(reason: "Low-order point detected"), level: .warning)
|
||||||
throw NoiseError.invalidPublicKey
|
throw NoiseError.invalidPublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -614,6 +619,7 @@ extension NoiseHandshakeState {
|
|||||||
return publicKey
|
return publicKey
|
||||||
} catch {
|
} catch {
|
||||||
// If CryptoKit rejects it, it's invalid
|
// If CryptoKit rejects it, it's invalid
|
||||||
|
SecureLogger.logSecurityEvent(.invalidKey(reason: "CryptoKit validation failed"), level: .warning)
|
||||||
throw NoiseError.invalidPublicKey
|
throw NoiseError.invalidPublicKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ class NoiseRateLimiter {
|
|||||||
// Check global rate limit first
|
// Check global rate limit first
|
||||||
globalHandshakeTimestamps = globalHandshakeTimestamps.filter { $0 > oneMinuteAgo }
|
globalHandshakeTimestamps = globalHandshakeTimestamps.filter { $0 > oneMinuteAgo }
|
||||||
if globalHandshakeTimestamps.count >= NoiseSecurityConstants.maxGlobalHandshakesPerMinute {
|
if globalHandshakeTimestamps.count >= NoiseSecurityConstants.maxGlobalHandshakesPerMinute {
|
||||||
|
SecureLogger.log("Global handshake rate limit exceeded: \(globalHandshakeTimestamps.count)/\(NoiseSecurityConstants.maxGlobalHandshakesPerMinute) per minute", category: SecureLogger.security, level: .warning)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,6 +176,7 @@ class NoiseRateLimiter {
|
|||||||
timestamps = timestamps.filter { $0 > oneMinuteAgo }
|
timestamps = timestamps.filter { $0 > oneMinuteAgo }
|
||||||
|
|
||||||
if timestamps.count >= NoiseSecurityConstants.maxHandshakesPerMinute {
|
if timestamps.count >= NoiseSecurityConstants.maxHandshakesPerMinute {
|
||||||
|
SecureLogger.log("Per-peer handshake rate limit exceeded for \(peerID): \(timestamps.count)/\(NoiseSecurityConstants.maxHandshakesPerMinute) per minute", category: SecureLogger.security, level: .warning)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,6 +196,7 @@ class NoiseRateLimiter {
|
|||||||
// Check global rate limit first
|
// Check global rate limit first
|
||||||
globalMessageTimestamps = globalMessageTimestamps.filter { $0 > oneSecondAgo }
|
globalMessageTimestamps = globalMessageTimestamps.filter { $0 > oneSecondAgo }
|
||||||
if globalMessageTimestamps.count >= NoiseSecurityConstants.maxGlobalMessagesPerSecond {
|
if globalMessageTimestamps.count >= NoiseSecurityConstants.maxGlobalMessagesPerSecond {
|
||||||
|
SecureLogger.log("Global message rate limit exceeded: \(globalMessageTimestamps.count)/\(NoiseSecurityConstants.maxGlobalMessagesPerSecond) per second", category: SecureLogger.security, level: .warning)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,6 +205,7 @@ class NoiseRateLimiter {
|
|||||||
timestamps = timestamps.filter { $0 > oneSecondAgo }
|
timestamps = timestamps.filter { $0 > oneSecondAgo }
|
||||||
|
|
||||||
if timestamps.count >= NoiseSecurityConstants.maxMessagesPerSecond {
|
if timestamps.count >= NoiseSecurityConstants.maxMessagesPerSecond {
|
||||||
|
SecureLogger.log("Per-peer message rate limit exceeded for \(peerID): \(timestamps.count)/\(NoiseSecurityConstants.maxMessagesPerSecond) per second", category: SecureLogger.security, level: .warning)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,8 @@ class NoiseSession {
|
|||||||
state = .established
|
state = .established
|
||||||
handshakeState = nil // Clear handshake state
|
handshakeState = nil // Clear handshake state
|
||||||
|
|
||||||
|
SecureLogger.logSecurityEvent(.handshakeCompleted(peerID: peerID))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
// Generate response
|
// Generate response
|
||||||
@@ -148,6 +150,8 @@ class NoiseSession {
|
|||||||
|
|
||||||
state = .established
|
state = .established
|
||||||
handshakeState = nil // Clear handshake state
|
handshakeState = nil // Clear handshake state
|
||||||
|
|
||||||
|
SecureLogger.logSecurityEvent(.handshakeCompleted(peerID: peerID))
|
||||||
}
|
}
|
||||||
|
|
||||||
return response
|
return response
|
||||||
@@ -208,12 +212,17 @@ class NoiseSession {
|
|||||||
|
|
||||||
func reset() {
|
func reset() {
|
||||||
sessionQueue.sync(flags: .barrier) {
|
sessionQueue.sync(flags: .barrier) {
|
||||||
|
let wasEstablished = state == .established
|
||||||
state = .uninitialized
|
state = .uninitialized
|
||||||
handshakeState = nil
|
handshakeState = nil
|
||||||
sendCipher = nil
|
sendCipher = nil
|
||||||
receiveCipher = nil
|
receiveCipher = nil
|
||||||
sentHandshakeMessages.removeAll()
|
sentHandshakeMessages.removeAll()
|
||||||
handshakeHash = nil
|
handshakeHash = nil
|
||||||
|
|
||||||
|
if wasEstablished {
|
||||||
|
SecureLogger.logSecurityEvent(.sessionExpired(peerID: peerID))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -255,6 +264,9 @@ class NoiseSessionManager {
|
|||||||
|
|
||||||
func removeSession(for peerID: String) {
|
func removeSession(for peerID: String) {
|
||||||
managerQueue.sync(flags: .barrier) {
|
managerQueue.sync(flags: .barrier) {
|
||||||
|
if let session = sessions[peerID], session.isEstablished() {
|
||||||
|
SecureLogger.logSecurityEvent(.sessionExpired(peerID: peerID))
|
||||||
|
}
|
||||||
_ = sessions.removeValue(forKey: peerID)
|
_ = sessions.removeValue(forKey: peerID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -267,7 +279,7 @@ class NoiseSessionManager {
|
|||||||
sessions[newPeerID] = session
|
sessions[newPeerID] = session
|
||||||
_ = sessions.removeValue(forKey: oldPeerID)
|
_ = sessions.removeValue(forKey: oldPeerID)
|
||||||
|
|
||||||
SecurityLogger.log("Migrated Noise session from \(oldPeerID) to \(newPeerID)", category: SecurityLogger.noise, level: .info)
|
SecureLogger.log("Migrated Noise session from \(oldPeerID) to \(newPeerID)", category: SecureLogger.noise, level: .info)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -307,6 +319,7 @@ class NoiseSessionManager {
|
|||||||
} catch {
|
} catch {
|
||||||
// Clean up failed session
|
// Clean up failed session
|
||||||
_ = sessions.removeValue(forKey: peerID)
|
_ = sessions.removeValue(forKey: peerID)
|
||||||
|
SecureLogger.logSecurityEvent(.handshakeFailed(peerID: peerID, error: error.localizedDescription), level: .error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -323,6 +336,7 @@ class NoiseSessionManager {
|
|||||||
if existing.isEstablished() {
|
if existing.isEstablished() {
|
||||||
// Don't destroy our working session just because the other side is confused
|
// Don't destroy our working session just because the other side is confused
|
||||||
// They should detect the established session through successful message exchange
|
// They should detect the established session through successful message exchange
|
||||||
|
SecureLogger.log("Rejecting handshake attempt - session already established with \(peerID)", category: SecureLogger.session, level: .debug)
|
||||||
throw NoiseSessionError.alreadyEstablished
|
throw NoiseSessionError.alreadyEstablished
|
||||||
} else {
|
} else {
|
||||||
// If we're in the middle of a handshake and receive a new initiation,
|
// If we're in the middle of a handshake and receive a new initiation,
|
||||||
@@ -376,6 +390,7 @@ class NoiseSessionManager {
|
|||||||
self?.onSessionFailed?(peerID, error)
|
self?.onSessionFailed?(peerID, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SecureLogger.logSecurityEvent(.handshakeFailed(peerID: peerID, error: error.localizedDescription), level: .error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -428,6 +443,8 @@ class NoiseSessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func initiateRekey(for peerID: String) throws {
|
func initiateRekey(for peerID: String) throws {
|
||||||
|
SecureLogger.logSecurityEvent(.keyRotation(channel: peerID))
|
||||||
|
|
||||||
// Remove old session
|
// Remove old session
|
||||||
removeSession(for: peerID)
|
removeSession(for: peerID)
|
||||||
|
|
||||||
|
|||||||
@@ -299,6 +299,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
// Remove old peer ID mapping if exists
|
// Remove old peer ID mapping if exists
|
||||||
if let existingPeerID = self.fingerprintToPeerID[fingerprint], existingPeerID != newPeerID {
|
if let existingPeerID = self.fingerprintToPeerID[fingerprint], existingPeerID != newPeerID {
|
||||||
oldPeerID = existingPeerID
|
oldPeerID = existingPeerID
|
||||||
|
SecureLogger.log("Peer ID rotation detected: \(existingPeerID) -> \(newPeerID) for fingerprint \(fingerprint)", category: SecureLogger.security, level: .info)
|
||||||
|
|
||||||
self.peerIDToFingerprint.removeValue(forKey: existingPeerID)
|
self.peerIDToFingerprint.removeValue(forKey: existingPeerID)
|
||||||
|
|
||||||
@@ -421,6 +422,8 @@ class BluetoothMeshService: NSObject {
|
|||||||
// Generate new peer ID
|
// Generate new peer ID
|
||||||
self.myPeerID = self.generateNewPeerID()
|
self.myPeerID = self.generateNewPeerID()
|
||||||
|
|
||||||
|
SecureLogger.log("Peer ID rotated from \(oldID) to \(self.myPeerID)", category: SecureLogger.security, level: .info)
|
||||||
|
|
||||||
// Update advertising with new peer ID
|
// Update advertising with new peer ID
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
self?.updateAdvertisement()
|
self?.updateAdvertisement()
|
||||||
@@ -904,7 +907,6 @@ class BluetoothMeshService: NSObject {
|
|||||||
|
|
||||||
// Encode the ACK
|
// Encode the ACK
|
||||||
let ackData = ack.toBinaryData()
|
let ackData = ack.toBinaryData()
|
||||||
print("📤 Sending delivery ACK for message \(ack.originalMessageID) to \(recipientID) - binary size: \(ackData.count)")
|
|
||||||
|
|
||||||
// Check if we have a Noise session with this peer
|
// Check if we have a Noise session with this peer
|
||||||
// Use noiseService directly
|
// Use noiseService directly
|
||||||
@@ -933,8 +935,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
|
|
||||||
self.broadcastPacket(outerPacket)
|
self.broadcastPacket(outerPacket)
|
||||||
} catch {
|
} catch {
|
||||||
SecurityLogger.log("Failed to encrypt delivery ACK via Noise for \(recipientID): \(error)",
|
SecureLogger.logError(error, context: "Failed to encrypt delivery ACK via Noise for \(recipientID)", category: SecureLogger.encryption)
|
||||||
category: SecurityLogger.encryption, level: .error)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fall back to legacy encryption
|
// Fall back to legacy encryption
|
||||||
@@ -942,8 +943,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
do {
|
do {
|
||||||
encryptedPayload = try self.noiseService.encrypt(ackData, for: recipientID)
|
encryptedPayload = try self.noiseService.encrypt(ackData, for: recipientID)
|
||||||
} catch {
|
} catch {
|
||||||
SecurityLogger.log("Failed to encrypt delivery ACK for \(recipientID): \(error)",
|
SecureLogger.logError(error, context: "Failed to encrypt delivery ACK for \(recipientID)", category: SecureLogger.encryption)
|
||||||
category: SecurityLogger.encryption, level: .error)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1005,8 +1005,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
self.broadcastPacket(outerPacket)
|
self.broadcastPacket(outerPacket)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
SecurityLogger.log("Failed to encrypt read receipt via Noise for \(recipientID): \(error)",
|
SecureLogger.logError(error, context: "Failed to encrypt read receipt via Noise for \(recipientID)", category: SecureLogger.encryption)
|
||||||
category: SecurityLogger.encryption, level: .error)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fall back to legacy encryption
|
// Fall back to legacy encryption
|
||||||
@@ -1203,6 +1202,8 @@ class BluetoothMeshService: NSObject {
|
|||||||
|
|
||||||
// Emergency disconnect for panic situations
|
// Emergency disconnect for panic situations
|
||||||
func emergencyDisconnectAll() {
|
func emergencyDisconnectAll() {
|
||||||
|
SecureLogger.logSecurityEvent(.invalidKey(reason: "Emergency disconnect triggered"), level: .warning)
|
||||||
|
|
||||||
// Stop advertising immediately
|
// Stop advertising immediately
|
||||||
if peripheralManager?.isAdvertising == true {
|
if peripheralManager?.isAdvertising == true {
|
||||||
peripheralManager?.stopAdvertising()
|
peripheralManager?.stopAdvertising()
|
||||||
@@ -1214,7 +1215,8 @@ class BluetoothMeshService: NSObject {
|
|||||||
scanDutyCycleTimer = nil
|
scanDutyCycleTimer = nil
|
||||||
|
|
||||||
// Disconnect all peripherals
|
// Disconnect all peripherals
|
||||||
for (_, peripheral) in connectedPeripherals {
|
for (peerID, peripheral) in connectedPeripherals {
|
||||||
|
SecureLogger.log("Emergency disconnect peer: \(peerID)", category: SecureLogger.session, level: .warning)
|
||||||
centralManager?.cancelPeripheralConnection(peripheral)
|
centralManager?.cancelPeripheralConnection(peripheral)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1662,6 +1664,8 @@ class BluetoothMeshService: NSObject {
|
|||||||
let currentTime = UInt64(Date().timeIntervalSince1970 * 1000) // milliseconds
|
let currentTime = UInt64(Date().timeIntervalSince1970 * 1000) // milliseconds
|
||||||
let timeDiff = abs(Int64(currentTime) - Int64(packet.timestamp))
|
let timeDiff = abs(Int64(currentTime) - Int64(packet.timestamp))
|
||||||
if timeDiff > 300000 { // 5 minutes in milliseconds
|
if timeDiff > 300000 { // 5 minutes in milliseconds
|
||||||
|
SecureLogger.logSecurityEvent(.replayAttackDetected(channel: "timestamp-\(senderID)"), level: .warning)
|
||||||
|
SecureLogger.log("Dropped message with stale timestamp. Age: \(timeDiff/1000)s from \(senderID)", category: SecureLogger.security, level: .warning)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1681,9 +1685,11 @@ class BluetoothMeshService: NSObject {
|
|||||||
if messageBloomFilter.contains(messageID) {
|
if messageBloomFilter.contains(messageID) {
|
||||||
// Also check exact set for accuracy (bloom filter can have false positives)
|
// Also check exact set for accuracy (bloom filter can have false positives)
|
||||||
if processedMessages.contains(messageID) {
|
if processedMessages.contains(messageID) {
|
||||||
|
SecureLogger.log("Dropped duplicate message: \(messageID.prefix(20))... from \(senderID)", category: SecureLogger.security, level: .debug)
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
// False positive from Bloom filter
|
// False positive from Bloom filter
|
||||||
|
SecureLogger.log("Bloom filter false positive for message: \(messageID.prefix(20))...", category: SecureLogger.security, level: .debug)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1800,11 +1806,14 @@ class BluetoothMeshService: NSObject {
|
|||||||
Double.random(in: 0...1) < relayProb
|
Double.random(in: 0...1) < relayProb
|
||||||
|
|
||||||
if shouldRelay {
|
if shouldRelay {
|
||||||
|
SecureLogger.log("Relaying broadcast from \(senderID), TTL: \(relayPacket.ttl), peers: \(self.activePeers.count)", category: SecureLogger.noise, level: .debug)
|
||||||
// Add random delay to prevent collision storms
|
// Add random delay to prevent collision storms
|
||||||
let delay = Double.random(in: minMessageDelay...maxMessageDelay)
|
let delay = Double.random(in: minMessageDelay...maxMessageDelay)
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
||||||
self?.broadcastPacket(relayPacket)
|
self?.broadcastPacket(relayPacket)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
SecureLogger.log("Dropped broadcast relay from \(senderID), TTL: \(relayPacket.ttl), prob: \(relayProb)", category: SecureLogger.noise, level: .debug)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1875,12 +1884,10 @@ class BluetoothMeshService: NSObject {
|
|||||||
myNickname: myNickname,
|
myNickname: myNickname,
|
||||||
hopCount: UInt8(self.maxTTL - packet.ttl)
|
hopCount: UInt8(self.maxTTL - packet.ttl)
|
||||||
) {
|
) {
|
||||||
print("💬 Generating delivery ACK for private message \(messageWithPeerID.id) from \(senderID)")
|
|
||||||
self.sendDeliveryAck(ack, to: senderID)
|
self.sendDeliveryAck(ack, to: senderID)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
SecurityLogger.log("Failed to parse private message from binary payload, payload size: \(decryptedPayload.count)",
|
SecureLogger.log("Failed to parse private message from binary, size: \(decryptedPayload.count)", category: SecureLogger.encryption, level: .error)
|
||||||
category: SecurityLogger.encryption, level: .error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if packet.ttl > 0 {
|
} else if packet.ttl > 0 {
|
||||||
@@ -1918,8 +1925,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No recipient ID - this shouldn't happen for messages
|
// No recipient ID - this shouldn't happen for messages
|
||||||
SecurityLogger.log("Message packet with no recipient ID from \(senderID)",
|
SecureLogger.log("Message packet with no recipient ID from \(senderID)", category: SecureLogger.security, level: .warning)
|
||||||
category: SecurityLogger.encryption, level: .warning)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: 0x02 was legacy keyExchange - removed
|
// Note: 0x02 was legacy keyExchange - removed
|
||||||
@@ -2052,7 +2058,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
let wasInserted = collectionsQueue.sync(flags: .barrier) {
|
let wasInserted = collectionsQueue.sync(flags: .barrier) {
|
||||||
// Final safety check
|
// Final safety check
|
||||||
if senderID == self.myPeerID {
|
if senderID == self.myPeerID {
|
||||||
SecurityLogger.log("Blocked self from being added to activePeers", category: SecurityLogger.noise, level: .error)
|
SecureLogger.log("Blocked self from being added to activePeers", category: SecureLogger.noise, level: .error)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
let result = self.activePeers.insert(senderID).inserted
|
let result = self.activePeers.insert(senderID).inserted
|
||||||
@@ -2236,8 +2242,8 @@ class BluetoothMeshService: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
SecurityLogger.log("Failed to decrypt delivery ACK from \(senderID): \(error)",
|
SecureLogger.log("Failed to decrypt delivery ACK from \(senderID): \(error)",
|
||||||
category: SecurityLogger.encryption, level: .error)
|
category: SecureLogger.encryption, level: .error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if packet.ttl > 0 {
|
} else if packet.ttl > 0 {
|
||||||
@@ -2304,7 +2310,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
let payloadCopy = Data(packet.payload)
|
let payloadCopy = Data(packet.payload)
|
||||||
|
|
||||||
guard !payloadCopy.isEmpty else {
|
guard !payloadCopy.isEmpty else {
|
||||||
SecurityLogger.log("Received empty NoiseIdentityAnnouncement from \(senderID)", category: SecurityLogger.noise, level: .error)
|
SecureLogger.log("Received empty NoiseIdentityAnnouncement from \(senderID)", category: SecureLogger.noise, level: .error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2317,7 +2323,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
guard let announcement = announcement else {
|
guard let announcement = announcement else {
|
||||||
SecurityLogger.log("Failed to decode NoiseIdentityAnnouncement from \(senderID), size: \(payloadCopy.count)", category: SecurityLogger.noise, level: .error)
|
SecureLogger.log("Failed to decode NoiseIdentityAnnouncement from \(senderID), size: \(payloadCopy.count)", category: SecureLogger.noise, level: .error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2325,7 +2331,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
let timestampData = String(Int64(announcement.timestamp.timeIntervalSince1970 * 1000)).data(using: .utf8)!
|
let timestampData = String(Int64(announcement.timestamp.timeIntervalSince1970 * 1000)).data(using: .utf8)!
|
||||||
let bindingData = announcement.peerID.data(using: .utf8)! + announcement.publicKey + timestampData
|
let bindingData = announcement.peerID.data(using: .utf8)! + announcement.publicKey + timestampData
|
||||||
if !noiseService.verifySignature(announcement.signature, for: bindingData, publicKey: announcement.signingPublicKey) {
|
if !noiseService.verifySignature(announcement.signature, for: bindingData, publicKey: announcement.signingPublicKey) {
|
||||||
SecurityLogger.log("Signature verification failed for \(senderID)", category: SecurityLogger.noise, level: .warning)
|
SecureLogger.log("Signature verification failed for \(senderID)", category: SecureLogger.noise, level: .warning)
|
||||||
return // Reject announcements with invalid signatures
|
return // Reject announcements with invalid signatures
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2344,6 +2350,8 @@ class BluetoothMeshService: NSObject {
|
|||||||
signature: announcement.signature
|
signature: announcement.signature
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SecureLogger.log("Creating identity binding for \(announcement.peerID) -> \(fingerprint)", category: SecureLogger.security, level: .info)
|
||||||
|
|
||||||
// Update our mappings
|
// Update our mappings
|
||||||
updatePeerBinding(announcement.peerID, fingerprint: fingerprint, binding: binding)
|
updatePeerBinding(announcement.peerID, fingerprint: fingerprint, binding: binding)
|
||||||
|
|
||||||
@@ -2379,7 +2387,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
case .noiseHandshakeInit:
|
case .noiseHandshakeInit:
|
||||||
// Handle incoming Noise handshake initiation
|
// Handle incoming Noise handshake initiation
|
||||||
let senderID = packet.senderID.hexEncodedString()
|
let senderID = packet.senderID.hexEncodedString()
|
||||||
print("🤝 Received Noise handshake init from \(senderID), payload size: \(packet.payload.count)")
|
SecureLogger.logHandshake("initiation received", peerID: senderID, success: true)
|
||||||
|
|
||||||
// Check if this handshake is for us or broadcast
|
// Check if this handshake is for us or broadcast
|
||||||
if let recipientID = packet.recipientID,
|
if let recipientID = packet.recipientID,
|
||||||
@@ -2403,8 +2411,8 @@ class BluetoothMeshService: NSObject {
|
|||||||
// Check if we've completed version negotiation with this peer
|
// Check if we've completed version negotiation with this peer
|
||||||
if negotiatedVersions[senderID] == nil {
|
if negotiatedVersions[senderID] == nil {
|
||||||
// Legacy peer - assume version 1 for backward compatibility
|
// Legacy peer - assume version 1 for backward compatibility
|
||||||
SecurityLogger.log("Received Noise handshake from \(senderID) without version negotiation, assuming v1",
|
SecureLogger.log("Received Noise handshake from \(senderID) without version negotiation, assuming v1",
|
||||||
category: SecurityLogger.session, level: .debug)
|
category: SecureLogger.session, level: .debug)
|
||||||
negotiatedVersions[senderID] = 1
|
negotiatedVersions[senderID] = 1
|
||||||
versionNegotiationState[senderID] = .ackReceived(version: 1)
|
versionNegotiationState[senderID] = .ackReceived(version: 1)
|
||||||
}
|
}
|
||||||
@@ -2414,7 +2422,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
case .noiseHandshakeResp:
|
case .noiseHandshakeResp:
|
||||||
// Handle Noise handshake response
|
// Handle Noise handshake response
|
||||||
let senderID = packet.senderID.hexEncodedString()
|
let senderID = packet.senderID.hexEncodedString()
|
||||||
print("🤝 Received Noise handshake response from \(senderID)")
|
SecureLogger.logHandshake("response received", peerID: senderID, success: true)
|
||||||
|
|
||||||
// Check if this handshake response is for us
|
// Check if this handshake response is for us
|
||||||
if let recipientID = packet.recipientID {
|
if let recipientID = packet.recipientID {
|
||||||
@@ -2769,7 +2777,7 @@ extension BluetoothMeshService: CBCentralManagerDelegate {
|
|||||||
|
|
||||||
peerRSSI[peerID] = RSSI
|
peerRSSI[peerID] = RSSI
|
||||||
// Discovered potential peer
|
// Discovered potential peer
|
||||||
SecurityLogger.log("Discovered peer with ID: \(peerID), self ID: \(myPeerID)", category: SecurityLogger.noise, level: .debug)
|
SecureLogger.log("Discovered peer with ID: \(peerID), self ID: \(myPeerID)", category: SecureLogger.noise, level: .debug)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connection pooling with exponential backoff
|
// Connection pooling with exponential backoff
|
||||||
@@ -2821,12 +2829,13 @@ extension BluetoothMeshService: CBCentralManagerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
||||||
|
let tempID = peripheral.identifier.uuidString
|
||||||
|
SecureLogger.log("Peripheral connected: \(tempID)", category: SecureLogger.session, level: .info)
|
||||||
|
|
||||||
peripheral.delegate = self
|
peripheral.delegate = self
|
||||||
peripheral.discoverServices([BluetoothMeshService.serviceUUID])
|
peripheral.discoverServices([BluetoothMeshService.serviceUUID])
|
||||||
|
|
||||||
// Store peripheral by its system ID temporarily until we get the real peer ID
|
// Store peripheral by its system ID temporarily until we get the real peer ID
|
||||||
let tempID = peripheral.identifier.uuidString
|
|
||||||
connectedPeripherals[tempID] = peripheral
|
connectedPeripherals[tempID] = peripheral
|
||||||
|
|
||||||
// Connected to peripheral
|
// Connected to peripheral
|
||||||
@@ -2850,10 +2859,18 @@ extension BluetoothMeshService: CBCentralManagerDelegate {
|
|||||||
// Check if this was an intentional disconnect
|
// Check if this was an intentional disconnect
|
||||||
if intentionalDisconnects.contains(peripheralID) {
|
if intentionalDisconnects.contains(peripheralID) {
|
||||||
intentionalDisconnects.remove(peripheralID)
|
intentionalDisconnects.remove(peripheralID)
|
||||||
|
SecureLogger.log("Intentional disconnect: \(peripheralID)", category: SecureLogger.session, level: .debug)
|
||||||
// Don't process this disconnect further
|
// Don't process this disconnect further
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log disconnect with error if present
|
||||||
|
if let error = error {
|
||||||
|
SecureLogger.logError(error, context: "Peripheral disconnected: \(peripheralID)", category: SecureLogger.session)
|
||||||
|
} else {
|
||||||
|
SecureLogger.log("Peripheral disconnected normally: \(peripheralID)", category: SecureLogger.session, level: .info)
|
||||||
|
}
|
||||||
|
|
||||||
// Implement exponential backoff for failed connections
|
// Implement exponential backoff for failed connections
|
||||||
if error != nil {
|
if error != nil {
|
||||||
let attempts = connectionAttempts[peripheralID] ?? 0
|
let attempts = connectionAttempts[peripheralID] ?? 0
|
||||||
@@ -2930,8 +2947,8 @@ extension BluetoothMeshService: CBCentralManagerDelegate {
|
|||||||
extension BluetoothMeshService: CBPeripheralDelegate {
|
extension BluetoothMeshService: CBPeripheralDelegate {
|
||||||
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
||||||
if let error = error {
|
if let error = error {
|
||||||
SecurityLogger.log("Error discovering services: \(error)",
|
SecureLogger.log("Error discovering services: \(error)",
|
||||||
category: SecurityLogger.encryption, level: .error)
|
category: SecureLogger.encryption, level: .error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2945,8 +2962,8 @@ extension BluetoothMeshService: CBPeripheralDelegate {
|
|||||||
|
|
||||||
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
|
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
|
||||||
if let error = error {
|
if let error = error {
|
||||||
SecurityLogger.log("Error discovering characteristics: \(error)",
|
SecureLogger.log("Error discovering characteristics: \(error)",
|
||||||
category: SecurityLogger.encryption, level: .error)
|
category: SecureLogger.encryption, level: .error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3146,7 +3163,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
if peerID != "unknown" && peerID != myPeerID {
|
if peerID != "unknown" && peerID != myPeerID {
|
||||||
// Double-check we're not adding ourselves
|
// Double-check we're not adding ourselves
|
||||||
if peerID == self.myPeerID {
|
if peerID == self.myPeerID {
|
||||||
SecurityLogger.log("Preventing self from being added as peer (peripheral manager)", category: SecurityLogger.noise, level: .warning)
|
SecureLogger.log("Preventing self from being added as peer (peripheral manager)", category: SecureLogger.noise, level: .warning)
|
||||||
peripheral.respond(to: request, withResult: .success)
|
peripheral.respond(to: request, withResult: .success)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -3411,11 +3428,11 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
private func initiateNoiseHandshake(with peerID: String) {
|
private func initiateNoiseHandshake(with peerID: String) {
|
||||||
// Use noiseService directly
|
// Use noiseService directly
|
||||||
|
|
||||||
SecurityLogger.log("Initiating Noise handshake with \(peerID)", category: SecurityLogger.noise, level: .info)
|
SecureLogger.log("Initiating Noise handshake with \(peerID)", category: SecureLogger.noise, level: .info)
|
||||||
|
|
||||||
// Check if we already have an established session
|
// Check if we already have an established session
|
||||||
if noiseService.hasEstablishedSession(with: peerID) {
|
if noiseService.hasEstablishedSession(with: peerID) {
|
||||||
SecurityLogger.log("Already have established session with \(peerID)", category: SecurityLogger.noise, level: .debug)
|
SecureLogger.log("Already have established session with \(peerID)", category: SecureLogger.noise, level: .debug)
|
||||||
// Clear any lingering handshake attempt time
|
// Clear any lingering handshake attempt time
|
||||||
handshakeAttemptTimes.removeValue(forKey: peerID)
|
handshakeAttemptTimes.removeValue(forKey: peerID)
|
||||||
return
|
return
|
||||||
@@ -3424,7 +3441,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
// Check if we've recently tried to handshake with this peer
|
// Check if we've recently tried to handshake with this peer
|
||||||
if let lastAttempt = handshakeAttemptTimes[peerID],
|
if let lastAttempt = handshakeAttemptTimes[peerID],
|
||||||
Date().timeIntervalSince(lastAttempt) < handshakeTimeout {
|
Date().timeIntervalSince(lastAttempt) < handshakeTimeout {
|
||||||
SecurityLogger.log("Skipping handshake with \(peerID) - too recent", category: SecurityLogger.noise, level: .debug)
|
SecureLogger.log("Skipping handshake with \(peerID) - too recent", category: SecureLogger.noise, level: .debug)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3434,6 +3451,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
do {
|
do {
|
||||||
// Generate handshake initiation message
|
// Generate handshake initiation message
|
||||||
let handshakeData = try noiseService.initiateHandshake(with: peerID)
|
let handshakeData = try noiseService.initiateHandshake(with: peerID)
|
||||||
|
SecureLogger.logHandshake("initiated", peerID: peerID, success: true)
|
||||||
|
|
||||||
// Send handshake initiation
|
// Send handshake initiation
|
||||||
let packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
@@ -3458,12 +3476,11 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
|
|
||||||
private func handleNoiseHandshakeMessage(from peerID: String, message: Data, isInitiation: Bool) {
|
private func handleNoiseHandshakeMessage(from peerID: String, message: Data, isInitiation: Bool) {
|
||||||
// Use noiseService directly
|
// Use noiseService directly
|
||||||
print("🤝 handleNoiseHandshakeMessage from \(peerID), isInitiation: \(isInitiation), messageSize: \(message.count)")
|
SecureLogger.logHandshake("processing \(isInitiation ? "init" : "response")", peerID: peerID, success: true)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
// Process handshake message
|
// Process handshake message
|
||||||
if let response = try noiseService.processHandshakeMessage(from: peerID, message: message) {
|
if let response = try noiseService.processHandshakeMessage(from: peerID, message: message) {
|
||||||
print("🤝 Got response from processHandshakeMessage, size: \(response.count)")
|
|
||||||
// Always send responses as handshake response type
|
// Always send responses as handshake response type
|
||||||
let packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.noiseHandshakeResp.rawValue,
|
type: MessageType.noiseHandshakeResp.rawValue,
|
||||||
@@ -3483,7 +3500,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
|
|
||||||
// Check if handshake is complete
|
// Check if handshake is complete
|
||||||
if noiseService.hasEstablishedSession(with: peerID) {
|
if noiseService.hasEstablishedSession(with: peerID) {
|
||||||
print("🎉 Handshake completed with peer: \(peerID)")
|
SecureLogger.logSecurityEvent(.handshakeCompleted(peerID: peerID))
|
||||||
// Unlock rotation now that handshake is complete
|
// Unlock rotation now that handshake is complete
|
||||||
unlockRotation()
|
unlockRotation()
|
||||||
|
|
||||||
@@ -3665,32 +3682,33 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
|
|
||||||
// Safety check for empty data
|
// Safety check for empty data
|
||||||
guard !dataCopy.isEmpty else {
|
guard !dataCopy.isEmpty else {
|
||||||
SecurityLogger.log("Received empty version hello data from \(peerID)", category: SecurityLogger.session, level: .error)
|
SecureLogger.log("Received empty version hello data from \(peerID)", category: SecureLogger.session, level: .error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try JSON first if it looks like JSON
|
// Try JSON first if it looks like JSON
|
||||||
let hello: VersionHello?
|
let hello: VersionHello?
|
||||||
if let firstByte = dataCopy.first, firstByte == 0x7B { // '{' character
|
if let firstByte = dataCopy.first, firstByte == 0x7B { // '{' character
|
||||||
SecurityLogger.log("Version hello from \(peerID) appears to be JSON (size: \(dataCopy.count))", category: SecurityLogger.session, level: .debug)
|
SecureLogger.log("Version hello from \(peerID) appears to be JSON (size: \(dataCopy.count))", category: SecureLogger.session, level: .debug)
|
||||||
hello = VersionHello.decode(from: dataCopy) ?? VersionHello.fromBinaryData(dataCopy)
|
hello = VersionHello.decode(from: dataCopy) ?? VersionHello.fromBinaryData(dataCopy)
|
||||||
} else {
|
} else {
|
||||||
SecurityLogger.log("Version hello from \(peerID) appears to be binary (size: \(dataCopy.count), first byte: \(dataCopy.first?.description ?? "nil"))", category: SecurityLogger.session, level: .debug)
|
SecureLogger.log("Version hello from \(peerID) appears to be binary (size: \(dataCopy.count), first byte: \(dataCopy.first?.description ?? "nil"))", category: SecureLogger.session, level: .debug)
|
||||||
hello = VersionHello.fromBinaryData(dataCopy) ?? VersionHello.decode(from: dataCopy)
|
hello = VersionHello.fromBinaryData(dataCopy) ?? VersionHello.decode(from: dataCopy)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let hello = hello else {
|
guard let hello = hello else {
|
||||||
SecurityLogger.log("Failed to decode version hello from \(peerID)", category: SecurityLogger.session, level: .error)
|
SecureLogger.log("Failed to decode version hello from \(peerID)", category: SecureLogger.session, level: .error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
SecurityLogger.log("Received version hello from \(peerID): supported versions \(hello.supportedVersions), preferred \(hello.preferredVersion)",
|
SecureLogger.log("Received version hello from \(peerID): supported versions \(hello.supportedVersions), preferred \(hello.preferredVersion)",
|
||||||
category: SecurityLogger.session, level: .debug)
|
category: SecureLogger.session, level: .debug)
|
||||||
|
|
||||||
// Find the best common version
|
// Find the best common version
|
||||||
let ourVersions = Array(ProtocolVersion.supportedVersions)
|
let ourVersions = Array(ProtocolVersion.supportedVersions)
|
||||||
if let agreedVersion = ProtocolVersion.negotiateVersion(clientVersions: hello.supportedVersions, serverVersions: ourVersions) {
|
if let agreedVersion = ProtocolVersion.negotiateVersion(clientVersions: hello.supportedVersions, serverVersions: ourVersions) {
|
||||||
// We can communicate! Send ACK
|
// We can communicate! Send ACK
|
||||||
|
SecureLogger.log("Version negotiation agreed with \(peerID): v\(agreedVersion) (client: \(hello.clientVersion), platform: \(hello.platform))", category: SecureLogger.session, level: .info)
|
||||||
negotiatedVersions[peerID] = agreedVersion
|
negotiatedVersions[peerID] = agreedVersion
|
||||||
versionNegotiationState[peerID] = .ackReceived(version: agreedVersion)
|
versionNegotiationState[peerID] = .ackReceived(version: agreedVersion)
|
||||||
|
|
||||||
@@ -3709,6 +3727,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No compatible version
|
// No compatible version
|
||||||
|
SecureLogger.log("Version negotiation failed with \(peerID): No compatible version (client supports: \(hello.supportedVersions))", category: SecureLogger.session, level: .warning)
|
||||||
versionNegotiationState[peerID] = .failed(reason: "No compatible protocol version")
|
versionNegotiationState[peerID] = .failed(reason: "No compatible protocol version")
|
||||||
|
|
||||||
let ack = VersionAck(
|
let ack = VersionAck(
|
||||||
@@ -3736,7 +3755,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
|
|
||||||
// Safety check for empty data
|
// Safety check for empty data
|
||||||
guard !dataCopy.isEmpty else {
|
guard !dataCopy.isEmpty else {
|
||||||
SecurityLogger.log("Received empty version ack data from \(peerID)", category: SecurityLogger.session, level: .error)
|
SecureLogger.log("Received empty version ack data from \(peerID)", category: SecureLogger.session, level: .error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3749,13 +3768,13 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
guard let ack = ack else {
|
guard let ack = ack else {
|
||||||
SecurityLogger.log("Failed to decode version ack from \(peerID)", category: SecurityLogger.session, level: .error)
|
SecureLogger.log("Failed to decode version ack from \(peerID)", category: SecureLogger.session, level: .error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ack.rejected {
|
if ack.rejected {
|
||||||
SecurityLogger.log("Version negotiation rejected by \(peerID): \(ack.reason ?? "Unknown reason")",
|
SecureLogger.log("Version negotiation rejected by \(peerID): \(ack.reason ?? "Unknown reason")",
|
||||||
category: SecurityLogger.session, level: .error)
|
category: SecureLogger.session, level: .error)
|
||||||
versionNegotiationState[peerID] = .failed(reason: ack.reason ?? "Version rejected")
|
versionNegotiationState[peerID] = .failed(reason: ack.reason ?? "Version rejected")
|
||||||
|
|
||||||
// Clean up state for incompatible peer
|
// Clean up state for incompatible peer
|
||||||
@@ -3773,8 +3792,8 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
self?.delegate?.didDisconnectFromPeer(peerID)
|
self?.delegate?.didDisconnectFromPeer(peerID)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
SecurityLogger.log("Version negotiation successful with \(peerID): agreed on version \(ack.agreedVersion)",
|
SecureLogger.log("Version negotiation successful with \(peerID): agreed on v\(ack.agreedVersion) (server: \(ack.serverVersion), platform: \(ack.platform))",
|
||||||
category: SecurityLogger.session, level: .debug)
|
category: SecureLogger.session, level: .info)
|
||||||
negotiatedVersions[peerID] = ack.agreedVersion
|
negotiatedVersions[peerID] = ack.agreedVersion
|
||||||
versionNegotiationState[peerID] = .ackReceived(version: ack.agreedVersion)
|
versionNegotiationState[peerID] = .ackReceived(version: ack.agreedVersion)
|
||||||
|
|
||||||
@@ -3987,7 +4006,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
|
|
||||||
// Check if we have a Noise session with this peer
|
// Check if we have a Noise session with this peer
|
||||||
if !noiseService.hasEstablishedSession(with: recipientPeerID) {
|
if !noiseService.hasEstablishedSession(with: recipientPeerID) {
|
||||||
SecurityLogger.log("No Noise session with \(recipientPeerID), initiating handshake", category: SecurityLogger.noise, level: .info)
|
SecureLogger.log("No Noise session with \(recipientPeerID), initiating handshake", category: SecureLogger.noise, level: .info)
|
||||||
|
|
||||||
// Apply tie-breaker logic for handshake initiation
|
// Apply tie-breaker logic for handshake initiation
|
||||||
if myPeerID < recipientPeerID {
|
if myPeerID < recipientPeerID {
|
||||||
@@ -4005,7 +4024,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
self.pendingPrivateMessages[recipientPeerID] = []
|
self.pendingPrivateMessages[recipientPeerID] = []
|
||||||
}
|
}
|
||||||
self.pendingPrivateMessages[recipientPeerID]?.append((content, recipientNickname, messageID ?? UUID().uuidString))
|
self.pendingPrivateMessages[recipientPeerID]?.append((content, recipientNickname, messageID ?? UUID().uuidString))
|
||||||
SecurityLogger.log("Queued private message for \(recipientPeerID), \(self.pendingPrivateMessages[recipientPeerID]?.count ?? 0) messages pending", category: SecurityLogger.noise, level: .info)
|
SecureLogger.log("Queued private message for \(recipientPeerID), \(self.pendingPrivateMessages[recipientPeerID]?.count ?? 0) messages pending", category: SecureLogger.noise, level: .info)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ class KeychainManager {
|
|||||||
// Save to new service
|
// Save to new service
|
||||||
if saveIdentityKey(data, forKey: "noiseStaticKey") {
|
if saveIdentityKey(data, forKey: "noiseStaticKey") {
|
||||||
migratedItems += 1
|
migratedItems += 1
|
||||||
|
SecureLogger.logKeyOperation("migrate", keyType: "noiseStaticKey", success: true)
|
||||||
}
|
}
|
||||||
// Delete from old service
|
// Delete from old service
|
||||||
let deleteQuery: [String: Any] = [
|
let deleteQuery: [String: Any] = [
|
||||||
@@ -98,7 +99,9 @@ class KeychainManager {
|
|||||||
|
|
||||||
func saveChannelPassword(_ password: String, for channel: String) -> Bool {
|
func saveChannelPassword(_ password: String, for channel: String) -> Bool {
|
||||||
let key = "channel_\(channel)"
|
let key = "channel_\(channel)"
|
||||||
return save(password, forKey: key)
|
let result = save(password, forKey: key)
|
||||||
|
SecureLogger.logKeyOperation("save", keyType: "channel password for \(channel)", success: result)
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func getChannelPassword(for channel: String) -> String? {
|
func getChannelPassword(for channel: String) -> String? {
|
||||||
@@ -108,7 +111,9 @@ class KeychainManager {
|
|||||||
|
|
||||||
func deleteChannelPassword(for channel: String) -> Bool {
|
func deleteChannelPassword(for channel: String) -> Bool {
|
||||||
let key = "channel_\(channel)"
|
let key = "channel_\(channel)"
|
||||||
return delete(forKey: key)
|
let result = delete(forKey: key)
|
||||||
|
SecureLogger.logKeyOperation("delete", keyType: "channel password for \(channel)", success: result)
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAllChannelPasswords() -> [String: String] {
|
func getAllChannelPasswords() -> [String: String] {
|
||||||
@@ -150,7 +155,9 @@ class KeychainManager {
|
|||||||
|
|
||||||
func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool {
|
func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool {
|
||||||
let fullKey = "identity_\(key)"
|
let fullKey = "identity_\(key)"
|
||||||
return saveData(keyData, forKey: fullKey)
|
let result = saveData(keyData, forKey: fullKey)
|
||||||
|
SecureLogger.logKeyOperation("save", keyType: key, success: result)
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func getIdentityKey(forKey key: String) -> Data? {
|
func getIdentityKey(forKey key: String) -> Data? {
|
||||||
@@ -159,7 +166,9 @@ class KeychainManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func deleteIdentityKey(forKey key: String) -> Bool {
|
func deleteIdentityKey(forKey key: String) -> Bool {
|
||||||
return delete(forKey: "identity_\(key)")
|
let result = delete(forKey: "identity_\(key)")
|
||||||
|
SecureLogger.logKeyOperation("delete", keyType: key, success: result)
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Generic Operations
|
// MARK: - Generic Operations
|
||||||
@@ -201,9 +210,9 @@ class KeychainManager {
|
|||||||
if status == errSecSuccess {
|
if status == errSecSuccess {
|
||||||
return true
|
return true
|
||||||
} else if status == -34018 {
|
} else if status == -34018 {
|
||||||
SecurityLogger.logError(NSError(domain: "Keychain", code: -34018), context: "Missing keychain entitlement", category: SecurityLogger.keychain)
|
SecureLogger.logError(NSError(domain: "Keychain", code: -34018), context: "Missing keychain entitlement", category: SecureLogger.keychain)
|
||||||
} else if status != errSecDuplicateItem {
|
} else if status != errSecDuplicateItem {
|
||||||
SecurityLogger.logError(NSError(domain: "Keychain", code: Int(status)), context: "Error saving to keychain", category: SecurityLogger.keychain)
|
SecureLogger.logError(NSError(domain: "Keychain", code: Int(status)), context: "Error saving to keychain", category: SecureLogger.keychain)
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@@ -234,7 +243,7 @@ class KeychainManager {
|
|||||||
if status == errSecSuccess {
|
if status == errSecSuccess {
|
||||||
return result as? Data
|
return result as? Data
|
||||||
} else if status == -34018 {
|
} else if status == -34018 {
|
||||||
SecurityLogger.logError(NSError(domain: "Keychain", code: -34018), context: "Missing keychain entitlement", category: SecurityLogger.keychain)
|
SecureLogger.logError(NSError(domain: "Keychain", code: -34018), context: "Missing keychain entitlement", category: SecureLogger.keychain)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -281,6 +290,7 @@ class KeychainManager {
|
|||||||
|
|
||||||
// Delete ALL keychain data for panic mode
|
// Delete ALL keychain data for panic mode
|
||||||
func deleteAllKeychainData() -> Bool {
|
func deleteAllKeychainData() -> Bool {
|
||||||
|
SecureLogger.logSecurityEvent(.invalidKey(reason: "Panic mode - deleting all keychain data"), level: .warning)
|
||||||
|
|
||||||
var totalDeleted = 0
|
var totalDeleted = 0
|
||||||
|
|
||||||
@@ -328,6 +338,7 @@ class KeychainManager {
|
|||||||
let deleteStatus = SecItemDelete(deleteQuery as CFDictionary)
|
let deleteStatus = SecItemDelete(deleteQuery as CFDictionary)
|
||||||
if deleteStatus == errSecSuccess {
|
if deleteStatus == errSecSuccess {
|
||||||
totalDeleted += 1
|
totalDeleted += 1
|
||||||
|
SecureLogger.log("Deleted keychain item: \(account) from \(service)", category: SecureLogger.keychain, level: .info)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -357,6 +368,7 @@ class KeychainManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SecureLogger.log("Panic mode cleanup completed. Total items deleted: \(totalDeleted)", category: SecureLogger.keychain, level: .warning)
|
||||||
|
|
||||||
return totalDeleted > 0
|
return totalDeleted > 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ class NoiseEncryptionService {
|
|||||||
if let identityData = KeychainManager.shared.getIdentityKey(forKey: "noiseStaticKey"),
|
if let identityData = KeychainManager.shared.getIdentityKey(forKey: "noiseStaticKey"),
|
||||||
let key = try? Curve25519.KeyAgreement.PrivateKey(rawRepresentation: identityData) {
|
let key = try? Curve25519.KeyAgreement.PrivateKey(rawRepresentation: identityData) {
|
||||||
loadedKey = key
|
loadedKey = key
|
||||||
|
SecureLogger.logKeyOperation("load", keyType: "noiseStaticKey", success: true)
|
||||||
}
|
}
|
||||||
// If no identity exists, create new one
|
// If no identity exists, create new one
|
||||||
else {
|
else {
|
||||||
@@ -60,7 +61,8 @@ class NoiseEncryptionService {
|
|||||||
let keyData = loadedKey.rawRepresentation
|
let keyData = loadedKey.rawRepresentation
|
||||||
|
|
||||||
// Save to keychain
|
// Save to keychain
|
||||||
_ = KeychainManager.shared.saveIdentityKey(keyData, forKey: "noiseStaticKey")
|
let saved = KeychainManager.shared.saveIdentityKey(keyData, forKey: "noiseStaticKey")
|
||||||
|
SecureLogger.logKeyOperation("create", keyType: "noiseStaticKey", success: saved)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now assign the final value
|
// Now assign the final value
|
||||||
@@ -74,6 +76,7 @@ class NoiseEncryptionService {
|
|||||||
if let signingData = KeychainManager.shared.getIdentityKey(forKey: "ed25519SigningKey"),
|
if let signingData = KeychainManager.shared.getIdentityKey(forKey: "ed25519SigningKey"),
|
||||||
let key = try? Curve25519.Signing.PrivateKey(rawRepresentation: signingData) {
|
let key = try? Curve25519.Signing.PrivateKey(rawRepresentation: signingData) {
|
||||||
loadedSigningKey = key
|
loadedSigningKey = key
|
||||||
|
SecureLogger.logKeyOperation("load", keyType: "ed25519SigningKey", success: true)
|
||||||
}
|
}
|
||||||
// If no signing key exists, create new one
|
// If no signing key exists, create new one
|
||||||
else {
|
else {
|
||||||
@@ -81,7 +84,8 @@ class NoiseEncryptionService {
|
|||||||
let keyData = loadedSigningKey.rawRepresentation
|
let keyData = loadedSigningKey.rawRepresentation
|
||||||
|
|
||||||
// Save to keychain
|
// Save to keychain
|
||||||
_ = KeychainManager.shared.saveIdentityKey(keyData, forKey: "ed25519SigningKey")
|
let saved = KeychainManager.shared.saveIdentityKey(keyData, forKey: "ed25519SigningKey")
|
||||||
|
SecureLogger.logKeyOperation("create", keyType: "ed25519SigningKey", success: saved)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now assign the signing keys
|
// Now assign the signing keys
|
||||||
@@ -126,8 +130,10 @@ class NoiseEncryptionService {
|
|||||||
/// Clear persistent identity (for panic mode)
|
/// Clear persistent identity (for panic mode)
|
||||||
func clearPersistentIdentity() {
|
func clearPersistentIdentity() {
|
||||||
// Clear from keychain
|
// Clear from keychain
|
||||||
_ = KeychainManager.shared.deleteIdentityKey(forKey: "noiseStaticKey")
|
let deletedStatic = KeychainManager.shared.deleteIdentityKey(forKey: "noiseStaticKey")
|
||||||
_ = KeychainManager.shared.deleteIdentityKey(forKey: "ed25519SigningKey")
|
let deletedSigning = KeychainManager.shared.deleteIdentityKey(forKey: "ed25519SigningKey")
|
||||||
|
SecureLogger.logKeyOperation("delete", keyType: "identity keys", success: deletedStatic && deletedSigning)
|
||||||
|
SecureLogger.logSecurityEvent(.invalidKey(reason: "Panic mode activated - identity cleared"), level: .warning)
|
||||||
// Stop rekey timer
|
// Stop rekey timer
|
||||||
stopRekeyTimer()
|
stopRekeyTimer()
|
||||||
}
|
}
|
||||||
@@ -138,7 +144,7 @@ class NoiseEncryptionService {
|
|||||||
let signature = try signingKey.signature(for: data)
|
let signature = try signingKey.signature(for: data)
|
||||||
return signature
|
return signature
|
||||||
} catch {
|
} catch {
|
||||||
SecurityLogger.logError(error, context: "Failed to sign data", category: SecurityLogger.noise)
|
SecureLogger.logError(error, context: "Failed to sign data", category: SecureLogger.noise)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,7 +155,7 @@ class NoiseEncryptionService {
|
|||||||
let signingPublicKey = try Curve25519.Signing.PublicKey(rawRepresentation: publicKey)
|
let signingPublicKey = try Curve25519.Signing.PublicKey(rawRepresentation: publicKey)
|
||||||
return signingPublicKey.isValidSignature(signature, for: data)
|
return signingPublicKey.isValidSignature(signature, for: data)
|
||||||
} catch {
|
} catch {
|
||||||
SecurityLogger.logError(error, context: "Failed to verify signature", category: SecurityLogger.noise)
|
SecureLogger.logError(error, context: "Failed to verify signature", category: SecureLogger.noise)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,14 +167,18 @@ class NoiseEncryptionService {
|
|||||||
|
|
||||||
// Validate peer ID
|
// Validate peer ID
|
||||||
guard NoiseSecurityValidator.validatePeerID(peerID) else {
|
guard NoiseSecurityValidator.validatePeerID(peerID) else {
|
||||||
|
SecureLogger.logSecurityEvent(.authenticationFailed(peerID: peerID), level: .warning)
|
||||||
throw NoiseSecurityError.invalidPeerID
|
throw NoiseSecurityError.invalidPeerID
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check rate limit
|
// Check rate limit
|
||||||
guard rateLimiter.allowHandshake(from: peerID) else {
|
guard rateLimiter.allowHandshake(from: peerID) else {
|
||||||
|
SecureLogger.logSecurityEvent(.authenticationFailed(peerID: "Rate limited: \(peerID)"), level: .warning)
|
||||||
throw NoiseSecurityError.rateLimitExceeded
|
throw NoiseSecurityError.rateLimitExceeded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SecureLogger.logSecurityEvent(.handshakeStarted(peerID: peerID))
|
||||||
|
|
||||||
// Return raw handshake data without wrapper
|
// Return raw handshake data without wrapper
|
||||||
// The Noise protocol handles its own message format
|
// The Noise protocol handles its own message format
|
||||||
let handshakeData = try sessionManager.initiateHandshake(with: peerID)
|
let handshakeData = try sessionManager.initiateHandshake(with: peerID)
|
||||||
@@ -180,16 +190,19 @@ class NoiseEncryptionService {
|
|||||||
|
|
||||||
// Validate peer ID
|
// Validate peer ID
|
||||||
guard NoiseSecurityValidator.validatePeerID(peerID) else {
|
guard NoiseSecurityValidator.validatePeerID(peerID) else {
|
||||||
|
SecureLogger.logSecurityEvent(.authenticationFailed(peerID: peerID), level: .warning)
|
||||||
throw NoiseSecurityError.invalidPeerID
|
throw NoiseSecurityError.invalidPeerID
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate message size
|
// Validate message size
|
||||||
guard NoiseSecurityValidator.validateHandshakeMessageSize(message) else {
|
guard NoiseSecurityValidator.validateHandshakeMessageSize(message) else {
|
||||||
|
SecureLogger.logSecurityEvent(.handshakeFailed(peerID: peerID, error: "Message too large"), level: .warning)
|
||||||
throw NoiseSecurityError.messageTooLarge
|
throw NoiseSecurityError.messageTooLarge
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check rate limit
|
// Check rate limit
|
||||||
guard rateLimiter.allowHandshake(from: peerID) else {
|
guard rateLimiter.allowHandshake(from: peerID) else {
|
||||||
|
SecureLogger.logSecurityEvent(.authenticationFailed(peerID: "Rate limited: \(peerID)"), level: .warning)
|
||||||
throw NoiseSecurityError.rateLimitExceeded
|
throw NoiseSecurityError.rateLimitExceeded
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,6 +295,8 @@ class NoiseEncryptionService {
|
|||||||
}
|
}
|
||||||
peerFingerprints.removeValue(forKey: peerID)
|
peerFingerprints.removeValue(forKey: peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SecureLogger.logSecurityEvent(.sessionExpired(peerID: peerID))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Migrate session when peer ID changes
|
/// Migrate session when peer ID changes
|
||||||
@@ -315,7 +330,7 @@ class NoiseEncryptionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Log security event
|
// Log security event
|
||||||
SecurityLogger.logSecurityEvent(.handshakeCompleted(peerID: peerID))
|
SecureLogger.logSecurityEvent(.handshakeCompleted(peerID: peerID))
|
||||||
|
|
||||||
// Notify about authentication
|
// Notify about authentication
|
||||||
onPeerAuthenticated?(peerID, fingerprint)
|
onPeerAuthenticated?(peerID, fingerprint)
|
||||||
@@ -332,15 +347,18 @@ class NoiseEncryptionService {
|
|||||||
func setChannelPassword(_ password: String, for channel: String) {
|
func setChannelPassword(_ password: String, for channel: String) {
|
||||||
// Validate channel name
|
// Validate channel name
|
||||||
guard NoiseSecurityValidator.validateChannelName(channel) else {
|
guard NoiseSecurityValidator.validateChannelName(channel) else {
|
||||||
|
SecureLogger.log("Invalid channel name for password", category: SecureLogger.security, level: .warning)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate password is not empty
|
// Validate password is not empty
|
||||||
guard !password.isEmpty else {
|
guard !password.isEmpty else {
|
||||||
|
SecureLogger.log("Empty password rejected for channel", category: SecureLogger.security, level: .warning)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
channelEncryption.setChannelPassword(password, for: channel)
|
channelEncryption.setChannelPassword(password, for: channel)
|
||||||
|
SecureLogger.logKeyOperation("set", keyType: "channel password", success: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load channel password from keychain
|
/// Load channel password from keychain
|
||||||
@@ -406,11 +424,12 @@ class NoiseEncryptionService {
|
|||||||
// Attempt to rekey the session
|
// Attempt to rekey the session
|
||||||
do {
|
do {
|
||||||
try sessionManager.initiateRekey(for: peerID)
|
try sessionManager.initiateRekey(for: peerID)
|
||||||
|
SecureLogger.logSecurityEvent(.keyRotation(channel: peerID))
|
||||||
|
|
||||||
// Signal that handshake is needed
|
// Signal that handshake is needed
|
||||||
onHandshakeRequired?(peerID)
|
onHandshakeRequired?(peerID)
|
||||||
} catch {
|
} catch {
|
||||||
SecurityLogger.logError(error, context: "Failed to initiate rekey for peer", category: SecurityLogger.session)
|
SecureLogger.logError(error, context: "Failed to initiate rekey for peer: \(peerID)", category: SecureLogger.session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// SecurityLogger.swift
|
// SecureLogger.swift
|
||||||
// bitchat
|
// bitchat
|
||||||
//
|
//
|
||||||
// This is free and unencumbered software released into the public domain.
|
// This is free and unencumbered software released into the public domain.
|
||||||
@@ -11,7 +11,7 @@ import os.log
|
|||||||
|
|
||||||
/// Centralized security-aware logging framework
|
/// Centralized security-aware logging framework
|
||||||
/// Provides safe logging that filters sensitive data and security events
|
/// Provides safe logging that filters sensitive data and security events
|
||||||
class SecurityLogger {
|
class SecureLogger {
|
||||||
|
|
||||||
// MARK: - Log Categories
|
// MARK: - Log Categories
|
||||||
|
|
||||||
@@ -23,6 +23,22 @@ class SecurityLogger {
|
|||||||
static let session = OSLog(subsystem: subsystem, category: "session")
|
static let session = OSLog(subsystem: subsystem, category: "session")
|
||||||
static let security = OSLog(subsystem: subsystem, category: "security")
|
static let security = OSLog(subsystem: subsystem, category: "security")
|
||||||
|
|
||||||
|
// MARK: - Cached Regex Patterns
|
||||||
|
|
||||||
|
private static let fingerprintPattern = #/[a-fA-F0-9]{64}/#
|
||||||
|
private static let base64Pattern = #/[A-Za-z0-9+/]{40,}={0,2}/#
|
||||||
|
private static let passwordPattern = #/password["\s:=]+["']?[^"'\s]+["']?/#
|
||||||
|
private static let peerIDPattern = #/peerID: ([a-zA-Z0-9]{8})[a-zA-Z0-9]+/#
|
||||||
|
|
||||||
|
// MARK: - Sanitization Cache
|
||||||
|
|
||||||
|
private static let sanitizationCache: NSCache<NSString, NSString> = {
|
||||||
|
let cache = NSCache<NSString, NSString>()
|
||||||
|
cache.countLimit = 100 // Keep last 100 sanitized strings
|
||||||
|
return cache
|
||||||
|
}()
|
||||||
|
private static let cacheQueue = DispatchQueue(label: "chat.bitchat.securelogger.cache", attributes: .concurrent)
|
||||||
|
|
||||||
// MARK: - Log Levels
|
// MARK: - Log Levels
|
||||||
|
|
||||||
enum LogLevel {
|
enum LogLevel {
|
||||||
@@ -80,18 +96,24 @@ class SecurityLogger {
|
|||||||
// MARK: - Public Logging Methods
|
// MARK: - Public Logging Methods
|
||||||
|
|
||||||
/// Log a security event
|
/// Log a security event
|
||||||
static func logSecurityEvent(_ event: SecurityEvent, level: LogLevel = .info) {
|
static func logSecurityEvent(_ event: SecurityEvent, level: LogLevel = .info,
|
||||||
|
file: String = #file, line: Int = #line, function: String = #function) {
|
||||||
|
let location = formatLocation(file: file, line: line, function: function)
|
||||||
|
let message = "\(location) \(event.message)"
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
os_log("%{public}@", log: security, type: level.osLogType, event.message)
|
os_log("%{public}@", log: security, type: level.osLogType, message)
|
||||||
#else
|
#else
|
||||||
// In release, use private logging to prevent sensitive data exposure
|
// In release, use private logging to prevent sensitive data exposure
|
||||||
os_log("%{private}@", log: security, type: level.osLogType, event.message)
|
os_log("%{private}@", log: security, type: level.osLogType, message)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Log general messages with automatic sensitive data filtering
|
/// Log general messages with automatic sensitive data filtering
|
||||||
static func log(_ message: String, category: OSLog = noise, level: LogLevel = .debug) {
|
static func log(_ message: String, category: OSLog = noise, level: LogLevel = .debug,
|
||||||
let sanitized = sanitize(message)
|
file: String = #file, line: Int = #line, function: String = #function) {
|
||||||
|
let location = formatLocation(file: file, line: line, function: function)
|
||||||
|
let sanitized = sanitize("\(location) \(message)")
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
os_log("%{public}@", log: category, type: level.osLogType, sanitized)
|
os_log("%{public}@", log: category, type: level.osLogType, sanitized)
|
||||||
@@ -104,48 +126,70 @@ class SecurityLogger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Log errors with context
|
/// Log errors with context
|
||||||
static func logError(_ error: Error, context: String, category: OSLog = noise) {
|
static func logError(_ error: Error, context: String, category: OSLog = noise,
|
||||||
|
file: String = #file, line: Int = #line, function: String = #function) {
|
||||||
|
let location = formatLocation(file: file, line: line, function: function)
|
||||||
let sanitized = sanitize(context)
|
let sanitized = sanitize(context)
|
||||||
let errorDesc = sanitize(error.localizedDescription)
|
let errorDesc = sanitize(error.localizedDescription)
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
os_log("Error in %{public}@: %{public}@", log: category, type: .error, sanitized, errorDesc)
|
os_log("%{public}@ Error in %{public}@: %{public}@", log: category, type: .error, location, sanitized, errorDesc)
|
||||||
#else
|
#else
|
||||||
os_log("Error in %{private}@: %{private}@", log: category, type: .error, sanitized, errorDesc)
|
os_log("%{private}@ Error in %{private}@: %{private}@", log: category, type: .error, location, sanitized, errorDesc)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private Helpers
|
// MARK: - Private Helpers
|
||||||
|
|
||||||
|
/// Format location information for logging
|
||||||
|
private static func formatLocation(file: String, line: Int, function: String) -> String {
|
||||||
|
let fileName = (file as NSString).lastPathComponent
|
||||||
|
return "[\(fileName):\(line) \(function)]"
|
||||||
|
}
|
||||||
|
|
||||||
/// Sanitize strings to remove potentially sensitive data
|
/// Sanitize strings to remove potentially sensitive data
|
||||||
private static func sanitize(_ input: String) -> String {
|
private static func sanitize(_ input: String) -> String {
|
||||||
|
let key = input as NSString
|
||||||
|
|
||||||
|
// Check cache first
|
||||||
|
var cachedValue: String?
|
||||||
|
cacheQueue.sync {
|
||||||
|
cachedValue = sanitizationCache.object(forKey: key) as String?
|
||||||
|
}
|
||||||
|
|
||||||
|
if let cached = cachedValue {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform sanitization
|
||||||
var sanitized = input
|
var sanitized = input
|
||||||
|
|
||||||
// Remove full fingerprints (keep first 8 chars for debugging)
|
// Remove full fingerprints (keep first 8 chars for debugging)
|
||||||
let fingerprintPattern = #/[a-fA-F0-9]{64}/#
|
|
||||||
sanitized = sanitized.replacing(fingerprintPattern) { match in
|
sanitized = sanitized.replacing(fingerprintPattern) { match in
|
||||||
let fingerprint = String(match.output)
|
let fingerprint = String(match.output)
|
||||||
return String(fingerprint.prefix(8)) + "..."
|
return String(fingerprint.prefix(8)) + "..."
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove base64 encoded data that might be keys
|
// Remove base64 encoded data that might be keys
|
||||||
let base64Pattern = #/[A-Za-z0-9+/]{40,}={0,2}/#
|
|
||||||
sanitized = sanitized.replacing(base64Pattern) { _ in
|
sanitized = sanitized.replacing(base64Pattern) { _ in
|
||||||
"<base64-data>"
|
"<base64-data>"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove potential passwords (assuming they're in quotes or after "password:")
|
// Remove potential passwords (assuming they're in quotes or after "password:")
|
||||||
let passwordPattern = #/password["\s:=]+["']?[^"'\s]+["']?/#
|
|
||||||
sanitized = sanitized.replacing(passwordPattern) { _ in
|
sanitized = sanitized.replacing(passwordPattern) { _ in
|
||||||
"password: <redacted>"
|
"password: <redacted>"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Truncate peer IDs to first 8 characters
|
// 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
|
sanitized = sanitized.replacing(peerIDPattern) { match in
|
||||||
"peerID: \(match.1)..."
|
"peerID: \(match.1)..."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
cacheQueue.async(flags: .barrier) {
|
||||||
|
sanitizationCache.setObject(sanitized as NSString, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
return sanitized
|
return sanitized
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,39 +202,45 @@ class SecurityLogger {
|
|||||||
|
|
||||||
// MARK: - Convenience Extensions
|
// MARK: - Convenience Extensions
|
||||||
|
|
||||||
extension SecurityLogger {
|
extension SecureLogger {
|
||||||
|
|
||||||
/// Log handshake events
|
/// Log handshake events
|
||||||
static func logHandshake(_ phase: String, peerID: String, success: Bool = true) {
|
static func logHandshake(_ phase: String, peerID: String, success: Bool = true,
|
||||||
|
file: String = #file, line: Int = #line, function: String = #function) {
|
||||||
if success {
|
if success {
|
||||||
log("Handshake \(phase) with peer: \(peerID)", category: session, level: .info)
|
log("Handshake \(phase) with peer: \(peerID)", category: session, level: .info,
|
||||||
|
file: file, line: line, function: function)
|
||||||
} else {
|
} else {
|
||||||
log("Handshake \(phase) failed with peer: \(peerID)", category: session, level: .warning)
|
log("Handshake \(phase) failed with peer: \(peerID)", category: session, level: .warning,
|
||||||
|
file: file, line: line, function: function)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Log encryption operations
|
/// Log encryption operations
|
||||||
static func logEncryption(_ operation: String, success: Bool = true) {
|
static func logEncryption(_ operation: String, success: Bool = true,
|
||||||
|
file: String = #file, line: Int = #line, function: String = #function) {
|
||||||
let level: LogLevel = success ? .debug : .error
|
let level: LogLevel = success ? .debug : .error
|
||||||
log("Encryption operation '\(operation)' \(success ? "succeeded" : "failed")",
|
log("Encryption operation '\(operation)' \(success ? "succeeded" : "failed")",
|
||||||
category: encryption, level: level)
|
category: encryption, level: level, file: file, line: line, function: function)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Log key management operations
|
/// Log key management operations
|
||||||
static func logKeyOperation(_ operation: String, keyType: String, success: Bool = true) {
|
static func logKeyOperation(_ operation: String, keyType: String, success: Bool = true,
|
||||||
|
file: String = #file, line: Int = #line, function: String = #function) {
|
||||||
let level: LogLevel = success ? .info : .error
|
let level: LogLevel = success ? .info : .error
|
||||||
log("Key operation '\(operation)' for \(keyType) \(success ? "succeeded" : "failed")",
|
log("Key operation '\(operation)' for \(keyType) \(success ? "succeeded" : "failed")",
|
||||||
category: keychain, level: level)
|
category: keychain, level: level, file: file, line: line, function: function)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Migration Helper
|
// MARK: - Migration Helper
|
||||||
|
|
||||||
/// Helper to migrate from print statements to SecurityLogger
|
/// Helper to migrate from print statements to SecureLogger
|
||||||
/// Usage: Replace print(...) with secureLog(...)
|
/// Usage: Replace print(...) with secureLog(...)
|
||||||
func secureLog(_ items: Any..., separator: String = " ", terminator: String = "\n") {
|
func secureLog(_ items: Any..., separator: String = " ", terminator: String = "\n",
|
||||||
|
file: String = #file, line: Int = #line, function: String = #function) {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
let message = items.map { String(describing: $0) }.joined(separator: separator)
|
let message = items.map { String(describing: $0) }.joined(separator: separator)
|
||||||
SecurityLogger.log(message, level: .debug)
|
SecureLogger.log(message, level: .debug, file: file, line: line, function: function)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -8,5 +8,7 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>group.chat.bitchat</string>
|
<string>group.chat.bitchat</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>com.apple.security.device.bluetooth</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -41,7 +41,7 @@ class VersionNegotiationScenarioTests: XCTestCase {
|
|||||||
|
|
||||||
func testMixedVersionNetwork() {
|
func testMixedVersionNetwork() {
|
||||||
// Scenario: Network with mixed client versions
|
// Scenario: Network with mixed client versions
|
||||||
let clients = [
|
let clients: [[UInt8]] = [
|
||||||
[1], // Old client
|
[1], // Old client
|
||||||
[1, 2], // Mid-version client
|
[1, 2], // Mid-version client
|
||||||
[1, 2, 3] // New client
|
[1, 2, 3] // New client
|
||||||
@@ -181,13 +181,13 @@ class VersionNegotiationScenarioTests: XCTestCase {
|
|||||||
let clientCapabilities = ["noise", "compression", "multipath"]
|
let clientCapabilities = ["noise", "compression", "multipath"]
|
||||||
let serverCapabilities = ["noise", "compression", "federation"]
|
let serverCapabilities = ["noise", "compression", "federation"]
|
||||||
|
|
||||||
let hello = VersionHello(
|
_ = VersionHello(
|
||||||
clientVersion: "1.0.0",
|
clientVersion: "1.0.0",
|
||||||
platform: "iOS",
|
platform: "iOS",
|
||||||
capabilities: clientCapabilities
|
capabilities: clientCapabilities
|
||||||
)
|
)
|
||||||
|
|
||||||
let ack = VersionAck(
|
_ = VersionAck(
|
||||||
agreedVersion: 1,
|
agreedVersion: 1,
|
||||||
serverVersion: "1.0.0",
|
serverVersion: "1.0.0",
|
||||||
platform: "macOS",
|
platform: "macOS",
|
||||||
|
|||||||
Reference in New Issue
Block a user