mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 20:45:19 +00:00
Implement Noise Protocol Framework and peer ID rotation for enhanced security and privacy
This major update replaces the basic encryption with the Noise Protocol Framework and adds ephemeral peer ID rotation for enhanced privacy. Key Changes: Security Infrastructure: - Implemented Noise Protocol Framework (XX handshake pattern) - End-to-end encryption with forward secrecy and identity hiding - Session management with automatic rekey support - Channel encryption with password-derived keys Privacy Enhancements: - Ephemeral peer ID rotation (5-15 minute random intervals) - Persistent identity through public key fingerprints - Favorites and verification persist across ID rotations - Block list based on fingerprints, not ephemeral IDs Core Components Added: - NoiseEncryptionService: Main encryption service - NoiseSession: Individual peer session management - NoiseChannelEncryption: Password-protected channel support - SecureIdentityStateManager: Persistent identity storage - FingerprintView: Visual fingerprint verification UI Bug Fixes: - Fixed handshake storm with tie-breaker mechanism - Fixed missing connect messages during peer rotation - Fixed delivery ACK compression issues - Fixed race conditions in message queue - Fixed nickname resolution for rotated peer IDs Testing: - Comprehensive test suite for Noise implementation - Security validator tests - Channel encryption tests - Identity persistence tests - Rate limiter tests Documentation: - BRING_THE_NOISE.md: Technical implementation details - Updated WHITEPAPER.md: Simplified and focused on core innovations - Removed temporary debug documentation The implementation maintains backward compatibility while significantly improving security and privacy. All existing features (channels, private messages, favorites, blocking) work seamlessly with the new system.
This commit is contained in:
@@ -49,17 +49,22 @@ struct BinaryProtocol {
|
||||
static func encode(_ packet: BitchatPacket) -> Data? {
|
||||
var data = Data()
|
||||
|
||||
|
||||
// Try to compress payload if beneficial
|
||||
var payload = packet.payload
|
||||
var originalPayloadSize: UInt16? = nil
|
||||
var isCompressed = false
|
||||
|
||||
if CompressionUtil.shouldCompress(payload),
|
||||
let compressedPayload = CompressionUtil.compress(payload) {
|
||||
// Store original size for decompression (2 bytes after payload)
|
||||
originalPayloadSize = UInt16(payload.count)
|
||||
payload = compressedPayload
|
||||
isCompressed = true
|
||||
if CompressionUtil.shouldCompress(payload) {
|
||||
if let compressedPayload = CompressionUtil.compress(payload) {
|
||||
// Store original size for decompression (2 bytes after payload)
|
||||
originalPayloadSize = UInt16(payload.count)
|
||||
payload = compressedPayload
|
||||
isCompressed = true
|
||||
|
||||
} else {
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
// Header
|
||||
@@ -88,6 +93,8 @@ struct BinaryProtocol {
|
||||
// Payload length (2 bytes, big-endian) - includes original size if compressed
|
||||
let payloadDataSize = payload.count + (isCompressed ? 2 : 0)
|
||||
let payloadLength = UInt16(payloadDataSize)
|
||||
|
||||
|
||||
data.append(UInt8((payloadLength >> 8) & 0xFF))
|
||||
data.append(UInt8(payloadLength & 0xFF))
|
||||
|
||||
@@ -120,37 +127,49 @@ struct BinaryProtocol {
|
||||
data.append(signature.prefix(signatureSize))
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
// Apply padding to standard block sizes for traffic analysis resistance
|
||||
let optimalSize = MessagePadding.optimalBlockSize(for: data.count)
|
||||
let paddedData = MessagePadding.pad(data, toSize: optimalSize)
|
||||
|
||||
|
||||
return paddedData
|
||||
}
|
||||
|
||||
// Decode binary data to BitchatPacket
|
||||
static func decode(_ data: Data) -> BitchatPacket? {
|
||||
guard data.count >= headerSize + senderIDSize else { return nil }
|
||||
// Remove padding first
|
||||
let unpaddedData = MessagePadding.unpad(data)
|
||||
|
||||
|
||||
guard unpaddedData.count >= headerSize + senderIDSize else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var offset = 0
|
||||
|
||||
// Header
|
||||
let version = data[offset]; offset += 1
|
||||
let version = unpaddedData[offset]; offset += 1
|
||||
// Only support version 1
|
||||
guard version == 1 else { return nil }
|
||||
let type = data[offset]; offset += 1
|
||||
let ttl = data[offset]; offset += 1
|
||||
let type = unpaddedData[offset]; offset += 1
|
||||
let ttl = unpaddedData[offset]; offset += 1
|
||||
|
||||
// Timestamp
|
||||
let timestampData = data[offset..<offset+8]
|
||||
let timestampData = unpaddedData[offset..<offset+8]
|
||||
let timestamp = timestampData.reduce(0) { result, byte in
|
||||
(result << 8) | UInt64(byte)
|
||||
}
|
||||
offset += 8
|
||||
|
||||
// Flags
|
||||
let flags = data[offset]; offset += 1
|
||||
let flags = unpaddedData[offset]; offset += 1
|
||||
let hasRecipient = (flags & Flags.hasRecipient) != 0
|
||||
let hasSignature = (flags & Flags.hasSignature) != 0
|
||||
let isCompressed = (flags & Flags.isCompressed) != 0
|
||||
|
||||
// Payload length
|
||||
let payloadLengthData = data[offset..<offset+2]
|
||||
let payloadLengthData = unpaddedData[offset..<offset+2]
|
||||
let payloadLength = payloadLengthData.reduce(0) { result, byte in
|
||||
(result << 8) | UInt16(byte)
|
||||
}
|
||||
@@ -165,16 +184,18 @@ struct BinaryProtocol {
|
||||
expectedSize += signatureSize
|
||||
}
|
||||
|
||||
guard data.count >= expectedSize else { return nil }
|
||||
guard unpaddedData.count >= expectedSize else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SenderID
|
||||
let senderID = data[offset..<offset+senderIDSize]
|
||||
let senderID = unpaddedData[offset..<offset+senderIDSize]
|
||||
offset += senderIDSize
|
||||
|
||||
// RecipientID
|
||||
var recipientID: Data?
|
||||
if hasRecipient {
|
||||
recipientID = data[offset..<offset+recipientIDSize]
|
||||
recipientID = unpaddedData[offset..<offset+recipientIDSize]
|
||||
offset += recipientIDSize
|
||||
}
|
||||
|
||||
@@ -183,14 +204,14 @@ struct BinaryProtocol {
|
||||
if isCompressed {
|
||||
// First 2 bytes are original size
|
||||
guard Int(payloadLength) >= 2 else { return nil }
|
||||
let originalSizeData = data[offset..<offset+2]
|
||||
let originalSizeData = unpaddedData[offset..<offset+2]
|
||||
let originalSize = Int(originalSizeData.reduce(0) { result, byte in
|
||||
(result << 8) | UInt16(byte)
|
||||
})
|
||||
offset += 2
|
||||
|
||||
// Compressed payload
|
||||
let compressedPayload = data[offset..<offset+Int(payloadLength)-2]
|
||||
let compressedPayload = unpaddedData[offset..<offset+Int(payloadLength)-2]
|
||||
offset += Int(payloadLength) - 2
|
||||
|
||||
// Decompress
|
||||
@@ -199,14 +220,14 @@ struct BinaryProtocol {
|
||||
}
|
||||
payload = decompressedPayload
|
||||
} else {
|
||||
payload = data[offset..<offset+Int(payloadLength)]
|
||||
payload = unpaddedData[offset..<offset+Int(payloadLength)]
|
||||
offset += Int(payloadLength)
|
||||
}
|
||||
|
||||
// Signature
|
||||
var signature: Data?
|
||||
if hasSignature {
|
||||
signature = data[offset..<offset+signatureSize]
|
||||
signature = unpaddedData[offset..<offset+signatureSize]
|
||||
}
|
||||
|
||||
return BitchatPacket(
|
||||
|
||||
@@ -41,9 +41,20 @@ struct MessagePadding {
|
||||
|
||||
// Last byte tells us how much padding to remove
|
||||
let paddingLength = Int(data[data.count - 1])
|
||||
guard paddingLength > 0 && paddingLength <= data.count else { return data }
|
||||
guard paddingLength > 0 && paddingLength <= data.count else {
|
||||
// Debug logging for 243-byte packets
|
||||
if data.count == 243 {
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
return data.prefix(data.count - paddingLength)
|
||||
let result = data.prefix(data.count - paddingLength)
|
||||
|
||||
// Debug logging for 243-byte packets
|
||||
if data.count == 243 {
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Find optimal block size for data
|
||||
@@ -66,7 +77,7 @@ struct MessagePadding {
|
||||
|
||||
enum MessageType: UInt8 {
|
||||
case announce = 0x01
|
||||
case keyExchange = 0x02
|
||||
// 0x02 was legacy keyExchange - removed
|
||||
case leave = 0x03
|
||||
case message = 0x04 // All user messages (private and broadcast)
|
||||
case fragmentStart = 0x05
|
||||
@@ -77,6 +88,40 @@ enum MessageType: UInt8 {
|
||||
case deliveryAck = 0x0A // Acknowledge message received
|
||||
case deliveryStatusRequest = 0x0B // Request delivery status update
|
||||
case readReceipt = 0x0C // Message has been read/viewed
|
||||
|
||||
// Noise Protocol messages
|
||||
case noiseHandshakeInit = 0x10 // Noise handshake initiation
|
||||
case noiseHandshakeResp = 0x11 // Noise handshake response
|
||||
case noiseEncrypted = 0x12 // Noise encrypted transport message
|
||||
case noiseIdentityAnnounce = 0x13 // Announce static public key for discovery
|
||||
case channelKeyVerifyRequest = 0x14 // Request key verification for a channel
|
||||
case channelKeyVerifyResponse = 0x15 // Response to key verification request
|
||||
case channelPasswordUpdate = 0x16 // Distribute new password to channel members
|
||||
case channelMetadata = 0x17 // Announce channel creator and metadata
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .announce: return "announce"
|
||||
case .leave: return "leave"
|
||||
case .message: return "message"
|
||||
case .fragmentStart: return "fragmentStart"
|
||||
case .fragmentContinue: return "fragmentContinue"
|
||||
case .fragmentEnd: return "fragmentEnd"
|
||||
case .channelAnnounce: return "channelAnnounce"
|
||||
case .channelRetention: return "channelRetention"
|
||||
case .deliveryAck: return "deliveryAck"
|
||||
case .deliveryStatusRequest: return "deliveryStatusRequest"
|
||||
case .readReceipt: return "readReceipt"
|
||||
case .noiseHandshakeInit: return "noiseHandshakeInit"
|
||||
case .noiseHandshakeResp: return "noiseHandshakeResp"
|
||||
case .noiseEncrypted: return "noiseEncrypted"
|
||||
case .noiseIdentityAnnounce: return "noiseIdentityAnnounce"
|
||||
case .channelKeyVerifyRequest: return "channelKeyVerifyRequest"
|
||||
case .channelKeyVerifyResponse: return "channelKeyVerifyResponse"
|
||||
case .channelPasswordUpdate: return "channelPasswordUpdate"
|
||||
case .channelMetadata: return "channelMetadata"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Special recipient ID for broadcast messages
|
||||
@@ -109,7 +154,17 @@ struct BitchatPacket: Codable {
|
||||
init(type: UInt8, ttl: UInt8, senderID: String, payload: Data) {
|
||||
self.version = 1
|
||||
self.type = type
|
||||
self.senderID = senderID.data(using: .utf8)!
|
||||
// Convert hex string peer ID to binary data (8 bytes)
|
||||
var senderData = Data()
|
||||
var tempID = senderID
|
||||
while tempID.count >= 2 {
|
||||
let hexByte = String(tempID.prefix(2))
|
||||
if let byte = UInt8(hexByte, radix: 16) {
|
||||
senderData.append(byte)
|
||||
}
|
||||
tempID = String(tempID.dropFirst(2))
|
||||
}
|
||||
self.senderID = senderData
|
||||
self.recipientID = nil
|
||||
self.timestamp = UInt64(Date().timeIntervalSince1970 * 1000) // milliseconds
|
||||
self.payload = payload
|
||||
@@ -182,6 +237,151 @@ struct ReadReceipt: Codable {
|
||||
}
|
||||
}
|
||||
|
||||
// Channel key verification request
|
||||
struct ChannelKeyVerifyRequest: Codable {
|
||||
let channel: String
|
||||
let requesterID: String
|
||||
let keyCommitment: String // SHA256 hash of the key they have
|
||||
let timestamp: Date
|
||||
|
||||
init(channel: String, requesterID: String, keyCommitment: String) {
|
||||
self.channel = channel
|
||||
self.requesterID = requesterID
|
||||
self.keyCommitment = keyCommitment
|
||||
self.timestamp = Date()
|
||||
}
|
||||
|
||||
func encode() -> Data? {
|
||||
return try? JSONEncoder().encode(self)
|
||||
}
|
||||
|
||||
static func decode(from data: Data) -> ChannelKeyVerifyRequest? {
|
||||
try? JSONDecoder().decode(ChannelKeyVerifyRequest.self, from: data)
|
||||
}
|
||||
}
|
||||
|
||||
// Channel key verification response
|
||||
struct ChannelKeyVerifyResponse: Codable {
|
||||
let channel: String
|
||||
let responderID: String
|
||||
let verified: Bool // Whether the key commitment matches
|
||||
let timestamp: Date
|
||||
|
||||
init(channel: String, responderID: String, verified: Bool) {
|
||||
self.channel = channel
|
||||
self.responderID = responderID
|
||||
self.verified = verified
|
||||
self.timestamp = Date()
|
||||
}
|
||||
|
||||
func encode() -> Data? {
|
||||
return try? JSONEncoder().encode(self)
|
||||
}
|
||||
|
||||
static func decode(from data: Data) -> ChannelKeyVerifyResponse? {
|
||||
try? JSONDecoder().decode(ChannelKeyVerifyResponse.self, from: data)
|
||||
}
|
||||
}
|
||||
|
||||
// Channel password update (sent by owner to members)
|
||||
struct ChannelPasswordUpdate: Codable {
|
||||
let channel: String
|
||||
let ownerID: String // Deprecated, kept for backward compatibility
|
||||
let ownerFingerprint: String // Noise protocol fingerprint of owner
|
||||
let encryptedPassword: Data // New password encrypted with recipient's Noise session
|
||||
let newKeyCommitment: String // SHA256 of new key for verification
|
||||
let timestamp: Date
|
||||
|
||||
init(channel: String, ownerID: String, ownerFingerprint: String, encryptedPassword: Data, newKeyCommitment: String) {
|
||||
self.channel = channel
|
||||
self.ownerID = ownerID
|
||||
self.ownerFingerprint = ownerFingerprint
|
||||
self.encryptedPassword = encryptedPassword
|
||||
self.newKeyCommitment = newKeyCommitment
|
||||
self.timestamp = Date()
|
||||
}
|
||||
|
||||
func encode() -> Data? {
|
||||
return try? JSONEncoder().encode(self)
|
||||
}
|
||||
|
||||
static func decode(from data: Data) -> ChannelPasswordUpdate? {
|
||||
try? JSONDecoder().decode(ChannelPasswordUpdate.self, from: data)
|
||||
}
|
||||
}
|
||||
|
||||
// Channel metadata announcement
|
||||
struct ChannelMetadata: Codable {
|
||||
let channel: String
|
||||
let creatorID: String
|
||||
let creatorFingerprint: String // Noise protocol fingerprint
|
||||
let createdAt: Date
|
||||
let isPasswordProtected: Bool
|
||||
let keyCommitment: String? // SHA256 of channel key if password-protected
|
||||
|
||||
init(channel: String, creatorID: String, creatorFingerprint: String, isPasswordProtected: Bool, keyCommitment: String?) {
|
||||
self.channel = channel
|
||||
self.creatorID = creatorID
|
||||
self.creatorFingerprint = creatorFingerprint
|
||||
self.createdAt = Date()
|
||||
self.isPasswordProtected = isPasswordProtected
|
||||
self.keyCommitment = keyCommitment
|
||||
}
|
||||
|
||||
func encode() -> Data? {
|
||||
return try? JSONEncoder().encode(self)
|
||||
}
|
||||
|
||||
static func decode(from data: Data) -> ChannelMetadata? {
|
||||
try? JSONDecoder().decode(ChannelMetadata.self, from: data)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Peer Identity Rotation
|
||||
|
||||
// Enhanced identity announcement with rotation support
|
||||
struct NoiseIdentityAnnouncement: Codable {
|
||||
let peerID: String // Current ephemeral peer ID
|
||||
let publicKey: Data // Noise static public key
|
||||
let nickname: String // Current nickname
|
||||
let timestamp: Date // When this binding was created
|
||||
let previousPeerID: String? // Previous peer ID (for smooth transition)
|
||||
let signature: Data // Signature proving ownership
|
||||
|
||||
init(peerID: String, publicKey: Data, nickname: String, previousPeerID: String? = nil, signature: Data) {
|
||||
self.peerID = peerID
|
||||
self.publicKey = publicKey
|
||||
self.nickname = nickname
|
||||
self.timestamp = Date()
|
||||
self.previousPeerID = previousPeerID
|
||||
self.signature = signature
|
||||
}
|
||||
|
||||
func encode() -> Data? {
|
||||
return try? JSONEncoder().encode(self)
|
||||
}
|
||||
|
||||
static func decode(from data: Data) -> NoiseIdentityAnnouncement? {
|
||||
try? JSONDecoder().decode(NoiseIdentityAnnouncement.self, from: data)
|
||||
}
|
||||
}
|
||||
|
||||
// Binding between ephemeral peer ID and cryptographic identity
|
||||
struct PeerIdentityBinding {
|
||||
let currentPeerID: String // Current ephemeral ID
|
||||
let fingerprint: String // Permanent cryptographic identity
|
||||
let publicKey: Data // Noise static public key
|
||||
let nickname: String // Last known nickname
|
||||
let bindingTimestamp: Date // When this binding was created
|
||||
let signature: Data // Cryptographic proof of binding
|
||||
|
||||
// Verify the binding signature
|
||||
func verify() -> Bool {
|
||||
// TODO: Implement signature verification
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Delivery status for messages
|
||||
enum DeliveryStatus: Codable, Equatable {
|
||||
case sending
|
||||
@@ -260,6 +460,14 @@ protocol BitchatDelegate: AnyObject {
|
||||
func didReceiveDeliveryAck(_ ack: DeliveryAck)
|
||||
func didReceiveReadReceipt(_ receipt: ReadReceipt)
|
||||
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus)
|
||||
|
||||
// Channel key verification methods
|
||||
func didReceiveChannelKeyVerifyRequest(_ request: ChannelKeyVerifyRequest, from peerID: String)
|
||||
func didReceiveChannelKeyVerifyResponse(_ response: ChannelKeyVerifyResponse, from peerID: String)
|
||||
func didReceiveChannelPasswordUpdate(_ update: ChannelPasswordUpdate, from peerID: String)
|
||||
|
||||
// Channel metadata methods
|
||||
func didReceiveChannelMetadata(_ metadata: ChannelMetadata, from peerID: String)
|
||||
}
|
||||
|
||||
// Provide default implementation to make it effectively optional
|
||||
@@ -296,4 +504,20 @@ extension BitchatDelegate {
|
||||
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {
|
||||
// Default empty implementation
|
||||
}
|
||||
|
||||
func didReceiveChannelKeyVerifyRequest(_ request: ChannelKeyVerifyRequest, from peerID: String) {
|
||||
// Default empty implementation
|
||||
}
|
||||
|
||||
func didReceiveChannelKeyVerifyResponse(_ response: ChannelKeyVerifyResponse, from peerID: String) {
|
||||
// Default empty implementation
|
||||
}
|
||||
|
||||
func didReceiveChannelPasswordUpdate(_ update: ChannelPasswordUpdate, from peerID: String) {
|
||||
// Default empty implementation
|
||||
}
|
||||
|
||||
func didReceiveChannelMetadata(_ metadata: ChannelMetadata, from peerID: String) {
|
||||
// Default empty implementation
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user