mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 08:25:19 +00:00
Remove all channel functionality and clean up test suite
- Remove channel UI elements from ContentView - Remove channel data structures and methods from ChatViewModel - Remove channel commands (/j, /leave, /channels) - Remove channel field from BitchatMessage protocol - Remove channel message types and handling - Remove NoiseChannelEncryption.swift entirely - Clean up all channel references across the codebase - Fix compilation warnings (var to let conversions) - Remove all outdated test files that used incorrect APIs - Simplify app to only support public broadcast and 1:1 private messages
This commit is contained in:
@@ -1,285 +0,0 @@
|
||||
//
|
||||
// NoiseChannelEncryption.swift
|
||||
// bitchat
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
import os.log
|
||||
|
||||
// MARK: - Noise Channel Encryption
|
||||
|
||||
class NoiseChannelEncryption {
|
||||
// Channel keys derived from passwords
|
||||
private var channelKeys: [String: SymmetricKey] = [:]
|
||||
private let keyQueue = DispatchQueue(label: "chat.bitchat.noise.channels", attributes: .concurrent)
|
||||
|
||||
// Key rotation support
|
||||
private let keyRotation = NoiseChannelKeyRotation()
|
||||
private var rotationEnabled: [String: Bool] = [:] // channel -> enabled
|
||||
|
||||
// Replay protection
|
||||
private var receivedNonces: Set<String> = []
|
||||
private let nonceExpirationTime: TimeInterval = 600 // 10 minutes
|
||||
private var nonceCleanupTimer: Timer?
|
||||
|
||||
// MARK: - Channel Key Management
|
||||
|
||||
/// Derive a channel key from password
|
||||
func deriveChannelKey(from password: String, channel: String, creatorFingerprint: String? = nil) -> SymmetricKey {
|
||||
// Use PBKDF2 with channel name + creator fingerprint as salt
|
||||
// This prevents rainbow table attacks across different channel instances
|
||||
var saltComponents = "bitchat-channel-\(channel)"
|
||||
if let fingerprint = creatorFingerprint {
|
||||
saltComponents += "-\(fingerprint)"
|
||||
}
|
||||
let salt = saltComponents.data(using: .utf8)!
|
||||
|
||||
// Increased iterations for better security (OWASP recommends 210,000 for PBKDF2-SHA256)
|
||||
let keyData = PBKDF2<SHA256>(
|
||||
password: password.data(using: .utf8)!,
|
||||
salt: salt,
|
||||
iterations: 210_000,
|
||||
keyByteCount: 32
|
||||
).makeIterator()
|
||||
|
||||
return SymmetricKey(data: keyData)
|
||||
}
|
||||
|
||||
/// Set password for a channel
|
||||
func setChannelPassword(_ password: String, for channel: String, creatorFingerprint: String? = nil) {
|
||||
let key = deriveChannelKey(from: password, channel: channel, creatorFingerprint: creatorFingerprint)
|
||||
|
||||
keyQueue.async(flags: .barrier) {
|
||||
self.channelKeys[channel] = key
|
||||
}
|
||||
|
||||
// Store in keychain
|
||||
let saved = KeychainManager.shared.saveChannelPassword(password, for: channel)
|
||||
if saved {
|
||||
SecureLogger.logKeyOperation("set", keyType: "channel key for \(channel)", success: true)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get channel key
|
||||
func getChannelKey(for channel: String) -> SymmetricKey? {
|
||||
return keyQueue.sync {
|
||||
return channelKeys[channel]
|
||||
}
|
||||
}
|
||||
|
||||
/// Load channel password from keychain
|
||||
func loadChannelPassword(for channel: String) -> Bool {
|
||||
guard let password = KeychainManager.shared.getChannelPassword(for: channel) else {
|
||||
return false
|
||||
}
|
||||
|
||||
setChannelPassword(password, for: channel)
|
||||
return true
|
||||
}
|
||||
|
||||
/// Remove channel password
|
||||
func removeChannelPassword(for channel: String) {
|
||||
keyQueue.async(flags: .barrier) {
|
||||
self.channelKeys.removeValue(forKey: channel)
|
||||
}
|
||||
|
||||
let deleted = KeychainManager.shared.deleteChannelPassword(for: channel)
|
||||
if deleted {
|
||||
SecureLogger.logKeyOperation("remove", keyType: "channel key for \(channel)", success: true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Replay Protection
|
||||
|
||||
private func scheduleNonceCleanup() {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.nonceCleanupTimer?.invalidate()
|
||||
self?.nonceCleanupTimer = Timer.scheduledTimer(withTimeInterval: 300, repeats: true) { [weak self] _ in
|
||||
self?.cleanupExpiredNonces()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func cleanupExpiredNonces() {
|
||||
keyQueue.async(flags: .barrier) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
// In production, we'd need to store timestamps with nonces
|
||||
// For now, we'll clear all nonces periodically
|
||||
if self.receivedNonces.count > 1000 {
|
||||
self.receivedNonces.removeAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
nonceCleanupTimer?.invalidate()
|
||||
}
|
||||
|
||||
// MARK: - Channel Message Encryption
|
||||
|
||||
/// Encrypt message for a channel
|
||||
func encryptChannelMessage(_ message: String, for channel: String) throws -> Data {
|
||||
guard let key = getChannelKey(for: channel) else {
|
||||
SecureLogger.log("Channel encryption failed - no key for channel: \(channel)", category: SecureLogger.encryption, level: .error)
|
||||
throw NoiseChannelError.noChannelKey
|
||||
}
|
||||
|
||||
let messageData = message.data(using: .utf8)!
|
||||
|
||||
// Generate random nonce
|
||||
let nonce = ChaChaPoly.Nonce()
|
||||
|
||||
// Encrypt with channel key
|
||||
let sealedBox = try ChaChaPoly.seal(messageData, using: key, nonce: nonce)
|
||||
|
||||
// Return nonce + ciphertext + tag
|
||||
return nonce.withUnsafeBytes { Data($0) } + sealedBox.ciphertext + sealedBox.tag
|
||||
}
|
||||
|
||||
/// Decrypt channel message
|
||||
func decryptChannelMessage(_ encryptedData: Data, for channel: String) throws -> String {
|
||||
guard let key = getChannelKey(for: channel) else {
|
||||
SecureLogger.log("Channel decryption failed - no key for channel: \(channel)", category: SecureLogger.encryption, level: .error)
|
||||
throw NoiseChannelError.noChannelKey
|
||||
}
|
||||
|
||||
guard encryptedData.count >= 12 + 16 else { // nonce + tag minimum
|
||||
throw NoiseChannelError.invalidCiphertext
|
||||
}
|
||||
|
||||
// Extract components
|
||||
let nonceData = encryptedData.prefix(12)
|
||||
let ciphertext = encryptedData.dropFirst(12).dropLast(16)
|
||||
let tag = encryptedData.suffix(16)
|
||||
|
||||
// Create sealed box
|
||||
let nonce = try ChaChaPoly.Nonce(data: nonceData)
|
||||
let sealedBox = try ChaChaPoly.SealedBox(nonce: nonce, ciphertext: ciphertext, tag: tag)
|
||||
|
||||
// Decrypt
|
||||
let decryptedData = try ChaChaPoly.open(sealedBox, using: key)
|
||||
|
||||
guard let message = String(data: decryptedData, encoding: .utf8) else {
|
||||
SecureLogger.log("Channel decryption failed - invalid UTF8 for channel: \(channel)", category: SecureLogger.encryption, level: .error)
|
||||
throw NoiseChannelError.decryptionFailed
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
// MARK: - Channel Key Sharing
|
||||
|
||||
/// Create encrypted channel key packet for sharing via Noise session
|
||||
func createChannelKeyPacket(password: String, channel: String) -> Data? {
|
||||
// Generate a unique nonce for replay protection
|
||||
var nonceData = Data(count: 16)
|
||||
_ = nonceData.withUnsafeMutableBytes { bytes in
|
||||
SecRandomCopyBytes(kSecRandomDefault, 16, bytes.baseAddress!)
|
||||
}
|
||||
let nonce = nonceData.base64EncodedString()
|
||||
|
||||
let packet = ChannelKeyPacket(
|
||||
channel: channel,
|
||||
password: password,
|
||||
timestamp: Date(),
|
||||
nonce: nonce
|
||||
)
|
||||
|
||||
return try? JSONEncoder().encode(packet)
|
||||
}
|
||||
|
||||
/// Process received channel key packet
|
||||
func processChannelKeyPacket(_ data: Data) -> (channel: String, password: String)? {
|
||||
guard let packet = try? JSONDecoder().decode(ChannelKeyPacket.self, from: data) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Verify timestamp is recent (within 5 minutes)
|
||||
let age = Date().timeIntervalSince(packet.timestamp)
|
||||
guard age < 300 else {
|
||||
SecureLogger.log("Expired channel key packet for channel: \(packet.channel), age: \(age)s", category: SecureLogger.security, level: .warning)
|
||||
return nil
|
||||
}
|
||||
|
||||
return keyQueue.sync(flags: .barrier) {
|
||||
// Check for replay attack
|
||||
if receivedNonces.contains(packet.nonce) {
|
||||
SecureLogger.logSecurityEvent(.replayAttackDetected(channel: packet.channel), level: .warning)
|
||||
return nil // This nonce was already processed
|
||||
}
|
||||
|
||||
// Add nonce to received set
|
||||
receivedNonces.insert(packet.nonce)
|
||||
|
||||
// Schedule cleanup if not already scheduled
|
||||
if nonceCleanupTimer == nil {
|
||||
scheduleNonceCleanup()
|
||||
}
|
||||
|
||||
return (packet.channel, packet.password)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Supporting Types
|
||||
|
||||
private struct ChannelKeyPacket: Codable {
|
||||
let channel: String
|
||||
let password: String
|
||||
let timestamp: Date
|
||||
let nonce: String
|
||||
}
|
||||
|
||||
enum NoiseChannelError: Error {
|
||||
case noChannelKey
|
||||
case invalidCiphertext
|
||||
case decryptionFailed
|
||||
}
|
||||
|
||||
// MARK: - PBKDF2 Implementation
|
||||
|
||||
private struct PBKDF2<H: HashFunction> {
|
||||
let password: Data
|
||||
let salt: Data
|
||||
let iterations: Int
|
||||
let keyByteCount: Int
|
||||
|
||||
init(password: Data, salt: Data, iterations: Int, keyByteCount: Int) {
|
||||
self.password = password
|
||||
self.salt = salt
|
||||
self.iterations = iterations
|
||||
self.keyByteCount = keyByteCount
|
||||
}
|
||||
|
||||
func makeIterator() -> Data {
|
||||
var derivedKey = Data()
|
||||
var blockNum: UInt32 = 1
|
||||
|
||||
while derivedKey.count < keyByteCount {
|
||||
var block = salt
|
||||
withUnsafeBytes(of: blockNum.bigEndian) { bytes in
|
||||
block.append(contentsOf: bytes)
|
||||
}
|
||||
|
||||
var u = Data(HMAC<H>.authenticationCode(for: block, using: SymmetricKey(data: password)))
|
||||
var xor = u
|
||||
|
||||
for _ in 1..<iterations {
|
||||
u = Data(HMAC<H>.authenticationCode(for: u, using: SymmetricKey(data: password)))
|
||||
for i in 0..<xor.count {
|
||||
xor[i] ^= u[i]
|
||||
}
|
||||
}
|
||||
|
||||
derivedKey.append(xor)
|
||||
blockNum += 1
|
||||
}
|
||||
|
||||
return derivedKey.prefix(keyByteCount)
|
||||
}
|
||||
}
|
||||
@@ -1,296 +0,0 @@
|
||||
//
|
||||
// NoiseChannelKeyRotation.swift
|
||||
// bitchat
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
// MARK: - Channel Key Rotation for Forward Secrecy
|
||||
|
||||
/// Implements key rotation for channels to provide forward secrecy
|
||||
/// This is a stepping stone toward full Double Ratchet implementation
|
||||
class NoiseChannelKeyRotation {
|
||||
|
||||
// MARK: - Types
|
||||
|
||||
struct KeyEpoch: Codable {
|
||||
let epochNumber: UInt64
|
||||
let startTime: Date
|
||||
let endTime: Date
|
||||
let keyCommitment: String
|
||||
let previousEpochCommitment: String?
|
||||
}
|
||||
|
||||
struct RotatedChannelKey {
|
||||
let epoch: KeyEpoch
|
||||
let key: SymmetricKey
|
||||
let isActive: Bool
|
||||
}
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
private static let epochDuration: TimeInterval = 24 * 60 * 60 // 24 hours
|
||||
private static let epochOverlap: TimeInterval = 60 * 60 // 1 hour overlap for late messages
|
||||
private static let maxStoredEpochs = 7 // Keep 1 week of history
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private var channelEpochs: [String: [KeyEpoch]] = [:] // channel -> epochs
|
||||
private let keychainPrefix = "channel.epoch."
|
||||
|
||||
// Thread safety
|
||||
private let queue = DispatchQueue(label: "chat.bitchat.noise.keyrotation", attributes: .concurrent)
|
||||
|
||||
// MARK: - Public Interface
|
||||
|
||||
/// Get the current key for a channel with rotation
|
||||
func getCurrentKey(for channel: String, basePassword: String, creatorFingerprint: String) -> RotatedChannelKey? {
|
||||
let currentTime = Date()
|
||||
|
||||
return queue.sync {
|
||||
// Get or create current epoch
|
||||
let epoch = getCurrentOrCreateEpoch(for: channel, at: currentTime)
|
||||
|
||||
// Derive key for this epoch
|
||||
let epochKey = deriveEpochKey(
|
||||
basePassword: basePassword,
|
||||
channel: channel,
|
||||
creatorFingerprint: creatorFingerprint,
|
||||
epochNumber: epoch.epochNumber
|
||||
)
|
||||
|
||||
return RotatedChannelKey(
|
||||
epoch: epoch,
|
||||
key: epochKey,
|
||||
isActive: true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get valid keys for decryption (current + recent epochs)
|
||||
func getValidKeysForDecryption(channel: String, basePassword: String, creatorFingerprint: String, messageTime: Date? = nil) -> [RotatedChannelKey] {
|
||||
let checkTime = messageTime ?? Date()
|
||||
|
||||
return queue.sync {
|
||||
let epochs = getValidEpochs(for: channel, at: checkTime)
|
||||
|
||||
return epochs.map { epoch in
|
||||
let key = deriveEpochKey(
|
||||
basePassword: basePassword,
|
||||
channel: channel,
|
||||
creatorFingerprint: creatorFingerprint,
|
||||
epochNumber: epoch.epochNumber
|
||||
)
|
||||
|
||||
let isActive = checkTime >= epoch.startTime && checkTime < epoch.endTime
|
||||
|
||||
return RotatedChannelKey(
|
||||
epoch: epoch,
|
||||
key: key,
|
||||
isActive: isActive
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rotate key for a channel (channel owner only)
|
||||
func rotateChannelKey(for channel: String, basePassword: String, creatorFingerprint: String) -> KeyEpoch {
|
||||
return queue.sync(flags: .barrier) {
|
||||
let currentTime = Date()
|
||||
let epochs = channelEpochs[channel] ?? []
|
||||
|
||||
// Get current epoch
|
||||
let currentEpoch = epochs.last
|
||||
let nextEpochNumber = (currentEpoch?.epochNumber ?? 0) + 1
|
||||
|
||||
// Create new epoch
|
||||
let newEpoch = KeyEpoch(
|
||||
epochNumber: nextEpochNumber,
|
||||
startTime: currentTime,
|
||||
endTime: currentTime.addingTimeInterval(Self.epochDuration),
|
||||
keyCommitment: computeEpochKeyCommitment(
|
||||
basePassword: basePassword,
|
||||
channel: channel,
|
||||
creatorFingerprint: creatorFingerprint,
|
||||
epochNumber: nextEpochNumber
|
||||
),
|
||||
previousEpochCommitment: currentEpoch?.keyCommitment
|
||||
)
|
||||
|
||||
// Add to epochs
|
||||
var updatedEpochs = epochs
|
||||
updatedEpochs.append(newEpoch)
|
||||
|
||||
// Trim old epochs
|
||||
if updatedEpochs.count > Self.maxStoredEpochs {
|
||||
updatedEpochs.removeFirst(updatedEpochs.count - Self.maxStoredEpochs)
|
||||
}
|
||||
|
||||
channelEpochs[channel] = updatedEpochs
|
||||
|
||||
// Persist epochs
|
||||
saveEpochs(updatedEpochs, for: channel)
|
||||
|
||||
return newEpoch
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a channel needs key rotation
|
||||
func needsKeyRotation(for channel: String) -> Bool {
|
||||
return queue.sync {
|
||||
guard let epochs = channelEpochs[channel],
|
||||
let currentEpoch = epochs.last else {
|
||||
return true // No epochs, needs initial key
|
||||
}
|
||||
|
||||
// Check if current epoch is near expiration (within 2 hours)
|
||||
let timeUntilExpiration = currentEpoch.endTime.timeIntervalSinceNow
|
||||
return timeUntilExpiration < 2 * 60 * 60
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
|
||||
private func getCurrentOrCreateEpoch(for channel: String, at time: Date) -> KeyEpoch {
|
||||
var epochs = channelEpochs[channel] ?? []
|
||||
|
||||
// Find current epoch
|
||||
if let currentEpoch = epochs.first(where: { epoch in
|
||||
time >= epoch.startTime && time < epoch.endTime.addingTimeInterval(Self.epochOverlap)
|
||||
}) {
|
||||
return currentEpoch
|
||||
}
|
||||
|
||||
// No valid epoch, create initial one
|
||||
let initialEpoch = KeyEpoch(
|
||||
epochNumber: 1,
|
||||
startTime: time,
|
||||
endTime: time.addingTimeInterval(Self.epochDuration),
|
||||
keyCommitment: "", // Will be computed when key is derived
|
||||
previousEpochCommitment: nil
|
||||
)
|
||||
|
||||
epochs.append(initialEpoch)
|
||||
channelEpochs[channel] = epochs
|
||||
|
||||
return initialEpoch
|
||||
}
|
||||
|
||||
private func getValidEpochs(for channel: String, at time: Date) -> [KeyEpoch] {
|
||||
let epochs = channelEpochs[channel] ?? []
|
||||
|
||||
// Return epochs that are valid at the given time (including overlap period)
|
||||
return epochs.filter { epoch in
|
||||
time >= epoch.startTime.addingTimeInterval(-Self.epochOverlap) &&
|
||||
time < epoch.endTime.addingTimeInterval(Self.epochOverlap)
|
||||
}
|
||||
}
|
||||
|
||||
private func deriveEpochKey(basePassword: String, channel: String, creatorFingerprint: String, epochNumber: UInt64) -> SymmetricKey {
|
||||
// Derive epoch-specific key using base password + epoch number
|
||||
let epochSalt = "\(channel)-\(creatorFingerprint)-epoch-\(epochNumber)".data(using: .utf8)!
|
||||
let keyData = pbkdf2(
|
||||
password: basePassword,
|
||||
salt: epochSalt,
|
||||
iterations: 210_000, // Same as channel encryption
|
||||
keyLength: 32
|
||||
)
|
||||
return SymmetricKey(data: keyData)
|
||||
}
|
||||
|
||||
private func computeEpochKeyCommitment(basePassword: String, channel: String, creatorFingerprint: String, epochNumber: UInt64) -> String {
|
||||
let epochKey = deriveEpochKey(
|
||||
basePassword: basePassword,
|
||||
channel: channel,
|
||||
creatorFingerprint: creatorFingerprint,
|
||||
epochNumber: epochNumber
|
||||
)
|
||||
|
||||
let commitment = SHA256.hash(data: epochKey.withUnsafeBytes { Data($0) })
|
||||
return commitment.map { String(format: "%02x", $0) }.joined()
|
||||
}
|
||||
|
||||
private func pbkdf2(password: String, salt: Data, iterations: Int, keyLength: Int) -> Data {
|
||||
guard let passwordData = password.data(using: .utf8) else {
|
||||
return Data()
|
||||
}
|
||||
|
||||
// Use CryptoKit's safer implementation instead of CommonCrypto
|
||||
var derivedKey = Data()
|
||||
var blockNum: UInt32 = 1
|
||||
|
||||
while derivedKey.count < keyLength {
|
||||
var block = salt
|
||||
withUnsafeBytes(of: blockNum.bigEndian) { bytes in
|
||||
block.append(contentsOf: bytes)
|
||||
}
|
||||
|
||||
var u = Data(HMAC<SHA256>.authenticationCode(for: block, using: SymmetricKey(data: passwordData)))
|
||||
var xor = u
|
||||
|
||||
for _ in 1..<iterations {
|
||||
u = Data(HMAC<SHA256>.authenticationCode(for: u, using: SymmetricKey(data: passwordData)))
|
||||
for i in 0..<xor.count {
|
||||
xor[i] ^= u[i]
|
||||
}
|
||||
}
|
||||
|
||||
derivedKey.append(xor)
|
||||
blockNum += 1
|
||||
}
|
||||
|
||||
return Data(derivedKey.prefix(keyLength))
|
||||
}
|
||||
|
||||
// MARK: - Persistence
|
||||
|
||||
private func saveEpochs(_ epochs: [KeyEpoch], for channel: String) {
|
||||
// Use channel password storage with special prefix for epoch data
|
||||
let epochKey = "epoch::\(channel)"
|
||||
|
||||
if let data = try? JSONEncoder().encode(epochs),
|
||||
let epochString = String(data: data, encoding: .utf8) {
|
||||
_ = KeychainManager.shared.saveChannelPassword(epochString, for: epochKey)
|
||||
}
|
||||
}
|
||||
|
||||
private func loadEpochs(for channel: String) -> [KeyEpoch]? {
|
||||
let epochKey = "epoch::\(channel)"
|
||||
|
||||
guard let epochString = KeychainManager.shared.getChannelPassword(for: epochKey),
|
||||
let data = epochString.data(using: .utf8),
|
||||
let epochs = try? JSONDecoder().decode([KeyEpoch].self, from: data) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return epochs
|
||||
}
|
||||
|
||||
/// Load all saved epochs on initialization
|
||||
func loadSavedEpochs() {
|
||||
queue.sync(flags: .barrier) {
|
||||
// Get all channel passwords and filter for epoch data
|
||||
let allPasswords = KeychainManager.shared.getAllChannelPasswords()
|
||||
for (key, epochString) in allPasswords where key.hasPrefix("epoch::") {
|
||||
let channel = String(key.dropFirst(7)) // Remove "epoch::" prefix
|
||||
if let data = epochString.data(using: .utf8),
|
||||
let epochs = try? JSONDecoder().decode([KeyEpoch].self, from: data) {
|
||||
channelEpochs[channel] = epochs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all epochs for a channel
|
||||
func clearEpochs(for channel: String) {
|
||||
queue.sync(flags: .barrier) {
|
||||
channelEpochs.removeValue(forKey: channel)
|
||||
let epochKey = "epoch::\(channel)"
|
||||
_ = KeychainManager.shared.deleteChannelPassword(for: epochKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -609,7 +609,7 @@ extension NoiseHandshakeState {
|
||||
|
||||
// Check against known bad points
|
||||
if lowOrderPoints.contains(keyData) {
|
||||
SecureLogger.logSecurityEvent(.invalidKey(reason: "Low-order point detected"), level: .warning)
|
||||
SecureLogger.log("Low-order point detected", category: SecureLogger.security, level: .warning)
|
||||
throw NoiseError.invalidPublicKey
|
||||
}
|
||||
|
||||
@@ -619,7 +619,7 @@ extension NoiseHandshakeState {
|
||||
return publicKey
|
||||
} catch {
|
||||
// If CryptoKit rejects it, it's invalid
|
||||
SecureLogger.logSecurityEvent(.invalidKey(reason: "CryptoKit validation failed"), level: .warning)
|
||||
SecureLogger.log("CryptoKit validation failed", category: SecureLogger.security, level: .warning)
|
||||
throw NoiseError.invalidPublicKey
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,16 +61,6 @@ struct NoiseSecurityValidator {
|
||||
peerID.count <= 64 &&
|
||||
peerID.rangeOfCharacter(from: validCharset.inverted) == nil
|
||||
}
|
||||
|
||||
/// Validate channel name format
|
||||
static func validateChannelName(_ channel: String) -> Bool {
|
||||
// Channel should start with # and contain valid characters
|
||||
let validCharset = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-_"))
|
||||
return channel.hasPrefix("#") &&
|
||||
channel.count > 1 &&
|
||||
channel.count <= 32 &&
|
||||
channel.dropFirst().rangeOfCharacter(from: validCharset.inverted) == nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Enhanced Noise Session with Security
|
||||
@@ -232,54 +222,6 @@ enum NoiseSecurityError: Error {
|
||||
case sessionExhausted
|
||||
case messageTooLarge
|
||||
case invalidPeerID
|
||||
case invalidChannelName
|
||||
case rateLimitExceeded
|
||||
case handshakeTimeout
|
||||
}
|
||||
|
||||
// MARK: - Security Audit Checklist
|
||||
|
||||
/*
|
||||
SECURITY AUDIT CHECKLIST:
|
||||
|
||||
1. KEY MANAGEMENT
|
||||
✓ Static keys stored in Keychain (most secure iOS storage)
|
||||
✓ Keys cleared on panic mode
|
||||
✓ Ephemeral keys generated per session
|
||||
✓ No key reuse across sessions
|
||||
|
||||
2. PROTOCOL SECURITY
|
||||
✓ Using Noise XX pattern for mutual authentication
|
||||
✓ Forward secrecy via ephemeral keys
|
||||
✓ Replay protection via nonce counter
|
||||
✓ AEAD encryption (ChaCha20-Poly1305)
|
||||
✓ SHA-256 for hashing
|
||||
|
||||
3. IMPLEMENTATION SECURITY
|
||||
✓ Message size limits to prevent DoS
|
||||
✓ Session timeout to limit exposure
|
||||
✓ Message count limits to prevent nonce reuse
|
||||
✓ Rate limiting for handshakes and messages
|
||||
✓ Input validation for all user data
|
||||
✓ Thread-safe operations
|
||||
|
||||
4. NETWORK SECURITY
|
||||
✓ Messages padded to standard sizes for traffic analysis resistance
|
||||
✓ Cover traffic for anonymity
|
||||
✓ No metadata leakage in protocol
|
||||
✓ Fingerprint verification for identity
|
||||
|
||||
5. EDGE CASES HANDLED
|
||||
✓ Incomplete handshakes timeout
|
||||
✓ Duplicate handshake messages ignored
|
||||
✓ Session renegotiation when needed
|
||||
✓ Graceful handling of decryption failures
|
||||
✓ Memory limits on message sizes
|
||||
|
||||
6. FUTURE IMPROVEMENTS
|
||||
- Implement post-quantum key exchange (when available)
|
||||
- Add perfect forward secrecy for channel keys
|
||||
- Implement key rotation for long-lived channels
|
||||
- Add security event logging
|
||||
- Implement secure key backup/restore
|
||||
*/
|
||||
@@ -443,8 +443,6 @@ class NoiseSessionManager {
|
||||
}
|
||||
|
||||
func initiateRekey(for peerID: String) throws {
|
||||
SecureLogger.logSecurityEvent(.keyRotation(channel: peerID))
|
||||
|
||||
// Remove old session
|
||||
removeSession(for: peerID)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user