Compare commits

...
Author SHA1 Message Date
islam fa45ee3aea Extract Nostr into a dedicated module 2026-04-14 20:23:16 +01:00
islam e8fb4a6333 Extract Noise into a dedicated module 2026-04-14 20:16:45 +01:00
IslamandGitHub 4cfcefcda6 BitFoundation module to centralize shared components (#1089)
* Run local packages’ tests as well on CI

* BitFoundation module to centralize shared components
2026-04-14 14:10:03 -05:00
119 changed files with 1275 additions and 906 deletions
+21 -9
View File
@@ -8,9 +8,24 @@ on:
jobs: jobs:
test: test:
name: Run Swift Tests name: Run Swift Tests (${{ matrix.name }})
runs-on: macos-latest runs-on: macos-latest
strategy:
fail-fast: false # Don't cancel other matrix jobs when one fails
matrix:
include:
- name: app
path: .
- name: BitLogger
path: localPackages/BitLogger
- name: BitFoundation
path: localPackages/BitFoundation
- name: Noise
path: localPackages/Noise
- name: Nostr
path: localPackages/Nostr
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v5
@@ -21,14 +36,11 @@ jobs:
- name: Cache build artifacts - name: Cache build artifacts
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: .build path: ${{ matrix.path }}/.build
key: ${{ runner.os }}-build-${{ hashFiles('**/*.swift', '**/Package.resolved') }} key: ${{ runner.os }}-${{ matrix.name }}-${{ hashFiles(format('{0}/**/*.swift', matrix.path), format('{0}/**/Package.resolved', matrix.path)) }}
restore-keys: | restore-keys: |
${{ runner.os }}-build-${{ hashFiles('**/Package.resolved') }} ${{ runner.os }}-${{ matrix.name }}-${{ hashFiles(format('{0}/**/Package.resolved', matrix.path)) }}
${{ runner.os }}-build- ${{ runner.os }}-${{ matrix.name }}-
- name: Build the package
run: swift build
- name: Run Tests - name: Run Tests
run: swift test --parallel run: swift test --parallel --quiet --package-path ${{ matrix.path }}
+12 -3
View File
@@ -17,7 +17,10 @@ let package = Package(
], ],
dependencies:[ dependencies:[
.package(path: "localPackages/Arti"), .package(path: "localPackages/Arti"),
.package(path: "localPackages/Noise"),
.package(path: "localPackages/BitFoundation"),
.package(path: "localPackages/BitLogger"), .package(path: "localPackages/BitLogger"),
.package(path: "localPackages/Nostr"),
.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")
], ],
targets: [ targets: [
@@ -25,7 +28,10 @@ let package = Package(
name: "bitchat", name: "bitchat",
dependencies: [ dependencies: [
.product(name: "P256K", package: "swift-secp256k1"), .product(name: "P256K", package: "swift-secp256k1"),
.product(name: "BitFoundation", package: "BitFoundation"),
.product(name: "BitLogger", package: "BitLogger"), .product(name: "BitLogger", package: "BitLogger"),
.product(name: "Noise", package: "Noise"),
.product(name: "Nostr", package: "Nostr"),
.product(name: "Tor", package: "Arti") .product(name: "Tor", package: "Arti")
], ],
path: "bitchat", path: "bitchat",
@@ -44,15 +50,18 @@ let package = Package(
), ),
.testTarget( .testTarget(
name: "bitchatTests", name: "bitchatTests",
dependencies: ["bitchat"], dependencies: [
"bitchat",
.product(name: "BitFoundation", package: "BitFoundation"),
.product(name: "Nostr", package: "Nostr")
],
path: "bitchatTests", path: "bitchatTests",
exclude: [ exclude: [
"Info.plist", "Info.plist",
"README.md" "README.md"
], ],
resources: [ resources: [
.process("Localization"), .process("Localization")
.process("Noise")
] ]
) )
] ]
+60
View File
@@ -10,6 +10,12 @@
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 */; };
A63180832F8103AB00B8B128 /* Nostr in Frameworks */ = {isa = PBXBuildFile; productRef = A63180822F8103AB00B8B128 /* Nostr */; };
A63180852F8103B600B8B128 /* Nostr in Frameworks */ = {isa = PBXBuildFile; productRef = A63180842F8103B600B8B128 /* Nostr */; };
A6BCF9482F80953E001CF9B9 /* BitFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = A6BCF9472F80953E001CF9B9 /* 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 */; };
A6E3E5722E7703760032EA8A /* BitLogger in Frameworks */ = {isa = PBXBuildFile; productRef = A6E3E5712E7703760032EA8A /* BitLogger */; }; A6E3E5722E7703760032EA8A /* BitLogger in Frameworks */ = {isa = PBXBuildFile; productRef = A6E3E5712E7703760032EA8A /* BitLogger */; };
A6E3EA7F2E7706720032EA8A /* Tor in Frameworks */ = {isa = PBXBuildFile; productRef = A6E3EA7E2E7706720032EA8A /* Tor */; }; A6E3EA7F2E7706720032EA8A /* Tor in Frameworks */ = {isa = PBXBuildFile; productRef = A6E3EA7E2E7706720032EA8A /* Tor */; };
@@ -154,16 +160,22 @@
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 */,
A63180852F8103B600B8B128 /* Nostr in Frameworks */,
A6E3EA812E7706A80032EA8A /* Tor in Frameworks */, A6E3EA812E7706A80032EA8A /* Tor in Frameworks */,
A6BCF94A2F809550001CF9B9 /* BitFoundation in Frameworks */,
); );
}; };
B5A5CC493FFB3D8966548140 /* Frameworks */ = { B5A5CC493FFB3D8966548140 /* Frameworks */ = {
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 */,
A63180832F8103AB00B8B128 /* Nostr in Frameworks */,
A6E3EA7F2E7706720032EA8A /* Tor in Frameworks */, A6E3EA7F2E7706720032EA8A /* Tor in Frameworks */,
A6BCF9482F80953E001CF9B9 /* BitFoundation in Frameworks */,
); );
}; };
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
@@ -223,6 +235,9 @@
B1D9136AA0083366353BFA2F /* P256K */, B1D9136AA0083366353BFA2F /* P256K */,
A6E3E5712E7703760032EA8A /* BitLogger */, A6E3E5712E7703760032EA8A /* BitLogger */,
A6E3EA802E7706A80032EA8A /* Tor */, A6E3EA802E7706A80032EA8A /* Tor */,
A6BCF9492F809550001CF9B9 /* BitFoundation */,
A63163B72F80CB2D00B8B128 /* Noise */,
A63180842F8103B600B8B128 /* Nostr */,
); );
productName = bitchat_macOS; productName = bitchat_macOS;
productReference = 8F3A7C058C2C8E1A06C8CF8B /* bitchat.app */; productReference = 8F3A7C058C2C8E1A06C8CF8B /* bitchat.app */;
@@ -303,6 +318,9 @@
4EB6BA1B8464F1EA38F4E286 /* P256K */, 4EB6BA1B8464F1EA38F4E286 /* P256K */,
A6E3E56F2E77036A0032EA8A /* BitLogger */, A6E3E56F2E77036A0032EA8A /* BitLogger */,
A6E3EA7E2E7706720032EA8A /* Tor */, A6E3EA7E2E7706720032EA8A /* Tor */,
A6BCF9472F80953E001CF9B9 /* BitFoundation */,
A63163B52F80CB2500B8B128 /* Noise */,
A63180822F8103AB00B8B128 /* Nostr */,
); );
productName = bitchat_iOS; productName = bitchat_iOS;
productReference = 96D0D41CA19EE5A772AA8434 /* bitchat.app */; productReference = 96D0D41CA19EE5A772AA8434 /* bitchat.app */;
@@ -344,6 +362,9 @@
B8C407587481BBB190741C93 /* XCRemoteSwiftPackageReference "swift-secp256k1" */, B8C407587481BBB190741C93 /* XCRemoteSwiftPackageReference "swift-secp256k1" */,
A6E3E56E2E77036A0032EA8A /* XCLocalSwiftPackageReference "localPackages/BitLogger" */, A6E3E56E2E77036A0032EA8A /* XCLocalSwiftPackageReference "localPackages/BitLogger" */,
A6E3EA7D2E7706720032EA8A /* XCLocalSwiftPackageReference "localPackages/Arti" */, A6E3EA7D2E7706720032EA8A /* XCLocalSwiftPackageReference "localPackages/Arti" */,
A6BCF9462F80953E001CF9B9 /* XCLocalSwiftPackageReference "localPackages/BitFoundation" */,
A63163B42F80CB2500B8B128 /* XCLocalSwiftPackageReference "localPackages/Noise" */,
A63180812F8103AB00B8B128 /* XCLocalSwiftPackageReference "localPackages/Nostr" */,
); );
preferredProjectObjectVersion = 90; preferredProjectObjectVersion = 90;
projectDirPath = ""; projectDirPath = "";
@@ -909,6 +930,18 @@
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */ /* Begin XCLocalSwiftPackageReference section */
A63163B42F80CB2500B8B128 /* XCLocalSwiftPackageReference "localPackages/Noise" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = localPackages/Noise;
};
A63180812F8103AB00B8B128 /* XCLocalSwiftPackageReference "localPackages/Nostr" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = localPackages/Nostr;
};
A6BCF9462F80953E001CF9B9 /* XCLocalSwiftPackageReference "localPackages/BitFoundation" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = localPackages/BitFoundation;
};
A6E3E56E2E77036A0032EA8A /* XCLocalSwiftPackageReference "localPackages/BitLogger" */ = { A6E3E56E2E77036A0032EA8A /* XCLocalSwiftPackageReference "localPackages/BitLogger" */ = {
isa = XCLocalSwiftPackageReference; isa = XCLocalSwiftPackageReference;
relativePath = localPackages/BitLogger; relativePath = localPackages/BitLogger;
@@ -936,6 +969,33 @@
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;
};
A63180822F8103AB00B8B128 /* Nostr */ = {
isa = XCSwiftPackageProductDependency;
productName = Nostr;
};
A63180842F8103B600B8B128 /* Nostr */ = {
isa = XCSwiftPackageProductDependency;
package = A63180812F8103AB00B8B128 /* XCLocalSwiftPackageReference "localPackages/Nostr" */;
productName = Nostr;
};
A6BCF9472F80953E001CF9B9 /* BitFoundation */ = {
isa = XCSwiftPackageProductDependency;
productName = BitFoundation;
};
A6BCF9492F809550001CF9B9 /* BitFoundation */ = {
isa = XCSwiftPackageProductDependency;
package = A6BCF9462F80953E001CF9B9 /* XCLocalSwiftPackageReference "localPackages/BitFoundation" */;
productName = BitFoundation;
};
A6E3E56F2E77036A0032EA8A /* BitLogger */ = { A6E3E56F2E77036A0032EA8A /* BitLogger */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
productName = BitLogger; productName = BitLogger;
+5 -1
View File
@@ -6,8 +6,10 @@
// For more information, see <https://unlicense.org> // For more information, see <https://unlicense.org>
// //
import Nostr
import Tor import Tor
import SwiftUI import SwiftUI
import BitFoundation
import UserNotifications import UserNotifications
@main @main
@@ -26,9 +28,11 @@ struct BitchatApp: App {
@NSApplicationDelegateAdaptor(MacAppDelegate.self) var appDelegate @NSApplicationDelegateAdaptor(MacAppDelegate.self) var appDelegate
#endif #endif
private let idBridge = NostrIdentityBridge() private let idBridge = NostrIdentityBridge(keychain: KeychainManager())
init() { init() {
GeoRelayDirectory.setupShared(dependencies: .live())
NostrRelayManager.setupShared(dependencies: .live())
let keychain = KeychainManager() let keychain = KeychainManager()
let idBridge = self.idBridge let idBridge = self.idBridge
_chatViewModel = StateObject( _chatViewModel = StateObject(
+1
View File
@@ -81,6 +81,7 @@
/// ///
import Foundation import Foundation
import BitFoundation
// MARK: - Three-Layer Identity Model // MARK: - Three-Layer Identity Model
@@ -91,6 +91,7 @@
/// ///
import BitLogger import BitLogger
import BitFoundation
import Foundation import Foundation
import CryptoKit import CryptoKit
+1
View File
@@ -7,6 +7,7 @@
// //
import Foundation import Foundation
import BitFoundation
/// Represents a user-visible message in the BitChat system. /// Represents a user-visible message in the BitChat system.
/// Handles both broadcast messages and private encrypted messages, /// Handles both broadcast messages and private encrypted messages,
+1
View File
@@ -7,6 +7,7 @@
// //
import Foundation import Foundation
import BitFoundation
/// The core packet structure for all BitChat protocol messages. /// The core packet structure for all BitChat protocol messages.
/// Encapsulates all data needed for routing through the mesh network, /// Encapsulates all data needed for routing through the mesh network,
+1
View File
@@ -1,5 +1,6 @@
import Foundation import Foundation
import CoreBluetooth import CoreBluetooth
import BitFoundation
/// Represents a peer in the BitChat network with all associated metadata /// Represents a peer in the BitChat network with all associated metadata
struct BitchatPeer: Equatable { struct BitchatPeer: Equatable {
+1
View File
@@ -7,6 +7,7 @@
// //
import Foundation import Foundation
import BitFoundation
struct ReadReceipt: Codable { struct ReadReceipt: Codable {
let originalMessageID: String let originalMessageID: String
+1
View File
@@ -1,4 +1,5 @@
import Foundation import Foundation
import BitFoundation
// MARK: - BitChat-over-Nostr Adapter // MARK: - BitChat-over-Nostr Adapter
+118
View File
@@ -0,0 +1,118 @@
import Nostr
import Combine
import Foundation
import Tor
#if os(iOS)
import UIKit
#elseif os(macOS)
import AppKit
#endif
// MARK: - GeoRelayDirectory Live Dependencies
extension GeoRelayDirectoryDependencies {
@MainActor
static func live() -> Self {
#if os(iOS)
let activeNotificationName: Notification.Name? = UIApplication.didBecomeActiveNotification
#elseif os(macOS)
let activeNotificationName: Notification.Name? = NSApplication.didBecomeActiveNotification
#else
let activeNotificationName: Notification.Name? = nil
#endif
return Self(
userDefaults: .standard,
notificationCenter: .default,
now: Date.init,
remoteURL: URL(string: "https://raw.githubusercontent.com/permissionlesstech/georelays/refs/heads/main/nostr_relays.csv")!,
fetchInterval: TransportConfig.geoRelayFetchIntervalSeconds,
refreshCheckInterval: TransportConfig.geoRelayRefreshCheckIntervalSeconds,
retryInitialSeconds: TransportConfig.geoRelayRetryInitialSeconds,
retryMaxSeconds: TransportConfig.geoRelayRetryMaxSeconds,
awaitTorReady: { await TorManager.shared.awaitReady() },
makeFetchData: {
let session = TorURLSession.shared.session
return { request in
let (data, _) = try await session.data(for: request)
return data
}
},
readData: { try? Data(contentsOf: $0) },
writeData: { data, url in
try data.write(to: url, options: .atomic)
},
cacheURL: {
do {
let base = try FileManager.default.url(
for: .applicationSupportDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: true
)
let dir = base.appendingPathComponent("bitchat", isDirectory: true)
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
return dir.appendingPathComponent("georelays_cache.csv")
} catch {
return nil
}
},
bundledCSVURLs: {
[
Bundle.main.url(forResource: "nostr_relays", withExtension: "csv"),
Bundle.main.url(forResource: "online_relays_gps", withExtension: "csv"),
Bundle.main.url(forResource: "online_relays_gps", withExtension: "csv", subdirectory: "relays")
].compactMap { $0 }
},
currentDirectoryPath: { FileManager.default.currentDirectoryPath },
retrySleep: { delay in
let nanoseconds = UInt64(delay * 1_000_000_000)
try? await Task.sleep(nanoseconds: nanoseconds)
},
torReadyNotificationName: .TorDidBecomeReady,
activeNotificationName: activeNotificationName,
autoStart: true
)
}
}
// MARK: - NostrRelayManager Live Dependencies
extension NostrRelayManagerDependencies {
@MainActor
static func live() -> Self {
Self(
activationAllowed: { NetworkActivationService.shared.activationAllowed },
userTorEnabled: { NetworkActivationService.shared.userTorEnabled },
hasMutualFavorites: { !FavoritesPersistenceService.shared.mutualFavorites.isEmpty },
hasLocationPermission: { LocationChannelManager.shared.permissionState == .authorized },
mutualFavoritesPublisher: FavoritesPersistenceService.shared.$mutualFavorites.eraseToAnyPublisher(),
locationPermissionPublisher: LocationChannelManager.shared.$permissionState
.map { state -> LocationPermissionState in
switch state {
case .notDetermined:.notDetermined
case .authorized: .authorized
case .denied: .denied
case .restricted: .denied
}
}
.eraseToAnyPublisher(),
torEnforced: { TorManager.shared.torEnforced },
torIsReady: { TorManager.shared.isReady },
torIsForeground: { TorManager.shared.isForeground() },
awaitTorReady: { completion in
Task.detached {
let ready = await TorManager.shared.awaitReady()
await MainActor.run {
completion(ready)
}
}
},
makeSession: { NostrRelayManager.makeURLSession(TorURLSession.shared.session) },
scheduleAfter: { delay, action in
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: action)
},
now: Date.init
)
}
}
+1 -56
View File
@@ -6,62 +6,7 @@
// //
import Foundation import Foundation
import CryptoKit import BitFoundation
// MARK: - Hex Encoding/Decoding
extension Data {
func hexEncodedString() -> String {
if self.isEmpty {
return ""
}
return self.map { String(format: "%02x", $0) }.joined()
}
func sha256Hex() -> String {
let digest = SHA256.hash(data: self)
return digest.map { String(format: "%02x", $0) }.joined()
}
/// Initialize Data from a hex string.
/// - Parameter hexString: A hex string, optionally prefixed with "0x" or "0X".
/// Whitespace is trimmed. Must have even length after prefix removal.
/// - Returns: nil if the string has odd length or contains invalid hex characters.
init?(hexString: String) {
var hex = hexString.trimmed
// Remove optional 0x prefix
if hex.hasPrefix("0x") || hex.hasPrefix("0X") {
hex = String(hex.dropFirst(2))
}
// Reject odd-length strings
guard hex.count % 2 == 0 else {
return nil
}
// Reject empty strings
guard !hex.isEmpty else {
self = Data()
return
}
let len = hex.count / 2
var data = Data(capacity: len)
var index = hex.startIndex
for _ in 0..<len {
let nextIndex = hex.index(index, offsetBy: 2)
guard let byte = UInt8(String(hex[index..<nextIndex]), radix: 16) else {
return nil
}
data.append(byte)
index = nextIndex
}
self = data
}
}
// MARK: - Binary Encoding Utilities // MARK: - Binary Encoding Utilities
+1
View File
@@ -60,6 +60,7 @@
import Foundation import Foundation
import CoreBluetooth import CoreBluetooth
import BitFoundation
// MARK: - Message Types // MARK: - Message Types
+2
View File
@@ -1,4 +1,6 @@
import BitLogger import BitLogger
import BitFoundation
import Nostr
import Foundation import Foundation
import CoreBluetooth import CoreBluetooth
import Combine import Combine
+2
View File
@@ -6,7 +6,9 @@
// This is free and unencumbered software released into the public domain. // This is free and unencumbered software released into the public domain.
// //
import Nostr
import Foundation import Foundation
import BitFoundation
/// Result of command processing /// Result of command processing
enum CommandResult { enum CommandResult {
@@ -1,4 +1,5 @@
import BitLogger import BitLogger
import BitFoundation
import Foundation import Foundation
import Combine import Combine
@@ -8,6 +8,7 @@
// This is free and unencumbered software released into the public domain. // This is free and unencumbered software released into the public domain.
// //
import Nostr
import Foundation import Foundation
import Combine import Combine
import BitLogger import BitLogger
@@ -74,7 +75,7 @@ final class GeohashPresenceService: ObservableObject {
] ]
private init() { private init() {
let idBridge = NostrIdentityBridge() let idBridge = NostrIdentityBridge(keychain: KeychainManager())
self.availableChannelsProvider = { LocationStateManager.shared.availableChannels } self.availableChannelsProvider = { LocationStateManager.shared.availableChannels }
self.locationChanges = LocationStateManager.shared.$availableChannels.eraseToAnyPublisher() self.locationChanges = LocationStateManager.shared.$availableChannels.eraseToAnyPublisher()
self.torReadyPublisher = NotificationCenter.default.publisher(for: .TorDidBecomeReady) self.torReadyPublisher = NotificationCenter.default.publisher(for: .TorDidBecomeReady)
+3 -1
View File
@@ -7,6 +7,8 @@
// //
import BitLogger import BitLogger
import protocol Nostr.NostrKeychainStoring
import protocol Noise.SecureMemoryCleaner
import Foundation import Foundation
import Security import Security
@@ -51,7 +53,7 @@ enum KeychainSaveResult {
} }
} }
protocol KeychainManagerProtocol { protocol KeychainManagerProtocol: SecureMemoryCleaner, NostrKeychainStoring {
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
+2 -1
View File
@@ -1,4 +1,5 @@
import BitLogger import BitLogger
import Nostr
import Foundation import Foundation
/// Dependencies for location notes, allowing tests to stub relay/identity behavior. /// Dependencies for location notes, allowing tests to stub relay/identity behavior.
@@ -15,7 +16,7 @@ struct LocationNotesDependencies {
var deriveIdentity: (_ geohash: String) throws -> NostrIdentity var deriveIdentity: (_ geohash: String) throws -> NostrIdentity
var now: () -> Date var now: () -> Date
private static let idBridge = NostrIdentityBridge() private static let idBridge = NostrIdentityBridge(keychain: KeychainManager())
static let live = LocationNotesDependencies( static let live = LocationNotesDependencies(
relayLookup: { geohash, count in relayLookup: { geohash, count in
@@ -6,6 +6,7 @@
// This is free and unencumbered software released into the public domain. // This is free and unencumbered software released into the public domain.
// //
import BitFoundation
import Foundation import Foundation
import SwiftUI import SwiftUI
+1
View File
@@ -1,4 +1,5 @@
import BitLogger import BitLogger
import BitFoundation
import Foundation import Foundation
/// Routes messages using available transports (Mesh, Nostr, etc.) /// Routes messages using available transports (Mesh, Nostr, etc.)
@@ -1,3 +1,4 @@
import Nostr
import Foundation import Foundation
import BitLogger import BitLogger
import Combine import Combine
@@ -83,6 +83,8 @@
/// ///
import BitLogger import BitLogger
import BitFoundation
import Noise
import Foundation import Foundation
import CryptoKit import CryptoKit
+2
View File
@@ -1,4 +1,6 @@
import BitLogger import BitLogger
import BitFoundation
import Nostr
import Foundation import Foundation
import Combine import Combine
@@ -6,6 +6,7 @@
// For more information, see <https://unlicense.org> // For more information, see <https://unlicense.org>
// //
import BitFoundation
import Foundation import Foundation
import UserNotifications import UserNotifications
#if os(iOS) #if os(iOS)
@@ -7,6 +7,7 @@
// //
import BitLogger import BitLogger
import BitFoundation
import Foundation import Foundation
import SwiftUI import SwiftUI
+1
View File
@@ -1,3 +1,4 @@
import BitFoundation
import Foundation import Foundation
import Combine import Combine
@@ -7,6 +7,8 @@
// //
import BitLogger import BitLogger
import BitFoundation
import Nostr
import Foundation import Foundation
import Combine import Combine
import SwiftUI import SwiftUI
+1
View File
@@ -1,5 +1,6 @@
import Foundation import Foundation
import BitLogger import BitLogger
import BitFoundation
// Gossip-based sync manager using on-demand GCS filters // Gossip-based sync manager using on-demand GCS filters
final class GossipSyncManager { final class GossipSyncManager {
+1
View File
@@ -8,6 +8,7 @@
import Foundation import Foundation
import BitLogger import BitLogger
import BitFoundation
/// Manages outgoing sync requests and validates incoming responses. /// Manages outgoing sync requests and validates incoming responses.
/// ///
-22
View File
@@ -1,22 +0,0 @@
//
// Data+SHA256.swift
// bitchat
//
// Created by Islam on 26/09/2025.
//
import struct Foundation.Data
import struct CryptoKit.SHA256
extension Data {
/// Returns the hex representation of SHA256 hash
func sha256Fingerprint() -> String {
// Implementation matches existing fingerprint generation in NoiseEncryptionService
sha256Hash().hexEncodedString()
}
/// Returns the SHA256 hash wrapped in Data
func sha256Hash() -> Data {
Data(SHA256.hash(data: self))
}
}
@@ -1,4 +1,5 @@
import Foundation import Foundation
import BitFoundation
/// Resolves a stable display name for peers, adding a short suffix when collisions exist. /// Resolves a stable display name for peers, adding a short suffix when collisions exist.
struct PeerDisplayNameResolver { struct PeerDisplayNameResolver {
+3 -1
View File
@@ -78,6 +78,8 @@
/// ///
import BitLogger import BitLogger
import BitFoundation
import Nostr
import Foundation import Foundation
import SwiftUI import SwiftUI
import Combine import Combine
@@ -1966,7 +1968,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, CommandContextProv
try? await Task.sleep(nanoseconds: TransportConfig.uiAsyncShortSleepNs) // 0.1 seconds try? await Task.sleep(nanoseconds: TransportConfig.uiAsyncShortSleepNs) // 0.1 seconds
// Reinitialize Nostr relay manager with new identity // Reinitialize Nostr relay manager with new identity
nostrRelayManager = NostrRelayManager() nostrRelayManager = NostrRelayManager(dependencies: .live())
setupNostrMessageHandling() setupNostrMessageHandling()
nostrRelayManager?.connect() nostrRelayManager?.connect()
} }
@@ -5,9 +5,11 @@
// Geohash and Nostr logic for ChatViewModel // Geohash and Nostr logic for ChatViewModel
// //
import Nostr
import Foundation import Foundation
import Combine import Combine
import BitLogger import BitLogger
import BitFoundation
import SwiftUI import SwiftUI
import Tor import Tor
@@ -5,9 +5,11 @@
// Private chat and media transfer logic for ChatViewModel // Private chat and media transfer logic for ChatViewModel
// //
import Nostr
import Foundation import Foundation
import Combine import Combine
import BitLogger import BitLogger
import BitFoundation
import SwiftUI import SwiftUI
extension ChatViewModel { extension ChatViewModel {
@@ -5,6 +5,7 @@
// Created by Islam on 29/10/2025. // Created by Islam on 29/10/2025.
// //
import Nostr
import SwiftUI import SwiftUI
struct CommandSuggestionsView: View { struct CommandSuggestionsView: View {
@@ -76,7 +77,7 @@ struct CommandSuggestionsView: View {
let keychain = KeychainManager() let keychain = KeychainManager()
let viewModel = ChatViewModel( let viewModel = ChatViewModel(
keychain: keychain, keychain: keychain,
idBridge: NostrIdentityBridge(), idBridge: NostrIdentityBridge(keychain: keychain),
identityManager: SecureIdentityStateManager(keychain) identityManager: SecureIdentityStateManager(keychain)
) )
@@ -6,6 +6,7 @@
// For more information, see <https://unlicense.org> // For more information, see <https://unlicense.org>
// //
import Nostr
import SwiftUI import SwiftUI
struct TextMessageView: View { struct TextMessageView: View {
@@ -89,7 +90,7 @@ struct TextMessageView: View {
.environmentObject( .environmentObject(
ChatViewModel( ChatViewModel(
keychain: keychain, keychain: keychain,
idBridge: NostrIdentityBridge(), idBridge: NostrIdentityBridge(keychain: keychain),
identityManager: SecureIdentityStateManager(keychain) identityManager: SecureIdentityStateManager(keychain)
) )
) )
+1
View File
@@ -15,6 +15,7 @@ import AppKit
#endif #endif
import UniformTypeIdentifiers import UniformTypeIdentifiers
import BitLogger import BitLogger
import BitFoundation
/// On macOS 14+, disables the default system focus ring on TextFields. /// On macOS 14+, disables the default system focus ring on TextFields.
/// On earlier macOS versions and on iOS this is a no-op. /// On earlier macOS versions and on iOS this is a no-op.
+1
View File
@@ -7,6 +7,7 @@
// //
import SwiftUI import SwiftUI
import BitFoundation
struct FingerprintView: View { struct FingerprintView: View {
@ObservedObject var viewModel: ChatViewModel @ObservedObject var viewModel: ChatViewModel
+1
View File
@@ -1,4 +1,5 @@
import SwiftUI import SwiftUI
import BitFoundation
struct MeshPeerList: View { struct MeshPeerList: View {
@ObservedObject var viewModel: ChatViewModel @ObservedObject var viewModel: ChatViewModel
+1
View File
@@ -5,6 +5,7 @@
// Created by Islam on 30/03/2026. // Created by Islam on 30/03/2026.
// //
import BitFoundation
import SwiftUI import SwiftUI
private struct MessageDisplayItem: Identifiable { private struct MessageDisplayItem: Identifiable {
+1
View File
@@ -1,3 +1,4 @@
import Nostr
import SwiftUI import SwiftUI
import CoreImage import CoreImage
import CoreImage.CIFilterBuiltins import CoreImage.CIFilterBuiltins
+2
View File
@@ -6,8 +6,10 @@
// //
import Testing import Testing
import Nostr
import Foundation import Foundation
import CoreBluetooth import CoreBluetooth
import BitFoundation
@testable import bitchat @testable import bitchat
struct BLEServiceCoreTests { struct BLEServiceCoreTests {
+1
View File
@@ -8,6 +8,7 @@
import Testing import Testing
import CoreBluetooth import CoreBluetooth
import BitFoundation
@testable import bitchat @testable import bitchat
struct BLEServiceTests { struct BLEServiceTests {
+1
View File
@@ -1,5 +1,6 @@
import Foundation import Foundation
import Testing import Testing
import BitFoundation
@testable import bitchat @testable import bitchat
@Suite("BitchatPeer Tests") @Suite("BitchatPeer Tests")
@@ -6,7 +6,9 @@
// //
import Testing import Testing
import Nostr
import Foundation import Foundation
import BitFoundation
@testable import bitchat @testable import bitchat
// MARK: - Test Helpers // MARK: - Test Helpers
@@ -6,6 +6,7 @@
// //
import Testing import Testing
import Nostr
import Foundation import Foundation
import Combine import Combine
#if os(iOS) #if os(iOS)
@@ -13,6 +14,7 @@ import UIKit
#else #else
import AppKit import AppKit
#endif #endif
import BitFoundation
@testable import bitchat @testable import bitchat
// MARK: - Test Helpers // MARK: - Test Helpers
@@ -7,7 +7,9 @@
// //
import Testing import Testing
import Nostr
import Foundation import Foundation
import BitFoundation
@testable import bitchat @testable import bitchat
struct ChatViewModelRefactoringTests { struct ChatViewModelRefactoringTests {
+2
View File
@@ -7,7 +7,9 @@
// //
import Testing import Testing
import Nostr
import Foundation import Foundation
import BitFoundation
@testable import bitchat @testable import bitchat
// MARK: - Test Helpers // MARK: - Test Helpers
+1
View File
@@ -6,6 +6,7 @@
// //
import Testing import Testing
import Nostr
import Foundation import Foundation
@testable import bitchat @testable import bitchat
+2
View File
@@ -1,5 +1,7 @@
import Foundation import Foundation
import Nostr
import Testing import Testing
import BitFoundation
@testable import bitchat @testable import bitchat
@Suite(.serialized) @Suite(.serialized)
@@ -9,6 +9,8 @@
import Testing import Testing
import CryptoKit import CryptoKit
import struct Foundation.UUID import struct Foundation.UUID
import BitFoundation
import Noise
@testable import bitchat @testable import bitchat
struct PrivateChatE2ETests { struct PrivateChatE2ETests {
@@ -8,6 +8,7 @@
import Testing import Testing
import struct Foundation.UUID import struct Foundation.UUID
import BitFoundation
@testable import bitchat @testable import bitchat
struct PublicChatE2ETests { struct PublicChatE2ETests {
@@ -7,8 +7,10 @@
// //
import Testing import Testing
import Nostr
import Foundation import Foundation
import CoreBluetooth import CoreBluetooth
import BitFoundation
@testable import bitchat @testable import bitchat
struct FragmentationTests { struct FragmentationTests {
+1
View File
@@ -7,6 +7,7 @@
// //
import Testing import Testing
import Nostr
import Foundation import Foundation
import Combine import Combine
@testable import bitchat @testable import bitchat
@@ -1,5 +1,6 @@
import Foundation import Foundation
import Testing import Testing
import BitFoundation
@testable import bitchat @testable import bitchat
struct GossipSyncManagerTests { struct GossipSyncManagerTests {
@@ -9,6 +9,8 @@
import Foundation import Foundation
import CryptoKit import CryptoKit
import Testing import Testing
import BitFoundation
import Noise
@testable import bitchat @testable import bitchat
struct IntegrationTests { struct IntegrationTests {
@@ -8,6 +8,8 @@
import Foundation import Foundation
import CryptoKit import CryptoKit
import BitFoundation
import Noise
@testable import bitchat @testable import bitchat
final class TestNetworkHelper { final class TestNetworkHelper {
+1
View File
@@ -1,4 +1,5 @@
import Testing import Testing
import Nostr
import Foundation import Foundation
@testable import bitchat @testable import bitchat
@@ -1,4 +1,5 @@
import Testing import Testing
import Nostr
import Foundation import Foundation
@testable import bitchat @testable import bitchat
@@ -9,6 +9,7 @@
import Testing import Testing
import Foundation import Foundation
import SwiftUI import SwiftUI
import BitFoundation
@testable import bitchat @testable import bitchat
struct MessageFormattingEngineTests { struct MessageFormattingEngineTests {
+1
View File
@@ -7,6 +7,7 @@
// //
import Foundation import Foundation
import BitFoundation
@testable import bitchat @testable import bitchat
final class MockBLEBus { final class MockBLEBus {
+1
View File
@@ -8,6 +8,7 @@
import Foundation import Foundation
import CoreBluetooth import CoreBluetooth
import BitFoundation
@testable import bitchat @testable import bitchat
/// In-memory BLE test harness used by E2E/Integration tests. /// In-memory BLE test harness used by E2E/Integration tests.
@@ -7,6 +7,7 @@
// //
import Foundation import Foundation
import BitFoundation
@testable import bitchat @testable import bitchat
final class MockIdentityManager: SecureIdentityStateManagerProtocol { final class MockIdentityManager: SecureIdentityStateManagerProtocol {
+1
View File
@@ -9,6 +9,7 @@
import Foundation import Foundation
import Combine import Combine
import CoreBluetooth import CoreBluetooth
import BitFoundation
@testable import bitchat @testable import bitchat
/// Mock Transport implementation for testing ChatViewModel in isolation. /// Mock Transport implementation for testing ChatViewModel in isolation.
@@ -1,4 +1,5 @@
import Foundation import Foundation
import Nostr
import Tor import Tor
import XCTest import XCTest
@testable import bitchat @testable import bitchat
@@ -303,6 +304,7 @@ final class GeoRelayDirectoryTests: XCTestCase {
retrySleep: { delay in retrySleep: { delay in
await retryRecorder.record(delay) await retryRecorder.record(delay)
}, },
torReadyNotificationName: .TorDidBecomeReady,
activeNotificationName: activeNotificationName, activeNotificationName: activeNotificationName,
autoStart: autoStart autoStart: autoStart
) )
+2
View File
@@ -5,9 +5,11 @@
// Tests for NIP-17 gift-wrapped private messages // Tests for NIP-17 gift-wrapped private messages
// //
import Nostr
import Testing import Testing
import CryptoKit import CryptoKit
import Foundation import Foundation
import BitFoundation
@testable import bitchat @testable import bitchat
struct NostrProtocolTests { struct NostrProtocolTests {
@@ -8,6 +8,7 @@
import Testing import Testing
import Foundation import Foundation
import BitFoundation
@testable import bitchat @testable import bitchat
struct BinaryProtocolTests { struct BinaryProtocolTests {
+1
View File
@@ -2,6 +2,7 @@ import Testing
import Foundation import Foundation
import Combine import Combine
import CoreBluetooth import CoreBluetooth
import BitFoundation
@testable import bitchat @testable import bitchat
private final class DefaultDelegateProbe: BitchatDelegate { private final class DefaultDelegateProbe: BitchatDelegate {
+1
View File
@@ -1,5 +1,6 @@
import Foundation import Foundation
import Testing import Testing
import BitFoundation
@testable import bitchat @testable import bitchat
@Suite("ReadReceipt Tests") @Suite("ReadReceipt Tests")
@@ -1,4 +1,5 @@
import XCTest import XCTest
import BitFoundation
@testable import bitchat @testable import bitchat
@MainActor @MainActor
@@ -1,4 +1,5 @@
import Combine import Combine
import Nostr
import XCTest import XCTest
@testable import bitchat @testable import bitchat
@@ -7,6 +7,7 @@
import Testing import Testing
import Foundation import Foundation
import BitFoundation
@testable import bitchat @testable import bitchat
struct MessageRouterTests { struct MessageRouterTests {
@@ -1,5 +1,6 @@
import Foundation import Foundation
import Testing import Testing
import BitFoundation
@testable import bitchat @testable import bitchat
@Suite("NoiseEncryptionService Tests") @Suite("NoiseEncryptionService Tests")
@@ -1,3 +1,4 @@
import Nostr
import Combine import Combine
import XCTest import XCTest
@testable import bitchat @testable import bitchat
@@ -697,7 +698,7 @@ final class NostrRelayManagerTests: XCTestCase {
} }
private func makeContext( private func makeContext(
permission: LocationChannelManager.PermissionState, permission: LocationPermissionState,
favorites: Set<Data> = [], favorites: Set<Data> = [],
activationAllowed: Bool = true, activationAllowed: Bool = true,
userTorEnabled: Bool = false, userTorEnabled: Bool = false,
@@ -705,7 +706,7 @@ final class NostrRelayManagerTests: XCTestCase {
torIsReady: Bool = true, torIsReady: Bool = true,
torIsForeground: Bool = true torIsForeground: Bool = true
) -> RelayManagerTestContext { ) -> RelayManagerTestContext {
let permissionSubject = CurrentValueSubject<LocationChannelManager.PermissionState, Never>(permission) let permissionSubject = CurrentValueSubject<LocationPermissionState, Never>(permission)
let favoritesSubject = CurrentValueSubject<Set<Data>, Never>(favorites) let favoritesSubject = CurrentValueSubject<Set<Data>, Never>(favorites)
let sessionFactory = MockRelaySessionFactory() let sessionFactory = MockRelaySessionFactory()
let scheduler = MockRelayScheduler() let scheduler = MockRelayScheduler()
@@ -782,7 +783,7 @@ final class NostrRelayManagerTests: XCTestCase {
@MainActor @MainActor
private struct RelayManagerTestContext { private struct RelayManagerTestContext {
let manager: NostrRelayManager let manager: NostrRelayManager
let permissionSubject: CurrentValueSubject<LocationChannelManager.PermissionState, Never> let permissionSubject: CurrentValueSubject<LocationPermissionState, Never>
let favoritesSubject: CurrentValueSubject<Set<Data>, Never> let favoritesSubject: CurrentValueSubject<Set<Data>, Never>
let sessionFactory: MockRelaySessionFactory let sessionFactory: MockRelaySessionFactory
let scheduler: MockRelayScheduler let scheduler: MockRelayScheduler
@@ -7,7 +7,9 @@
// //
import Foundation import Foundation
import Nostr
import Testing import Testing
import BitFoundation
@testable import bitchat @testable import bitchat
@Suite("NostrTransport Tests") @Suite("NostrTransport Tests")
@@ -1,5 +1,6 @@
import XCTest import XCTest
import UserNotifications import UserNotifications
import BitFoundation
@testable import bitchat @testable import bitchat
final class NotificationServiceTests: XCTestCase { final class NotificationServiceTests: XCTestCase {
@@ -6,7 +6,9 @@
// //
import Testing import Testing
import Nostr
import Foundation import Foundation
import BitFoundation
@testable import bitchat @testable import bitchat
struct PrivateChatManagerTests { struct PrivateChatManagerTests {
@@ -1,5 +1,6 @@
import Foundation import Foundation
import XCTest import XCTest
import BitFoundation
@testable import bitchat @testable import bitchat
final class SecureIdentityStateManagerTests: XCTestCase { final class SecureIdentityStateManagerTests: XCTestCase {
@@ -6,7 +6,9 @@
// //
import Testing import Testing
import Nostr
import Foundation import Foundation
import BitFoundation
@testable import bitchat @testable import bitchat
struct UnifiedPeerServiceTests { struct UnifiedPeerServiceTests {
@@ -1,4 +1,5 @@
import XCTest import XCTest
import BitFoundation
@testable import bitchat @testable import bitchat
final class RequestSyncManagerTests: XCTestCase { final class RequestSyncManagerTests: XCTestCase {
@@ -8,6 +8,7 @@
import Foundation import Foundation
import CryptoKit import CryptoKit
import BitFoundation
@testable import bitchat @testable import bitchat
final class TestHelpers { final class TestHelpers {
+2
View File
@@ -1,4 +1,5 @@
import Testing import Testing
import Nostr
import Foundation import Foundation
import SwiftUI import SwiftUI
import CoreGraphics import CoreGraphics
@@ -8,6 +9,7 @@ import UIKit
#else #else
import AppKit import AppKit
#endif #endif
import BitFoundation
@testable import bitchat @testable import bitchat
@MainActor @MainActor
+27
View File
@@ -0,0 +1,27 @@
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "BitFoundation",
platforms: [
.iOS(.v16),
.macOS(.v13)
],
products: [
.library(
name: "BitFoundation",
targets: ["BitFoundation"]
)
],
targets: [
.target(
name: "BitFoundation",
path: "Sources"
),
.testTarget(
name: "BitFoundationTests",
dependencies: ["BitFoundation"],
)
]
)
@@ -0,0 +1,57 @@
//
// Data+Hex.swift
// BitFoundation
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import struct Foundation.Data
public extension Data {
func hexEncodedString() -> String {
if self.isEmpty {
return ""
}
return self.map { String(format: "%02x", $0) }.joined()
}
/// Initialize Data from a hex string.
/// - Parameter hexString: A hex string, optionally prefixed with "0x" or "0X".
/// Whitespace is trimmed. Must have even length after prefix removal.
/// - Returns: nil if the string has odd length or contains invalid hex characters.
init?(hexString: String) {
var hex = hexString.trimmed
// Remove optional 0x prefix
if hex.hasPrefix("0x") || hex.hasPrefix("0X") {
hex = String(hex.dropFirst(2))
}
// Reject odd-length strings
guard hex.count % 2 == 0 else {
return nil
}
// Reject empty strings
guard !hex.isEmpty else {
self = Data()
return
}
let len = hex.count / 2
var data = Data(capacity: len)
var index = hex.startIndex
for _ in 0..<len {
let nextIndex = hex.index(index, offsetBy: 2)
guard let byte = UInt8(String(hex[index..<nextIndex]), radix: 16) else {
return nil
}
data.append(byte)
index = nextIndex
}
self = data
}
}
@@ -0,0 +1,33 @@
//
// Data+SHA256.swift
// BitFoundation
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import struct Foundation.Data
private import struct CryptoKit.SHA256
public extension Data {
/// Returns the hex representation of SHA256 hash
func sha256Fingerprint() -> String {
// Implementation matches existing fingerprint generation in NoiseEncryptionService
sha256Hash().hexEncodedString()
}
/// Returns the SHA256 hash wrapped in Data
func sha256Hash() -> Data {
Data(sha256Digest)
}
func sha256Hex() -> String {
sha256Digest.map { String(format: "%02x", $0) }.joined()
}
}
private extension Data {
var sha256Digest: SHA256.Digest {
SHA256.hash(data: self)
}
}
@@ -1,15 +1,27 @@
// //
// PeerID.swift // PeerID.swift
// bitchat // BitFoundation
// //
// This is free and unencumbered software released into the public domain. // This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org> // For more information, see <https://unlicense.org>
// //
import Foundation import struct Foundation.Data
import struct Foundation.CharacterSet
struct PeerID: Equatable, Hashable { public struct PeerID: Equatable, Hashable, Sendable {
enum Prefix: String, CaseIterable { enum Constants {
/// 16
static let nostrConvKeyPrefixLength = 16
/// 8
static let nostrShortKeyDisplayLength = 8
/// 64
fileprivate static let maxIDLength = 64
/// 16
fileprivate static let hexIDLength = 16 // 8 bytes = 16 hex chars
}
public enum Prefix: String, CaseIterable, Sendable {
/// When no prefix is provided /// When no prefix is provided
case empty = "" case empty = ""
/// `"mesh:"` /// `"mesh:"`
@@ -23,15 +35,15 @@ struct PeerID: Equatable, Hashable {
/// `"nostr:"` (+ 8 characters hex) /// `"nostr:"` (+ 8 characters hex)
case geoChat = "nostr:" case geoChat = "nostr:"
} }
let prefix: Prefix public let prefix: Prefix
/// Returns the actual value without any prefix /// Returns the actual value without any prefix
let bare: String public let bare: String
/// Returns the full `id` value by combining `(prefix + bare)` /// Returns the full `id` value by combining `(prefix + bare)`
var id: String { prefix.rawValue + bare } public var id: String { prefix.rawValue + bare }
// Private so the callers have to go through a convenience init // Private so the callers have to go through a convenience init
private init(prefix: Prefix, bare: any StringProtocol) { private init(prefix: Prefix, bare: any StringProtocol) {
self.prefix = prefix self.prefix = prefix
@@ -41,17 +53,17 @@ struct PeerID: Equatable, Hashable {
// MARK: - Convenience Inits // MARK: - Convenience Inits
extension PeerID { public extension PeerID {
/// Convenience init to create GeoDM PeerID by appending `"nostr_"` to the first 16 characters of `pubKey` /// Convenience init to create GeoDM PeerID by appending `"nostr_"` to the first 16 characters of `pubKey`
init(nostr_ pubKey: String) { init(nostr_ pubKey: String) {
self.init(prefix: .geoDM, bare: pubKey.prefix(TransportConfig.nostrConvKeyPrefixLength)) self.init(prefix: .geoDM, bare: pubKey.prefix(Constants.nostrConvKeyPrefixLength))
} }
/// Convenience init to create GeoChat PeerID by appending `"nostr:"` to the first 8 characters of `pubKey` /// Convenience init to create GeoChat PeerID by appending `"nostr:"` to the first 8 characters of `pubKey`
init(nostr pubKey: String) { init(nostr pubKey: String) {
self.init(prefix: .geoChat, bare: pubKey.prefix(TransportConfig.nostrShortKeyDisplayLength)) self.init(prefix: .geoChat, bare: pubKey.prefix(Constants.nostrShortKeyDisplayLength))
} }
/// Convenience init to create PeerID from String/Substring by splitting it into prefix and bare parts /// Convenience init to create PeerID from String/Substring by splitting it into prefix and bare parts
init(str: any StringProtocol) { init(str: any StringProtocol) {
if let prefix = Prefix.allCases.first(where: { $0 != .empty && str.hasPrefix($0.rawValue) }) { if let prefix = Prefix.allCases.first(where: { $0 != .empty && str.hasPrefix($0.rawValue) }) {
@@ -60,23 +72,23 @@ extension PeerID {
self.init(prefix: .empty, bare: str) self.init(prefix: .empty, bare: str)
} }
} }
/// Convenience init to handle `Optional<String>` /// Convenience init to handle `Optional<String>`
init?(str: (any StringProtocol)?) { init?(str: (any StringProtocol)?) {
guard let str else { return nil } guard let str else { return nil }
self.init(str: str) self.init(str: str)
} }
/// Convenience init to create PeerID by converting Data to String /// Convenience init to create PeerID by converting Data to String
init?(data: Data) { init?(data: Data) {
self.init(str: String(data: data, encoding: .utf8)) self.init(str: String(data: data, encoding: .utf8))
} }
/// Convenience init to "hide" hex-encoding implementation detail /// Convenience init to "hide" hex-encoding implementation detail
init(hexData: Data) { init(hexData: Data) {
self.init(str: hexData.hexEncodedString()) self.init(str: hexData.hexEncodedString())
} }
/// Convenience init to "hide" hex-encoding implementation detail /// Convenience init to "hide" hex-encoding implementation detail
init?(hexData: Data?) { init?(hexData: Data?) {
guard let hexData else { return nil } guard let hexData else { return nil }
@@ -86,12 +98,12 @@ extension PeerID {
// MARK: - Noise Public Key Helpers // MARK: - Noise Public Key Helpers
extension PeerID { public extension PeerID {
/// Derive the stable 16-hex peer ID from a Noise static public key /// Derive the stable 16-hex peer ID from a Noise static public key
init(publicKey: Data) { init(publicKey: Data) {
self.init(str: publicKey.sha256Fingerprint().prefix(16)) self.init(str: publicKey.sha256Fingerprint().prefix(16))
} }
/// Returns a 16-hex short peer ID derived from a 64-hex Noise public key if needed /// Returns a 16-hex short peer ID derived from a 64-hex Noise public key if needed
func toShort() -> PeerID { func toShort() -> PeerID {
if let noiseKey { if let noiseKey {
@@ -104,11 +116,11 @@ extension PeerID {
// MARK: - Codable // MARK: - Codable
extension PeerID: Codable { extension PeerID: Codable {
init(from decoder: any Decoder) throws { public init(from decoder: any Decoder) throws {
self.init(str: try decoder.singleValueContainer().decode(String.self)) self.init(str: try decoder.singleValueContainer().decode(String.self))
} }
func encode(to encoder: any Encoder) throws { public func encode(to encoder: any Encoder) throws {
var container = encoder.singleValueContainer() var container = encoder.singleValueContainer()
try container.encode(id) try container.encode(id)
} }
@@ -116,27 +128,27 @@ extension PeerID: Codable {
// MARK: - Helpers // MARK: - Helpers
extension PeerID { public extension PeerID {
var isEmpty: Bool { var isEmpty: Bool {
id.isEmpty id.isEmpty
} }
/// Returns true if `id` starts with "`nostr:`" /// Returns true if `id` starts with "`nostr:`"
var isGeoChat: Bool { var isGeoChat: Bool {
prefix == .geoChat prefix == .geoChat
} }
/// Returns true if `id` starts with "`nostr_`" /// Returns true if `id` starts with "`nostr_`"
var isGeoDM: Bool { var isGeoDM: Bool {
prefix == .geoDM prefix == .geoDM
} }
func toPercentEncoded() -> String { func toPercentEncoded() -> String {
id.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? id id.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? id
} }
} }
extension PeerID { public extension PeerID {
var routingData: Data? { var routingData: Data? {
if let direct = Data(hexString: id), direct.count == 8 { return direct } if let direct = Data(hexString: id), direct.count == 8 { return direct }
if let bareData = Data(hexString: bare), bareData.count == 8 { return bareData } if let bareData = Data(hexString: bare), bareData.count == 8 { return bareData }
@@ -152,50 +164,45 @@ extension PeerID {
// MARK: - Validation // MARK: - Validation
extension PeerID { public extension PeerID {
private enum Constants {
static let maxIDLength = 64
static let hexIDLength = 16 // 8 bytes = 16 hex chars
}
/// Validates a peer ID from any source (short 16-hex, full 64-hex, or internal alnum/-/_ up to 64) /// Validates a peer ID from any source (short 16-hex, full 64-hex, or internal alnum/-/_ up to 64)
var isValid: Bool { var isValid: Bool {
if prefix != .empty { if prefix != .empty {
return PeerID(str: bare).isValid return PeerID(str: bare).isValid
} }
// Accept short routing IDs (exact 16-hex) or Full Noise key hex (exact 64-hex) // Accept short routing IDs (exact 16-hex) or Full Noise key hex (exact 64-hex)
if isShort || isNoiseKeyHex { if isShort || isNoiseKeyHex {
return true return true
} }
// If length equals short or full but isn't valid hex, reject // If length equals short or full but isn't valid hex, reject
if id.count == Constants.hexIDLength || id.count == Constants.maxIDLength { if id.count == Constants.hexIDLength || id.count == Constants.maxIDLength {
return false return false
} }
// Internal format: alphanumeric + dash/underscore up to 63 (not 16 or 64) // Internal format: alphanumeric + dash/underscore up to 63 (not 16 or 64)
let validCharset = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-_")) let validCharset = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-_"))
return !id.isEmpty && return !id.isEmpty &&
id.count < Constants.maxIDLength && id.count < Constants.maxIDLength &&
id.rangeOfCharacter(from: validCharset.inverted) == nil id.rangeOfCharacter(from: validCharset.inverted) == nil
} }
/// Returns true if the `bare` id is all hex /// Returns true if the `bare` id is all hex
var isHex: Bool { var isHex: Bool {
bare.allSatisfy { $0.isHexDigit } bare.allSatisfy { $0.isHexDigit }
} }
/// Short routing IDs (exact 16-hex) /// Short routing IDs (exact 16-hex)
var isShort: Bool { var isShort: Bool {
bare.count == Constants.hexIDLength && isHex bare.count == Constants.hexIDLength && isHex
} }
/// Full Noise key hex (exact 64-hex) /// Full Noise key hex (exact 64-hex)
var isNoiseKeyHex: Bool { var isNoiseKeyHex: Bool {
noiseKey != nil noiseKey != nil
} }
/// Full Noise key (exact 64-hex) as Data /// Full Noise key (exact 64-hex) as Data
var noiseKey: Data? { var noiseKey: Data? {
guard bare.count == Constants.maxIDLength else { return nil } guard bare.count == Constants.maxIDLength else { return nil }
@@ -206,7 +213,7 @@ extension PeerID {
// MARK: - Comparable // MARK: - Comparable
extension PeerID: Comparable { extension PeerID: Comparable {
static func < (lhs: PeerID, rhs: PeerID) -> Bool { public static func < (lhs: PeerID, rhs: PeerID) -> Bool {
lhs.id < rhs.id lhs.id < rhs.id
} }
} }
@@ -215,7 +222,7 @@ extension PeerID: Comparable {
extension PeerID: CustomStringConvertible { extension PeerID: CustomStringConvertible {
/// So it returns the actual `id` like before even inside another String /// So it returns the actual `id` like before even inside another String
var description: String { public var description: String {
id id
} }
} }
@@ -1,12 +1,12 @@
// //
// String+Ext.swift // String+Ext.swift
// bitchat // BitFoundation
// //
// This is free and unencumbered software released into the public domain. // This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org> // For more information, see <https://unlicense.org>
// //
extension StringProtocol { public extension StringProtocol {
var trimmed: String { var trimmed: String {
trimmingCharacters(in: .whitespacesAndNewlines) trimmingCharacters(in: .whitespacesAndNewlines)
} }
@@ -8,7 +8,7 @@
import Testing import Testing
import Foundation import Foundation
@testable import bitchat @testable import BitFoundation
struct PeerIDTests { struct PeerIDTests {
private let hex16 = "0011223344556677" private let hex16 = "0011223344556677"
@@ -168,8 +168,8 @@ struct PeerIDTests {
@Test func nostrUnderscore_pubKey() { @Test func nostrUnderscore_pubKey() {
let pubKey = hex64 let pubKey = hex64
let peerID = PeerID(nostr_: pubKey) let peerID = PeerID(nostr_: pubKey)
#expect(peerID.id == "nostr_\(pubKey.prefix(TransportConfig.nostrConvKeyPrefixLength))") #expect(peerID.id == "nostr_\(pubKey.prefix(PeerID.Constants.nostrConvKeyPrefixLength))")
#expect(peerID.bare == String(pubKey.prefix(TransportConfig.nostrConvKeyPrefixLength))) #expect(peerID.bare == String(pubKey.prefix(PeerID.Constants.nostrConvKeyPrefixLength)))
#expect(peerID.prefix == .geoDM) #expect(peerID.prefix == .geoDM)
} }
@@ -178,8 +178,8 @@ struct PeerIDTests {
@Test func nostr_pubKey() { @Test func nostr_pubKey() {
let pubKey = hex64 let pubKey = hex64
let peerID = PeerID(nostr: pubKey) let peerID = PeerID(nostr: pubKey)
#expect(peerID.id == "nostr:\(pubKey.prefix(TransportConfig.nostrShortKeyDisplayLength))") #expect(peerID.id == "nostr:\(pubKey.prefix(PeerID.Constants.nostrShortKeyDisplayLength))")
#expect(peerID.bare == String(pubKey.prefix(TransportConfig.nostrShortKeyDisplayLength))) #expect(peerID.bare == String(pubKey.prefix(PeerID.Constants.nostrShortKeyDisplayLength)))
#expect(peerID.prefix == .geoChat) #expect(peerID.prefix == .geoChat)
} }
+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
@@ -7,9 +7,10 @@
// //
import BitLogger import BitLogger
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]] = [:]
@@ -18,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)
@@ -48,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)
@@ -84,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
} }
} }
@@ -9,11 +9,12 @@
import BitLogger import BitLogger
import Foundation import Foundation
import CryptoKit import CryptoKit
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?
@@ -33,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
) { ) {
@@ -181,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
@@ -216,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
@@ -9,19 +9,20 @@
import BitLogger import BitLogger
import CryptoKit import CryptoKit
import Foundation import Foundation
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
@@ -37,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
@@ -48,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
@@ -62,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()
@@ -73,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() {
@@ -102,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
@@ -172,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
} }
@@ -180,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
} }
@@ -190,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)] = []
@@ -212,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)

Some files were not shown because too many files have changed in this diff Show More