Extract Noise into a dedicated module

This commit is contained in:
islam
2026-04-14 20:16:45 +01:00
parent 4cfcefcda6
commit e8fb4a6333
26 changed files with 209 additions and 53 deletions
+2
View File
@@ -21,6 +21,8 @@ jobs:
path: localPackages/BitLogger path: localPackages/BitLogger
- name: BitFoundation - name: BitFoundation
path: localPackages/BitFoundation path: localPackages/BitFoundation
- name: Noise
path: localPackages/Noise
steps: steps:
- name: Checkout code - name: Checkout code
+3 -2
View File
@@ -17,6 +17,7 @@ let package = Package(
], ],
dependencies:[ dependencies:[
.package(path: "localPackages/Arti"), .package(path: "localPackages/Arti"),
.package(path: "localPackages/Noise"),
.package(path: "localPackages/BitFoundation"), .package(path: "localPackages/BitFoundation"),
.package(path: "localPackages/BitLogger"), .package(path: "localPackages/BitLogger"),
.package(url: "https://github.com/21-DOT-DEV/swift-secp256k1", exact: "0.21.1") .package(url: "https://github.com/21-DOT-DEV/swift-secp256k1", exact: "0.21.1")
@@ -28,6 +29,7 @@ let package = Package(
.product(name: "P256K", package: "swift-secp256k1"), .product(name: "P256K", package: "swift-secp256k1"),
.product(name: "BitFoundation", package: "BitFoundation"), .product(name: "BitFoundation", package: "BitFoundation"),
.product(name: "BitLogger", package: "BitLogger"), .product(name: "BitLogger", package: "BitLogger"),
.product(name: "Noise", package: "Noise"),
.product(name: "Tor", package: "Arti") .product(name: "Tor", package: "Arti")
], ],
path: "bitchat", path: "bitchat",
@@ -56,8 +58,7 @@ let package = Package(
"README.md" "README.md"
], ],
resources: [ resources: [
.process("Localization"), .process("Localization")
.process("Noise")
] ]
) )
] ]
+20
View File
@@ -10,6 +10,8 @@
17901751FD8010AFC8E750F2 /* bitchatShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 17901751FD8010AFC8E750F2 /* bitchatShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
3EE336D150427F736F32B56C /* P256K in Frameworks */ = {isa = PBXBuildFile; productRef = B1D9136AA0083366353BFA2F /* P256K */; }; 3EE336D150427F736F32B56C /* P256K in Frameworks */ = {isa = PBXBuildFile; productRef = B1D9136AA0083366353BFA2F /* P256K */; };
885BBED78092484A5B069461 /* P256K in Frameworks */ = {isa = PBXBuildFile; productRef = 4EB6BA1B8464F1EA38F4E286 /* P256K */; }; 885BBED78092484A5B069461 /* P256K in Frameworks */ = {isa = PBXBuildFile; productRef = 4EB6BA1B8464F1EA38F4E286 /* P256K */; };
A63163B62F80CB2500B8B128 /* Noise in Frameworks */ = {isa = PBXBuildFile; productRef = A63163B52F80CB2500B8B128 /* Noise */; };
A63163B82F80CB2D00B8B128 /* Noise in Frameworks */ = {isa = PBXBuildFile; productRef = A63163B72F80CB2D00B8B128 /* Noise */; };
A6BCF9482F80953E001CF9B9 /* BitFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = A6BCF9472F80953E001CF9B9 /* BitFoundation */; }; A6BCF9482F80953E001CF9B9 /* BitFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = A6BCF9472F80953E001CF9B9 /* BitFoundation */; };
A6BCF94A2F809550001CF9B9 /* BitFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = A6BCF9492F809550001CF9B9 /* BitFoundation */; }; A6BCF94A2F809550001CF9B9 /* BitFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = A6BCF9492F809550001CF9B9 /* BitFoundation */; };
A6E3E5702E77036A0032EA8A /* BitLogger in Frameworks */ = {isa = PBXBuildFile; productRef = A6E3E56F2E77036A0032EA8A /* BitLogger */; }; A6E3E5702E77036A0032EA8A /* BitLogger in Frameworks */ = {isa = PBXBuildFile; productRef = A6E3E56F2E77036A0032EA8A /* BitLogger */; };
@@ -156,6 +158,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
files = ( files = (
A6E3E5722E7703760032EA8A /* BitLogger in Frameworks */, A6E3E5722E7703760032EA8A /* BitLogger in Frameworks */,
A63163B82F80CB2D00B8B128 /* Noise in Frameworks */,
3EE336D150427F736F32B56C /* P256K in Frameworks */, 3EE336D150427F736F32B56C /* P256K in Frameworks */,
A6E3EA812E7706A80032EA8A /* Tor in Frameworks */, A6E3EA812E7706A80032EA8A /* Tor in Frameworks */,
A6BCF94A2F809550001CF9B9 /* BitFoundation in Frameworks */, A6BCF94A2F809550001CF9B9 /* BitFoundation in Frameworks */,
@@ -165,6 +168,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
files = ( files = (
A6E3E5702E77036A0032EA8A /* BitLogger in Frameworks */, A6E3E5702E77036A0032EA8A /* BitLogger in Frameworks */,
A63163B62F80CB2500B8B128 /* Noise in Frameworks */,
885BBED78092484A5B069461 /* P256K in Frameworks */, 885BBED78092484A5B069461 /* P256K in Frameworks */,
A6E3EA7F2E7706720032EA8A /* Tor in Frameworks */, A6E3EA7F2E7706720032EA8A /* Tor in Frameworks */,
A6BCF9482F80953E001CF9B9 /* BitFoundation in Frameworks */, A6BCF9482F80953E001CF9B9 /* BitFoundation in Frameworks */,
@@ -228,6 +232,7 @@
A6E3E5712E7703760032EA8A /* BitLogger */, A6E3E5712E7703760032EA8A /* BitLogger */,
A6E3EA802E7706A80032EA8A /* Tor */, A6E3EA802E7706A80032EA8A /* Tor */,
A6BCF9492F809550001CF9B9 /* BitFoundation */, A6BCF9492F809550001CF9B9 /* BitFoundation */,
A63163B72F80CB2D00B8B128 /* Noise */,
); );
productName = bitchat_macOS; productName = bitchat_macOS;
productReference = 8F3A7C058C2C8E1A06C8CF8B /* bitchat.app */; productReference = 8F3A7C058C2C8E1A06C8CF8B /* bitchat.app */;
@@ -309,6 +314,7 @@
A6E3E56F2E77036A0032EA8A /* BitLogger */, A6E3E56F2E77036A0032EA8A /* BitLogger */,
A6E3EA7E2E7706720032EA8A /* Tor */, A6E3EA7E2E7706720032EA8A /* Tor */,
A6BCF9472F80953E001CF9B9 /* BitFoundation */, A6BCF9472F80953E001CF9B9 /* BitFoundation */,
A63163B52F80CB2500B8B128 /* Noise */,
); );
productName = bitchat_iOS; productName = bitchat_iOS;
productReference = 96D0D41CA19EE5A772AA8434 /* bitchat.app */; productReference = 96D0D41CA19EE5A772AA8434 /* bitchat.app */;
@@ -351,6 +357,7 @@
A6E3E56E2E77036A0032EA8A /* XCLocalSwiftPackageReference "localPackages/BitLogger" */, A6E3E56E2E77036A0032EA8A /* XCLocalSwiftPackageReference "localPackages/BitLogger" */,
A6E3EA7D2E7706720032EA8A /* XCLocalSwiftPackageReference "localPackages/Arti" */, A6E3EA7D2E7706720032EA8A /* XCLocalSwiftPackageReference "localPackages/Arti" */,
A6BCF9462F80953E001CF9B9 /* XCLocalSwiftPackageReference "localPackages/BitFoundation" */, A6BCF9462F80953E001CF9B9 /* XCLocalSwiftPackageReference "localPackages/BitFoundation" */,
A63163B42F80CB2500B8B128 /* XCLocalSwiftPackageReference "localPackages/Noise" */,
); );
preferredProjectObjectVersion = 90; preferredProjectObjectVersion = 90;
projectDirPath = ""; projectDirPath = "";
@@ -916,6 +923,10 @@
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */ /* Begin XCLocalSwiftPackageReference section */
A63163B42F80CB2500B8B128 /* XCLocalSwiftPackageReference "localPackages/Noise" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = localPackages/Noise;
};
A6BCF9462F80953E001CF9B9 /* XCLocalSwiftPackageReference "localPackages/BitFoundation" */ = { A6BCF9462F80953E001CF9B9 /* XCLocalSwiftPackageReference "localPackages/BitFoundation" */ = {
isa = XCLocalSwiftPackageReference; isa = XCLocalSwiftPackageReference;
relativePath = localPackages/BitFoundation; relativePath = localPackages/BitFoundation;
@@ -947,6 +958,15 @@
package = B8C407587481BBB190741C93 /* XCRemoteSwiftPackageReference "swift-secp256k1" */; package = B8C407587481BBB190741C93 /* XCRemoteSwiftPackageReference "swift-secp256k1" */;
productName = P256K; productName = P256K;
}; };
A63163B52F80CB2500B8B128 /* Noise */ = {
isa = XCSwiftPackageProductDependency;
productName = Noise;
};
A63163B72F80CB2D00B8B128 /* Noise */ = {
isa = XCSwiftPackageProductDependency;
package = A63163B42F80CB2500B8B128 /* XCLocalSwiftPackageReference "localPackages/Noise" */;
productName = Noise;
};
A6BCF9472F80953E001CF9B9 /* BitFoundation */ = { A6BCF9472F80953E001CF9B9 /* BitFoundation */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
productName = BitFoundation; productName = BitFoundation;
+2 -1
View File
@@ -7,6 +7,7 @@
// //
import BitLogger import BitLogger
import protocol Noise.SecureMemoryCleaner
import Foundation import Foundation
import Security import Security
@@ -51,7 +52,7 @@ enum KeychainSaveResult {
} }
} }
protocol KeychainManagerProtocol { protocol KeychainManagerProtocol: SecureMemoryCleaner {
func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool
func getIdentityKey(forKey key: String) -> Data? func getIdentityKey(forKey key: String) -> Data?
func deleteIdentityKey(forKey key: String) -> Bool func deleteIdentityKey(forKey key: String) -> Bool
@@ -84,6 +84,7 @@
import BitLogger import BitLogger
import BitFoundation import BitFoundation
import Noise
import Foundation import Foundation
import CryptoKit import CryptoKit
@@ -10,6 +10,7 @@ import Testing
import CryptoKit import CryptoKit
import struct Foundation.UUID import struct Foundation.UUID
import BitFoundation import BitFoundation
import Noise
@testable import bitchat @testable import bitchat
struct PrivateChatE2ETests { struct PrivateChatE2ETests {
@@ -10,6 +10,7 @@ import Foundation
import CryptoKit import CryptoKit
import Testing import Testing
import BitFoundation import BitFoundation
import Noise
@testable import bitchat @testable import bitchat
struct IntegrationTests { struct IntegrationTests {
@@ -9,6 +9,7 @@
import Foundation import Foundation
import CryptoKit import CryptoKit
import BitFoundation import BitFoundation
import Noise
@testable import bitchat @testable import bitchat
final class TestNetworkHelper { final class TestNetworkHelper {
+38
View File
@@ -0,0 +1,38 @@
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "Noise",
platforms: [
.iOS(.v16),
.macOS(.v13)
],
products: [
.library(
name: "Noise",
targets: ["Noise"]
),
],
dependencies: [
.package(path: "../BitLogger"),
.package(path: "../BitFoundation"),
],
targets: [
.target(
name: "Noise",
dependencies: [
.product(name: "BitLogger", package: "BitLogger"),
.product(name: "BitFoundation", package: "BitFoundation"),
],
path: "Sources"
),
.testTarget(
name: "NoiseTests",
dependencies: ["Noise"],
resources: [
.process("NoiseTestVectors.json")
]
),
]
)
@@ -78,6 +78,7 @@
/// ///
import BitLogger import BitLogger
import BitFoundation
import Foundation import Foundation
import CryptoKit import CryptoKit
@@ -532,7 +533,7 @@ final class NoiseSymmetricState {
final class NoiseHandshakeState { final class NoiseHandshakeState {
private let role: NoiseRole private let role: NoiseRole
private let pattern: NoisePattern private let pattern: NoisePattern
private let keychain: KeychainManagerProtocol private let keychain: SecureMemoryCleaner
private var symmetricState: NoiseSymmetricState private var symmetricState: NoiseSymmetricState
// Keys // Keys
@@ -555,7 +556,7 @@ final class NoiseHandshakeState {
init( init(
role: NoiseRole, role: NoiseRole,
pattern: NoisePattern, pattern: NoisePattern,
keychain: KeychainManagerProtocol, keychain: SecureMemoryCleaner,
localStaticKey: Curve25519.KeyAgreement.PrivateKey? = nil, localStaticKey: Curve25519.KeyAgreement.PrivateKey? = nil,
remoteStaticKey: Curve25519.KeyAgreement.PublicKey? = nil, remoteStaticKey: Curve25519.KeyAgreement.PublicKey? = nil,
prologue: Data = Data(), prologue: Data = Data(),
@@ -923,7 +924,7 @@ extension NoisePattern {
// MARK: - Errors // MARK: - Errors
enum NoiseError: Error { public enum NoiseError: Error {
case uninitializedCipher case uninitializedCipher
case invalidCiphertext case invalidCiphertext
case handshakeComplete case handshakeComplete
@@ -10,7 +10,7 @@ import BitLogger
import BitFoundation import BitFoundation
import Foundation import Foundation
final class NoiseRateLimiter { public final class NoiseRateLimiter {
private var handshakeTimestamps: [PeerID: [Date]] = [:] private var handshakeTimestamps: [PeerID: [Date]] = [:]
private var messageTimestamps: [PeerID: [Date]] = [:] private var messageTimestamps: [PeerID: [Date]] = [:]
@@ -19,8 +19,10 @@ final class NoiseRateLimiter {
private var globalMessageTimestamps: [Date] = [] private var globalMessageTimestamps: [Date] = []
private let queue = DispatchQueue(label: "chat.bitchat.noise.ratelimit", attributes: .concurrent) private let queue = DispatchQueue(label: "chat.bitchat.noise.ratelimit", attributes: .concurrent)
func allowHandshake(from peerID: PeerID) -> Bool { public init() {}
public func allowHandshake(from peerID: PeerID) -> Bool {
return queue.sync(flags: .barrier) { return queue.sync(flags: .barrier) {
let now = Date() let now = Date()
let oneMinuteAgo = now.addingTimeInterval(-60) let oneMinuteAgo = now.addingTimeInterval(-60)
@@ -49,7 +51,7 @@ final class NoiseRateLimiter {
} }
} }
func allowMessage(from peerID: PeerID) -> Bool { public func allowMessage(from peerID: PeerID) -> Bool {
return queue.sync(flags: .barrier) { return queue.sync(flags: .barrier) {
let now = Date() let now = Date()
let oneSecondAgo = now.addingTimeInterval(-1) let oneSecondAgo = now.addingTimeInterval(-1)
@@ -85,7 +87,7 @@ final class NoiseRateLimiter {
} }
} }
func resetAll() { public func resetAll() {
queue.async(flags: .barrier) { queue.async(flags: .barrier) {
self.handshakeTimestamps.removeAll() self.handshakeTimestamps.removeAll()
self.messageTimestamps.removeAll() self.messageTimestamps.removeAll()
@@ -8,7 +8,7 @@
import Foundation import Foundation
enum NoiseSecurityConstants { public enum NoiseSecurityConstants {
// Maximum message size to prevent memory exhaustion // Maximum message size to prevent memory exhaustion
static let maxMessageSize = 65535 // 64KB as per Noise spec static let maxMessageSize = 65535 // 64KB as per Noise spec
@@ -28,10 +28,10 @@ enum NoiseSecurityConstants {
static let maxSessionsPerPeer = 3 static let maxSessionsPerPeer = 3
// Rate limiting // Rate limiting
static let maxHandshakesPerMinute = 10 public static let maxHandshakesPerMinute = 10
static let maxMessagesPerSecond = 100 public static let maxMessagesPerSecond = 100
// Global rate limiting (across all peers) // Global rate limiting (across all peers)
static let maxGlobalHandshakesPerMinute = 30 public static let maxGlobalHandshakesPerMinute = 30
static let maxGlobalMessagesPerSecond = 500 public static let maxGlobalMessagesPerSecond = 500
} }
@@ -8,7 +8,7 @@
import Foundation import Foundation
enum NoiseSecurityError: Error { public enum NoiseSecurityError: Error {
case sessionExpired case sessionExpired
case sessionExhausted case sessionExhausted
case messageTooLarge case messageTooLarge
@@ -8,15 +8,15 @@
import Foundation import Foundation
struct NoiseSecurityValidator { public struct NoiseSecurityValidator {
/// Validate message size /// Validate message size
static func validateMessageSize(_ data: Data) -> Bool { public static func validateMessageSize(_ data: Data) -> Bool {
return data.count <= NoiseSecurityConstants.maxMessageSize return data.count <= NoiseSecurityConstants.maxMessageSize
} }
/// Validate handshake message size /// Validate handshake message size
static func validateHandshakeMessageSize(_ data: Data) -> Bool { public static func validateHandshakeMessageSize(_ data: Data) -> Bool {
return data.count <= NoiseSecurityConstants.maxHandshakeMessageSize return data.count <= NoiseSecurityConstants.maxHandshakeMessageSize
} }
} }
@@ -11,10 +11,10 @@ import Foundation
import CryptoKit import CryptoKit
import BitFoundation import BitFoundation
class NoiseSession { public class NoiseSession {
let peerID: PeerID let peerID: PeerID
let role: NoiseRole let role: NoiseRole
private let keychain: KeychainManagerProtocol private let keychain: SecureMemoryCleaner
private var state: NoiseSessionState = .uninitialized private var state: NoiseSessionState = .uninitialized
private var handshakeState: NoiseHandshakeState? private var handshakeState: NoiseHandshakeState?
private var sendCipher: NoiseCipherState? private var sendCipher: NoiseCipherState?
@@ -34,7 +34,7 @@ class NoiseSession {
init( init(
peerID: PeerID, peerID: PeerID,
role: NoiseRole, role: NoiseRole,
keychain: KeychainManagerProtocol, keychain: SecureMemoryCleaner,
localStaticKey: Curve25519.KeyAgreement.PrivateKey, localStaticKey: Curve25519.KeyAgreement.PrivateKey,
remoteStaticKey: Curve25519.KeyAgreement.PublicKey? = nil remoteStaticKey: Curve25519.KeyAgreement.PublicKey? = nil
) { ) {
@@ -182,7 +182,7 @@ class NoiseSession {
} }
} }
func isEstablished() -> Bool { public func isEstablished() -> Bool {
return sessionQueue.sync { return sessionQueue.sync {
if case .established = state { if case .established = state {
return true return true
@@ -217,8 +217,8 @@ class NoiseSession {
sentHandshakeMessages.removeAll() sentHandshakeMessages.removeAll()
// Clear handshake hash // Clear handshake hash
if var hash = handshakeHash { if handshakeHash != nil {
keychain.secureClear(&hash) keychain.secureClear(&handshakeHash!)
} }
handshakeHash = nil handshakeHash = nil
@@ -6,7 +6,7 @@
// For more information, see <https://unlicense.org> // For more information, see <https://unlicense.org>
// //
enum NoiseSessionError: Error, Equatable { public enum NoiseSessionError: Error, Equatable {
case invalidState case invalidState
case notEstablished case notEstablished
case sessionNotFound case sessionNotFound
@@ -11,18 +11,18 @@ import CryptoKit
import Foundation import Foundation
import BitFoundation import BitFoundation
final class NoiseSessionManager { public final class NoiseSessionManager {
private var sessions: [PeerID: NoiseSession] = [:] private var sessions: [PeerID: NoiseSession] = [:]
private let localStaticKey: Curve25519.KeyAgreement.PrivateKey private let localStaticKey: Curve25519.KeyAgreement.PrivateKey
private let keychain: KeychainManagerProtocol private let keychain: SecureMemoryCleaner
private let sessionFactory: (PeerID, NoiseRole) -> NoiseSession private let sessionFactory: (PeerID, NoiseRole) -> NoiseSession
private let managerQueue = DispatchQueue(label: "chat.bitchat.noise.manager", attributes: .concurrent) private let managerQueue = DispatchQueue(label: "chat.bitchat.noise.manager", attributes: .concurrent)
// Callbacks // Callbacks
var onSessionEstablished: ((PeerID, Curve25519.KeyAgreement.PublicKey) -> Void)? public var onSessionEstablished: ((PeerID, Curve25519.KeyAgreement.PublicKey) -> Void)?
var onSessionFailed: ((PeerID, Error) -> Void)? var onSessionFailed: ((PeerID, Error) -> Void)?
init(localStaticKey: Curve25519.KeyAgreement.PrivateKey, keychain: KeychainManagerProtocol) { public init(localStaticKey: Curve25519.KeyAgreement.PrivateKey, keychain: SecureMemoryCleaner) {
self.localStaticKey = localStaticKey self.localStaticKey = localStaticKey
self.keychain = keychain self.keychain = keychain
self.sessionFactory = { peerID, role in self.sessionFactory = { peerID, role in
@@ -38,7 +38,7 @@ final class NoiseSessionManager {
#if DEBUG #if DEBUG
init( init(
localStaticKey: Curve25519.KeyAgreement.PrivateKey, localStaticKey: Curve25519.KeyAgreement.PrivateKey,
keychain: KeychainManagerProtocol, keychain: SecureMemoryCleaner,
sessionFactory: @escaping (PeerID, NoiseRole) -> NoiseSession sessionFactory: @escaping (PeerID, NoiseRole) -> NoiseSession
) { ) {
self.localStaticKey = localStaticKey self.localStaticKey = localStaticKey
@@ -49,13 +49,13 @@ final class NoiseSessionManager {
// MARK: - Session Management // MARK: - Session Management
func getSession(for peerID: PeerID) -> NoiseSession? { public func getSession(for peerID: PeerID) -> NoiseSession? {
return managerQueue.sync { return managerQueue.sync {
return sessions[peerID] return sessions[peerID]
} }
} }
func removeSession(for peerID: PeerID) { public func removeSession(for peerID: PeerID) {
managerQueue.sync(flags: .barrier) { managerQueue.sync(flags: .barrier) {
if let session = sessions.removeValue(forKey: peerID) { if let session = sessions.removeValue(forKey: peerID) {
session.reset() // Clear sensitive data before removing session.reset() // Clear sensitive data before removing
@@ -63,7 +63,7 @@ final class NoiseSessionManager {
} }
} }
func removeAllSessions() { public func removeAllSessions() {
managerQueue.sync(flags: .barrier) { managerQueue.sync(flags: .barrier) {
for (_, session) in sessions { for (_, session) in sessions {
session.reset() session.reset()
@@ -74,7 +74,7 @@ final class NoiseSessionManager {
// MARK: - Handshake Helpers // MARK: - Handshake Helpers
func initiateHandshake(with peerID: PeerID) throws -> Data { public func initiateHandshake(with peerID: PeerID) throws -> Data {
return try managerQueue.sync(flags: .barrier) { return try managerQueue.sync(flags: .barrier) {
// Check if we already have an established session // Check if we already have an established session
if let existingSession = sessions[peerID], existingSession.isEstablished() { if let existingSession = sessions[peerID], existingSession.isEstablished() {
@@ -103,7 +103,7 @@ final class NoiseSessionManager {
} }
} }
func handleIncomingHandshake(from peerID: PeerID, message: Data) throws -> Data? { public func handleIncomingHandshake(from peerID: PeerID, message: Data) throws -> Data? {
// Process everything within the synchronized block to prevent race conditions // Process everything within the synchronized block to prevent race conditions
return try managerQueue.sync(flags: .barrier) { return try managerQueue.sync(flags: .barrier) {
var shouldCreateNew = false var shouldCreateNew = false
@@ -173,7 +173,7 @@ final class NoiseSessionManager {
// MARK: - Encryption/Decryption // MARK: - Encryption/Decryption
func encrypt(_ plaintext: Data, for peerID: PeerID) throws -> Data { public func encrypt(_ plaintext: Data, for peerID: PeerID) throws -> Data {
guard let session = getSession(for: peerID) else { guard let session = getSession(for: peerID) else {
throw NoiseSessionError.sessionNotFound throw NoiseSessionError.sessionNotFound
} }
@@ -181,7 +181,7 @@ final class NoiseSessionManager {
return try session.encrypt(plaintext) return try session.encrypt(plaintext)
} }
func decrypt(_ ciphertext: Data, from peerID: PeerID) throws -> Data { public func decrypt(_ ciphertext: Data, from peerID: PeerID) throws -> Data {
guard let session = getSession(for: peerID) else { guard let session = getSession(for: peerID) else {
throw NoiseSessionError.sessionNotFound throw NoiseSessionError.sessionNotFound
} }
@@ -191,13 +191,13 @@ final class NoiseSessionManager {
// MARK: - Key Management // MARK: - Key Management
func getRemoteStaticKey(for peerID: PeerID) -> Curve25519.KeyAgreement.PublicKey? { public func getRemoteStaticKey(for peerID: PeerID) -> Curve25519.KeyAgreement.PublicKey? {
return getSession(for: peerID)?.getRemoteStaticPublicKey() return getSession(for: peerID)?.getRemoteStaticPublicKey()
} }
// MARK: - Session Rekeying // MARK: - Session Rekeying
func getSessionsNeedingRekey() -> [(peerID: PeerID, needsRekey: Bool)] { public func getSessionsNeedingRekey() -> [(peerID: PeerID, needsRekey: Bool)] {
return managerQueue.sync { return managerQueue.sync {
var needingRekey: [(peerID: PeerID, needsRekey: Bool)] = [] var needingRekey: [(peerID: PeerID, needsRekey: Bool)] = []
@@ -213,7 +213,7 @@ final class NoiseSessionManager {
} }
} }
func initiateRekey(for peerID: PeerID) throws { public func initiateRekey(for peerID: PeerID) throws {
// Remove old session // Remove old session
removeSession(for: peerID) removeSession(for: peerID)
@@ -0,0 +1,14 @@
//
// SecureMemoryCleaner.swift
// bitchat
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import struct Foundation.Data
public protocol SecureMemoryCleaner {
func secureClear(_ data: inout Data)
func secureClear(_ string: inout String)
}
@@ -0,0 +1,34 @@
//
// MockKeychain.swift
// bitchat
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import Foundation
@testable import Noise
// TODO: Combine with the one from the main target
final class MockKeychain: SecureMemoryCleaner {
/// Thread-safe counter for secureClear calls
private let lock = NSLock()
private var _secureClearDataCallCount = 0
var secureClearDataCallCount: Int {
lock.lock()
defer { lock.unlock() }
return _secureClearDataCallCount
}
func secureClear(_ data: inout Data) {
lock.lock()
_secureClearDataCallCount += 1
lock.unlock()
data = Data()
}
func secureClear(_ string: inout String) {
string = ""
}
}
@@ -2,8 +2,7 @@ import CryptoKit
import Foundation import Foundation
import Testing import Testing
import BitFoundation import BitFoundation
@testable import Noise
@testable import bitchat
@Suite("Noise Coverage Tests") @Suite("Noise Coverage Tests")
struct NoiseCoverageTests { struct NoiseCoverageTests {
@@ -10,7 +10,7 @@ import CryptoKit
import Foundation import Foundation
import Testing import Testing
import BitFoundation import BitFoundation
@testable import bitchat @testable import Noise
// MARK: - Test Vector Support // MARK: - Test Vector Support
@@ -613,9 +613,7 @@ struct NoiseProtocolTests {
private func loadTestVectors() throws -> [NoiseTestVector] { private func loadTestVectors() throws -> [NoiseTestVector] {
// Try to load from test bundle // Try to load from test bundle
let testBundle = Bundle(for: MockKeychain.self) guard let url = Bundle.module.url(forResource: "NoiseTestVectors", withExtension: "json") else {
guard let url = testBundle.url(forResource: "NoiseTestVectors", withExtension: "json")
else {
throw NSError( throw NSError(
domain: "NoiseTests", code: 1, domain: "NoiseTests", code: 1,
userInfo: [ userInfo: [
@@ -794,7 +792,7 @@ struct NoiseProtocolTests {
@Test func secureClearCalledDuringHandshake() throws { @Test func secureClearCalledDuringHandshake() throws {
// Use TrackingMockKeychain to verify secureClear is called // Use TrackingMockKeychain to verify secureClear is called
let trackingKeychain = TrackingMockKeychain() let trackingKeychain = MockKeychain()
let aliceKey = Curve25519.KeyAgreement.PrivateKey() let aliceKey = Curve25519.KeyAgreement.PrivateKey()
let bobKey = Curve25519.KeyAgreement.PrivateKey() let bobKey = Curve25519.KeyAgreement.PrivateKey()
@@ -843,7 +841,7 @@ struct NoiseProtocolTests {
@Test func encryptionWorksAfterSecureClear() throws { @Test func encryptionWorksAfterSecureClear() throws {
// Verify that encryption/decryption still works correctly after adding secureClear // Verify that encryption/decryption still works correctly after adding secureClear
let trackingKeychain = TrackingMockKeychain() let trackingKeychain = MockKeychain()
let aliceKey = Curve25519.KeyAgreement.PrivateKey() let aliceKey = Curve25519.KeyAgreement.PrivateKey()
let bobKey = Curve25519.KeyAgreement.PrivateKey() let bobKey = Curve25519.KeyAgreement.PrivateKey()
@@ -900,8 +898,8 @@ struct NoiseProtocolTests {
// Verify secureClear is called in both writeMessage and readMessage paths // Verify secureClear is called in both writeMessage and readMessage paths
// We do this by checking the count increases at each step // We do this by checking the count increases at each step
let aliceKeychain = TrackingMockKeychain() let aliceKeychain = MockKeychain()
let bobKeychain = TrackingMockKeychain() let bobKeychain = MockKeychain()
let aliceKey = Curve25519.KeyAgreement.PrivateKey() let aliceKey = Curve25519.KeyAgreement.PrivateKey()
let bobKey = Curve25519.KeyAgreement.PrivateKey() let bobKey = Curve25519.KeyAgreement.PrivateKey()
@@ -1,6 +1,6 @@
import XCTest import XCTest
import BitFoundation import BitFoundation
@testable import bitchat @testable import Noise
final class NoiseRateLimiterTests: XCTestCase { final class NoiseRateLimiterTests: XCTestCase {
func test_allowHandshake_blocksAfterPerPeerLimit() { func test_allowHandshake_blocksAfterPerPeerLimit() {
@@ -0,0 +1,42 @@
//
// TestHelpers.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import Foundation
import CryptoKit
import BitFoundation
// TODO: Combine with the one from the main target
final class TestHelpers {
static func generateRandomData(length: Int) -> Data {
var data = Data(count: length)
_ = data.withUnsafeMutableBytes { bytes in
SecRandomCopyBytes(kSecRandomDefault, length, bytes.baseAddress!)
}
return data
}
@MainActor
static func waitUntil(
_ condition: @escaping () -> Bool,
timeout: TimeInterval = 5,
pollInterval: TimeInterval = 0.01
) async -> Bool {
let start = Date()
while !condition() {
if Date().timeIntervalSince(start) > timeout {
return condition()
}
try? await sleep(pollInterval)
}
return true
}
}
func sleep(_ seconds: TimeInterval) async throws {
try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
}