mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 22:25:20 +00:00
Implement handshake request notifications for pending messages
- Add handshakeRequest packet type (0x25) to notify recipients about queued messages - Create HandshakeRequest struct with binary encoding for efficient transmission - Send handshake requests when messages are queued due to missing session - Display notifications when someone wants to send messages - Fix verification persistence bug by adding forceSave on app termination - Update UI to handle optional encryption icons (hide when no handshake attempted)
This commit is contained in:
@@ -371,4 +371,10 @@ class SecureIdentityStateManager {
|
|||||||
return cache.verifiedFingerprints.contains(fingerprint)
|
return cache.verifiedFingerprints.contains(fingerprint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getVerifiedFingerprints() -> Set<String> {
|
||||||
|
queue.sync {
|
||||||
|
return cache.verifiedFingerprints
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -100,6 +100,7 @@ enum MessageType: UInt8 {
|
|||||||
case protocolAck = 0x22 // Generic protocol acknowledgment
|
case protocolAck = 0x22 // Generic protocol acknowledgment
|
||||||
case protocolNack = 0x23 // Negative acknowledgment (failure)
|
case protocolNack = 0x23 // Negative acknowledgment (failure)
|
||||||
case systemValidation = 0x24 // Session validation ping
|
case systemValidation = 0x24 // Session validation ping
|
||||||
|
case handshakeRequest = 0x25 // Request handshake for pending messages
|
||||||
|
|
||||||
var description: String {
|
var description: String {
|
||||||
switch self {
|
switch self {
|
||||||
@@ -121,10 +122,20 @@ enum MessageType: UInt8 {
|
|||||||
case .protocolAck: return "protocolAck"
|
case .protocolAck: return "protocolAck"
|
||||||
case .protocolNack: return "protocolNack"
|
case .protocolNack: return "protocolNack"
|
||||||
case .systemValidation: return "systemValidation"
|
case .systemValidation: return "systemValidation"
|
||||||
|
case .handshakeRequest: return "handshakeRequest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lazy handshake state tracking
|
||||||
|
enum LazyHandshakeState {
|
||||||
|
case none // No session, no handshake attempted
|
||||||
|
case handshakeQueued // User action requires handshake
|
||||||
|
case handshaking // Currently in handshake process
|
||||||
|
case established // Session ready for use
|
||||||
|
case failed(Error) // Handshake failed
|
||||||
|
}
|
||||||
|
|
||||||
// Special recipient ID for broadcast messages
|
// Special recipient ID for broadcast messages
|
||||||
struct SpecialRecipients {
|
struct SpecialRecipients {
|
||||||
static let broadcast = Data(repeating: 0xFF, count: 8) // All 0xFF = broadcast
|
static let broadcast = Data(repeating: 0xFF, count: 8) // All 0xFF = broadcast
|
||||||
@@ -360,6 +371,106 @@ struct ReadReceipt: Codable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handshake request for pending messages
|
||||||
|
struct HandshakeRequest: Codable {
|
||||||
|
let requestID: String
|
||||||
|
let requesterID: String // Who needs the handshake
|
||||||
|
let requesterNickname: String // Nickname of requester
|
||||||
|
let targetID: String // Who should initiate handshake
|
||||||
|
let pendingMessageCount: UInt8 // Number of messages queued
|
||||||
|
let timestamp: Date
|
||||||
|
|
||||||
|
init(requesterID: String, requesterNickname: String, targetID: String, pendingMessageCount: UInt8) {
|
||||||
|
self.requestID = UUID().uuidString
|
||||||
|
self.requesterID = requesterID
|
||||||
|
self.requesterNickname = requesterNickname
|
||||||
|
self.targetID = targetID
|
||||||
|
self.pendingMessageCount = pendingMessageCount
|
||||||
|
self.timestamp = Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
// For binary decoding
|
||||||
|
private init(requestID: String, requesterID: String, requesterNickname: String, targetID: String, pendingMessageCount: UInt8, timestamp: Date) {
|
||||||
|
self.requestID = requestID
|
||||||
|
self.requesterID = requesterID
|
||||||
|
self.requesterNickname = requesterNickname
|
||||||
|
self.targetID = targetID
|
||||||
|
self.pendingMessageCount = pendingMessageCount
|
||||||
|
self.timestamp = timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Binary Encoding
|
||||||
|
|
||||||
|
func toBinaryData() -> Data {
|
||||||
|
var data = Data()
|
||||||
|
data.appendUUID(requestID)
|
||||||
|
|
||||||
|
// RequesterID as 8-byte hex string
|
||||||
|
var requesterData = Data()
|
||||||
|
var tempID = requesterID
|
||||||
|
while tempID.count >= 2 && requesterData.count < 8 {
|
||||||
|
let hexByte = String(tempID.prefix(2))
|
||||||
|
if let byte = UInt8(hexByte, radix: 16) {
|
||||||
|
requesterData.append(byte)
|
||||||
|
}
|
||||||
|
tempID = String(tempID.dropFirst(2))
|
||||||
|
}
|
||||||
|
while requesterData.count < 8 {
|
||||||
|
requesterData.append(0)
|
||||||
|
}
|
||||||
|
data.append(requesterData)
|
||||||
|
|
||||||
|
// TargetID as 8-byte hex string
|
||||||
|
var targetData = Data()
|
||||||
|
tempID = targetID
|
||||||
|
while tempID.count >= 2 && targetData.count < 8 {
|
||||||
|
let hexByte = String(tempID.prefix(2))
|
||||||
|
if let byte = UInt8(hexByte, radix: 16) {
|
||||||
|
targetData.append(byte)
|
||||||
|
}
|
||||||
|
tempID = String(tempID.dropFirst(2))
|
||||||
|
}
|
||||||
|
while targetData.count < 8 {
|
||||||
|
targetData.append(0)
|
||||||
|
}
|
||||||
|
data.append(targetData)
|
||||||
|
|
||||||
|
data.appendUInt8(pendingMessageCount)
|
||||||
|
data.appendDate(timestamp)
|
||||||
|
data.appendString(requesterNickname)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static func fromBinaryData(_ data: Data) -> HandshakeRequest? {
|
||||||
|
// Create defensive copy
|
||||||
|
let dataCopy = Data(data)
|
||||||
|
|
||||||
|
// Minimum size: UUID (16) + requesterID (8) + targetID (8) + count (1) + timestamp (8) + min nickname
|
||||||
|
guard dataCopy.count >= 42 else { return nil }
|
||||||
|
|
||||||
|
var offset = 0
|
||||||
|
|
||||||
|
guard let requestID = dataCopy.readUUID(at: &offset) else { return nil }
|
||||||
|
|
||||||
|
guard let requesterIDData = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil }
|
||||||
|
let requesterID = requesterIDData.hexEncodedString()
|
||||||
|
|
||||||
|
guard let targetIDData = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil }
|
||||||
|
let targetID = targetIDData.hexEncodedString()
|
||||||
|
|
||||||
|
guard let pendingMessageCount = dataCopy.readUInt8(at: &offset),
|
||||||
|
let timestamp = dataCopy.readDate(at: &offset),
|
||||||
|
let requesterNickname = dataCopy.readString(at: &offset) else { return nil }
|
||||||
|
|
||||||
|
return HandshakeRequest(requestID: requestID,
|
||||||
|
requesterID: requesterID,
|
||||||
|
requesterNickname: requesterNickname,
|
||||||
|
targetID: targetID,
|
||||||
|
pendingMessageCount: pendingMessageCount,
|
||||||
|
timestamp: timestamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Protocol Acknowledgments
|
// MARK: - Protocol Acknowledgments
|
||||||
|
|
||||||
// Protocol-level acknowledgment for reliable delivery
|
// Protocol-level acknowledgment for reliable delivery
|
||||||
|
|||||||
@@ -400,6 +400,9 @@ class BluetoothMeshService: NSObject {
|
|||||||
// Pending private messages waiting for handshake
|
// Pending private messages waiting for handshake
|
||||||
private var pendingPrivateMessages: [String: [(content: String, recipientNickname: String, messageID: String)]] = [:]
|
private var pendingPrivateMessages: [String: [(content: String, recipientNickname: String, messageID: String)]] = [:]
|
||||||
|
|
||||||
|
// Noise session state tracking for lazy handshakes
|
||||||
|
private var noiseSessionStates: [String: LazyHandshakeState] = [:]
|
||||||
|
|
||||||
// Cover traffic for privacy
|
// Cover traffic for privacy
|
||||||
private var coverTrafficTimer: Timer?
|
private var coverTrafficTimer: Timer?
|
||||||
private let coverTrafficPrefix = "☂DUMMY☂" // Prefix to identify dummy messages after decryption
|
private let coverTrafficPrefix = "☂DUMMY☂" // Prefix to identify dummy messages after decryption
|
||||||
@@ -758,10 +761,8 @@ class BluetoothMeshService: NSObject {
|
|||||||
|
|
||||||
self.notifyPeerIDChange(oldPeerID: oldID, newPeerID: newPeerID, fingerprint: fingerprint)
|
self.notifyPeerIDChange(oldPeerID: oldID, newPeerID: newPeerID, fingerprint: fingerprint)
|
||||||
|
|
||||||
// Trigger handshake after a short delay to allow the peer to settle
|
// Lazy handshake: No longer initiate handshake on peer ID rotation
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
// Handshake will be initiated when first private message is sent
|
||||||
self?.initiateNoiseHandshake(with: newPeerID)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1443,28 +1444,9 @@ class BluetoothMeshService: NSObject {
|
|||||||
SecureLogger.logError(error, context: "Failed to encrypt delivery ACK via Noise for \(recipientID)", category: SecureLogger.encryption)
|
SecureLogger.logError(error, context: "Failed to encrypt delivery ACK via Noise for \(recipientID)", category: SecureLogger.encryption)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fall back to legacy encryption
|
// Lazy handshake: No session available, drop the ACK
|
||||||
let encryptedPayload: Data
|
SecureLogger.log("No Noise session with \(recipientID) for delivery ACK - dropping (lazy handshake mode)",
|
||||||
do {
|
category: SecureLogger.noise, level: .info)
|
||||||
encryptedPayload = try self.noiseService.encrypt(ackData, for: recipientID)
|
|
||||||
} catch {
|
|
||||||
SecureLogger.logError(error, context: "Failed to encrypt delivery ACK for \(recipientID)", category: SecureLogger.encryption)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create ACK packet with direct routing to original sender
|
|
||||||
let packet = BitchatPacket(
|
|
||||||
type: MessageType.deliveryAck.rawValue,
|
|
||||||
senderID: Data(hexString: self.myPeerID) ?? Data(),
|
|
||||||
recipientID: Data(hexString: recipientID) ?? Data(),
|
|
||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
|
||||||
payload: encryptedPayload,
|
|
||||||
signature: nil, // ACKs don't need signatures
|
|
||||||
ttl: 3 // Limited TTL for ACKs
|
|
||||||
)
|
|
||||||
|
|
||||||
// Send immediately without delay (ACKs should be fast)
|
|
||||||
self.broadcastPacket(packet)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1549,31 +1531,9 @@ class BluetoothMeshService: NSObject {
|
|||||||
SecureLogger.logError(error, context: "Failed to encrypt read receipt via Noise for \(recipientID)", category: SecureLogger.encryption)
|
SecureLogger.logError(error, context: "Failed to encrypt read receipt via Noise for \(recipientID)", category: SecureLogger.encryption)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No session - initiate handshake and queue the read receipt
|
// Lazy handshake: No session available, drop the read receipt
|
||||||
SecureLogger.log("No Noise session with \(recipientID) for read receipt, initiating handshake", category: SecureLogger.noise, level: .info)
|
SecureLogger.log("No Noise session with \(recipientID) for read receipt - dropping (lazy handshake mode)",
|
||||||
|
category: SecureLogger.noise, level: .info)
|
||||||
// Initiate handshake regardless of our role if we need to send data
|
|
||||||
self.initiateNoiseHandshake(with: recipientID)
|
|
||||||
|
|
||||||
// Queue the read receipt as a pending message
|
|
||||||
// Create a synthetic message ID for the read receipt
|
|
||||||
let readReceiptMessageID = "READ_RECEIPT_\(receipt.originalMessageID)"
|
|
||||||
|
|
||||||
collectionsQueue.sync(flags: .barrier) {
|
|
||||||
if self.pendingPrivateMessages[recipientID] == nil {
|
|
||||||
self.pendingPrivateMessages[recipientID] = []
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the read receipt data as a pending "message"
|
|
||||||
self.pendingPrivateMessages[recipientID]?.append((
|
|
||||||
content: "READ_RECEIPT:\(receipt.originalMessageID)",
|
|
||||||
recipientNickname: receipt.readerNickname,
|
|
||||||
messageID: readReceiptMessageID
|
|
||||||
))
|
|
||||||
|
|
||||||
let count = self.pendingPrivateMessages[recipientID]?.count ?? 0
|
|
||||||
SecureLogger.log("Queued read receipt for \(recipientID), pending messages: \(count)", category: SecureLogger.noise, level: .info)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1631,6 +1591,49 @@ class BluetoothMeshService: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get Noise session state for UI display
|
||||||
|
func getNoiseSessionState(for peerID: String) -> LazyHandshakeState {
|
||||||
|
return collectionsQueue.sync {
|
||||||
|
// First check our tracked state
|
||||||
|
if let state = noiseSessionStates[peerID] {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no tracked state, check if we have an established session
|
||||||
|
if noiseService.hasEstablishedSession(with: peerID) {
|
||||||
|
return .established
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to none
|
||||||
|
return .none
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger handshake with a peer (for UI)
|
||||||
|
func triggerHandshake(with peerID: String) {
|
||||||
|
SecureLogger.log("UI triggered handshake with \(peerID)", category: SecureLogger.noise, level: .info)
|
||||||
|
|
||||||
|
// Check if we already have a session
|
||||||
|
if noiseService.hasEstablishedSession(with: peerID) {
|
||||||
|
SecureLogger.log("Already have session with \(peerID), skipping handshake", category: SecureLogger.noise, level: .debug)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state to handshakeQueued
|
||||||
|
collectionsQueue.sync(flags: .barrier) {
|
||||||
|
noiseSessionStates[peerID] = .handshakeQueued
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply tie-breaker logic for handshake initiation
|
||||||
|
if myPeerID < peerID {
|
||||||
|
// We have lower ID, initiate handshake
|
||||||
|
initiateNoiseHandshake(with: peerID)
|
||||||
|
} else {
|
||||||
|
// We have higher ID, send targeted identity announce to prompt them to initiate
|
||||||
|
sendNoiseIdentityAnnounce(to: peerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getPeerRSSI() -> [String: NSNumber] {
|
func getPeerRSSI() -> [String: NSNumber] {
|
||||||
var rssiValues = peerRSSI
|
var rssiValues = peerRSSI
|
||||||
|
|
||||||
@@ -2295,6 +2298,8 @@ class BluetoothMeshService: NSObject {
|
|||||||
messageTypeName = "VERSION_ACK"
|
messageTypeName = "VERSION_ACK"
|
||||||
case .systemValidation:
|
case .systemValidation:
|
||||||
messageTypeName = "SYSTEM_VALIDATION"
|
messageTypeName = "SYSTEM_VALIDATION"
|
||||||
|
case .handshakeRequest:
|
||||||
|
messageTypeName = "HANDSHAKE_REQUEST"
|
||||||
default:
|
default:
|
||||||
messageTypeName = "UNKNOWN(\(packet.type))"
|
messageTypeName = "UNKNOWN(\(packet.type))"
|
||||||
}
|
}
|
||||||
@@ -3146,45 +3151,15 @@ class BluetoothMeshService: NSObject {
|
|||||||
(self?.delegate as? ChatViewModel)?.registerPeerPublicKey(peerID: announcement.peerID, publicKeyData: announcement.publicKey)
|
(self?.delegate as? ChatViewModel)?.registerPeerPublicKey(peerID: announcement.peerID, publicKeyData: announcement.publicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we don't have a session yet, check if we should initiate
|
// Lazy handshake: No longer initiate handshake on identity announcement
|
||||||
|
// Just respond with our own identity announcement
|
||||||
if !noiseService.hasEstablishedSession(with: announcement.peerID) {
|
if !noiseService.hasEstablishedSession(with: announcement.peerID) {
|
||||||
// Lock rotation during handshake
|
// Send our identity back so they know we're here
|
||||||
lockRotation()
|
SecureLogger.log("Responding to identity announce from \(announcement.peerID) with our own (lazy handshake mode)",
|
||||||
|
category: SecureLogger.noise, level: .info)
|
||||||
// Use lexicographic comparison as tie-breaker to prevent simultaneous handshakes
|
sendNoiseIdentityAnnounce(to: announcement.peerID)
|
||||||
// Only the peer with the "lower" ID initiates
|
|
||||||
// Use coordinator to determine if we should initiate
|
|
||||||
let shouldInitiate = handshakeCoordinator.determineHandshakeRole(
|
|
||||||
myPeerID: myPeerID,
|
|
||||||
remotePeerID: announcement.peerID
|
|
||||||
) == .initiator
|
|
||||||
|
|
||||||
if shouldInitiate {
|
|
||||||
// Add small delay on fresh startup to let connections stabilize
|
|
||||||
let lastConnection = lastConnectionTime[announcement.peerID] ?? Date.distantPast
|
|
||||||
let timeSinceConnection = Date().timeIntervalSince(lastConnection)
|
|
||||||
|
|
||||||
if timeSinceConnection > 60.0 { // Fresh connection
|
|
||||||
// Delay handshake initiation slightly for connection stability
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
|
||||||
SecureLogger.log("Initiating handshake after identity announce from \(announcement.peerID)",
|
|
||||||
category: SecureLogger.noise, level: .info)
|
|
||||||
self?.attemptHandshakeIfNeeded(with: announcement.peerID, forceIfStale: true)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Quick reconnection, initiate immediately
|
|
||||||
SecureLogger.log("Quick reconnection - initiating handshake with \(announcement.peerID)",
|
|
||||||
category: SecureLogger.noise, level: .info)
|
|
||||||
attemptHandshakeIfNeeded(with: announcement.peerID, forceIfStale: true)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Send our identity back so they know we're ready
|
|
||||||
SecureLogger.log("Responding to identity announce from \(announcement.peerID) with our own",
|
|
||||||
category: SecureLogger.noise, level: .info)
|
|
||||||
sendNoiseIdentityAnnounce(to: announcement.peerID)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// We already have a session, but ensure ChatViewModel knows about the fingerprint
|
// We already have a session, ensure ChatViewModel knows about the fingerprint
|
||||||
// This handles the case where handshake completed before identity announcement
|
// This handles the case where handshake completed before identity announcement
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
if let publicKeyData = self?.noiseService.getPeerPublicKeyData(announcement.peerID) {
|
if let publicKeyData = self?.noiseService.getPeerPublicKeyData(announcement.peerID) {
|
||||||
@@ -3396,6 +3371,13 @@ class BluetoothMeshService: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case .handshakeRequest:
|
||||||
|
// Handle handshake request for pending messages
|
||||||
|
let senderID = packet.senderID.hexEncodedString()
|
||||||
|
if !isPeerIDOurs(senderID) {
|
||||||
|
handleHandshakeRequest(from: senderID, data: packet.payload)
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -4430,7 +4412,8 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
private func isHighPriorityMessage(type: UInt8) -> Bool {
|
private func isHighPriorityMessage(type: UInt8) -> Bool {
|
||||||
switch MessageType(rawValue: type) {
|
switch MessageType(rawValue: type) {
|
||||||
case .noiseHandshakeInit, .noiseHandshakeResp, .protocolAck,
|
case .noiseHandshakeInit, .noiseHandshakeResp, .protocolAck,
|
||||||
.versionHello, .versionAck, .deliveryAck, .systemValidation:
|
.versionHello, .versionAck, .deliveryAck, .systemValidation,
|
||||||
|
.handshakeRequest:
|
||||||
return true
|
return true
|
||||||
case .message, .announce, .leave, .readReceipt, .deliveryStatusRequest,
|
case .message, .announce, .leave, .readReceipt, .deliveryStatusRequest,
|
||||||
.fragmentStart, .fragmentContinue, .fragmentEnd,
|
.fragmentStart, .fragmentContinue, .fragmentEnd,
|
||||||
@@ -4985,6 +4968,11 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
handshakeAttemptTimes.removeValue(forKey: peerID)
|
handshakeAttemptTimes.removeValue(forKey: peerID)
|
||||||
handshakeCoordinator.recordHandshakeSuccess(peerID: peerID)
|
handshakeCoordinator.recordHandshakeSuccess(peerID: peerID)
|
||||||
|
|
||||||
|
// Update session state to established
|
||||||
|
collectionsQueue.sync(flags: .barrier) {
|
||||||
|
self.noiseSessionStates[peerID] = .established
|
||||||
|
}
|
||||||
|
|
||||||
// Update connection state to authenticated
|
// Update connection state to authenticated
|
||||||
updatePeerConnectionState(peerID, state: .authenticated)
|
updatePeerConnectionState(peerID, state: .authenticated)
|
||||||
|
|
||||||
@@ -4996,6 +4984,11 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update state to handshaking
|
||||||
|
collectionsQueue.sync(flags: .barrier) {
|
||||||
|
self.noiseSessionStates[peerID] = .handshaking
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we have pending messages
|
// Check if we have pending messages
|
||||||
let hasPendingMessages = collectionsQueue.sync {
|
let hasPendingMessages = collectionsQueue.sync {
|
||||||
return pendingPrivateMessages[peerID]?.isEmpty == false
|
return pendingPrivateMessages[peerID]?.isEmpty == false
|
||||||
@@ -5178,6 +5171,11 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
// Session established successfully
|
// Session established successfully
|
||||||
handshakeCoordinator.recordHandshakeSuccess(peerID: peerID)
|
handshakeCoordinator.recordHandshakeSuccess(peerID: peerID)
|
||||||
|
|
||||||
|
// Update session state to established
|
||||||
|
collectionsQueue.sync(flags: .barrier) {
|
||||||
|
self.noiseSessionStates[peerID] = .established
|
||||||
|
}
|
||||||
|
|
||||||
// Update connection state to authenticated
|
// Update connection state to authenticated
|
||||||
updatePeerConnectionState(peerID, state: .authenticated)
|
updatePeerConnectionState(peerID, state: .authenticated)
|
||||||
|
|
||||||
@@ -5211,12 +5209,22 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
// Session already established, ignore handshake
|
// Session already established, ignore handshake
|
||||||
SecureLogger.log("Handshake already established with \(peerID)", category: SecureLogger.noise, level: .info)
|
SecureLogger.log("Handshake already established with \(peerID)", category: SecureLogger.noise, level: .info)
|
||||||
handshakeCoordinator.recordHandshakeSuccess(peerID: peerID)
|
handshakeCoordinator.recordHandshakeSuccess(peerID: peerID)
|
||||||
|
|
||||||
|
// Update session state to established
|
||||||
|
collectionsQueue.sync(flags: .barrier) {
|
||||||
|
self.noiseSessionStates[peerID] = .established
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Handshake failed
|
// Handshake failed
|
||||||
handshakeCoordinator.recordHandshakeFailure(peerID: peerID, reason: error.localizedDescription)
|
handshakeCoordinator.recordHandshakeFailure(peerID: peerID, reason: error.localizedDescription)
|
||||||
SecureLogger.logSecurityEvent(.handshakeFailed(peerID: peerID, error: error.localizedDescription))
|
SecureLogger.logSecurityEvent(.handshakeFailed(peerID: peerID, error: error.localizedDescription))
|
||||||
SecureLogger.log("Handshake failed with \(peerID): \(error)", category: SecureLogger.noise, level: .error)
|
SecureLogger.log("Handshake failed with \(peerID): \(error)", category: SecureLogger.noise, level: .error)
|
||||||
|
|
||||||
|
// Update session state to failed
|
||||||
|
collectionsQueue.sync(flags: .barrier) {
|
||||||
|
self.noiseSessionStates[peerID] = .failed(error)
|
||||||
|
}
|
||||||
|
|
||||||
// If handshake failed due to authentication error, clear the session to allow retry
|
// If handshake failed due to authentication error, clear the session to allow retry
|
||||||
if case NoiseError.authenticationFailure = error {
|
if case NoiseError.authenticationFailure = error {
|
||||||
SecureLogger.log("Handshake failed with \(peerID): authenticationFailure - clearing session", category: SecureLogger.noise, level: .warning)
|
SecureLogger.log("Handshake failed with \(peerID): authenticationFailure - clearing session", category: SecureLogger.noise, level: .warning)
|
||||||
@@ -5428,25 +5436,16 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
|
|
||||||
sendVersionAck(ack, to: peerID)
|
sendVersionAck(ack, to: peerID)
|
||||||
|
|
||||||
// Proceed with Noise handshake after successful version negotiation
|
// Lazy handshake: No longer initiate handshake after version negotiation
|
||||||
|
// Just announce our identity
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
// First announce our identity
|
// Just announce our identity
|
||||||
self.sendNoiseIdentityAnnounce()
|
self.sendNoiseIdentityAnnounce()
|
||||||
|
|
||||||
// Then check if we should initiate handshake
|
SecureLogger.log("Version negotiation complete with \(peerID) - lazy handshake mode",
|
||||||
// If we already have a valid session, skip handshake
|
category: SecureLogger.handshake, level: .info)
|
||||||
if self.noiseService.hasEstablishedSession(with: peerID) {
|
|
||||||
SecureLogger.log("Already have session with \(peerID) after version negotiation, skipping handshake",
|
|
||||||
category: SecureLogger.handshake, level: .info)
|
|
||||||
|
|
||||||
// Force a session validation by sending a small encrypted ping
|
|
||||||
self.validateNoiseSession(with: peerID)
|
|
||||||
} else {
|
|
||||||
// Use attemptHandshakeIfNeeded to coordinate properly
|
|
||||||
self.attemptHandshakeIfNeeded(with: peerID)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No compatible version
|
// No compatible version
|
||||||
@@ -5692,6 +5691,50 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func handleHandshakeRequest(from peerID: String, data: Data) {
|
||||||
|
guard let request = HandshakeRequest.fromBinaryData(data) else {
|
||||||
|
SecureLogger.log("Failed to decode handshake request from \(peerID)", category: SecureLogger.noise, level: .error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify this request is for us
|
||||||
|
guard request.targetID == myPeerID else {
|
||||||
|
// This request is not for us, might need to relay
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
SecureLogger.log("Received handshake request from \(request.requesterID) (\(request.requesterNickname)) with \(request.pendingMessageCount) pending messages",
|
||||||
|
category: SecureLogger.noise, level: .info)
|
||||||
|
|
||||||
|
// Notify UI about pending messages
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
if let chatVM = self?.delegate as? ChatViewModel {
|
||||||
|
// Notify the UI that someone wants to send messages
|
||||||
|
chatVM.handleHandshakeRequest(from: request.requesterID,
|
||||||
|
nickname: request.requesterNickname,
|
||||||
|
pendingCount: request.pendingMessageCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we already have a session
|
||||||
|
if noiseService.hasEstablishedSession(with: peerID) {
|
||||||
|
// We already have a session, no action needed
|
||||||
|
SecureLogger.log("Already have session with \(peerID), ignoring handshake request", category: SecureLogger.noise, level: .debug)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply tie-breaker logic for handshake initiation
|
||||||
|
if myPeerID < peerID {
|
||||||
|
// We have lower ID, initiate handshake
|
||||||
|
SecureLogger.log("Initiating handshake with \(peerID) in response to handshake request", category: SecureLogger.noise, level: .info)
|
||||||
|
initiateNoiseHandshake(with: peerID)
|
||||||
|
} else {
|
||||||
|
// We have higher ID, send identity announce to prompt them
|
||||||
|
SecureLogger.log("Sending identity announce to \(peerID) in response to handshake request", category: SecureLogger.noise, level: .info)
|
||||||
|
sendNoiseIdentityAnnounce(to: peerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Send protocol ACK for important packets
|
// Send protocol ACK for important packets
|
||||||
private func sendProtocolAck(for packet: BitchatPacket, to peerID: String, hopCount: UInt8 = 0) {
|
private func sendProtocolAck(for packet: BitchatPacket, to peerID: String, hopCount: UInt8 = 0) {
|
||||||
// Generate packet ID from packet content hash
|
// Generate packet ID from packet content hash
|
||||||
@@ -6088,7 +6131,35 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
// Clear stale session first
|
// Clear stale session first
|
||||||
cleanupPeerCryptoState(recipientPeerID)
|
cleanupPeerCryptoState(recipientPeerID)
|
||||||
}
|
}
|
||||||
SecureLogger.log("No valid Noise session with \(recipientPeerID), initiating handshake", category: SecureLogger.noise, level: .info)
|
|
||||||
|
// Update state to handshakeQueued
|
||||||
|
collectionsQueue.sync(flags: .barrier) {
|
||||||
|
self.noiseSessionStates[recipientPeerID] = .handshakeQueued
|
||||||
|
}
|
||||||
|
|
||||||
|
SecureLogger.log("No valid Noise session with \(recipientPeerID), initiating handshake (lazy mode)", category: SecureLogger.noise, level: .info)
|
||||||
|
|
||||||
|
// Notify UI that we're establishing encryption
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
if let chatVM = self?.delegate as? ChatViewModel {
|
||||||
|
// This will update the UI to show "establishing encryption" state
|
||||||
|
chatVM.updateEncryptionStatusForPeer(recipientPeerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue message for sending after handshake completes
|
||||||
|
collectionsQueue.sync(flags: .barrier) { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if self.pendingPrivateMessages[recipientPeerID] == nil {
|
||||||
|
self.pendingPrivateMessages[recipientPeerID] = []
|
||||||
|
}
|
||||||
|
self.pendingPrivateMessages[recipientPeerID]?.append((content, recipientNickname, messageID ?? UUID().uuidString))
|
||||||
|
let count = self.pendingPrivateMessages[recipientPeerID]?.count ?? 0
|
||||||
|
SecureLogger.log("Queued private message for \(recipientPeerID), \(count) messages pending", category: SecureLogger.noise, level: .info)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send handshake request to notify recipient of pending messages
|
||||||
|
sendHandshakeRequest(to: recipientPeerID, pendingCount: UInt8(pendingPrivateMessages[recipientPeerID]?.count ?? 1))
|
||||||
|
|
||||||
// Apply tie-breaker logic for handshake initiation
|
// Apply tie-breaker logic for handshake initiation
|
||||||
if myPeerID < recipientPeerID {
|
if myPeerID < recipientPeerID {
|
||||||
@@ -6099,16 +6170,6 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
sendNoiseIdentityAnnounce(to: recipientPeerID)
|
sendNoiseIdentityAnnounce(to: recipientPeerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Queue message for sending after handshake completes
|
|
||||||
messageQueue.async(flags: .barrier) { [weak self] in
|
|
||||||
guard let self = self else { return }
|
|
||||||
if self.pendingPrivateMessages[recipientPeerID] == nil {
|
|
||||||
self.pendingPrivateMessages[recipientPeerID] = []
|
|
||||||
}
|
|
||||||
self.pendingPrivateMessages[recipientPeerID]?.append((content, recipientNickname, messageID ?? UUID().uuidString))
|
|
||||||
let count = self.pendingPrivateMessages[recipientPeerID]?.count ?? 0
|
|
||||||
SecureLogger.log("Queued private message for \(recipientPeerID), \(count) messages pending", category: SecureLogger.noise, level: .info)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6233,6 +6294,31 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func sendHandshakeRequest(to targetPeerID: String, pendingCount: UInt8) {
|
||||||
|
// Create handshake request
|
||||||
|
let request = HandshakeRequest(requesterID: myPeerID,
|
||||||
|
requesterNickname: (delegate as? ChatViewModel)?.nickname ?? myPeerID,
|
||||||
|
targetID: targetPeerID,
|
||||||
|
pendingMessageCount: pendingCount)
|
||||||
|
|
||||||
|
let requestData = request.toBinaryData()
|
||||||
|
|
||||||
|
// Create packet for handshake request
|
||||||
|
let packet = BitchatPacket(type: MessageType.handshakeRequest.rawValue,
|
||||||
|
ttl: 6,
|
||||||
|
senderID: myPeerID,
|
||||||
|
payload: requestData)
|
||||||
|
|
||||||
|
// Try direct delivery first
|
||||||
|
if sendDirectToRecipient(packet, recipientPeerID: targetPeerID) {
|
||||||
|
SecureLogger.log("Sent handshake request directly to \(targetPeerID)", category: SecureLogger.noise, level: .info)
|
||||||
|
} else {
|
||||||
|
// Use selective relay if direct delivery fails
|
||||||
|
sendViaSelectiveRelay(packet, recipientPeerID: targetPeerID)
|
||||||
|
SecureLogger.log("Sent handshake request via relay to \(targetPeerID)", category: SecureLogger.noise, level: .info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func selectBestRelayPeers(excluding: String, maxPeers: Int = 3) -> [String] {
|
private func selectBestRelayPeers(excluding: String, maxPeers: Int = 3) -> [String] {
|
||||||
// Select peers with best RSSI for relay, excluding the target recipient
|
// Select peers with best RSSI for relay, excluding the target recipient
|
||||||
var candidates: [(peerID: String, rssi: Int)] = []
|
var candidates: [(peerID: String, rssi: Int)] = []
|
||||||
|
|||||||
@@ -13,15 +13,18 @@ import os.log
|
|||||||
// MARK: - Encryption Status
|
// MARK: - Encryption Status
|
||||||
|
|
||||||
enum EncryptionStatus: Equatable {
|
enum EncryptionStatus: Equatable {
|
||||||
case none
|
case none // Failed or incompatible
|
||||||
case noiseHandshaking
|
case noHandshake // No handshake attempted yet
|
||||||
case noiseSecured
|
case noiseHandshaking // Currently establishing
|
||||||
case noiseVerified
|
case noiseSecured // Established but not verified
|
||||||
|
case noiseVerified // Established and verified
|
||||||
|
|
||||||
var icon: String {
|
var icon: String? { // Made optional to hide icon when no handshake
|
||||||
switch self {
|
switch self {
|
||||||
case .none:
|
case .none:
|
||||||
return "lock.slash"
|
return "lock.slash" // Failed handshake
|
||||||
|
case .noHandshake:
|
||||||
|
return nil // No icon when no handshake attempted
|
||||||
case .noiseHandshaking:
|
case .noiseHandshaking:
|
||||||
return "lock.rotation"
|
return "lock.rotation"
|
||||||
case .noiseSecured:
|
case .noiseSecured:
|
||||||
@@ -34,6 +37,8 @@ enum EncryptionStatus: Equatable {
|
|||||||
var description: String {
|
var description: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .none:
|
case .none:
|
||||||
|
return "Encryption failed"
|
||||||
|
case .noHandshake:
|
||||||
return "Not encrypted"
|
return "Not encrypted"
|
||||||
case .noiseHandshaking:
|
case .noiseHandshaking:
|
||||||
return "Establishing encryption..."
|
return "Establishing encryption..."
|
||||||
|
|||||||
@@ -480,6 +480,16 @@ class ChatViewModel: ObservableObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trigger handshake if we don't have a session yet
|
||||||
|
let sessionState = meshService.getNoiseSessionState(for: peerID)
|
||||||
|
switch sessionState {
|
||||||
|
case .none, .failed:
|
||||||
|
// Initiate handshake when opening PM
|
||||||
|
meshService.triggerHandshake(with: peerID)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
selectedPrivateChatPeer = peerID
|
selectedPrivateChatPeer = peerID
|
||||||
// Also track by fingerprint for persistence across reconnections
|
// Also track by fingerprint for persistence across reconnections
|
||||||
selectedPrivateChatFingerprint = peerIDToPublicKeyFingerprint[peerID]
|
selectedPrivateChatFingerprint = peerIDToPublicKeyFingerprint[peerID]
|
||||||
@@ -648,6 +658,9 @@ class ChatViewModel: ObservableObject {
|
|||||||
// Flush any pending messages immediately
|
// Flush any pending messages immediately
|
||||||
flushMessageBatchImmediately()
|
flushMessageBatchImmediately()
|
||||||
|
|
||||||
|
// Force save any pending identity changes (verifications, favorites, etc)
|
||||||
|
SecureIdentityStateManager.shared.forceSave()
|
||||||
|
|
||||||
// Verify identity key is still there
|
// Verify identity key is still there
|
||||||
_ = KeychainManager.shared.verifyIdentityKeyExists()
|
_ = KeychainManager.shared.verifyIdentityKeyExists()
|
||||||
|
|
||||||
@@ -1300,14 +1313,14 @@ class ChatViewModel: ObservableObject {
|
|||||||
// This must be a pure function - no state mutations allowed
|
// This must be a pure function - no state mutations allowed
|
||||||
// to avoid SwiftUI update loops
|
// to avoid SwiftUI update loops
|
||||||
|
|
||||||
let hasEstablished = meshService.getNoiseService().hasEstablishedSession(with: peerID)
|
let sessionState = meshService.getNoiseSessionState(for: peerID)
|
||||||
let hasSession = meshService.getNoiseService().hasSession(with: peerID)
|
|
||||||
let storedStatus = peerEncryptionStatus[peerID]
|
let storedStatus = peerEncryptionStatus[peerID]
|
||||||
|
|
||||||
let status: EncryptionStatus
|
let status: EncryptionStatus
|
||||||
|
|
||||||
// First check if we have an established session
|
// Determine status based on session state
|
||||||
if hasEstablished {
|
switch sessionState {
|
||||||
|
case .established:
|
||||||
// We have encryption, now check if it's verified
|
// We have encryption, now check if it's verified
|
||||||
if let fingerprint = getFingerprint(for: peerID) {
|
if let fingerprint = getFingerprint(for: peerID) {
|
||||||
if verifiedFingerprints.contains(fingerprint) {
|
if verifiedFingerprints.contains(fingerprint) {
|
||||||
@@ -1319,12 +1332,15 @@ class ChatViewModel: ObservableObject {
|
|||||||
// We have a session but no fingerprint yet - still secured
|
// We have a session but no fingerprint yet - still secured
|
||||||
status = .noiseSecured
|
status = .noiseSecured
|
||||||
}
|
}
|
||||||
} else if hasSession {
|
case .handshaking, .handshakeQueued:
|
||||||
// Check if handshaking
|
// Currently establishing encryption
|
||||||
status = .noiseHandshaking
|
status = .noiseHandshaking
|
||||||
} else {
|
case .none:
|
||||||
// Fall back to stored status
|
// No handshake attempted
|
||||||
status = storedStatus ?? .none
|
status = .noHandshake
|
||||||
|
case .failed:
|
||||||
|
// Handshake failed - show broken lock
|
||||||
|
status = .none
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache the result
|
// Cache the result
|
||||||
@@ -1332,7 +1348,7 @@ class ChatViewModel: ObservableObject {
|
|||||||
|
|
||||||
// Only log occasionally to avoid spam
|
// Only log occasionally to avoid spam
|
||||||
if Int.random(in: 0..<100) == 0 {
|
if Int.random(in: 0..<100) == 0 {
|
||||||
SecureLogger.log("getEncryptionStatus for \(peerID): hasEstablished=\(hasEstablished), hasSession=\(hasSession), stored=\(String(describing: storedStatus)), final=\(status)", category: SecureLogger.security, level: .debug)
|
SecureLogger.log("getEncryptionStatus for \(peerID): sessionState=\(sessionState), stored=\(String(describing: storedStatus)), final=\(status)", category: SecureLogger.security, level: .debug)
|
||||||
}
|
}
|
||||||
|
|
||||||
return status
|
return status
|
||||||
@@ -1562,9 +1578,8 @@ class ChatViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loadVerifiedFingerprints() {
|
func loadVerifiedFingerprints() {
|
||||||
// Load verified fingerprints from secure storage
|
// Load verified fingerprints directly from secure storage
|
||||||
let allIdentities = SecureIdentityStateManager.shared.getAllSocialIdentities()
|
verifiedFingerprints = SecureIdentityStateManager.shared.getVerifiedFingerprints()
|
||||||
verifiedFingerprints = Set(allIdentities.filter { $0.trustLevel == .verified }.map { $0.fingerprint })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupNoiseCallbacks() {
|
private func setupNoiseCallbacks() {
|
||||||
@@ -1608,6 +1623,44 @@ class ChatViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleHandshakeRequest(from peerID: String, nickname: String, pendingCount: UInt8) {
|
||||||
|
// Create a notification message
|
||||||
|
let notificationMessage = BitchatMessage(
|
||||||
|
sender: "system",
|
||||||
|
content: "📨 \(nickname) wants to send you \(pendingCount) message\(pendingCount == 1 ? "" : "s"). Open the conversation to receive.",
|
||||||
|
timestamp: Date(),
|
||||||
|
isRelay: false,
|
||||||
|
originalSender: nil,
|
||||||
|
isPrivate: false,
|
||||||
|
recipientNickname: nil,
|
||||||
|
senderPeerID: "system",
|
||||||
|
mentions: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add to messages
|
||||||
|
messages.append(notificationMessage)
|
||||||
|
trimMessagesIfNeeded()
|
||||||
|
|
||||||
|
// Show system notification
|
||||||
|
if let fingerprint = getFingerprint(for: peerID) {
|
||||||
|
let isFavorite = favoritePeers.contains(fingerprint)
|
||||||
|
if isFavorite {
|
||||||
|
// Send favorite notification
|
||||||
|
NotificationService.shared.sendPrivateMessageNotification(
|
||||||
|
from: nickname,
|
||||||
|
message: "\(pendingCount) message\(pendingCount == 1 ? "" : "s") pending",
|
||||||
|
peerID: peerID
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Send regular notification
|
||||||
|
NotificationService.shared.sendMentionNotification(
|
||||||
|
from: nickname,
|
||||||
|
message: "\(pendingCount) message\(pendingCount == 1 ? "" : "s") pending. Open conversation to receive."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ChatViewModel: BitchatDelegate {
|
extension ChatViewModel: BitchatDelegate {
|
||||||
|
|||||||
@@ -669,13 +669,15 @@ struct ContentView: View {
|
|||||||
.foregroundColor(peerNicknames[peer.id] != nil ? textColor : secondaryTextColor)
|
.foregroundColor(peerNicknames[peer.id] != nil ? textColor : secondaryTextColor)
|
||||||
|
|
||||||
// Encryption status icon (after peer name)
|
// Encryption status icon (after peer name)
|
||||||
Image(systemName: peer.encryptionStatus.icon)
|
if let icon = peer.encryptionStatus.icon {
|
||||||
.font(.system(size: 10))
|
Image(systemName: icon)
|
||||||
.foregroundColor(peer.encryptionStatus == .noiseVerified ? Color.green :
|
.font(.system(size: 10))
|
||||||
peer.encryptionStatus == .noiseSecured ? textColor :
|
.foregroundColor(peer.encryptionStatus == .noiseVerified ? Color.green :
|
||||||
peer.encryptionStatus == .noiseHandshaking ? Color.orange :
|
peer.encryptionStatus == .noiseSecured ? textColor :
|
||||||
Color.red)
|
peer.encryptionStatus == .noiseHandshaking ? Color.orange :
|
||||||
.accessibilityLabel("Encryption: \(peer.encryptionStatus == .noiseVerified ? "verified" : peer.encryptionStatus == .noiseSecured ? "secured" : peer.encryptionStatus == .noiseHandshaking ? "establishing" : "none")")
|
Color.red)
|
||||||
|
.accessibilityLabel("Encryption: \(peer.encryptionStatus == .noiseVerified ? "verified" : peer.encryptionStatus == .noiseSecured ? "secured" : peer.encryptionStatus == .noiseHandshaking ? "establishing" : "none")")
|
||||||
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
@@ -897,12 +899,14 @@ struct ContentView: View {
|
|||||||
.foregroundColor(Color.orange)
|
.foregroundColor(Color.orange)
|
||||||
// Dynamic encryption status icon
|
// Dynamic encryption status icon
|
||||||
let encryptionStatus = viewModel.getEncryptionStatus(for: privatePeerID)
|
let encryptionStatus = viewModel.getEncryptionStatus(for: privatePeerID)
|
||||||
Image(systemName: encryptionStatus.icon)
|
if let icon = encryptionStatus.icon {
|
||||||
.font(.system(size: 14))
|
Image(systemName: icon)
|
||||||
.foregroundColor(encryptionStatus == .noiseVerified ? Color.green :
|
.font(.system(size: 14))
|
||||||
encryptionStatus == .noiseSecured ? Color.orange :
|
.foregroundColor(encryptionStatus == .noiseVerified ? Color.green :
|
||||||
Color.red)
|
encryptionStatus == .noiseSecured ? Color.orange :
|
||||||
.accessibilityLabel("Encryption status: \(encryptionStatus == .noiseVerified ? "verified" : encryptionStatus == .noiseSecured ? "secured" : "not encrypted")")
|
Color.red)
|
||||||
|
.accessibilityLabel("Encryption status: \(encryptionStatus == .noiseVerified ? "verified" : encryptionStatus == .noiseSecured ? "secured" : "not encrypted")")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.accessibilityLabel("Private chat with \(privatePeerNick)")
|
.accessibilityLabel("Private chat with \(privatePeerNick)")
|
||||||
|
|||||||
@@ -45,9 +45,11 @@ struct FingerprintView: View {
|
|||||||
let encryptionStatus = viewModel.getEncryptionStatus(for: peerID)
|
let encryptionStatus = viewModel.getEncryptionStatus(for: peerID)
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: encryptionStatus.icon)
|
if let icon = encryptionStatus.icon {
|
||||||
.font(.system(size: 20))
|
Image(systemName: icon)
|
||||||
.foregroundColor(encryptionStatus == .noiseVerified ? Color.green : textColor)
|
.font(.system(size: 20))
|
||||||
|
.foregroundColor(encryptionStatus == .noiseVerified ? Color.green : textColor)
|
||||||
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text(peerNickname)
|
Text(peerNickname)
|
||||||
|
|||||||
Reference in New Issue
Block a user