Skip to content

Commit 5fd9140

Browse files
authored
QR verification: live challenge/response over Noise; persistence, offline badges, and UX/perf polish (#510)
* QR verification scaffold: add Noise verify payload types, VerificationService with QR schema/signing, placeholder MyQR/Scan views, and UI entry points in header * QR: fix VerificationQR mutability (sigHex var) and remove duplicate Data hex helpers to resolve redeclaration; wire signed payload assembly * QR: render actual QR images with CoreImage; add copy button; keep scanner placeholder for now * QR: fix SwiftUI modifiers — apply .interpolation(.none) and .resizable() to platform Image inside ImageWrapper; remove from wrapper usage * QR: add iOS camera scanner using AVFoundation; integrate into Scan view; add NSCameraUsageDescription to Info.plist * QR: make NoisePayloadType exhaustive in ChatViewModel switches by ignoring verifyChallenge/verifyResponse for now (placeholder) * QR verification: speed + persistence + UX - Inject live Noise into VerificationService; prewarm QR on app start - Keep camera active; remove intermediate responder toast - One-shot/dupe guards and deferred send on handshake - Persist verified status immediately; standardize fingerprint (SHA-256) - Show verified badge for offline favorites; mutual verification toast - VERIFY sheet styling to match peer sheet; UI polish - Logs to diagnose verified load + favorites mapping --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com>
1 parent e690345 commit 5fd9140

File tree

13 files changed

+842
-8
lines changed

13 files changed

+842
-8
lines changed

bitchat.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,10 @@
142142
FBC409E105493C491531B59A /* NostrProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5A9FF4AEA8A923317ED26A /* NostrProtocol.swift */; };
143143
A1B2C3D44E5F60718293A4B5 /* XChaCha20Poly1305Compat.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D44E5F60718293A4B4 /* XChaCha20Poly1305Compat.swift */; };
144144
A1B2C3D54E5F60718293A4B6 /* XChaCha20Poly1305Compat.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D44E5F60718293A4B4 /* XChaCha20Poly1305Compat.swift */; };
145+
AA77BB11CC22DD33EE44FF55 /* VerificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA77BB10CC22DD33EE44FF55 /* VerificationService.swift */; };
146+
AA77BB12CC22DD33EE44FF56 /* VerificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA77BB10CC22DD33EE44FF55 /* VerificationService.swift */; };
147+
AA77BB14CC22DD33EE44FF58 /* VerificationViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA77BB13CC22DD33EE44FF57 /* VerificationViews.swift */; };
148+
AA77BB15CC22DD33EE44FF59 /* VerificationViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA77BB13CC22DD33EE44FF57 /* VerificationViews.swift */; };
145149
/* End PBXBuildFile section */
146150

147151
/* Begin PBXContainerItemProxy section */
@@ -261,6 +265,8 @@
261265
FE7CCF2BD78A3F3DAE6DA145 /* MockBLEService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBLEService.swift; sourceTree = "<group>"; };
262266
FF7AF93D874001FBD94C8306 /* bitchat-macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "bitchat-macOS.entitlements"; sourceTree = "<group>"; };
263267
A1B2C3D44E5F60718293A4B4 /* XChaCha20Poly1305Compat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XChaCha20Poly1305Compat.swift; sourceTree = "<group>"; };
268+
AA77BB10CC22DD33EE44FF55 /* VerificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerificationService.swift; sourceTree = "<group>"; };
269+
AA77BB13CC22DD33EE44FF57 /* VerificationViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerificationViews.swift; sourceTree = "<group>"; };
264270
/* End PBXFileReference section */
265271

266272
/* Begin PBXFrameworksBuildPhase section */
@@ -431,6 +437,7 @@
431437
A55126E93155456CAA8D6656 /* Views */ = {
432438
isa = PBXGroup;
433439
children = (
440+
AA77BB13CC22DD33EE44FF57 /* VerificationViews.swift */,
434441
047502B22E55FED60083520F /* GeohashPeopleList.swift */,
435442
047502B32E55FED60083520F /* MeshPeerList.swift */,
436443
0475028E2E5417660083520F /* LocationChannelsSheet.swift */,
@@ -504,6 +511,7 @@
504511
D98A3186D7E4C72E35BDF7FE /* Services */ = {
505512
isa = PBXGroup;
506513
children = (
514+
AA77BB10CC22DD33EE44FF55 /* VerificationService.swift */,
507515
047502B82E560F690083520F /* RelayController.swift */,
508516
0475028B2E54171C0083520F /* LocationChannelManager.swift */,
509517
049BD3B02E51F319001A566B /* MessageRouter.swift */,
@@ -724,6 +732,8 @@
724732
isa = PBXSourcesBuildPhase;
725733
buildActionMask = 2147483647;
726734
files = (
735+
AA77BB12CC22DD33EE44FF56 /* VerificationService.swift in Sources */,
736+
AA77BB15CC22DD33EE44FF59 /* VerificationViews.swift in Sources */,
727737
A1B2C3D54E5F60718293A4B6 /* XChaCha20Poly1305Compat.swift in Sources */,
728738
AD11E46940D742AEAF547EB2 /* AppInfoView.swift in Sources */,
729739
9B51E9B63A3EA59B1A7874BD /* BinaryEncodingUtils.swift in Sources */,
@@ -779,6 +789,8 @@
779789
isa = PBXSourcesBuildPhase;
780790
buildActionMask = 2147483647;
781791
files = (
792+
AA77BB11CC22DD33EE44FF55 /* VerificationService.swift in Sources */,
793+
AA77BB14CC22DD33EE44FF58 /* VerificationViews.swift in Sources */,
782794
A1B2C3D44E5F60718293A4B5 /* XChaCha20Poly1305Compat.swift in Sources */,
783795
ABAF130D88561F4A646F0430 /* AppInfoView.swift in Sources */,
784796
AFB6AEFCABBE97441CB3102B /* BinaryEncodingUtils.swift in Sources */,

bitchat/BitchatApp.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ struct BitchatApp: App {
3131
.environmentObject(chatViewModel)
3232
.onAppear {
3333
NotificationDelegate.shared.chatViewModel = chatViewModel
34+
// Inject live Noise service into VerificationService to avoid creating new BLE instances
35+
VerificationService.shared.configure(with: chatViewModel.meshService.getNoiseService())
36+
// Prewarm Nostr identity and QR to make first VERIFY sheet fast
37+
DispatchQueue.global(qos: .utility).async {
38+
let npub = try? NostrIdentityBridge.getCurrentNostrIdentity()?.npub
39+
_ = VerificationService.shared.buildMyQRString(nickname: chatViewModel.nickname, npub: npub)
40+
}
3441
#if os(iOS)
3542
appDelegate.chatViewModel = chatViewModel
3643
#elseif os(macOS)

bitchat/Info.plist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
<string>bitchat uses Bluetooth to discover and connect with other bitchat users nearby.</string>
3838
<key>NSLocationWhenInUseUsageDescription</key>
3939
<string>bitchat uses your approximate location to compute local geohash channels for optional public chats. Exact GPS is never shared.</string>
40+
<key>NSCameraUsageDescription</key>
41+
<string>bitchat uses the camera to scan QR codes to verify peers.</string>
4042
<key>UIBackgroundModes</key>
4143
<array>
4244
<string>bluetooth-central</string>

bitchat/Protocols/BitchatProtocol.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,12 +155,17 @@ enum NoisePayloadType: UInt8 {
155155
case privateMessage = 0x01 // Private chat message
156156
case readReceipt = 0x02 // Message was read
157157
case delivered = 0x03 // Message was delivered
158+
// Verification (QR-based OOB binding)
159+
case verifyChallenge = 0x10 // Verification challenge
160+
case verifyResponse = 0x11 // Verification response
158161

159162
var description: String {
160163
switch self {
161164
case .privateMessage: return "privateMessage"
162165
case .readReceipt: return "readReceipt"
163166
case .delivered: return "delivered"
167+
case .verifyChallenge: return "verifyChallenge"
168+
case .verifyResponse: return "verifyResponse"
164169
}
165170
}
166171
}

bitchat/Services/BLEService.swift

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -508,11 +508,51 @@ final class BLEService: NSObject {
508508
func sendBroadcastAnnounce() {
509509
sendAnnounce()
510510
}
511+
512+
// MARK: - QR Verification over Noise
513+
func sendVerifyChallenge(to peerID: String, noiseKeyHex: String, nonceA: Data) {
514+
let payload = VerificationService.shared.buildVerifyChallenge(noiseKeyHex: noiseKeyHex, nonceA: nonceA)
515+
sendNoisePayload(payload, to: peerID)
516+
}
517+
518+
func sendVerifyResponse(to peerID: String, noiseKeyHex: String, nonceA: Data) {
519+
guard let payload = VerificationService.shared.buildVerifyResponse(noiseKeyHex: noiseKeyHex, nonceA: nonceA) else { return }
520+
sendNoisePayload(payload, to: peerID)
521+
}
522+
523+
private func sendNoisePayload(_ typedPayload: Data, to peerID: String) {
524+
guard noiseService.hasSession(with: peerID) else {
525+
// Lazy-handshake path: queue? For now, initiate handshake and drop
526+
initiateNoiseHandshake(with: peerID)
527+
return
528+
}
529+
do {
530+
let encrypted = try noiseService.encrypt(typedPayload, for: peerID)
531+
let packet = BitchatPacket(
532+
type: MessageType.noiseEncrypted.rawValue,
533+
senderID: Data(hexString: myPeerID) ?? Data(),
534+
recipientID: Data(hexString: peerID),
535+
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
536+
payload: encrypted,
537+
signature: nil,
538+
ttl: messageTTL
539+
)
540+
if DispatchQueue.getSpecific(key: messageQueueKey) != nil {
541+
broadcastPacket(packet)
542+
} else {
543+
messageQueue.async { [weak self] in self?.broadcastPacket(packet) }
544+
}
545+
} catch {
546+
SecureLogger.log("Failed to send verification payload: \(error)", category: SecureLogger.noise, level: .error)
547+
}
548+
}
511549

512550
func getPeerFingerprint(_ peerID: String) -> String? {
513551
return collectionsQueue.sync {
514552
if let publicKey = peers[peerID]?.noisePublicKey {
515-
return publicKey.hexEncodedString()
553+
// Use the same fingerprinting method as NoiseEncryptionService/UnifiedPeerService (SHA-256 of raw key)
554+
let hash = SHA256.hash(data: publicKey)
555+
return hash.map { String(format: "%02x", $0) }.joined()
516556
}
517557
return nil
518558
}
@@ -1383,6 +1423,16 @@ final class BLEService: NSObject {
13831423
notifyUI { [weak self] in
13841424
self?.delegate?.didReceiveNoisePayload(from: peerID, type: .readReceipt, payload: Data(payloadData), timestamp: ts)
13851425
}
1426+
case .verifyChallenge:
1427+
let ts = Date(timeIntervalSince1970: Double(packet.timestamp) / 1000)
1428+
notifyUI { [weak self] in
1429+
self?.delegate?.didReceiveNoisePayload(from: peerID, type: .verifyChallenge, payload: Data(payloadData), timestamp: ts)
1430+
}
1431+
case .verifyResponse:
1432+
let ts = Date(timeIntervalSince1970: Double(packet.timestamp) / 1000)
1433+
notifyUI { [weak self] in
1434+
self?.delegate?.didReceiveNoisePayload(from: peerID, type: .verifyResponse, payload: Data(payloadData), timestamp: ts)
1435+
}
13861436
default:
13871437
SecureLogger.log("⚠️ Unknown noise payload type: \(payloadType)", category: SecureLogger.noise, level: .warning)
13881438
}

bitchat/Services/CommandProcessor.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,12 @@ class CommandProcessor {
165165
let geoBlocked = Array(SecureIdentityStateManager.shared.getBlockedNostrPubkeys())
166166
var geoNames: [String] = []
167167
if let vm = chatViewModel {
168+
#if os(iOS)
168169
let visible = vm.visibleGeohashPeople()
169170
let visibleIndex = Dictionary(uniqueKeysWithValues: visible.map { ($0.id.lowercased(), $0.displayName) })
171+
#else
172+
let visibleIndex: [String: String] = [:]
173+
#endif
170174
for pk in geoBlocked {
171175
if let name = visibleIndex[pk.lowercased()] {
172176
geoNames.append(name)

bitchat/Services/Transport.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,20 @@ protocol Transport: AnyObject {
4646
func sendBroadcastAnnounce()
4747
func sendDeliveryAck(for messageID: String, to peerID: String)
4848

49+
// QR verification (optional for transports)
50+
func sendVerifyChallenge(to peerID: String, noiseKeyHex: String, nonceA: Data)
51+
func sendVerifyResponse(to peerID: String, noiseKeyHex: String, nonceA: Data)
52+
4953
// Peer snapshots (for non-UI services)
5054
var peerSnapshotPublisher: AnyPublisher<[TransportPeerSnapshot], Never> { get }
5155
func currentPeerSnapshots() -> [TransportPeerSnapshot]
5256
}
5357

58+
extension Transport {
59+
func sendVerifyChallenge(to peerID: String, noiseKeyHex: String, nonceA: Data) {}
60+
func sendVerifyResponse(to peerID: String, noiseKeyHex: String, nonceA: Data) {}
61+
}
62+
5463
protocol TransportPeerEventsDelegate: AnyObject {
5564
@MainActor func didUpdatePeerSnapshots(_ peers: [TransportPeerSnapshot])
5665
}

0 commit comments

Comments
 (0)