mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 17:45:19 +00:00
Refactor/repo hardening 01 (#462)
* BinaryProtocol: add optional padding control; BitchatPacket API for padded/unpadded bytes; BLEService: use unpadded encoding and remove ad-hoc unpadding on BLE writes * BLEService: balance BLE padding — pad Noise handshake/encrypted frames; leave public/announce/leave unpadded; keep fragmentation consistent with chosen padding * BLEService: replace Timer with DispatchSourceTimer on bleQueue; NostrTransport: cache placeholder NoiseEncryptionService to avoid reallocation * Unify peerID validation: InputValidator handles 16-hex, 64-hex, or alnum-/_; NoiseSecurityValidator now delegates to InputValidator * UI: use standard green for geohash toolbar badge and count (less bright in light mode) * UI: standardize geohash sheet green to app standard (dark: system green, light: darker green) for buttons and checkmark * Docs: align BinaryProtocol compression docs to zlib; Logs: reduce NostrTransport DELIVERED ack logs to debug to cut noise * Tests: add InputValidator peerID coverage and BinaryProtocol padding round-trip/length tests * Project: ensure Xcode project reflects new tests (references added) --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
@@ -19,6 +19,10 @@
|
|||||||
047502902E5417660083520F /* LocationChannelsSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0475028E2E5417660083520F /* LocationChannelsSheet.swift */; };
|
047502902E5417660083520F /* LocationChannelsSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0475028E2E5417660083520F /* LocationChannelsSheet.swift */; };
|
||||||
047502922E547ACC0083520F /* LocationChannelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047502912E547ACC0083520F /* LocationChannelsTests.swift */; };
|
047502922E547ACC0083520F /* LocationChannelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047502912E547ACC0083520F /* LocationChannelsTests.swift */; };
|
||||||
047502932E547ACC0083520F /* LocationChannelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047502912E547ACC0083520F /* LocationChannelsTests.swift */; };
|
047502932E547ACC0083520F /* LocationChannelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047502912E547ACC0083520F /* LocationChannelsTests.swift */; };
|
||||||
|
047502AC2E55E8360083520F /* BinaryProtocolPaddingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047502AB2E55E8360083520F /* BinaryProtocolPaddingTests.swift */; };
|
||||||
|
047502AD2E55E8360083520F /* BinaryProtocolPaddingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047502AB2E55E8360083520F /* BinaryProtocolPaddingTests.swift */; };
|
||||||
|
047502B02E55E8450083520F /* InputValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047502AE2E55E8450083520F /* InputValidatorTests.swift */; };
|
||||||
|
047502B12E55E8450083520F /* InputValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047502AE2E55E8450083520F /* InputValidatorTests.swift */; };
|
||||||
049BD3902E4EC4F0001A566B /* PrivateChatManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD38F2E4EC4F0001A566B /* PrivateChatManager.swift */; };
|
049BD3902E4EC4F0001A566B /* PrivateChatManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD38F2E4EC4F0001A566B /* PrivateChatManager.swift */; };
|
||||||
049BD3912E4EC4F0001A566B /* AutocompleteService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD38C2E4EC4F0001A566B /* AutocompleteService.swift */; };
|
049BD3912E4EC4F0001A566B /* AutocompleteService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD38C2E4EC4F0001A566B /* AutocompleteService.swift */; };
|
||||||
049BD3922E4EC4F0001A566B /* CommandProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD38D2E4EC4F0001A566B /* CommandProcessor.swift */; };
|
049BD3922E4EC4F0001A566B /* CommandProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD38D2E4EC4F0001A566B /* CommandProcessor.swift */; };
|
||||||
@@ -174,6 +178,8 @@
|
|||||||
0475028B2E54171C0083520F /* LocationChannelManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationChannelManager.swift; sourceTree = "<group>"; };
|
0475028B2E54171C0083520F /* LocationChannelManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationChannelManager.swift; sourceTree = "<group>"; };
|
||||||
0475028E2E5417660083520F /* LocationChannelsSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationChannelsSheet.swift; sourceTree = "<group>"; };
|
0475028E2E5417660083520F /* LocationChannelsSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationChannelsSheet.swift; sourceTree = "<group>"; };
|
||||||
047502912E547ACC0083520F /* LocationChannelsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationChannelsTests.swift; sourceTree = "<group>"; };
|
047502912E547ACC0083520F /* LocationChannelsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationChannelsTests.swift; sourceTree = "<group>"; };
|
||||||
|
047502AB2E55E8360083520F /* BinaryProtocolPaddingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocolPaddingTests.swift; sourceTree = "<group>"; };
|
||||||
|
047502AE2E55E8450083520F /* InputValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputValidatorTests.swift; sourceTree = "<group>"; };
|
||||||
049BD38C2E4EC4F0001A566B /* AutocompleteService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteService.swift; sourceTree = "<group>"; };
|
049BD38C2E4EC4F0001A566B /* AutocompleteService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteService.swift; sourceTree = "<group>"; };
|
||||||
049BD38D2E4EC4F0001A566B /* CommandProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandProcessor.swift; sourceTree = "<group>"; };
|
049BD38D2E4EC4F0001A566B /* CommandProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandProcessor.swift; sourceTree = "<group>"; };
|
||||||
049BD38F2E4EC4F0001A566B /* PrivateChatManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateChatManager.swift; sourceTree = "<group>"; };
|
049BD38F2E4EC4F0001A566B /* PrivateChatManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateChatManager.swift; sourceTree = "<group>"; };
|
||||||
@@ -197,7 +203,7 @@
|
|||||||
3448F84BF86A42A3CC4A9379 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
3448F84BF86A42A3CC4A9379 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
||||||
3668EEBB42FD4A24D5D83B7B /* bitchatShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = bitchatShareExtension.entitlements; sourceTree = "<group>"; };
|
3668EEBB42FD4A24D5D83B7B /* bitchatShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = bitchatShareExtension.entitlements; sourceTree = "<group>"; };
|
||||||
394E8A1AC76EFAE352075BE9 /* NoiseEncryptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseEncryptionService.swift; sourceTree = "<group>"; };
|
394E8A1AC76EFAE352075BE9 /* NoiseEncryptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseEncryptionService.swift; sourceTree = "<group>"; };
|
||||||
3A556661F74B7D5AE2F0521B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
3A556661F74B7D5AE2F0521B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
3A69677D382F1C3D5ED03F7D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
3A69677D382F1C3D5ED03F7D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
419BFFF209EBA93F410E9E9F /* FavoritesPersistenceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesPersistenceService.swift; sourceTree = "<group>"; };
|
419BFFF209EBA93F410E9E9F /* FavoritesPersistenceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesPersistenceService.swift; sourceTree = "<group>"; };
|
||||||
43613045E63D21D429396805 /* NoiseProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseProtocol.swift; sourceTree = "<group>"; };
|
43613045E63D21D429396805 /* NoiseProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseProtocol.swift; sourceTree = "<group>"; };
|
||||||
@@ -267,6 +273,14 @@
|
|||||||
path = Fragmentation;
|
path = Fragmentation;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
047502AF2E55E8450083520F /* Utils */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
047502AE2E55E8450083520F /* InputValidatorTests.swift */,
|
||||||
|
);
|
||||||
|
path = Utils;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
0575DCBD15C7C719ADDCB67E /* Models */ = {
|
0575DCBD15C7C719ADDCB67E /* Models */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -346,6 +360,7 @@
|
|||||||
84933DAE9D7E5D0155BA7AEA /* Protocol */ = {
|
84933DAE9D7E5D0155BA7AEA /* Protocol */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
047502AB2E55E8360083520F /* BinaryProtocolPaddingTests.swift */,
|
||||||
0B3CC6FA298729906109F61B /* BinaryProtocolTests.swift */,
|
0B3CC6FA298729906109F61B /* BinaryProtocolTests.swift */,
|
||||||
);
|
);
|
||||||
path = Protocol;
|
path = Protocol;
|
||||||
@@ -443,6 +458,7 @@
|
|||||||
D80E19E04513C0046D611574 /* Noise */,
|
D80E19E04513C0046D611574 /* Noise */,
|
||||||
84933DAE9D7E5D0155BA7AEA /* Protocol */,
|
84933DAE9D7E5D0155BA7AEA /* Protocol */,
|
||||||
204CC4C7704C7348D456E374 /* TestUtilities */,
|
204CC4C7704C7348D456E374 /* TestUtilities */,
|
||||||
|
047502AF2E55E8450083520F /* Utils */,
|
||||||
);
|
);
|
||||||
path = bitchatTests;
|
path = bitchatTests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -785,6 +801,7 @@
|
|||||||
AA6E067DB034FC0FA23C28A9 /* BinaryProtocolTests.swift in Sources */,
|
AA6E067DB034FC0FA23C28A9 /* BinaryProtocolTests.swift in Sources */,
|
||||||
047502802E53A0FC0083520F /* FragmentationTests.swift in Sources */,
|
047502802E53A0FC0083520F /* FragmentationTests.swift in Sources */,
|
||||||
8F282E9CCA5AE1ECC001D2E4 /* IntegrationTests.swift in Sources */,
|
8F282E9CCA5AE1ECC001D2E4 /* IntegrationTests.swift in Sources */,
|
||||||
|
047502B12E55E8450083520F /* InputValidatorTests.swift in Sources */,
|
||||||
D727EA273CB214FC32612469 /* MockBluetoothMeshService.swift in Sources */,
|
D727EA273CB214FC32612469 /* MockBluetoothMeshService.swift in Sources */,
|
||||||
047502932E547ACC0083520F /* LocationChannelsTests.swift in Sources */,
|
047502932E547ACC0083520F /* LocationChannelsTests.swift in Sources */,
|
||||||
6C803BF930E7E19BE6E99EAA /* MockBLEService.swift in Sources */,
|
6C803BF930E7E19BE6E99EAA /* MockBLEService.swift in Sources */,
|
||||||
@@ -794,6 +811,7 @@
|
|||||||
A0A1C26EFBFDD5B8EFEEDE57 /* PublicChatE2ETests.swift in Sources */,
|
A0A1C26EFBFDD5B8EFEEDE57 /* PublicChatE2ETests.swift in Sources */,
|
||||||
A2977428C1D9EF9944C4BFAF /* BLEServiceTests.swift in Sources */,
|
A2977428C1D9EF9944C4BFAF /* BLEServiceTests.swift in Sources */,
|
||||||
2EFCCAA297B16FA2B56747C7 /* TestConstants.swift in Sources */,
|
2EFCCAA297B16FA2B56747C7 /* TestConstants.swift in Sources */,
|
||||||
|
047502AD2E55E8360083520F /* BinaryProtocolPaddingTests.swift in Sources */,
|
||||||
B45AD5BF95220A0289216D32 /* TestHelpers.swift in Sources */,
|
B45AD5BF95220A0289216D32 /* TestHelpers.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -805,6 +823,7 @@
|
|||||||
0B6F25559A21F8C69C8357C6 /* BinaryProtocolTests.swift in Sources */,
|
0B6F25559A21F8C69C8357C6 /* BinaryProtocolTests.swift in Sources */,
|
||||||
047502812E53A0FC0083520F /* FragmentationTests.swift in Sources */,
|
047502812E53A0FC0083520F /* FragmentationTests.swift in Sources */,
|
||||||
686441ABC2AF83EE98E6ECF2 /* IntegrationTests.swift in Sources */,
|
686441ABC2AF83EE98E6ECF2 /* IntegrationTests.swift in Sources */,
|
||||||
|
047502B02E55E8450083520F /* InputValidatorTests.swift in Sources */,
|
||||||
8851F08D88C5B1DE7B9F55C6 /* MockBluetoothMeshService.swift in Sources */,
|
8851F08D88C5B1DE7B9F55C6 /* MockBluetoothMeshService.swift in Sources */,
|
||||||
047502922E547ACC0083520F /* LocationChannelsTests.swift in Sources */,
|
047502922E547ACC0083520F /* LocationChannelsTests.swift in Sources */,
|
||||||
3849CA6D99B2D536636DF4A6 /* MockBLEService.swift in Sources */,
|
3849CA6D99B2D536636DF4A6 /* MockBLEService.swift in Sources */,
|
||||||
@@ -814,6 +833,7 @@
|
|||||||
8CE446C9364F54DF89E7A364 /* PublicChatE2ETests.swift in Sources */,
|
8CE446C9364F54DF89E7A364 /* PublicChatE2ETests.swift in Sources */,
|
||||||
BE729E149C98F775D9622D9C /* BLEServiceTests.swift in Sources */,
|
BE729E149C98F775D9622D9C /* BLEServiceTests.swift in Sources */,
|
||||||
8D0196EAEE56973679F6A655 /* TestConstants.swift in Sources */,
|
8D0196EAEE56973679F6A655 /* TestConstants.swift in Sources */,
|
||||||
|
047502AC2E55E8360083520F /* BinaryProtocolPaddingTests.swift in Sources */,
|
||||||
37DDF3D09E2BAB92A5A8A9C1 /* TestHelpers.swift in Sources */,
|
37DDF3D09E2BAB92A5A8A9C1 /* TestHelpers.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
|||||||
@@ -53,13 +53,9 @@ struct NoiseSecurityValidator {
|
|||||||
return data.count <= NoiseSecurityConstants.maxHandshakeMessageSize
|
return data.count <= NoiseSecurityConstants.maxHandshakeMessageSize
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validate peer ID format
|
/// Validate peer ID format using unified validator
|
||||||
static func validatePeerID(_ peerID: String) -> Bool {
|
static func validatePeerID(_ peerID: String) -> Bool {
|
||||||
// Peer ID should be reasonable length and contain valid characters
|
return InputValidator.validatePeerID(peerID)
|
||||||
let validCharset = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-_"))
|
|
||||||
return peerID.count > 0 &&
|
|
||||||
peerID.count <= 64 &&
|
|
||||||
peerID.rangeOfCharacter(from: validCharset.inverted) == nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
///
|
///
|
||||||
/// ## Compression Strategy
|
/// ## Compression Strategy
|
||||||
/// - Automatic compression for payloads > 256 bytes
|
/// - Automatic compression for payloads > 256 bytes
|
||||||
/// - LZ4 algorithm for speed over ratio
|
/// - zlib compression for broad compatibility on Apple platforms
|
||||||
/// - Original size stored for decompression
|
/// - Original size stored for decompression
|
||||||
/// - Flag bit indicates compressed payload
|
/// - Flag bit indicates compressed payload
|
||||||
///
|
///
|
||||||
@@ -117,7 +117,7 @@ struct BinaryProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Encode BitchatPacket to binary format
|
// Encode BitchatPacket to binary format
|
||||||
static func encode(_ packet: BitchatPacket) -> Data? {
|
static func encode(_ packet: BitchatPacket, padding: Bool = true) -> Data? {
|
||||||
var data = Data()
|
var data = Data()
|
||||||
|
|
||||||
|
|
||||||
@@ -200,11 +200,14 @@ struct BinaryProtocol {
|
|||||||
|
|
||||||
|
|
||||||
// Apply padding to standard block sizes for traffic analysis resistance
|
// Apply padding to standard block sizes for traffic analysis resistance
|
||||||
|
if padding {
|
||||||
let optimalSize = MessagePadding.optimalBlockSize(for: data.count)
|
let optimalSize = MessagePadding.optimalBlockSize(for: data.count)
|
||||||
let paddedData = MessagePadding.pad(data, toSize: optimalSize)
|
let paddedData = MessagePadding.pad(data, toSize: optimalSize)
|
||||||
|
|
||||||
|
|
||||||
return paddedData
|
return paddedData
|
||||||
|
} else {
|
||||||
|
// Caller explicitly requested no padding (e.g., BLE write path)
|
||||||
|
return data
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode binary data to BitchatPacket
|
// Decode binary data to BitchatPacket
|
||||||
|
|||||||
@@ -232,8 +232,13 @@ struct BitchatPacket: Codable {
|
|||||||
BinaryProtocol.encode(self)
|
BinaryProtocol.encode(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toBinaryData(padding: Bool = true) -> Data? {
|
||||||
|
BinaryProtocol.encode(self, padding: padding)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backward-compatible helper (defaults to padded encoding)
|
||||||
func toBinaryData() -> Data? {
|
func toBinaryData() -> Data? {
|
||||||
BinaryProtocol.encode(self)
|
toBinaryData(padding: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create binary representation for signing (without signature and TTL fields)
|
/// Create binary representation for signing (without signature and TTL fields)
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ final class BLEService: NSObject {
|
|||||||
|
|
||||||
// MARK: - Maintenance Timer
|
// MARK: - Maintenance Timer
|
||||||
|
|
||||||
private weak var maintenanceTimer: Timer? // Single timer for all maintenance tasks
|
private var maintenanceTimer: DispatchSourceTimer? // Single timer for all maintenance tasks
|
||||||
private var maintenanceCounter = 0 // Track maintenance cycles
|
private var maintenanceCounter = 0 // Track maintenance cycles
|
||||||
|
|
||||||
// MARK: - Peer snapshots publisher (non-UI convenience)
|
// MARK: - Peer snapshots publisher (non-UI convenience)
|
||||||
@@ -206,10 +206,14 @@ final class BLEService: NSObject {
|
|||||||
centralManager = CBCentralManager(delegate: self, queue: bleQueue)
|
centralManager = CBCentralManager(delegate: self, queue: bleQueue)
|
||||||
peripheralManager = CBPeripheralManager(delegate: self, queue: bleQueue)
|
peripheralManager = CBPeripheralManager(delegate: self, queue: bleQueue)
|
||||||
|
|
||||||
// Single maintenance timer for all periodic tasks
|
// Single maintenance timer for all periodic tasks (dispatch-based for determinism)
|
||||||
maintenanceTimer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: true) { [weak self] _ in
|
let timer = DispatchSource.makeTimerSource(queue: bleQueue)
|
||||||
|
timer.schedule(deadline: .now() + 10.0, repeating: 10.0, leeway: .seconds(1))
|
||||||
|
timer.setEventHandler { [weak self] in
|
||||||
self?.performMaintenance()
|
self?.performMaintenance()
|
||||||
}
|
}
|
||||||
|
timer.resume()
|
||||||
|
maintenanceTimer = timer
|
||||||
|
|
||||||
// Publish initial empty state
|
// Publish initial empty state
|
||||||
publishFullPeerData()
|
publishFullPeerData()
|
||||||
@@ -224,7 +228,7 @@ final class BLEService: NSObject {
|
|||||||
// No advertising policy to set; we never include Local Name in adverts.
|
// No advertising policy to set; we never include Local Name in adverts.
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
maintenanceTimer?.invalidate()
|
maintenanceTimer?.cancel()
|
||||||
centralManager?.stopScan()
|
centralManager?.stopScan()
|
||||||
peripheralManager?.stopAdvertising()
|
peripheralManager?.stopAdvertising()
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
@@ -308,7 +312,7 @@ final class BLEService: NSObject {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Send immediately to all connected peers
|
// Send immediately to all connected peers
|
||||||
if let data = leavePacket.toBinaryData() {
|
if let data = leavePacket.toBinaryData(padding: false) {
|
||||||
// Send to peripherals we're connected to as central
|
// Send to peripherals we're connected to as central
|
||||||
for state in peripherals.values where state.isConnected {
|
for state in peripherals.values where state.isConnected {
|
||||||
if let characteristic = state.characteristic {
|
if let characteristic = state.characteristic {
|
||||||
@@ -331,7 +335,7 @@ final class BLEService: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stop timer
|
// Stop timer
|
||||||
maintenanceTimer?.invalidate()
|
maintenanceTimer?.cancel()
|
||||||
maintenanceTimer = nil
|
maintenanceTimer = nil
|
||||||
|
|
||||||
centralManager?.stopScan()
|
centralManager?.stopScan()
|
||||||
@@ -718,12 +722,24 @@ final class BLEService: NSObject {
|
|||||||
// MARK: - Packet Broadcasting
|
// MARK: - Packet Broadcasting
|
||||||
|
|
||||||
private func broadcastPacket(_ packet: BitchatPacket) {
|
private func broadcastPacket(_ packet: BitchatPacket) {
|
||||||
guard let rawData = packet.toBinaryData() else {
|
// Balanced privacy: pad sensitive payloads (Noise handshake/encrypted),
|
||||||
|
// keep public/announce/leave unpadded to reduce airtime.
|
||||||
|
let padForBLE: Bool = {
|
||||||
|
if let t = MessageType(rawValue: packet.type) {
|
||||||
|
switch t {
|
||||||
|
case .noiseEncrypted, .noiseHandshake:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}()
|
||||||
|
|
||||||
|
guard let data = packet.toBinaryData(padding: padForBLE) else {
|
||||||
SecureLogger.log("❌ Failed to convert packet to binary data", category: SecureLogger.session, level: .error)
|
SecureLogger.log("❌ Failed to convert packet to binary data", category: SecureLogger.session, level: .error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Avoid sending padded data over BLE to reduce truncation risk
|
|
||||||
let data = MessagePadding.unpad(rawData)
|
|
||||||
|
|
||||||
// Only log broadcasts for non-announce packets
|
// Only log broadcasts for non-announce packets
|
||||||
// Log encrypted and relayed packets for debugging
|
// Log encrypted and relayed packets for debugging
|
||||||
@@ -737,7 +753,7 @@ final class BLEService: NSObject {
|
|||||||
// Check if application-level fragmentation needed for large messages
|
// Check if application-level fragmentation needed for large messages
|
||||||
// (CoreBluetooth only handles ATT-level fragmentation for single writes)
|
// (CoreBluetooth only handles ATT-level fragmentation for single writes)
|
||||||
if data.count > 512 && packet.type != MessageType.fragment.rawValue {
|
if data.count > 512 && packet.type != MessageType.fragment.rawValue {
|
||||||
sendFragmentedPacket(packet)
|
sendFragmentedPacket(packet, pad: padForBLE)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -832,8 +848,8 @@ final class BLEService: NSObject {
|
|||||||
// Cannot deliver any BitChat frame via notifications on this link; skip notify
|
// Cannot deliver any BitChat frame via notifications on this link; skip notify
|
||||||
SecureLogger.log("⚠️ Skipping notify: central max update length (\(minAllowed)) < 21", category: SecureLogger.session, level: .debug)
|
SecureLogger.log("⚠️ Skipping notify: central max update length (\(minAllowed)) < 21", category: SecureLogger.session, level: .debug)
|
||||||
} else if data.count > minAllowed {
|
} else if data.count > minAllowed {
|
||||||
// Fragment via protocol
|
// Fragment via protocol (preserve chosen padding for BLE)
|
||||||
sendFragmentedPacket(packet)
|
sendFragmentedPacket(packet, pad: padForBLE)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let success = peripheralManager?.updateValue(data, for: characteristic, onSubscribedCentrals: nil) ?? false
|
let success = peripheralManager?.updateValue(data, for: characteristic, onSubscribedCentrals: nil) ?? false
|
||||||
@@ -886,10 +902,9 @@ final class BLEService: NSObject {
|
|||||||
|
|
||||||
// MARK: - Fragmentation (Required for messages > BLE MTU)
|
// MARK: - Fragmentation (Required for messages > BLE MTU)
|
||||||
|
|
||||||
private func sendFragmentedPacket(_ packet: BitchatPacket) {
|
private func sendFragmentedPacket(_ packet: BitchatPacket, pad: Bool) {
|
||||||
guard let encoded = packet.toBinaryData() else { return }
|
guard let fullData = packet.toBinaryData(padding: pad) else { return }
|
||||||
// Fragment the unpadded frame; each fragment will be encoded (and padded) independently
|
// Fragment the unpadded frame; each fragment will be encoded independently
|
||||||
let fullData = MessagePadding.unpad(encoded)
|
|
||||||
|
|
||||||
let fragmentID = Data((0..<8).map { _ in UInt8.random(in: 0...255) })
|
let fragmentID = Data((0..<8).map { _ in UInt8.random(in: 0...255) })
|
||||||
let fragments = stride(from: 0, to: fullData.count, by: maxFragmentSize).map { offset in
|
let fragments = stride(from: 0, to: fullData.count, by: maxFragmentSize).map { offset in
|
||||||
|
|||||||
@@ -37,7 +37,11 @@ final class NostrTransport: Transport {
|
|||||||
func getFingerprint(for peerID: String) -> String? { nil }
|
func getFingerprint(for peerID: String) -> String? { nil }
|
||||||
func getNoiseSessionState(for peerID: String) -> LazyHandshakeState { .none }
|
func getNoiseSessionState(for peerID: String) -> LazyHandshakeState { .none }
|
||||||
func triggerHandshake(with peerID: String) { /* no-op */ }
|
func triggerHandshake(with peerID: String) { /* no-op */ }
|
||||||
func getNoiseService() -> NoiseEncryptionService { NoiseEncryptionService() }
|
// Nostr does not use Noise sessions here; return a cached placeholder to avoid reallocation
|
||||||
|
private static var cachedNoiseService: NoiseEncryptionService = {
|
||||||
|
NoiseEncryptionService()
|
||||||
|
}()
|
||||||
|
func getNoiseService() -> NoiseEncryptionService { Self.cachedNoiseService }
|
||||||
|
|
||||||
// Public broadcast not supported over Nostr here
|
// Public broadcast not supported over Nostr here
|
||||||
func sendMessage(_ content: String, mentions: [String]) { /* no-op */ }
|
func sendMessage(_ content: String, mentions: [String]) { /* no-op */ }
|
||||||
@@ -193,7 +197,7 @@ final class NostrTransport: Transport {
|
|||||||
guard let recipientNpub = recipientNostrPubkey else { return }
|
guard let recipientNpub = recipientNostrPubkey else { return }
|
||||||
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return }
|
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return }
|
||||||
SecureLogger.log("NostrTransport: preparing DELIVERED ack for id=\(messageID.prefix(8))… to \(recipientNpub.prefix(16))…",
|
SecureLogger.log("NostrTransport: preparing DELIVERED ack for id=\(messageID.prefix(8))… to \(recipientNpub.prefix(16))…",
|
||||||
category: SecureLogger.session, level: .info)
|
category: SecureLogger.session, level: .debug)
|
||||||
let recipientHex: String
|
let recipientHex: String
|
||||||
do {
|
do {
|
||||||
let (hrp, data) = try Bech32.decode(recipientNpub)
|
let (hrp, data) = try Bech32.decode(recipientNpub)
|
||||||
@@ -209,7 +213,7 @@ final class NostrTransport: Transport {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
SecureLogger.log("NostrTransport: sending DELIVERED ack giftWrap id=\(event.id.prefix(16))…",
|
SecureLogger.log("NostrTransport: sending DELIVERED ack giftWrap id=\(event.id.prefix(16))…",
|
||||||
category: SecureLogger.session, level: .info)
|
category: SecureLogger.session, level: .debug)
|
||||||
NostrRelayManager.shared.sendEvent(event)
|
NostrRelayManager.shared.sendEvent(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,20 +16,18 @@ struct InputValidator {
|
|||||||
|
|
||||||
// MARK: - Peer ID Validation
|
// MARK: - Peer ID Validation
|
||||||
|
|
||||||
/// Validates a peer ID from any source
|
/// Validates a peer ID from any source (short 16-hex, full 64-hex, or internal alnum/-/_ up to 64)
|
||||||
static func validatePeerID(_ peerID: String) -> Bool {
|
static func validatePeerID(_ peerID: String) -> Bool {
|
||||||
// Handle both hex-encoded (from network) and alphanumeric (internal) formats
|
// Accept short routing IDs (16-hex)
|
||||||
if peerID.count == Limits.hexPeerIDLength {
|
if PeerIDResolver.isShortID(peerID) { return true }
|
||||||
// Network format: 16 hex characters
|
// Accept full Noise key hex (64-hex)
|
||||||
return peerID.allSatisfy { $0.isHexDigit }
|
if PeerIDResolver.isNoiseKeyHex(peerID) { return true }
|
||||||
} else {
|
// Internal format: alphanumeric + dash/underscore up to 64
|
||||||
// Internal format: alphanumeric + dash/underscore
|
|
||||||
let validCharset = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-_"))
|
let validCharset = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-_"))
|
||||||
return peerID.count > 0 &&
|
return !peerID.isEmpty &&
|
||||||
peerID.count <= Limits.maxPeerIDLength &&
|
peerID.count <= Limits.maxPeerIDLength &&
|
||||||
peerID.rangeOfCharacter(from: validCharset.inverted) == nil
|
peerID.rangeOfCharacter(from: validCharset.inverted) == nil
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - String Content Validation
|
// MARK: - String Content Validation
|
||||||
|
|
||||||
|
|||||||
@@ -938,7 +938,9 @@ struct ContentView: View {
|
|||||||
switch locationManager.selectedChannel {
|
switch locationManager.selectedChannel {
|
||||||
case .location:
|
case .location:
|
||||||
let n = viewModel.geohashPeople.count
|
let n = viewModel.geohashPeople.count
|
||||||
return (n, n > 0 ? Color.green : Color.secondary)
|
// Use standard green (dark: system green; light: custom darker green)
|
||||||
|
let standardGreen = (colorScheme == .dark) ? Color.green : Color(red: 0, green: 0.5, blue: 0)
|
||||||
|
return (n, n > 0 ? standardGreen : Color.secondary)
|
||||||
case .mesh:
|
case .mesh:
|
||||||
let counts = viewModel.allPeers.reduce(into: (others: 0, mesh: 0)) { counts, peer in
|
let counts = viewModel.allPeers.reduce(into: (others: 0, mesh: 0)) { counts, peer in
|
||||||
guard peer.id != viewModel.meshService.myPeerID else { return }
|
guard peer.id != viewModel.meshService.myPeerID else { return }
|
||||||
@@ -1037,7 +1039,8 @@ struct ContentView: View {
|
|||||||
case .mesh:
|
case .mesh:
|
||||||
return Color.blue
|
return Color.blue
|
||||||
case .location:
|
case .location:
|
||||||
return Color.green
|
// Standard green to avoid overly bright appearance in light mode
|
||||||
|
return (colorScheme == .dark) ? Color.green : Color(red: 0, green: 0.5, blue: 0)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
Text(badgeText)
|
Text(badgeText)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ struct LocationChannelsSheet: View {
|
|||||||
@Binding var isPresented: Bool
|
@Binding var isPresented: Bool
|
||||||
@ObservedObject private var manager = LocationChannelManager.shared
|
@ObservedObject private var manager = LocationChannelManager.shared
|
||||||
@EnvironmentObject var viewModel: ChatViewModel
|
@EnvironmentObject var viewModel: ChatViewModel
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
@State private var customGeohash: String = ""
|
@State private var customGeohash: String = ""
|
||||||
@State private var customError: String? = nil
|
@State private var customError: String? = nil
|
||||||
|
|
||||||
@@ -24,10 +25,10 @@ struct LocationChannelsSheet: View {
|
|||||||
Button(action: { manager.enableLocationChannels() }) {
|
Button(action: { manager.enableLocationChannels() }) {
|
||||||
Text("get location and my geohashes")
|
Text("get location and my geohashes")
|
||||||
.font(.system(size: 12, design: .monospaced))
|
.font(.system(size: 12, design: .monospaced))
|
||||||
.foregroundColor(Color.green)
|
.foregroundColor(standardGreen)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.padding(.vertical, 6)
|
.padding(.vertical, 6)
|
||||||
.background(Color.green.opacity(0.12))
|
.background(standardGreen.opacity(0.12))
|
||||||
.cornerRadius(6)
|
.cornerRadius(6)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
@@ -220,7 +221,7 @@ struct LocationChannelsSheet: View {
|
|||||||
if isSelected {
|
if isSelected {
|
||||||
Text("✔︎")
|
Text("✔︎")
|
||||||
.font(.system(size: 16, design: .monospaced))
|
.font(.system(size: 16, design: .monospaced))
|
||||||
.foregroundColor(.green)
|
.foregroundColor(standardGreen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
@@ -274,4 +275,11 @@ struct LocationChannelsSheet: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Standardized Colors
|
||||||
|
extension LocationChannelsSheet {
|
||||||
|
private var standardGreen: Color {
|
||||||
|
(colorScheme == .dark) ? Color.green : Color(red: 0, green: 0.5, blue: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
//
|
||||||
|
// BinaryProtocolPaddingTests.swift
|
||||||
|
// bitchatTests
|
||||||
|
//
|
||||||
|
// This is free and unencumbered software released into the public domain.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
@testable import bitchat
|
||||||
|
|
||||||
|
final class BinaryProtocolPaddingTests: XCTestCase {
|
||||||
|
func test_padded_vs_unpadded_length() throws {
|
||||||
|
// Use helper to create a small test packet
|
||||||
|
let packet = TestHelpers.createTestPacket()
|
||||||
|
guard let padded = BinaryProtocol.encode(packet, padding: true) else { return XCTFail("encode padded") }
|
||||||
|
guard let unpadded = BinaryProtocol.encode(packet, padding: false) else { return XCTFail("encode unpadded") }
|
||||||
|
XCTAssertGreaterThanOrEqual(padded.count, unpadded.count, "Padded frame should be >= unpadded")
|
||||||
|
}
|
||||||
|
|
||||||
|
func test_decode_padded_and_unpadded_round_trip() throws {
|
||||||
|
let packet = TestHelpers.createTestPacket()
|
||||||
|
// Padded
|
||||||
|
guard let padded = BinaryProtocol.encode(packet, padding: true) else { return XCTFail("encode padded") }
|
||||||
|
guard let dec1 = BinaryProtocol.decode(padded) else { return XCTFail("decode padded") }
|
||||||
|
XCTAssertEqual(dec1.type, packet.type)
|
||||||
|
XCTAssertEqual(dec1.payload, packet.payload)
|
||||||
|
// Unpadded
|
||||||
|
guard let unpadded = BinaryProtocol.encode(packet, padding: false) else { return XCTFail("encode unpadded") }
|
||||||
|
guard let dec2 = BinaryProtocol.decode(unpadded) else { return XCTFail("decode unpadded") }
|
||||||
|
XCTAssertEqual(dec2.type, packet.type)
|
||||||
|
XCTAssertEqual(dec2.payload, packet.payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
//
|
||||||
|
// InputValidatorTests.swift
|
||||||
|
// bitchatTests
|
||||||
|
//
|
||||||
|
// This is free and unencumbered software released into the public domain.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
@testable import bitchat
|
||||||
|
|
||||||
|
final class InputValidatorTests: XCTestCase {
|
||||||
|
func test_accepts_short_hex_peer_id() {
|
||||||
|
XCTAssertTrue(InputValidator.validatePeerID("0011223344556677"))
|
||||||
|
XCTAssertTrue(InputValidator.validatePeerID("aabbccddeeff0011"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func test_accepts_full_noise_key_hex() {
|
||||||
|
let hex64 = String(repeating: "ab", count: 32) // 64 hex chars
|
||||||
|
XCTAssertTrue(InputValidator.validatePeerID(hex64))
|
||||||
|
}
|
||||||
|
|
||||||
|
func test_accepts_internal_alnum_dash_underscore() {
|
||||||
|
XCTAssertTrue(InputValidator.validatePeerID("peer_123-ABC"))
|
||||||
|
XCTAssertTrue(InputValidator.validatePeerID("nostr_user_01"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func test_rejects_invalid_characters() {
|
||||||
|
XCTAssertFalse(InputValidator.validatePeerID("peer!@#"))
|
||||||
|
XCTAssertFalse(InputValidator.validatePeerID("gggggggggggggggg")) // not hex for short form
|
||||||
|
}
|
||||||
|
|
||||||
|
func test_rejects_too_long() {
|
||||||
|
let tooLong = String(repeating: "a", count: 65)
|
||||||
|
XCTAssertFalse(InputValidator.validatePeerID(tooLong))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user