mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 23:25:20 +00:00
Security fixes and improvements (#374)
- Fix force unwrapping in NostrIdentity bech32 functions that could crash on non-ASCII input - Add comprehensive input validation for all protocol messages (peer IDs, nicknames, timestamps) - Strengthen keychain security with better sandbox detection and consistent app group usage - Implement secure memory clearing for cryptographic keys and shared secrets - Fix panic mode not reconnecting to mesh by restarting services after emergency disconnect Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
@@ -32,6 +32,8 @@
|
|||||||
04636BEB2E30BEC600FBCFA8 /* IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BE12E30BEC600FBCFA8 /* IntegrationTests.swift */; };
|
04636BEB2E30BEC600FBCFA8 /* IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BE12E30BEC600FBCFA8 /* IntegrationTests.swift */; };
|
||||||
04636BED2E30CD4A00FBCFA8 /* NoiseHandshakeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BEC2E30CD4A00FBCFA8 /* NoiseHandshakeCoordinator.swift */; };
|
04636BED2E30CD4A00FBCFA8 /* NoiseHandshakeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BEC2E30CD4A00FBCFA8 /* NoiseHandshakeCoordinator.swift */; };
|
||||||
04636BEE2E30CD4A00FBCFA8 /* NoiseHandshakeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BEC2E30CD4A00FBCFA8 /* NoiseHandshakeCoordinator.swift */; };
|
04636BEE2E30CD4A00FBCFA8 /* NoiseHandshakeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BEC2E30CD4A00FBCFA8 /* NoiseHandshakeCoordinator.swift */; };
|
||||||
|
046D705D2E3C105D00C00594 /* InputValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 046D705C2E3C105D00C00594 /* InputValidator.swift */; };
|
||||||
|
046D705E2E3C105D00C00594 /* InputValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 046D705C2E3C105D00C00594 /* InputValidator.swift */; };
|
||||||
04891CA92E22971E0064A111 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04891CA82E22971E0064A111 /* LRUCache.swift */; };
|
04891CA92E22971E0064A111 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04891CA82E22971E0064A111 /* LRUCache.swift */; };
|
||||||
04891CAA2E22971E0064A111 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04891CA82E22971E0064A111 /* LRUCache.swift */; };
|
04891CAA2E22971E0064A111 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04891CA82E22971E0064A111 /* LRUCache.swift */; };
|
||||||
04AD0B4E2E25B9580002A40A /* IdentityModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2446380E7A44E49A35B664 /* IdentityModels.swift */; };
|
04AD0B4E2E25B9580002A40A /* IdentityModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2446380E7A44E49A35B664 /* IdentityModels.swift */; };
|
||||||
@@ -154,6 +156,7 @@
|
|||||||
04636BCF2E30BE5100FBCFA8 /* TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestHelpers.swift; sourceTree = "<group>"; };
|
04636BCF2E30BE5100FBCFA8 /* TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestHelpers.swift; sourceTree = "<group>"; };
|
||||||
04636BE12E30BEC600FBCFA8 /* IntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationTests.swift; sourceTree = "<group>"; };
|
04636BE12E30BEC600FBCFA8 /* IntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationTests.swift; sourceTree = "<group>"; };
|
||||||
04636BEC2E30CD4A00FBCFA8 /* NoiseHandshakeCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseHandshakeCoordinator.swift; sourceTree = "<group>"; };
|
04636BEC2E30CD4A00FBCFA8 /* NoiseHandshakeCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseHandshakeCoordinator.swift; sourceTree = "<group>"; };
|
||||||
|
046D705C2E3C105D00C00594 /* InputValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputValidator.swift; sourceTree = "<group>"; };
|
||||||
04891CA82E22971E0064A111 /* LRUCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LRUCache.swift; sourceTree = "<group>"; };
|
04891CA82E22971E0064A111 /* LRUCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LRUCache.swift; sourceTree = "<group>"; };
|
||||||
04B6BA412E2035530090FE39 /* NoiseProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseProtocol.swift; sourceTree = "<group>"; };
|
04B6BA412E2035530090FE39 /* NoiseProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseProtocol.swift; sourceTree = "<group>"; };
|
||||||
04B6BA422E2035530090FE39 /* NoiseSecurityConsiderations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseSecurityConsiderations.swift; sourceTree = "<group>"; };
|
04B6BA422E2035530090FE39 /* NoiseSecurityConsiderations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseSecurityConsiderations.swift; sourceTree = "<group>"; };
|
||||||
@@ -361,6 +364,7 @@
|
|||||||
9A78348821A7D3374607D4E3 /* Utils */ = {
|
9A78348821A7D3374607D4E3 /* Utils */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
046D705C2E3C105D00C00594 /* InputValidator.swift */,
|
||||||
04636BBE2E2FCA8A00FBCFA8 /* SecureLogger.swift */,
|
04636BBE2E2FCA8A00FBCFA8 /* SecureLogger.swift */,
|
||||||
04891CA82E22971E0064A111 /* LRUCache.swift */,
|
04891CA82E22971E0064A111 /* LRUCache.swift */,
|
||||||
ED176FF3B274E35C2D827894 /* BatteryOptimizer.swift */,
|
ED176FF3B274E35C2D827894 /* BatteryOptimizer.swift */,
|
||||||
@@ -655,6 +659,7 @@
|
|||||||
31D147471B9F4E2815352DDA /* LinkPreviewView.swift in Sources */,
|
31D147471B9F4E2815352DDA /* LinkPreviewView.swift in Sources */,
|
||||||
04F127FE2E37EF3D00FFBA8D /* BitchatPeer.swift in Sources */,
|
04F127FE2E37EF3D00FFBA8D /* BitchatPeer.swift in Sources */,
|
||||||
04F128062E37F10000FFBA8D /* PeerSession.swift in Sources */,
|
04F128062E37F10000FFBA8D /* PeerSession.swift in Sources */,
|
||||||
|
046D705E2E3C105D00C00594 /* InputValidator.swift in Sources */,
|
||||||
04B6BA572E203D6C0090FE39 /* FingerprintView.swift in Sources */,
|
04B6BA572E203D6C0090FE39 /* FingerprintView.swift in Sources */,
|
||||||
04636BED2E30CD4A00FBCFA8 /* NoiseHandshakeCoordinator.swift in Sources */,
|
04636BED2E30CD4A00FBCFA8 /* NoiseHandshakeCoordinator.swift in Sources */,
|
||||||
C99763A4761567F587D21688 /* MessageRetryService.swift in Sources */,
|
C99763A4761567F587D21688 /* MessageRetryService.swift in Sources */,
|
||||||
@@ -696,6 +701,7 @@
|
|||||||
7A5B1AB5642FEC168E917949 /* LinkPreviewView.swift in Sources */,
|
7A5B1AB5642FEC168E917949 /* LinkPreviewView.swift in Sources */,
|
||||||
04F127FF2E37EF3D00FFBA8D /* BitchatPeer.swift in Sources */,
|
04F127FF2E37EF3D00FFBA8D /* BitchatPeer.swift in Sources */,
|
||||||
04F128082E37F10000FFBA8D /* PeerSession.swift in Sources */,
|
04F128082E37F10000FFBA8D /* PeerSession.swift in Sources */,
|
||||||
|
046D705D2E3C105D00C00594 /* InputValidator.swift in Sources */,
|
||||||
04B6BA552E203D6C0090FE39 /* FingerprintView.swift in Sources */,
|
04B6BA552E203D6C0090FE39 /* FingerprintView.swift in Sources */,
|
||||||
04636BEE2E30CD4A00FBCFA8 /* NoiseHandshakeCoordinator.swift in Sources */,
|
04636BEE2E30CD4A00FBCFA8 /* NoiseHandshakeCoordinator.swift in Sources */,
|
||||||
CEAE115C9C3EB3C4ED82F128 /* MessageRetryService.swift in Sources */,
|
CEAE115C9C3EB3C4ED82F128 /* MessageRetryService.swift in Sources */,
|
||||||
|
|||||||
@@ -149,6 +149,10 @@ class NoiseCipherState {
|
|||||||
self.useExtractedNonce = useExtractedNonce
|
self.useExtractedNonce = useExtractedNonce
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
clearSensitiveData()
|
||||||
|
}
|
||||||
|
|
||||||
func initializeKey(_ key: SymmetricKey) {
|
func initializeKey(_ key: SymmetricKey) {
|
||||||
self.key = key
|
self.key = key
|
||||||
self.nonce = 0
|
self.nonce = 0
|
||||||
@@ -357,6 +361,21 @@ class NoiseCipherState {
|
|||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Securely clear sensitive cryptographic data from memory
|
||||||
|
func clearSensitiveData() {
|
||||||
|
// Clear the symmetric key
|
||||||
|
key = nil
|
||||||
|
|
||||||
|
// Reset nonce
|
||||||
|
nonce = 0
|
||||||
|
highestReceivedNonce = 0
|
||||||
|
|
||||||
|
// Clear replay window
|
||||||
|
for i in 0..<replayWindow.count {
|
||||||
|
replayWindow[i] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Symmetric State
|
// MARK: - Symmetric State
|
||||||
@@ -557,7 +576,10 @@ class NoiseHandshakeState {
|
|||||||
throw NoiseError.missingKeys
|
throw NoiseError.missingKeys
|
||||||
}
|
}
|
||||||
let shared = try localEphemeral.sharedSecretFromKeyAgreement(with: remoteEphemeral)
|
let shared = try localEphemeral.sharedSecretFromKeyAgreement(with: remoteEphemeral)
|
||||||
symmetricState.mixKey(shared.withUnsafeBytes { Data($0) })
|
var sharedData = shared.withUnsafeBytes { Data($0) }
|
||||||
|
symmetricState.mixKey(sharedData)
|
||||||
|
// Clear sensitive shared secret
|
||||||
|
KeychainManager.secureClear(&sharedData)
|
||||||
|
|
||||||
case .es:
|
case .es:
|
||||||
// DH(ephemeral, static) - direction depends on role
|
// DH(ephemeral, static) - direction depends on role
|
||||||
@@ -602,7 +624,10 @@ class NoiseHandshakeState {
|
|||||||
throw NoiseError.missingKeys
|
throw NoiseError.missingKeys
|
||||||
}
|
}
|
||||||
let shared = try localStatic.sharedSecretFromKeyAgreement(with: remoteStatic)
|
let shared = try localStatic.sharedSecretFromKeyAgreement(with: remoteStatic)
|
||||||
symmetricState.mixKey(shared.withUnsafeBytes { Data($0) })
|
var sharedData = shared.withUnsafeBytes { Data($0) }
|
||||||
|
symmetricState.mixKey(sharedData)
|
||||||
|
// Clear sensitive shared secret
|
||||||
|
KeychainManager.secureClear(&sharedData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -687,14 +712,20 @@ class NoiseHandshakeState {
|
|||||||
throw NoiseError.missingKeys
|
throw NoiseError.missingKeys
|
||||||
}
|
}
|
||||||
let shared = try localEphemeral.sharedSecretFromKeyAgreement(with: remoteStatic)
|
let shared = try localEphemeral.sharedSecretFromKeyAgreement(with: remoteStatic)
|
||||||
symmetricState.mixKey(shared.withUnsafeBytes { Data($0) })
|
var sharedData = shared.withUnsafeBytes { Data($0) }
|
||||||
|
symmetricState.mixKey(sharedData)
|
||||||
|
// Clear sensitive shared secret
|
||||||
|
KeychainManager.secureClear(&sharedData)
|
||||||
} else {
|
} else {
|
||||||
guard let localStatic = localStaticPrivate,
|
guard let localStatic = localStaticPrivate,
|
||||||
let remoteEphemeral = remoteEphemeralPublic else {
|
let remoteEphemeral = remoteEphemeralPublic else {
|
||||||
throw NoiseError.missingKeys
|
throw NoiseError.missingKeys
|
||||||
}
|
}
|
||||||
let shared = try localStatic.sharedSecretFromKeyAgreement(with: remoteEphemeral)
|
let shared = try localStatic.sharedSecretFromKeyAgreement(with: remoteEphemeral)
|
||||||
symmetricState.mixKey(shared.withUnsafeBytes { Data($0) })
|
var sharedData = shared.withUnsafeBytes { Data($0) }
|
||||||
|
symmetricState.mixKey(sharedData)
|
||||||
|
// Clear sensitive shared secret
|
||||||
|
KeychainManager.secureClear(&sharedData)
|
||||||
}
|
}
|
||||||
|
|
||||||
case .se:
|
case .se:
|
||||||
@@ -704,14 +735,20 @@ class NoiseHandshakeState {
|
|||||||
throw NoiseError.missingKeys
|
throw NoiseError.missingKeys
|
||||||
}
|
}
|
||||||
let shared = try localStatic.sharedSecretFromKeyAgreement(with: remoteEphemeral)
|
let shared = try localStatic.sharedSecretFromKeyAgreement(with: remoteEphemeral)
|
||||||
symmetricState.mixKey(shared.withUnsafeBytes { Data($0) })
|
var sharedData = shared.withUnsafeBytes { Data($0) }
|
||||||
|
symmetricState.mixKey(sharedData)
|
||||||
|
// Clear sensitive shared secret
|
||||||
|
KeychainManager.secureClear(&sharedData)
|
||||||
} else {
|
} else {
|
||||||
guard let localEphemeral = localEphemeralPrivate,
|
guard let localEphemeral = localEphemeralPrivate,
|
||||||
let remoteStatic = remoteStaticPublic else {
|
let remoteStatic = remoteStaticPublic else {
|
||||||
throw NoiseError.missingKeys
|
throw NoiseError.missingKeys
|
||||||
}
|
}
|
||||||
let shared = try localEphemeral.sharedSecretFromKeyAgreement(with: remoteStatic)
|
let shared = try localEphemeral.sharedSecretFromKeyAgreement(with: remoteStatic)
|
||||||
symmetricState.mixKey(shared.withUnsafeBytes { Data($0) })
|
var sharedData = shared.withUnsafeBytes { Data($0) }
|
||||||
|
symmetricState.mixKey(sharedData)
|
||||||
|
// Clear sensitive shared secret
|
||||||
|
KeychainManager.secureClear(&sharedData)
|
||||||
}
|
}
|
||||||
|
|
||||||
case .ss:
|
case .ss:
|
||||||
|
|||||||
@@ -221,9 +221,24 @@ class NoiseSession {
|
|||||||
let wasEstablished = state == .established
|
let wasEstablished = state == .established
|
||||||
state = .uninitialized
|
state = .uninitialized
|
||||||
handshakeState = nil
|
handshakeState = nil
|
||||||
|
|
||||||
|
// Clear sensitive cipher states
|
||||||
|
sendCipher?.clearSensitiveData()
|
||||||
|
receiveCipher?.clearSensitiveData()
|
||||||
sendCipher = nil
|
sendCipher = nil
|
||||||
receiveCipher = nil
|
receiveCipher = nil
|
||||||
|
|
||||||
|
// Clear sent handshake messages
|
||||||
|
for i in 0..<sentHandshakeMessages.count {
|
||||||
|
var message = sentHandshakeMessages[i]
|
||||||
|
KeychainManager.secureClear(&message)
|
||||||
|
}
|
||||||
sentHandshakeMessages.removeAll()
|
sentHandshakeMessages.removeAll()
|
||||||
|
|
||||||
|
// Clear handshake hash
|
||||||
|
if var hash = handshakeHash {
|
||||||
|
KeychainManager.secureClear(&hash)
|
||||||
|
}
|
||||||
handshakeHash = nil
|
handshakeHash = nil
|
||||||
|
|
||||||
if wasEstablished {
|
if wasEstablished {
|
||||||
@@ -270,8 +285,12 @@ class NoiseSessionManager {
|
|||||||
|
|
||||||
func removeSession(for peerID: String) {
|
func removeSession(for peerID: String) {
|
||||||
managerQueue.sync(flags: .barrier) {
|
managerQueue.sync(flags: .barrier) {
|
||||||
if let session = sessions[peerID], session.isEstablished() {
|
if let session = sessions[peerID] {
|
||||||
SecureLogger.logSecurityEvent(.sessionExpired(peerID: peerID))
|
if session.isEstablished() {
|
||||||
|
SecureLogger.logSecurityEvent(.sessionExpired(peerID: peerID))
|
||||||
|
}
|
||||||
|
// Clear sensitive data before removing
|
||||||
|
session.reset()
|
||||||
}
|
}
|
||||||
_ = sessions.removeValue(forKey: peerID)
|
_ = sessions.removeValue(forKey: peerID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,6 +165,14 @@ enum Bech32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let hrp = String(bech32String[..<separatorIndex])
|
let hrp = String(bech32String[..<separatorIndex])
|
||||||
|
|
||||||
|
// Validate HRP contains only ASCII characters
|
||||||
|
for char in hrp {
|
||||||
|
guard char.asciiValue != nil else {
|
||||||
|
throw Bech32Error.invalidCharacter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let dataString = String(bech32String[bech32String.index(after: separatorIndex)...])
|
let dataString = String(bech32String[bech32String.index(after: separatorIndex)...])
|
||||||
|
|
||||||
// Convert characters to values
|
// Convert characters to values
|
||||||
@@ -238,11 +246,17 @@ enum Bech32 {
|
|||||||
private static func hrpExpand(_ hrp: String) -> [UInt8] {
|
private static func hrpExpand(_ hrp: String) -> [UInt8] {
|
||||||
var result = [UInt8]()
|
var result = [UInt8]()
|
||||||
for c in hrp {
|
for c in hrp {
|
||||||
result.append(UInt8(c.asciiValue! >> 5))
|
guard let asciiValue = c.asciiValue else {
|
||||||
|
return [] // Return empty array for invalid input
|
||||||
|
}
|
||||||
|
result.append(UInt8(asciiValue >> 5))
|
||||||
}
|
}
|
||||||
result.append(0)
|
result.append(0)
|
||||||
for c in hrp {
|
for c in hrp {
|
||||||
result.append(UInt8(c.asciiValue! & 31))
|
guard let asciiValue = c.asciiValue else {
|
||||||
|
return [] // Return empty array for invalid input
|
||||||
|
}
|
||||||
|
result.append(UInt8(asciiValue & 31))
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -355,10 +355,14 @@ struct DeliveryAck: Codable {
|
|||||||
|
|
||||||
guard let recipientIDData = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil }
|
guard let recipientIDData = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil }
|
||||||
let recipientID = recipientIDData.hexEncodedString()
|
let recipientID = recipientIDData.hexEncodedString()
|
||||||
|
guard InputValidator.validatePeerID(recipientID) else { return nil }
|
||||||
|
|
||||||
guard let hopCount = dataCopy.readUInt8(at: &offset),
|
guard let hopCount = dataCopy.readUInt8(at: &offset),
|
||||||
|
InputValidator.validateHopCount(hopCount),
|
||||||
let timestamp = dataCopy.readDate(at: &offset),
|
let timestamp = dataCopy.readDate(at: &offset),
|
||||||
let recipientNickname = dataCopy.readString(at: &offset) else { return nil }
|
InputValidator.validateTimestamp(timestamp),
|
||||||
|
let recipientNicknameRaw = dataCopy.readString(at: &offset),
|
||||||
|
let recipientNickname = InputValidator.validateNickname(recipientNicknameRaw) else { return nil }
|
||||||
|
|
||||||
return DeliveryAck(originalMessageID: originalMessageID,
|
return DeliveryAck(originalMessageID: originalMessageID,
|
||||||
ackID: ackID,
|
ackID: ackID,
|
||||||
@@ -443,9 +447,12 @@ struct ReadReceipt: Codable {
|
|||||||
|
|
||||||
guard let readerIDData = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil }
|
guard let readerIDData = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil }
|
||||||
let readerID = readerIDData.hexEncodedString()
|
let readerID = readerIDData.hexEncodedString()
|
||||||
|
guard InputValidator.validatePeerID(readerID) else { return nil }
|
||||||
|
|
||||||
guard let timestamp = dataCopy.readDate(at: &offset),
|
guard let timestamp = dataCopy.readDate(at: &offset),
|
||||||
let readerNickname = dataCopy.readString(at: &offset) else { return nil }
|
InputValidator.validateTimestamp(timestamp),
|
||||||
|
let readerNicknameRaw = dataCopy.readString(at: &offset),
|
||||||
|
let readerNickname = InputValidator.validateNickname(readerNicknameRaw) else { return nil }
|
||||||
|
|
||||||
return ReadReceipt(originalMessageID: originalMessageID,
|
return ReadReceipt(originalMessageID: originalMessageID,
|
||||||
receiptID: receiptID,
|
receiptID: receiptID,
|
||||||
@@ -540,13 +547,17 @@ struct HandshakeRequest: Codable {
|
|||||||
|
|
||||||
guard let requesterIDData = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil }
|
guard let requesterIDData = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil }
|
||||||
let requesterID = requesterIDData.hexEncodedString()
|
let requesterID = requesterIDData.hexEncodedString()
|
||||||
|
guard InputValidator.validatePeerID(requesterID) else { return nil }
|
||||||
|
|
||||||
guard let targetIDData = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil }
|
guard let targetIDData = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil }
|
||||||
let targetID = targetIDData.hexEncodedString()
|
let targetID = targetIDData.hexEncodedString()
|
||||||
|
guard InputValidator.validatePeerID(targetID) else { return nil }
|
||||||
|
|
||||||
guard let pendingMessageCount = dataCopy.readUInt8(at: &offset),
|
guard let pendingMessageCount = dataCopy.readUInt8(at: &offset),
|
||||||
let timestamp = dataCopy.readDate(at: &offset),
|
let timestamp = dataCopy.readDate(at: &offset),
|
||||||
let requesterNickname = dataCopy.readString(at: &offset) else { return nil }
|
InputValidator.validateTimestamp(timestamp),
|
||||||
|
let requesterNicknameRaw = dataCopy.readString(at: &offset),
|
||||||
|
let requesterNickname = InputValidator.validateNickname(requesterNicknameRaw) else { return nil }
|
||||||
|
|
||||||
return HandshakeRequest(requestID: requestID,
|
return HandshakeRequest(requestID: requestID,
|
||||||
requesterID: requesterID,
|
requesterID: requesterID,
|
||||||
@@ -616,11 +627,16 @@ struct ProtocolAck: Codable {
|
|||||||
let senderIDData = dataCopy.readFixedBytes(at: &offset, count: 8),
|
let senderIDData = dataCopy.readFixedBytes(at: &offset, count: 8),
|
||||||
let receiverIDData = dataCopy.readFixedBytes(at: &offset, count: 8),
|
let receiverIDData = dataCopy.readFixedBytes(at: &offset, count: 8),
|
||||||
let packetType = dataCopy.readUInt8(at: &offset),
|
let packetType = dataCopy.readUInt8(at: &offset),
|
||||||
|
InputValidator.validateMessageType(packetType),
|
||||||
let hopCount = dataCopy.readUInt8(at: &offset),
|
let hopCount = dataCopy.readUInt8(at: &offset),
|
||||||
let timestamp = dataCopy.readDate(at: &offset) else { return nil }
|
InputValidator.validateHopCount(hopCount),
|
||||||
|
let timestamp = dataCopy.readDate(at: &offset),
|
||||||
|
InputValidator.validateTimestamp(timestamp) else { return nil }
|
||||||
|
|
||||||
let senderID = senderIDData.hexEncodedString()
|
let senderID = senderIDData.hexEncodedString()
|
||||||
let receiverID = receiverIDData.hexEncodedString()
|
let receiverID = receiverIDData.hexEncodedString()
|
||||||
|
guard InputValidator.validatePeerID(senderID),
|
||||||
|
InputValidator.validatePeerID(receiverID) else { return nil }
|
||||||
|
|
||||||
return ProtocolAck(originalPacketID: originalPacketID,
|
return ProtocolAck(originalPacketID: originalPacketID,
|
||||||
ackID: ackID,
|
ackID: ackID,
|
||||||
@@ -706,12 +722,17 @@ struct ProtocolNack: Codable {
|
|||||||
let senderIDData = dataCopy.readFixedBytes(at: &offset, count: 8),
|
let senderIDData = dataCopy.readFixedBytes(at: &offset, count: 8),
|
||||||
let receiverIDData = dataCopy.readFixedBytes(at: &offset, count: 8),
|
let receiverIDData = dataCopy.readFixedBytes(at: &offset, count: 8),
|
||||||
let packetType = dataCopy.readUInt8(at: &offset),
|
let packetType = dataCopy.readUInt8(at: &offset),
|
||||||
|
InputValidator.validateMessageType(packetType),
|
||||||
let errorCode = dataCopy.readUInt8(at: &offset),
|
let errorCode = dataCopy.readUInt8(at: &offset),
|
||||||
let timestamp = dataCopy.readDate(at: &offset),
|
let timestamp = dataCopy.readDate(at: &offset),
|
||||||
let reason = dataCopy.readString(at: &offset) else { return nil }
|
InputValidator.validateTimestamp(timestamp),
|
||||||
|
let reasonRaw = dataCopy.readString(at: &offset),
|
||||||
|
let reason = InputValidator.validateReasonString(reasonRaw) else { return nil }
|
||||||
|
|
||||||
let senderID = senderIDData.hexEncodedString()
|
let senderID = senderIDData.hexEncodedString()
|
||||||
let receiverID = receiverIDData.hexEncodedString()
|
let receiverID = receiverIDData.hexEncodedString()
|
||||||
|
guard InputValidator.validatePeerID(senderID),
|
||||||
|
InputValidator.validatePeerID(receiverID) else { return nil }
|
||||||
|
|
||||||
return ProtocolNack(originalPacketID: originalPacketID,
|
return ProtocolNack(originalPacketID: originalPacketID,
|
||||||
nackID: nackID,
|
nackID: nackID,
|
||||||
@@ -839,23 +860,28 @@ struct NoiseIdentityAnnouncement: Codable {
|
|||||||
// Read peerID using safe method
|
// Read peerID using safe method
|
||||||
guard let peerIDBytes = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil }
|
guard let peerIDBytes = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil }
|
||||||
let peerID = peerIDBytes.hexEncodedString()
|
let peerID = peerIDBytes.hexEncodedString()
|
||||||
|
guard InputValidator.validatePeerID(peerID) else { return nil }
|
||||||
|
|
||||||
guard let publicKey = dataCopy.readData(at: &offset),
|
guard let publicKey = dataCopy.readData(at: &offset),
|
||||||
|
InputValidator.validatePublicKey(publicKey),
|
||||||
let signingPublicKey = dataCopy.readData(at: &offset),
|
let signingPublicKey = dataCopy.readData(at: &offset),
|
||||||
|
InputValidator.validatePublicKey(signingPublicKey),
|
||||||
let rawNickname = dataCopy.readString(at: &offset),
|
let rawNickname = dataCopy.readString(at: &offset),
|
||||||
let timestamp = dataCopy.readDate(at: &offset) else { return nil }
|
let nickname = InputValidator.validateNickname(rawNickname),
|
||||||
|
let timestamp = dataCopy.readDate(at: &offset),
|
||||||
// Trim whitespace from nickname
|
InputValidator.validateTimestamp(timestamp) else { return nil }
|
||||||
let nickname = rawNickname.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
|
|
||||||
var previousPeerID: String? = nil
|
var previousPeerID: String? = nil
|
||||||
if hasPreviousPeerID {
|
if hasPreviousPeerID {
|
||||||
// Read previousPeerID using safe method
|
// Read previousPeerID using safe method
|
||||||
guard let prevIDBytes = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil }
|
guard let prevIDBytes = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil }
|
||||||
previousPeerID = prevIDBytes.hexEncodedString()
|
let prevID = prevIDBytes.hexEncodedString()
|
||||||
|
guard InputValidator.validatePeerID(prevID) else { return nil }
|
||||||
|
previousPeerID = prevID
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let signature = dataCopy.readData(at: &offset) else { return nil }
|
guard let signature = dataCopy.readData(at: &offset),
|
||||||
|
InputValidator.validateSignature(signature) else { return nil }
|
||||||
|
|
||||||
return NoiseIdentityAnnouncement(peerID: peerID,
|
return NoiseIdentityAnnouncement(peerID: peerID,
|
||||||
publicKey: publicKey,
|
publicKey: publicKey,
|
||||||
|
|||||||
@@ -2424,6 +2424,45 @@ class BluetoothMeshService: NSObject {
|
|||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
self?.delegate?.didUpdatePeerList([])
|
self?.delegate?.didUpdatePeerList([])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restart services after a short delay to create new identity
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
|
||||||
|
self?.restartAfterPanic()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func restartAfterPanic() {
|
||||||
|
SecureLogger.log("Restarting mesh services after panic mode", category: SecureLogger.session, level: .info)
|
||||||
|
|
||||||
|
// Regenerate peer ID with new identity
|
||||||
|
myPeerID = generateNewPeerID()
|
||||||
|
|
||||||
|
// Reset identity tracking since we have a new identity
|
||||||
|
peerIDToFingerprint.removeAll()
|
||||||
|
fingerprintToPeerID.removeAll()
|
||||||
|
peerIdentityBindings.removeAll()
|
||||||
|
|
||||||
|
// Reset rotation tracking
|
||||||
|
rotationTimestamp = nil
|
||||||
|
rotationLocked = false
|
||||||
|
|
||||||
|
// Restart advertising if peripheral is powered on
|
||||||
|
if peripheralManager?.state == .poweredOn {
|
||||||
|
setupPeripheral()
|
||||||
|
startAdvertising()
|
||||||
|
|
||||||
|
// Send announce after restart with new identity
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
||||||
|
self?.sendBroadcastAnnounce()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart scanning if central is powered on
|
||||||
|
if centralManager?.state == .poweredOn {
|
||||||
|
startScanning()
|
||||||
|
}
|
||||||
|
|
||||||
|
SecureLogger.log("Mesh services restarted with new peer ID: \(myPeerID)", category: SecureLogger.session, level: .info)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getAllConnectedPeerIDs() -> [String] {
|
private func getAllConnectedPeerIDs() -> [String] {
|
||||||
|
|||||||
@@ -88,10 +88,29 @@ class KeychainManager {
|
|||||||
|
|
||||||
private func isSandboxed() -> Bool {
|
private func isSandboxed() -> Bool {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
|
// More robust sandbox detection using multiple methods
|
||||||
|
|
||||||
|
// Method 1: Check environment variable (can be spoofed)
|
||||||
let environment = ProcessInfo.processInfo.environment
|
let environment = ProcessInfo.processInfo.environment
|
||||||
return environment["APP_SANDBOX_CONTAINER_ID"] != nil
|
let hasEnvVar = environment["APP_SANDBOX_CONTAINER_ID"] != nil
|
||||||
|
|
||||||
|
// Method 2: Check if we can access a path outside sandbox
|
||||||
|
let homeDir = FileManager.default.homeDirectoryForCurrentUser
|
||||||
|
let testPath = homeDir.appendingPathComponent("../../../tmp/bitchat_sandbox_test_\(UUID().uuidString)")
|
||||||
|
let canWriteOutsideSandbox = FileManager.default.createFile(atPath: testPath.path, contents: nil, attributes: nil)
|
||||||
|
if canWriteOutsideSandbox {
|
||||||
|
try? FileManager.default.removeItem(at: testPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 3: Check container path
|
||||||
|
let containerPath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first?.path ?? ""
|
||||||
|
let hasContainerPath = containerPath.contains("/Containers/")
|
||||||
|
|
||||||
|
// If any method indicates sandbox, we consider it sandboxed
|
||||||
|
return hasEnvVar || !canWriteOutsideSandbox || hasContainerPath
|
||||||
#else
|
#else
|
||||||
return false
|
// iOS is always sandboxed
|
||||||
|
return true
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,10 +158,9 @@ class KeychainManager {
|
|||||||
// Add a label for easier debugging
|
// Add a label for easier debugging
|
||||||
query[kSecAttrLabel as String] = "bitchat-\(key)"
|
query[kSecAttrLabel as String] = "bitchat-\(key)"
|
||||||
|
|
||||||
// For sandboxed apps, use the app group for sharing between app instances
|
// Always use app group when available for consistent behavior
|
||||||
if isSandboxed() {
|
// This ensures keychain items are properly isolated to our app
|
||||||
query[kSecAttrAccessGroup as String] = appGroup
|
query[kSecAttrAccessGroup as String] = appGroup
|
||||||
}
|
|
||||||
|
|
||||||
// For sandboxed macOS apps, we need to ensure the item is NOT synchronized
|
// For sandboxed macOS apps, we need to ensure the item is NOT synchronized
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
@@ -176,10 +194,8 @@ class KeychainManager {
|
|||||||
kSecMatchLimit as String: kSecMatchLimitOne
|
kSecMatchLimit as String: kSecMatchLimitOne
|
||||||
]
|
]
|
||||||
|
|
||||||
// For sandboxed apps, use the app group
|
// Always use app group for consistent behavior
|
||||||
if isSandboxed() {
|
query[kSecAttrAccessGroup as String] = appGroup
|
||||||
query[kSecAttrAccessGroup as String] = appGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
var result: AnyObject?
|
var result: AnyObject?
|
||||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||||
@@ -201,10 +217,8 @@ class KeychainManager {
|
|||||||
kSecAttrService as String: service
|
kSecAttrService as String: service
|
||||||
]
|
]
|
||||||
|
|
||||||
// For sandboxed apps, use the app group
|
// Always use app group for consistent behavior
|
||||||
if isSandboxed() {
|
query[kSecAttrAccessGroup as String] = appGroup
|
||||||
query[kSecAttrAccessGroup as String] = appGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
let status = SecItemDelete(query as CFDictionary)
|
let status = SecItemDelete(query as CFDictionary)
|
||||||
return status == errSecSuccess || status == errSecItemNotFound
|
return status == errSecSuccess || status == errSecItemNotFound
|
||||||
@@ -253,10 +267,25 @@ class KeychainManager {
|
|||||||
var shouldDelete = false
|
var shouldDelete = false
|
||||||
let account = item[kSecAttrAccount as String] as? String ?? ""
|
let account = item[kSecAttrAccount as String] as? String ?? ""
|
||||||
let service = item[kSecAttrService as String] as? String ?? ""
|
let service = item[kSecAttrService as String] as? String ?? ""
|
||||||
|
let accessGroup = item[kSecAttrAccessGroup as String] as? String
|
||||||
|
|
||||||
// ONLY delete if service name contains "bitchat"
|
// More precise deletion criteria:
|
||||||
// This is the safest approach - we only touch items we know are ours
|
// 1. Check for our specific app group
|
||||||
if service.lowercased().contains("bitchat") {
|
// 2. OR check for our exact service name
|
||||||
|
// 3. OR check for known legacy service names
|
||||||
|
if accessGroup == appGroup {
|
||||||
|
shouldDelete = true
|
||||||
|
} else if service == self.service {
|
||||||
|
shouldDelete = true
|
||||||
|
} else if [
|
||||||
|
"com.bitchat.passwords",
|
||||||
|
"com.bitchat.deviceidentity",
|
||||||
|
"com.bitchat.noise.identity",
|
||||||
|
"chat.bitchat.passwords",
|
||||||
|
"bitchat.keychain",
|
||||||
|
"bitchat",
|
||||||
|
"com.bitchat"
|
||||||
|
].contains(service) {
|
||||||
shouldDelete = true
|
shouldDelete = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,9 +317,10 @@ class KeychainManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also try to delete by known service names (in case we missed any)
|
// Also try to delete by known service names and app group
|
||||||
|
// This catches any items that might have been missed above
|
||||||
let knownServices = [
|
let knownServices = [
|
||||||
"chat.bitchat",
|
self.service, // Current service name
|
||||||
"com.bitchat.passwords",
|
"com.bitchat.passwords",
|
||||||
"com.bitchat.deviceidentity",
|
"com.bitchat.deviceidentity",
|
||||||
"com.bitchat.noise.identity",
|
"com.bitchat.noise.identity",
|
||||||
@@ -312,11 +342,42 @@ class KeychainManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also delete by app group to ensure complete cleanup
|
||||||
|
let groupQuery: [String: Any] = [
|
||||||
|
kSecClass as String: kSecClassGenericPassword,
|
||||||
|
kSecAttrAccessGroup as String: appGroup
|
||||||
|
]
|
||||||
|
|
||||||
|
let groupStatus = SecItemDelete(groupQuery as CFDictionary)
|
||||||
|
if groupStatus == errSecSuccess {
|
||||||
|
totalDeleted += 1
|
||||||
|
}
|
||||||
|
|
||||||
SecureLogger.log("Panic mode cleanup completed. Total items deleted: \(totalDeleted)", category: SecureLogger.keychain, level: .warning)
|
SecureLogger.log("Panic mode cleanup completed. Total items deleted: \(totalDeleted)", category: SecureLogger.keychain, level: .warning)
|
||||||
|
|
||||||
return totalDeleted > 0
|
return totalDeleted > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Security Utilities
|
||||||
|
|
||||||
|
/// Securely clear sensitive data from memory
|
||||||
|
static func secureClear(_ data: inout Data) {
|
||||||
|
_ = data.withUnsafeMutableBytes { bytes in
|
||||||
|
// Use volatile memset to prevent compiler optimization
|
||||||
|
memset_s(bytes.baseAddress, bytes.count, 0, bytes.count)
|
||||||
|
}
|
||||||
|
data = Data() // Clear the data object
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Securely clear sensitive string from memory
|
||||||
|
static func secureClear(_ string: inout String) {
|
||||||
|
// Convert to mutable data and clear
|
||||||
|
if var data = string.data(using: .utf8) {
|
||||||
|
secureClear(&data)
|
||||||
|
}
|
||||||
|
string = "" // Clear the string object
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Debug
|
// MARK: - Debug
|
||||||
|
|
||||||
func verifyIdentityKeyExists() -> Bool {
|
func verifyIdentityKeyExists() -> Bool {
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Comprehensive input validation for BitChat protocol
|
||||||
|
/// Prevents injection attacks, buffer overflows, and malformed data
|
||||||
|
struct InputValidator {
|
||||||
|
|
||||||
|
// MARK: - Constants
|
||||||
|
|
||||||
|
struct Limits {
|
||||||
|
static let maxNicknameLength = 50
|
||||||
|
static let maxMessageLength = 10_000
|
||||||
|
static let maxReasonLength = 200
|
||||||
|
static let maxPeerIDLength = 64
|
||||||
|
static let hexPeerIDLength = 16 // 8 bytes = 16 hex chars
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Peer ID Validation
|
||||||
|
|
||||||
|
/// Validates a peer ID from any source
|
||||||
|
static func validatePeerID(_ peerID: String) -> Bool {
|
||||||
|
// Handle both hex-encoded (from network) and alphanumeric (internal) formats
|
||||||
|
if peerID.count == Limits.hexPeerIDLength {
|
||||||
|
// Network format: 16 hex characters
|
||||||
|
return peerID.allSatisfy { $0.isHexDigit }
|
||||||
|
} else {
|
||||||
|
// Internal format: alphanumeric + dash/underscore
|
||||||
|
let validCharset = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-_"))
|
||||||
|
return peerID.count > 0 &&
|
||||||
|
peerID.count <= Limits.maxPeerIDLength &&
|
||||||
|
peerID.rangeOfCharacter(from: validCharset.inverted) == nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - String Content Validation
|
||||||
|
|
||||||
|
/// Validates and sanitizes user-provided strings (nicknames, messages)
|
||||||
|
static func validateUserString(_ string: String, maxLength: Int, allowNewlines: Bool = false) -> String? {
|
||||||
|
// Check empty
|
||||||
|
guard !string.isEmpty else { return nil }
|
||||||
|
|
||||||
|
// Trim whitespace
|
||||||
|
let trimmed = string.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !trimmed.isEmpty else { return nil }
|
||||||
|
|
||||||
|
// Check length
|
||||||
|
guard trimmed.count <= maxLength else { return nil }
|
||||||
|
|
||||||
|
// Remove control characters except allowed ones
|
||||||
|
var allowedControlChars = CharacterSet()
|
||||||
|
if allowNewlines {
|
||||||
|
allowedControlChars.insert(charactersIn: "\n\r")
|
||||||
|
}
|
||||||
|
|
||||||
|
let controlChars = CharacterSet.controlCharacters.subtracting(allowedControlChars)
|
||||||
|
let cleaned = trimmed.components(separatedBy: controlChars).joined()
|
||||||
|
|
||||||
|
// Ensure valid UTF-8 (should already be, but double-check)
|
||||||
|
guard cleaned.data(using: .utf8) != nil else { return nil }
|
||||||
|
|
||||||
|
// Prevent zero-width characters and other invisible unicode
|
||||||
|
let invisibleChars = CharacterSet(charactersIn: "\u{200B}\u{200C}\u{200D}\u{FEFF}")
|
||||||
|
let visible = cleaned.components(separatedBy: invisibleChars).joined()
|
||||||
|
|
||||||
|
return visible.isEmpty ? nil : visible
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates nickname
|
||||||
|
static func validateNickname(_ nickname: String) -> String? {
|
||||||
|
return validateUserString(nickname, maxLength: Limits.maxNicknameLength, allowNewlines: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates message content
|
||||||
|
static func validateMessageContent(_ content: String) -> String? {
|
||||||
|
return validateUserString(content, maxLength: Limits.maxMessageLength, allowNewlines: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates error/reason strings
|
||||||
|
static func validateReasonString(_ reason: String) -> String? {
|
||||||
|
return validateUserString(reason, maxLength: Limits.maxReasonLength, allowNewlines: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Protocol Field Validation
|
||||||
|
|
||||||
|
/// Validates message type is within valid range
|
||||||
|
static func validateMessageType(_ type: UInt8) -> Bool {
|
||||||
|
// Check against known message types
|
||||||
|
let validTypes: Set<UInt8> = [
|
||||||
|
0x01, 0x03, 0x04, 0x05, 0x06, 0x07, 0x0A, 0x0B, 0x0C,
|
||||||
|
0x10, 0x11, 0x12, 0x13,
|
||||||
|
0x20, 0x21, 0x22, 0x23, 0x24, 0x25,
|
||||||
|
0x30, 0x31
|
||||||
|
]
|
||||||
|
return validTypes.contains(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates hop count is reasonable
|
||||||
|
static func validateHopCount(_ hopCount: UInt8) -> Bool {
|
||||||
|
return hopCount <= 10 // Prevent excessive forwarding
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates timestamp is reasonable (not too far in past or future)
|
||||||
|
static func validateTimestamp(_ timestamp: Date) -> Bool {
|
||||||
|
let now = Date()
|
||||||
|
let oneHourAgo = now.addingTimeInterval(-3600)
|
||||||
|
let oneHourFromNow = now.addingTimeInterval(3600)
|
||||||
|
return timestamp >= oneHourAgo && timestamp <= oneHourFromNow
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates data size for different contexts
|
||||||
|
static func validateDataSize(_ data: Data, maxSize: Int) -> Bool {
|
||||||
|
return data.count > 0 && data.count <= maxSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Binary Data Validation
|
||||||
|
|
||||||
|
/// Validates UUID format
|
||||||
|
static func validateUUID(_ uuid: String) -> Bool {
|
||||||
|
// Remove dashes and validate hex
|
||||||
|
let cleaned = uuid.replacingOccurrences(of: "-", with: "")
|
||||||
|
return cleaned.count == 32 && cleaned.allSatisfy { $0.isHexDigit }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates public key data
|
||||||
|
static func validatePublicKey(_ keyData: Data) -> Bool {
|
||||||
|
// Curve25519 public keys are 32 bytes
|
||||||
|
return keyData.count == 32
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates signature data
|
||||||
|
static func validateSignature(_ signature: Data) -> Bool {
|
||||||
|
// Ed25519 signatures are 64 bytes
|
||||||
|
return signature.count == 64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Character Extensions
|
||||||
|
|
||||||
|
private extension Character {
|
||||||
|
var isHexDigit: Bool {
|
||||||
|
return "0123456789abcdefABCDEF".contains(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user