mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 04:45:20 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa45ee3aea | ||
|
|
e8fb4a6333 | ||
|
|
4cfcefcda6 |
@@ -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
@@ -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")
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|||||||
Generated
+60
@@ -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;
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,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 {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import BitFoundation
|
||||||
|
|
||||||
struct ReadReceipt: Codable {
|
struct ReadReceipt: Codable {
|
||||||
let originalMessageID: String
|
let originalMessageID: String
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import BitFoundation
|
||||||
|
|
||||||
// MARK: - BitChat-over-Nostr Adapter
|
// MARK: - BitChat-over-Nostr Adapter
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import CoreBluetooth
|
import CoreBluetooth
|
||||||
|
import BitFoundation
|
||||||
|
|
||||||
// MARK: - Message Types
|
// MARK: - Message Types
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import BitLogger
|
import BitLogger
|
||||||
|
import BitFoundation
|
||||||
|
import Nostr
|
||||||
import Foundation
|
import Foundation
|
||||||
import CoreBluetooth
|
import CoreBluetooth
|
||||||
import Combine
|
import Combine
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|
||||||
|
|||||||
@@ -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,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,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 {
|
||||||
|
|||||||
@@ -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.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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,4 +1,5 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import BitFoundation
|
||||||
|
|
||||||
struct MeshPeerList: View {
|
struct MeshPeerList: View {
|
||||||
@ObservedObject var viewModel: ChatViewModel
|
@ObservedObject var viewModel: ChatViewModel
|
||||||
|
|||||||
@@ -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,3 +1,4 @@
|
|||||||
|
import Nostr
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import CoreImage
|
import CoreImage
|
||||||
import CoreImage.CIFilterBuiltins
|
import CoreImage.CIFilterBuiltins
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Testing
|
import Testing
|
||||||
|
import Nostr
|
||||||
import Foundation
|
import Foundation
|
||||||
@testable import bitchat
|
@testable import bitchat
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,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 {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import BitFoundation
|
||||||
@testable import bitchat
|
@testable import bitchat
|
||||||
|
|
||||||
final class MockBLEBus {
|
final class MockBLEBus {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
-23
@@ -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:"`
|
||||||
@@ -24,13 +36,13 @@ struct PeerID: Equatable, Hashable {
|
|||||||
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) {
|
||||||
@@ -41,15 +53,15 @@ 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
|
||||||
@@ -86,7 +98,7 @@ 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))
|
||||||
@@ -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,7 +128,7 @@ extension PeerID: Codable {
|
|||||||
|
|
||||||
// MARK: - Helpers
|
// MARK: - Helpers
|
||||||
|
|
||||||
extension PeerID {
|
public extension PeerID {
|
||||||
var isEmpty: Bool {
|
var isEmpty: Bool {
|
||||||
id.isEmpty
|
id.isEmpty
|
||||||
}
|
}
|
||||||
@@ -136,7 +148,7 @@ extension PeerID {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,12 +164,7 @@ 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 {
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+2
-2
@@ -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)
|
||||||
}
|
}
|
||||||
+5
-5
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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")
|
||||||
|
]
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
+4
-3
@@ -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
-4
@@ -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]] = [:]
|
||||||
|
|
||||||
@@ -19,7 +20,9 @@ final class NoiseRateLimiter {
|
|||||||
|
|
||||||
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()
|
||||||
+5
-5
@@ -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
|
||||||
}
|
}
|
||||||
+1
-1
@@ -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
|
||||||
+3
-3
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+7
-6
@@ -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
|
||||||
|
|
||||||
+1
-1
@@ -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
|
||||||
+16
-15
@@ -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
Reference in New Issue
Block a user