Implement handshake request notifications for pending messages (#321)

- 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)

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
jack
2025-07-25 14:55:26 +02:00
committed by GitHub
co-authored by jack
parent 07c5e2e097
commit 3513228736
7 changed files with 416 additions and 149 deletions
+66 -13
View File
@@ -480,6 +480,16 @@ class ChatViewModel: ObservableObject {
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
// Also track by fingerprint for persistence across reconnections
selectedPrivateChatFingerprint = peerIDToPublicKeyFingerprint[peerID]
@@ -648,6 +658,9 @@ class ChatViewModel: ObservableObject {
// Flush any pending messages immediately
flushMessageBatchImmediately()
// Force save any pending identity changes (verifications, favorites, etc)
SecureIdentityStateManager.shared.forceSave()
// Verify identity key is still there
_ = KeychainManager.shared.verifyIdentityKeyExists()
@@ -1300,14 +1313,14 @@ class ChatViewModel: ObservableObject {
// This must be a pure function - no state mutations allowed
// to avoid SwiftUI update loops
let hasEstablished = meshService.getNoiseService().hasEstablishedSession(with: peerID)
let hasSession = meshService.getNoiseService().hasSession(with: peerID)
let sessionState = meshService.getNoiseSessionState(for: peerID)
let storedStatus = peerEncryptionStatus[peerID]
let status: EncryptionStatus
// First check if we have an established session
if hasEstablished {
// Determine status based on session state
switch sessionState {
case .established:
// We have encryption, now check if it's verified
if let fingerprint = getFingerprint(for: peerID) {
if verifiedFingerprints.contains(fingerprint) {
@@ -1319,12 +1332,15 @@ class ChatViewModel: ObservableObject {
// We have a session but no fingerprint yet - still secured
status = .noiseSecured
}
} else if hasSession {
// Check if handshaking
case .handshaking, .handshakeQueued:
// Currently establishing encryption
status = .noiseHandshaking
} else {
// Fall back to stored status
status = storedStatus ?? .none
case .none:
// No handshake attempted
status = .noHandshake
case .failed:
// Handshake failed - show broken lock
status = .none
}
// Cache the result
@@ -1332,7 +1348,7 @@ class ChatViewModel: ObservableObject {
// Only log occasionally to avoid spam
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
@@ -1562,9 +1578,8 @@ class ChatViewModel: ObservableObject {
}
func loadVerifiedFingerprints() {
// Load verified fingerprints from secure storage
let allIdentities = SecureIdentityStateManager.shared.getAllSocialIdentities()
verifiedFingerprints = Set(allIdentities.filter { $0.trustLevel == .verified }.map { $0.fingerprint })
// Load verified fingerprints directly from secure storage
verifiedFingerprints = SecureIdentityStateManager.shared.getVerifiedFingerprints()
}
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 {