mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 02:25:20 +00:00
Merge pull request #289 from permissionlesstech/implement-ed25519-signatures
Implement Ed25519 signatures and fix session management
This commit is contained in:
@@ -259,6 +259,19 @@ class NoiseSessionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func migrateSession(from oldPeerID: String, to newPeerID: String) {
|
||||||
|
_ = managerQueue.sync(flags: .barrier) {
|
||||||
|
// Check if we have a session for the old peer ID
|
||||||
|
if let session = sessions[oldPeerID] {
|
||||||
|
// Move the session to the new peer ID
|
||||||
|
sessions[newPeerID] = session
|
||||||
|
sessions.removeValue(forKey: oldPeerID)
|
||||||
|
|
||||||
|
SecurityLogger.log("Migrated Noise session from \(oldPeerID) to \(newPeerID)", category: SecurityLogger.noise, level: .info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getEstablishedSessions() -> [String: NoiseSession] {
|
func getEstablishedSessions() -> [String: NoiseSession] {
|
||||||
return managerQueue.sync {
|
return managerQueue.sync {
|
||||||
return sessions.filter { $0.value.isEstablished() }
|
return sessions.filter { $0.value.isEstablished() }
|
||||||
@@ -306,18 +319,11 @@ class NoiseSessionManager {
|
|||||||
var existingSession: NoiseSession? = nil
|
var existingSession: NoiseSession? = nil
|
||||||
|
|
||||||
if let existing = sessions[peerID] {
|
if let existing = sessions[peerID] {
|
||||||
// If we have an established session, we might need to help the other side complete theirs
|
// If we have an established session, reject new handshake attempts
|
||||||
if existing.isEstablished() {
|
if existing.isEstablished() {
|
||||||
// If this is a handshake initiation (32 bytes), the other side doesn't have a session
|
// Don't destroy our working session just because the other side is confused
|
||||||
// We should complete the handshake to help them establish their session
|
// They should detect the established session through successful message exchange
|
||||||
if message.count == 32 {
|
throw NoiseSessionError.alreadyEstablished
|
||||||
// Remove existing session and create new one
|
|
||||||
sessions.removeValue(forKey: peerID)
|
|
||||||
shouldCreateNew = true
|
|
||||||
} else {
|
|
||||||
// For other handshake messages, ignore if already established
|
|
||||||
throw NoiseSessionError.alreadyEstablished
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// If we're in the middle of a handshake and receive a new initiation,
|
// If we're in the middle of a handshake and receive a new initiation,
|
||||||
// reset and start fresh (the other side may have restarted)
|
// reset and start fresh (the other side may have restarted)
|
||||||
|
|||||||
@@ -347,16 +347,18 @@ struct ChannelMetadata: Codable {
|
|||||||
struct NoiseIdentityAnnouncement: Codable {
|
struct NoiseIdentityAnnouncement: Codable {
|
||||||
let peerID: String // Current ephemeral peer ID
|
let peerID: String // Current ephemeral peer ID
|
||||||
let publicKey: Data // Noise static public key
|
let publicKey: Data // Noise static public key
|
||||||
|
let signingPublicKey: Data // Ed25519 signing public key
|
||||||
let nickname: String // Current nickname
|
let nickname: String // Current nickname
|
||||||
let timestamp: Date // When this binding was created
|
let timestamp: Date // When this binding was created
|
||||||
let previousPeerID: String? // Previous peer ID (for smooth transition)
|
let previousPeerID: String? // Previous peer ID (for smooth transition)
|
||||||
let signature: Data // Signature proving ownership
|
let signature: Data // Signature proving ownership
|
||||||
|
|
||||||
init(peerID: String, publicKey: Data, nickname: String, previousPeerID: String? = nil, signature: Data) {
|
init(peerID: String, publicKey: Data, signingPublicKey: Data, nickname: String, timestamp: Date, previousPeerID: String? = nil, signature: Data) {
|
||||||
self.peerID = peerID
|
self.peerID = peerID
|
||||||
self.publicKey = publicKey
|
self.publicKey = publicKey
|
||||||
|
self.signingPublicKey = signingPublicKey
|
||||||
self.nickname = nickname
|
self.nickname = nickname
|
||||||
self.timestamp = Date()
|
self.timestamp = timestamp
|
||||||
self.previousPeerID = previousPeerID
|
self.previousPeerID = previousPeerID
|
||||||
self.signature = signature
|
self.signature = signature
|
||||||
}
|
}
|
||||||
@@ -366,7 +368,7 @@ struct NoiseIdentityAnnouncement: Codable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func decode(from data: Data) -> NoiseIdentityAnnouncement? {
|
static func decode(from data: Data) -> NoiseIdentityAnnouncement? {
|
||||||
try? JSONDecoder().decode(NoiseIdentityAnnouncement.self, from: data)
|
return try? JSONDecoder().decode(NoiseIdentityAnnouncement.self, from: data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,14 +377,22 @@ struct PeerIdentityBinding {
|
|||||||
let currentPeerID: String // Current ephemeral ID
|
let currentPeerID: String // Current ephemeral ID
|
||||||
let fingerprint: String // Permanent cryptographic identity
|
let fingerprint: String // Permanent cryptographic identity
|
||||||
let publicKey: Data // Noise static public key
|
let publicKey: Data // Noise static public key
|
||||||
|
let signingPublicKey: Data // Ed25519 signing public key
|
||||||
let nickname: String // Last known nickname
|
let nickname: String // Last known nickname
|
||||||
let bindingTimestamp: Date // When this binding was created
|
let bindingTimestamp: Date // When this binding was created
|
||||||
let signature: Data // Cryptographic proof of binding
|
let signature: Data // Cryptographic proof of binding
|
||||||
|
|
||||||
// Verify the binding signature
|
// Verify the binding signature
|
||||||
func verify() -> Bool {
|
func verify() -> Bool {
|
||||||
// TODO: Implement signature verification
|
let bindingData = currentPeerID.data(using: .utf8)! + publicKey +
|
||||||
return true
|
String(Int64(bindingTimestamp.timeIntervalSince1970 * 1000)).data(using: .utf8)!
|
||||||
|
|
||||||
|
do {
|
||||||
|
let signingKey = try Curve25519.Signing.PublicKey(rawRepresentation: signingPublicKey)
|
||||||
|
return signingKey.isValidSignature(signature, for: bindingData)
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -364,6 +364,9 @@ class BluetoothMeshService: NSObject {
|
|||||||
|
|
||||||
// Notify about the change if it's a rotation
|
// Notify about the change if it's a rotation
|
||||||
if let oldID = oldPeerID {
|
if let oldID = oldPeerID {
|
||||||
|
// Migrate Noise session to new peer ID
|
||||||
|
self.noiseService.migratePeerSession(from: oldID, to: newPeerID, fingerprint: fingerprint)
|
||||||
|
|
||||||
self.notifyPeerIDChange(oldPeerID: oldID, newPeerID: newPeerID, fingerprint: fingerprint)
|
self.notifyPeerIDChange(oldPeerID: oldID, newPeerID: newPeerID, fingerprint: fingerprint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2303,11 +2306,12 @@ class BluetoothMeshService: NSObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the signature (currently always returns true for compatibility)
|
// Verify the signature using the signing public key
|
||||||
let bindingData = announcement.peerID.data(using: .utf8)! + announcement.publicKey + announcement.timestamp.timeIntervalSince1970.data
|
let timestampData = String(Int64(announcement.timestamp.timeIntervalSince1970 * 1000)).data(using: .utf8)!
|
||||||
if !noiseService.verifySignature(announcement.signature, for: bindingData, publicKey: announcement.publicKey) {
|
let bindingData = announcement.peerID.data(using: .utf8)! + announcement.publicKey + timestampData
|
||||||
// Log but don't reject - signature verification is temporarily disabled
|
if !noiseService.verifySignature(announcement.signature, for: bindingData, publicKey: announcement.signingPublicKey) {
|
||||||
SecurityLogger.log("Signature verification skipped for \(senderID)", category: SecurityLogger.noise, level: .debug)
|
SecurityLogger.log("Signature verification failed for \(senderID)", category: SecurityLogger.noise, level: .warning)
|
||||||
|
return // Reject announcements with invalid signatures
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate fingerprint from public key
|
// Calculate fingerprint from public key
|
||||||
@@ -2319,6 +2323,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
currentPeerID: announcement.peerID,
|
currentPeerID: announcement.peerID,
|
||||||
fingerprint: fingerprint,
|
fingerprint: fingerprint,
|
||||||
publicKey: announcement.publicKey,
|
publicKey: announcement.publicKey,
|
||||||
|
signingPublicKey: announcement.signingPublicKey,
|
||||||
nickname: announcement.nickname,
|
nickname: announcement.nickname,
|
||||||
bindingTimestamp: announcement.timestamp,
|
bindingTimestamp: announcement.timestamp,
|
||||||
signature: announcement.signature
|
signature: announcement.signature
|
||||||
@@ -3363,6 +3368,14 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
|
|
||||||
SecurityLogger.log("Initiating Noise handshake with \(peerID)", category: SecurityLogger.noise, level: .info)
|
SecurityLogger.log("Initiating Noise handshake with \(peerID)", category: SecurityLogger.noise, level: .info)
|
||||||
|
|
||||||
|
// Check if we already have an established session
|
||||||
|
if noiseService.hasEstablishedSession(with: peerID) {
|
||||||
|
SecurityLogger.log("Already have established session with \(peerID)", category: SecurityLogger.noise, level: .debug)
|
||||||
|
// Clear any lingering handshake attempt time
|
||||||
|
handshakeAttemptTimes.removeValue(forKey: peerID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we've recently tried to handshake with this peer
|
// Check if we've recently tried to handshake with this peer
|
||||||
if let lastAttempt = handshakeAttemptTimes[peerID],
|
if let lastAttempt = handshakeAttemptTimes[peerID],
|
||||||
Date().timeIntervalSince(lastAttempt) < handshakeTimeout {
|
Date().timeIntervalSince(lastAttempt) < handshakeTimeout {
|
||||||
@@ -3800,23 +3813,27 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
lastIdentityAnnounceTimes["*broadcast*"] = now
|
lastIdentityAnnounceTimes["*broadcast*"] = now
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get our Noise static public key
|
// Get our Noise static public key and signing public key
|
||||||
let staticKey = noiseService.getStaticPublicKeyData()
|
let staticKey = noiseService.getStaticPublicKeyData()
|
||||||
|
let signingKey = noiseService.getSigningPublicKeyData()
|
||||||
|
|
||||||
// Get nickname from delegate
|
// Get nickname from delegate
|
||||||
let nickname = (delegate as? ChatViewModel)?.nickname ?? "Anonymous"
|
let nickname = (delegate as? ChatViewModel)?.nickname ?? "Anonymous"
|
||||||
|
|
||||||
// Create the binding data to sign
|
// Create the binding data to sign (peerID + publicKey + timestamp)
|
||||||
let bindingData = myPeerID.data(using: .utf8)! + staticKey + now.timeIntervalSince1970.data
|
let timestampData = String(Int64(now.timeIntervalSince1970 * 1000)).data(using: .utf8)!
|
||||||
|
let bindingData = myPeerID.data(using: .utf8)! + staticKey + timestampData
|
||||||
|
|
||||||
// Sign the binding with our private key
|
// Sign the binding with our Ed25519 signing key
|
||||||
let signature = noiseService.signData(bindingData) ?? Data()
|
let signature = noiseService.signData(bindingData) ?? Data()
|
||||||
|
|
||||||
// Create the identity announcement
|
// Create the identity announcement
|
||||||
let announcement = NoiseIdentityAnnouncement(
|
let announcement = NoiseIdentityAnnouncement(
|
||||||
peerID: myPeerID,
|
peerID: myPeerID,
|
||||||
publicKey: staticKey,
|
publicKey: staticKey,
|
||||||
|
signingPublicKey: signingKey,
|
||||||
nickname: nickname,
|
nickname: nickname,
|
||||||
|
timestamp: now,
|
||||||
previousPeerID: previousPeerID,
|
previousPeerID: previousPeerID,
|
||||||
signature: signature
|
signature: signature
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ class NoiseEncryptionService {
|
|||||||
private let staticIdentityKey: Curve25519.KeyAgreement.PrivateKey
|
private let staticIdentityKey: Curve25519.KeyAgreement.PrivateKey
|
||||||
public let staticIdentityPublicKey: Curve25519.KeyAgreement.PublicKey
|
public let staticIdentityPublicKey: Curve25519.KeyAgreement.PublicKey
|
||||||
|
|
||||||
|
// Ed25519 signing key (persistent across sessions)
|
||||||
|
private let signingKey: Curve25519.Signing.PrivateKey
|
||||||
|
public let signingPublicKey: Curve25519.Signing.PublicKey
|
||||||
|
|
||||||
// Session manager
|
// Session manager
|
||||||
private let sessionManager: NoiseSessionManager
|
private let sessionManager: NoiseSessionManager
|
||||||
|
|
||||||
@@ -63,6 +67,27 @@ class NoiseEncryptionService {
|
|||||||
self.staticIdentityKey = loadedKey
|
self.staticIdentityKey = loadedKey
|
||||||
self.staticIdentityPublicKey = staticIdentityKey.publicKey
|
self.staticIdentityPublicKey = staticIdentityKey.publicKey
|
||||||
|
|
||||||
|
// Load or create signing key pair
|
||||||
|
let loadedSigningKey: Curve25519.Signing.PrivateKey
|
||||||
|
|
||||||
|
// Try to load from keychain
|
||||||
|
if let signingData = KeychainManager.shared.getIdentityKey(forKey: "ed25519SigningKey"),
|
||||||
|
let key = try? Curve25519.Signing.PrivateKey(rawRepresentation: signingData) {
|
||||||
|
loadedSigningKey = key
|
||||||
|
}
|
||||||
|
// If no signing key exists, create new one
|
||||||
|
else {
|
||||||
|
loadedSigningKey = Curve25519.Signing.PrivateKey()
|
||||||
|
let keyData = loadedSigningKey.rawRepresentation
|
||||||
|
|
||||||
|
// Save to keychain
|
||||||
|
_ = KeychainManager.shared.saveIdentityKey(keyData, forKey: "ed25519SigningKey")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now assign the signing keys
|
||||||
|
self.signingKey = loadedSigningKey
|
||||||
|
self.signingPublicKey = signingKey.publicKey
|
||||||
|
|
||||||
// Initialize session manager
|
// Initialize session manager
|
||||||
self.sessionManager = NoiseSessionManager(localStaticKey: staticIdentityKey)
|
self.sessionManager = NoiseSessionManager(localStaticKey: staticIdentityKey)
|
||||||
|
|
||||||
@@ -82,6 +107,11 @@ class NoiseEncryptionService {
|
|||||||
return staticIdentityPublicKey.rawRepresentation
|
return staticIdentityPublicKey.rawRepresentation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get our signing public key for sharing
|
||||||
|
func getSigningPublicKeyData() -> Data {
|
||||||
|
return signingPublicKey.rawRepresentation
|
||||||
|
}
|
||||||
|
|
||||||
/// Get our identity fingerprint
|
/// Get our identity fingerprint
|
||||||
func getIdentityFingerprint() -> String {
|
func getIdentityFingerprint() -> String {
|
||||||
let hash = SHA256.hash(data: staticIdentityPublicKey.rawRepresentation)
|
let hash = SHA256.hash(data: staticIdentityPublicKey.rawRepresentation)
|
||||||
@@ -97,25 +127,31 @@ class NoiseEncryptionService {
|
|||||||
func clearPersistentIdentity() {
|
func clearPersistentIdentity() {
|
||||||
// Clear from keychain
|
// Clear from keychain
|
||||||
_ = KeychainManager.shared.deleteIdentityKey(forKey: "noiseStaticKey")
|
_ = KeychainManager.shared.deleteIdentityKey(forKey: "noiseStaticKey")
|
||||||
|
_ = KeychainManager.shared.deleteIdentityKey(forKey: "ed25519SigningKey")
|
||||||
// Stop rekey timer
|
// Stop rekey timer
|
||||||
stopRekeyTimer()
|
stopRekeyTimer()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sign data with our static identity key
|
/// Sign data with our Ed25519 signing key
|
||||||
func signData(_ data: Data) -> Data? {
|
func signData(_ data: Data) -> Data? {
|
||||||
// For now, use HMAC with the private key as a simple signature
|
do {
|
||||||
// This is not cryptographically ideal but works for identity binding
|
let signature = try signingKey.signature(for: data)
|
||||||
let key = SymmetricKey(data: staticIdentityKey.rawRepresentation)
|
return signature
|
||||||
let signature = HMAC<SHA256>.authenticationCode(for: data, using: key)
|
} catch {
|
||||||
return Data(signature)
|
SecurityLogger.logError(error, context: "Failed to sign data", category: SecurityLogger.noise)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verify signature with a peer's public key
|
/// Verify signature with a peer's Ed25519 public key
|
||||||
func verifySignature(_ signature: Data, for data: Data, publicKey: Data) -> Bool {
|
func verifySignature(_ signature: Data, for data: Data, publicKey: Data) -> Bool {
|
||||||
// For verification, we can't use the same HMAC approach since we don't have the private key
|
do {
|
||||||
// For now, we'll skip signature verification but maintain the protocol structure
|
let signingPublicKey = try Curve25519.Signing.PublicKey(rawRepresentation: publicKey)
|
||||||
// In production, this should use proper Ed25519 signatures
|
return signingPublicKey.isValidSignature(signature, for: data)
|
||||||
return true // Temporarily accept all signatures to fix the immediate issue
|
} catch {
|
||||||
|
SecurityLogger.logError(error, context: "Failed to verify signature", category: SecurityLogger.noise)
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Handshake Management
|
// MARK: - Handshake Management
|
||||||
@@ -243,6 +279,24 @@ class NoiseEncryptionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Migrate session when peer ID changes
|
||||||
|
func migratePeerSession(from oldPeerID: String, to newPeerID: String, fingerprint: String) {
|
||||||
|
// First update the fingerprint mappings
|
||||||
|
serviceQueue.sync(flags: .barrier) {
|
||||||
|
// Remove old mapping
|
||||||
|
if let oldFingerprint = peerFingerprints[oldPeerID], oldFingerprint == fingerprint {
|
||||||
|
peerFingerprints.removeValue(forKey: oldPeerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new mapping
|
||||||
|
peerFingerprints[newPeerID] = fingerprint
|
||||||
|
fingerprintToPeerID[fingerprint] = newPeerID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate the session in session manager
|
||||||
|
sessionManager.migrateSession(from: oldPeerID, to: newPeerID)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Private Helpers
|
// MARK: - Private Helpers
|
||||||
|
|
||||||
private func handleSessionEstablished(peerID: String, remoteStaticKey: Curve25519.KeyAgreement.PublicKey) {
|
private func handleSessionEstablished(peerID: String, remoteStaticKey: Curve25519.KeyAgreement.PublicKey) {
|
||||||
|
|||||||
Reference in New Issue
Block a user