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:
jack
2025-07-15 13:15:31 +02:00
parent 6d39222ea0
commit 3070a4d307
42 changed files with 10952 additions and 2233 deletions
+99 -50
View File
@@ -120,6 +120,14 @@ struct ContentView: View {
.sheet(isPresented: $showAppInfo) {
AppInfoView()
}
.sheet(isPresented: Binding(
get: { viewModel.showingFingerprintFor != nil },
set: { _ in viewModel.showingFingerprintFor = nil }
)) {
if let peerID = viewModel.showingFingerprintFor {
FingerprintView(viewModel: viewModel, peerID: peerID)
}
}
.alert("Set Channel Password", isPresented: $showPasswordInput) {
SecureField("Password", text: $passwordInput)
Button("Cancel", role: .cancel) {
@@ -188,16 +196,27 @@ struct ContentView: View {
Spacer()
HStack(spacing: 6) {
Image(systemName: "lock.fill")
.font(.system(size: 14))
.foregroundColor(Color.orange)
.accessibilityLabel("Private chat with \(privatePeerNick)")
Text("\(privatePeerNick)")
.font(.system(size: 16, weight: .medium, design: .monospaced))
.foregroundColor(Color.orange)
Button(action: {
viewModel.showFingerprint(for: privatePeerID)
}) {
HStack(spacing: 6) {
// Dynamic encryption status icon
let encryptionStatus = viewModel.getEncryptionStatus(for: privatePeerID)
Image(systemName: encryptionStatus.icon)
.font(.system(size: 14))
.foregroundColor(encryptionStatus == .noiseVerified ? Color.green :
encryptionStatus == .noiseSecured ? Color.orange :
Color.red)
.accessibilityLabel("Encryption status: \(encryptionStatus == .noiseVerified ? "verified" : encryptionStatus == .noiseSecured ? "secured" : "not encrypted")")
Text("\(privatePeerNick)")
.font(.system(size: 16, weight: .medium, design: .monospaced))
.foregroundColor(Color.orange)
}
.frame(maxWidth: .infinity)
.accessibilityLabel("Private chat with \(privatePeerNick)")
.accessibilityHint("Tap to view encryption fingerprint")
}
.frame(maxWidth: .infinity)
.buttonStyle(.plain)
Spacer()
@@ -236,16 +255,42 @@ struct ContentView: View {
sidebarDragOffset = 0
}
}) {
HStack(spacing: 6) {
HStack(spacing: 4) {
if viewModel.passwordProtectedChannels.contains(currentChannel) {
Image(systemName: "lock.fill")
.font(.system(size: 14))
.foregroundColor(Color.orange)
.accessibilityLabel("Password protected channel")
}
Text("channel: \(currentChannel)")
Text(currentChannel)
.font(.system(size: 16, weight: .medium, design: .monospaced))
.foregroundColor(viewModel.passwordProtectedChannels.contains(currentChannel) ? Color.orange : Color.blue)
// Verification status indicator after channel name
if viewModel.passwordProtectedChannels.contains(currentChannel),
let status = viewModel.channelVerificationStatus[currentChannel] {
switch status {
case .verifying:
ProgressView()
.scaleEffect(0.5)
.frame(width: 12, height: 12)
case .verified:
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 12))
.foregroundColor(Color.green)
case .failed:
Image(systemName: "xmark.circle.fill")
.font(.system(size: 12))
.foregroundColor(Color.red)
case .unverified:
Image(systemName: "questionmark.circle")
.font(.system(size: 12))
.foregroundColor(Color.gray)
.help("Password verification pending")
}
} else if viewModel.passwordProtectedChannels.contains(currentChannel) {
}
}
}
.buttonStyle(.plain)
@@ -264,7 +309,7 @@ struct ContentView: View {
}
// Save button - only for channel owner
if viewModel.channelCreators[currentChannel] == viewModel.meshService.myPeerID {
if viewModel.isChannelOwner(currentChannel) {
Button(action: {
viewModel.sendMessage("/save")
}) {
@@ -278,7 +323,7 @@ struct ContentView: View {
}
// Password button for channel creator only
if viewModel.channelCreators[currentChannel] == viewModel.meshService.myPeerID {
if viewModel.isChannelOwner(currentChannel) {
Button(action: {
// Toggle password protection
if viewModel.passwordProtectedChannels.contains(currentChannel) {
@@ -301,9 +346,9 @@ struct ContentView: View {
Button(action: {
showLeaveChannelAlert = true
}) {
Text("leave")
.font(.system(size: 12, design: .monospaced))
.foregroundColor(Color.red)
Image(systemName: "xmark.circle")
.font(.system(size: 16))
.foregroundColor(Color.red.opacity(0.8))
}
.buttonStyle(.plain)
.alert("leave channel?", isPresented: $showLeaveChannelAlert) {
@@ -416,11 +461,10 @@ struct ContentView: View {
let messages: [BitchatMessage] = {
if let privatePeer = viewModel.selectedPrivateChatPeer {
let msgs = viewModel.getPrivateChatMessages(for: privatePeer)
// Log what we're showing
// Removed debug logging
return msgs
} else if let currentChannel = viewModel.currentChannel {
return viewModel.getChannelMessages(currentChannel)
let msgs = viewModel.getChannelMessages(currentChannel)
return msgs
} else {
return viewModel.messages
}
@@ -466,7 +510,6 @@ struct ContentView: View {
} else {
// Check for plain URLs
let urls = message.content.extractURLs()
let _ = urls.isEmpty ? nil : print("DEBUG: Found \(urls.count) plain URLs in message")
ForEach(urls.prefix(3), id: \.url) { urlInfo in
LinkPreviewView(url: urlInfo.url, title: nil)
.padding(.top, 4)
@@ -829,7 +872,7 @@ struct ContentView: View {
private func channelControls(for channel: String) -> some View {
HStack(spacing: 4) {
// Password button for channel creator only
if viewModel.channelCreators[channel] == viewModel.meshService.myPeerID {
if viewModel.isChannelOwner(channel) {
Button(action: {
// Toggle password protection
if viewModel.passwordProtectedChannels.contains(channel) {
@@ -861,15 +904,9 @@ struct ContentView: View {
Button(action: {
showLeaveChannelAlert = true
}) {
Text("leave channel")
.font(.system(size: 10, design: .monospaced))
.foregroundColor(secondaryTextColor)
.padding(.horizontal, 8)
.padding(.vertical, 2)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(secondaryTextColor.opacity(0.5), lineWidth: 1)
)
Image(systemName: "xmark.circle.fill")
.font(.system(size: 14))
.foregroundColor(Color.red.opacity(0.6))
}
.buttonStyle(.plain)
.alert("leave channel", isPresented: $showLeaveChannelAlert) {
@@ -982,13 +1019,13 @@ struct ContentView: View {
return isFav1 // Favorites come first
}
let name1 = peerNicknames[peer1] ?? "person-\(peer1.prefix(4))"
let name2 = peerNicknames[peer2] ?? "person-\(peer2.prefix(4))"
let name1 = peerNicknames[peer1] ?? "anon\(peer1.prefix(4))"
let name2 = peerNicknames[peer2] ?? "anon\(peer2.prefix(4))"
return name1 < name2
}
ForEach(sortedPeers, id: \.self) { peerID in
let displayName = peerID == myPeerID ? viewModel.nickname : (peerNicknames[peerID] ?? "person-\(peerID.prefix(4))")
let displayName = peerID == myPeerID ? viewModel.nickname : (peerNicknames[peerID] ?? "anon\(peerID.prefix(4))")
let rssi = peerRSSI[peerID]?.intValue ?? -100
let isFavorite = viewModel.isFavorite(peerID: peerID)
let isMe = peerID == myPeerID
@@ -1012,17 +1049,16 @@ struct ContentView: View {
.accessibilityLabel("Signal strength: \(rssi > -60 ? "excellent" : rssi > -70 ? "good" : rssi > -80 ? "fair" : "poor")")
}
// Favorite star (not for self)
// Encryption status icon (between connection dot and name)
if !isMe {
Button(action: {
viewModel.toggleFavorite(peerID: peerID)
}) {
Image(systemName: isFavorite ? "star.fill" : "star")
.font(.system(size: 12))
.foregroundColor(isFavorite ? Color.yellow : secondaryTextColor)
}
.buttonStyle(.plain)
.accessibilityLabel(isFavorite ? "Remove \(displayName) from favorites" : "Add \(displayName) to favorites")
let encryptionStatus = viewModel.getEncryptionStatus(for: peerID)
Image(systemName: encryptionStatus.icon)
.font(.system(size: 10))
.foregroundColor(encryptionStatus == .noiseVerified ? Color.green :
encryptionStatus == .noiseSecured ? textColor :
encryptionStatus == .noiseHandshaking ? Color.orange :
Color.red)
.accessibilityLabel("Encryption: \(encryptionStatus == .noiseVerified ? "verified" : encryptionStatus == .noiseSecured ? "secured" : encryptionStatus == .noiseHandshaking ? "establishing" : "none")")
}
// Peer name
@@ -1044,16 +1080,29 @@ struct ContentView: View {
}
}
}) {
HStack {
Text(displayName)
.font(.system(size: 14, design: .monospaced))
.foregroundColor(peerNicknames[peerID] != nil ? textColor : secondaryTextColor)
Spacer()
}
Text(displayName)
.font(.system(size: 14, design: .monospaced))
.foregroundColor(peerNicknames[peerID] != nil ? textColor : secondaryTextColor)
}
.buttonStyle(.plain)
.disabled(peerNicknames[peerID] == nil)
.onTapGesture(count: 2) {
// Show fingerprint on double tap
viewModel.showFingerprint(for: peerID)
}
Spacer()
// Favorite star
Button(action: {
viewModel.toggleFavorite(peerID: peerID)
}) {
Image(systemName: isFavorite ? "star.fill" : "star")
.font(.system(size: 12))
.foregroundColor(isFavorite ? Color.yellow : secondaryTextColor)
}
.buttonStyle(.plain)
.accessibilityLabel(isFavorite ? "Remove \(displayName) from favorites" : "Add \(displayName) to favorites")
}
}
.padding(.horizontal)