diff --git a/app/src/main/java/com/bitchat/android/crypto/EncryptionService.kt b/app/src/main/java/com/bitchat/android/crypto/EncryptionService.kt index c45885a3..449d705f 100644 --- a/app/src/main/java/com/bitchat/android/crypto/EncryptionService.kt +++ b/app/src/main/java/com/bitchat/android/crypto/EncryptionService.kt @@ -3,6 +3,12 @@ package com.bitchat.android.crypto import android.content.Context import android.util.Log import com.bitchat.android.noise.NoiseEncryptionService +import org.bouncycastle.crypto.AsymmetricCipherKeyPair +import org.bouncycastle.crypto.generators.Ed25519KeyPairGenerator +import org.bouncycastle.crypto.params.Ed25519KeyGenerationParameters +import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters +import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters +import org.bouncycastle.crypto.signers.Ed25519Signer import java.security.SecureRandom import java.util.concurrent.ConcurrentHashMap @@ -17,6 +23,7 @@ class EncryptionService(private val context: Context) { companion object { private const val TAG = "EncryptionService" + private const val ED25519_PRIVATE_KEY_PREF = "ed25519_signing_private_key" } // Core Noise encryption service @@ -25,12 +32,23 @@ class EncryptionService(private val context: Context) { // Session tracking for established connections private val establishedSessions = ConcurrentHashMap() // peerID -> fingerprint + // Ed25519 signing keys (separate from Noise static keys) + private val ed25519PrivateKey: Ed25519PrivateKeyParameters + private val ed25519PublicKey: Ed25519PublicKeyParameters + // Callbacks for UI state updates var onSessionEstablished: ((String) -> Unit)? = null // peerID var onSessionLost: ((String) -> Unit)? = null // peerID var onHandshakeRequired: ((String) -> Unit)? = null // peerID init { + // Initialize or load Ed25519 signing keys + val keyPair = loadOrCreateEd25519KeyPair() + ed25519PrivateKey = keyPair.private as Ed25519PrivateKeyParameters + ed25519PublicKey = keyPair.public as Ed25519PublicKeyParameters + + Log.d(TAG, "βœ… Ed25519 signing keys initialized") + // Set up NoiseEncryptionService callbacks noiseService.onPeerAuthenticated = { peerID, fingerprint -> Log.d(TAG, "βœ… Noise session established with $peerID, fingerprint: ${fingerprint.take(16)}...") @@ -63,24 +81,26 @@ class EncryptionService(private val context: Context) { /** * Get our signing public key for Ed25519 signatures (for identity announcements) - * Note: In the current implementation, this returns the same as static key - * In a full implementation, this would be a separate Ed25519 key */ fun getSigningPublicKey(): ByteArray? { - // For now, return the static public key as placeholder - // In a full implementation, this would be a separate Ed25519 signing key - return noiseService.getStaticPublicKeyData() + return ed25519PublicKey.encoded } /** - * Sign data using our signing key (for identity announcements) - * Note: In the current simplified implementation, this returns empty signature - * In a full implementation, this would use Ed25519 signing + * Sign data using our Ed25519 signing key (for identity announcements) */ fun signData(data: ByteArray): ByteArray? { - // For now, return empty signature as placeholder - // In a full implementation, this would use Ed25519 to sign the data - return ByteArray(64) // Ed25519 signature length placeholder + return try { + val signer = Ed25519Signer() + signer.init(true, ed25519PrivateKey) + signer.update(data, 0, data.size) + val signature = signer.generateSignature() + Log.d(TAG, "βœ… Generated Ed25519 signature (${signature.size} bytes)") + signature + } catch (e: Exception) { + Log.e(TAG, "❌ Failed to sign data with Ed25519: ${e.message}") + null + } } /** @@ -112,6 +132,15 @@ class EncryptionService(private val context: Context) { fun clearPersistentIdentity() { noiseService.clearPersistentIdentity() establishedSessions.clear() + + // Clear Ed25519 signing key from preferences + try { + val prefs = context.getSharedPreferences("bitchat_crypto", Context.MODE_PRIVATE) + prefs.edit().remove(ED25519_PRIVATE_KEY_PREF).apply() + Log.d(TAG, "πŸ—‘οΈ Cleared Ed25519 signing keys from preferences") + } catch (e: Exception) { + Log.e(TAG, "❌ Failed to clear Ed25519 keys: ${e.message}") + } } /** @@ -321,4 +350,67 @@ class EncryptionService(private val context: Context) { noiseService.shutdown() Log.d(TAG, "πŸ”Œ EncryptionService shut down") } + + // MARK: - Ed25519 Signature Verification + + /** + * Verify Ed25519 signature against data using a public key + */ + fun verifyEd25519Signature(signature: ByteArray, data: ByteArray, publicKeyBytes: ByteArray): Boolean { + return try { + val publicKey = Ed25519PublicKeyParameters(publicKeyBytes, 0) + val verifier = Ed25519Signer() + verifier.init(false, publicKey) + verifier.update(data, 0, data.size) + val isValid = verifier.verifySignature(signature) + Log.d(TAG, "βœ… Ed25519 signature verification: $isValid") + isValid + } catch (e: Exception) { + Log.e(TAG, "❌ Failed to verify Ed25519 signature: ${e.message}") + false + } + } + + // MARK: - Private Key Management + + /** + * Load existing Ed25519 key pair from preferences or create a new one + */ + private fun loadOrCreateEd25519KeyPair(): AsymmetricCipherKeyPair { + try { + val prefs = context.getSharedPreferences("bitchat_crypto", Context.MODE_PRIVATE) + val storedKey = prefs.getString(ED25519_PRIVATE_KEY_PREF, null) + + if (storedKey != null) { + // Load existing key + val privateKeyBytes = android.util.Base64.decode(storedKey, android.util.Base64.DEFAULT) + val privateKey = Ed25519PrivateKeyParameters(privateKeyBytes, 0) + val publicKey = privateKey.generatePublicKey() + Log.d(TAG, "βœ… Loaded existing Ed25519 signing key pair") + return AsymmetricCipherKeyPair(publicKey, privateKey) + } + } catch (e: Exception) { + Log.w(TAG, "⚠️ Failed to load existing Ed25519 key, creating new one: ${e.message}") + } + + // Create new key pair + val keyGen = Ed25519KeyPairGenerator() + keyGen.init(Ed25519KeyGenerationParameters(SecureRandom())) + val keyPair = keyGen.generateKeyPair() + + // Store private key in preferences + try { + val privateKey = keyPair.private as Ed25519PrivateKeyParameters + val privateKeyBytes = privateKey.encoded + val encodedKey = android.util.Base64.encodeToString(privateKeyBytes, android.util.Base64.DEFAULT) + + val prefs = context.getSharedPreferences("bitchat_crypto", Context.MODE_PRIVATE) + prefs.edit().putString(ED25519_PRIVATE_KEY_PREF, encodedKey).apply() + Log.d(TAG, "βœ… Created and stored new Ed25519 signing key pair") + } catch (e: Exception) { + Log.e(TAG, "❌ Failed to store Ed25519 private key: ${e.message}") + } + + return keyPair + } } diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt index 34d2bcef..e34d9b59 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt @@ -41,13 +41,23 @@ class BluetoothConnectionManager( // Delegate for component managers to call back to main manager private val componentDelegate = object : BluetoothConnectionManagerDelegate { override fun onPacketReceived(packet: BitchatPacket, peerID: String, device: BluetoothDevice?) { + Log.d(TAG, "onPacketReceived: Packet received from ${device?.address} ($peerID)") device?.let { bluetoothDevice -> + // if connection does not have a peerID yet, we assume that the first package + // we receive from that connection is from the peer + if (!connectionTracker.addressPeerMap.containsKey(device.address)) { + Log.d(TAG, "First packet received from new device: ${bluetoothDevice.address}, assuming peerID: $peerID") + connectionTracker.addressPeerMap[device.address] = peerID + } // Get current RSSI for this device and update if available val currentRSSI = connectionTracker.getBestRSSI(bluetoothDevice.address) if (currentRSSI != null) { delegate?.onRSSIUpdated(bluetoothDevice.address, currentRSSI) } } + + if (peerID == myPeerID) return // Ignore messages from self + delegate?.onPacketReceived(packet, peerID, device) } diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt index e4406f1f..92d91ff5 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt @@ -88,7 +88,7 @@ class BluetoothConnectionTracker( * Add a device connection */ fun addDeviceConnection(deviceAddress: String, deviceConn: DeviceConnection) { - Log.d(TAG, "Tracker: Adding device connection for $deviceAddress") + Log.d(TAG, "Tracker: Adding device connection for $deviceAddress (isClient: ${deviceConn.isClient}") connectedDevices[deviceAddress] = deviceConn pendingConnections.remove(deviceAddress) } diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt index d6db6ed5..99c23f0c 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt @@ -133,6 +133,13 @@ class BluetoothGattServerManager( isClient = false ) connectionTracker.addDeviceConnection(device.address, deviceConn) + + connectionScope.launch { + delay(1000) + if (isActive) { // Check if still active + delegate?.onDeviceConnected(device) + } + } } BluetoothProfile.STATE_DISCONNECTED -> { Log.i(TAG, "Server: Device disconnected ${device.address}") @@ -204,9 +211,9 @@ class BluetoothGattServerManager( } if (BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE.contentEquals(value)) { - Log.d(TAG, "Device ${device.address} subscribed to notifications") connectionTracker.addSubscribedDevice(device) - + + Log.d(TAG, "Server: Connection setup complete for ${device.address}") connectionScope.launch { delay(100) if (isActive) { // Check if still active @@ -272,7 +279,9 @@ class BluetoothGattServerManager( */ @Suppress("DEPRECATION") private fun startAdvertising() { - if (!permissionManager.hasBluetoothPermissions() || bleAdvertiser == null || !isActive) return + if (!permissionManager.hasBluetoothPermissions() || bleAdvertiser == null || !isActive || bluetoothAdapter == null || !bluetoothAdapter.isMultipleAdvertisementSupported()) { + throw Exception("Missing Bluetooth permissions or BLE advertiser not available") + } val settings = powerManager.getAdvertiseSettings() diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt index 22c53443..8c848dc8 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -63,11 +63,8 @@ class BluetoothMeshService(private val context: Context) { init { setupDelegates() - - // Wire up PacketProcessor reference for recursive handling in MessageHandler messageHandler.packetProcessor = packetProcessor - sendPeriodicBroadcastAnnounce() - // startPeriodicDebugLogging() + startPeriodicDebugLogging() } /** @@ -98,6 +95,7 @@ class BluetoothMeshService(private val context: Context) { try { delay(30000) // 30 seconds sendBroadcastAnnounce() + broadcastNoiseIdentityAnnouncement() } catch (e: Exception) { Log.e(TAG, "Error in periodic broadcast announce: ${e.message}") } @@ -126,11 +124,7 @@ class BluetoothMeshService(private val context: Context) { // SecurityManager delegate for key exchange notifications securityManager.delegate = object : SecurityManagerDelegate { - override fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray, receivedAddress: String?) { - receivedAddress?.let { address -> - connectionManager.addressPeerMap[address] = peerID - } - + override fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray) { // Send announcement and cached messages after key exchange serviceScope.launch { delay(100) @@ -150,7 +144,7 @@ class BluetoothMeshService(private val context: Context) { recipientID = hexStringToByteArray(peerID), timestamp = System.currentTimeMillis().toULong(), payload = response, - ttl = 1u + ttl = MAX_TTL ) connectionManager.broadcastPacket(RoutedPacket(responsePacket)) Log.d(TAG, "Sent Noise handshake response to $peerID (${response.size} bytes)") @@ -225,6 +219,10 @@ class BluetoothMeshService(private val context: Context) { return securityManager.decryptFromPeer(encryptedData, senderPeerID) } + override fun verifyEd25519Signature(signature: ByteArray, data: ByteArray, publicKey: ByteArray): Boolean { + return encryptionService.verifyEd25519Signature(signature, data, publicKey) + } + // Noise protocol operations override fun hasNoiseSession(peerID: String): Boolean { return encryptionService.hasEstablishedSession(peerID) @@ -243,7 +241,7 @@ class BluetoothMeshService(private val context: Context) { recipientID = hexStringToByteArray(peerID), timestamp = System.currentTimeMillis().toULong(), payload = handshakeData, - ttl = 1u + ttl = MAX_TTL ) connectionManager.broadcastPacket(RoutedPacket(packet)) @@ -387,13 +385,13 @@ class BluetoothMeshService(private val context: Context) { override fun onDeviceConnected(device: android.bluetooth.BluetoothDevice) { // Send initial announcements after services are ready serviceScope.launch { - delay(100) + delay(200) sendBroadcastAnnounce() } // Send key exchange to newly connected device serviceScope.launch { - delay(100) // Ensure connection is stable - sendKeyExchangeToDevice() + delay(400) // Ensure connection is stable + broadcastNoiseIdentityAnnouncement() } } @@ -685,7 +683,7 @@ class BluetoothMeshService(private val context: Context) { val announcePacket = BitchatPacket( type = MessageType.ANNOUNCE.value, - ttl = 3u, + ttl = MAX_TTL, senderID = myPeerID, payload = nickname.toByteArray() ) @@ -703,7 +701,7 @@ class BluetoothMeshService(private val context: Context) { val nickname = delegate?.getNickname() ?: myPeerID val packet = BitchatPacket( type = MessageType.ANNOUNCE.value, - ttl = 3u, + ttl = MAX_TTL, senderID = myPeerID, payload = nickname.toByteArray() ) @@ -715,7 +713,7 @@ class BluetoothMeshService(private val context: Context) { /** * Send key exchange to newly connected device */ - fun sendKeyExchangeToDevice() { + fun broadcastNoiseIdentityAnnouncement() { serviceScope.launch { try { val nickname = delegate?.getNickname() ?: myPeerID @@ -726,13 +724,11 @@ class BluetoothMeshService(private val context: Context) { val announcementData = announcement.toBinaryData() val packet = BitchatPacket( - version = 1u, type = MessageType.NOISE_IDENTITY_ANNOUNCE.value, - senderID = hexStringToByteArray(myPeerID), - recipientID = SpecialRecipients.BROADCAST, - timestamp = System.currentTimeMillis().toULong(), + ttl = MAX_TTL, + senderID = myPeerID, payload = announcementData, - ttl = 1u + ) connectionManager.broadcastPacket(RoutedPacket(packet)) @@ -838,7 +834,7 @@ class BluetoothMeshService(private val context: Context) { val nickname = delegate?.getNickname() ?: myPeerID val packet = BitchatPacket( type = MessageType.LEAVE.value, - ttl = 1u, + ttl = MAX_TTL, senderID = myPeerID, payload = nickname.toByteArray() ) @@ -984,6 +980,40 @@ class BluetoothMeshService(private val context: Context) { return result } + + // MARK: - Panic Mode Support + + /** + * Clear all internal mesh service data (for panic mode) + */ + fun clearAllInternalData() { + Log.w(TAG, "🚨 Clearing all mesh service internal data") + try { + // Clear all managers + fragmentManager.clearAllFragments() + storeForwardManager.clearAllCache() + securityManager.clearAllData() + peerManager.clearAllPeers() + peerManager.clearAllFingerprints() + Log.d(TAG, "βœ… Cleared all mesh service internal data") + } catch (e: Exception) { + Log.e(TAG, "❌ Error clearing mesh service internal data: ${e.message}") + } + } + + /** + * Clear all encryption and cryptographic data (for panic mode) + */ + fun clearAllEncryptionData() { + Log.w(TAG, "🚨 Clearing all encryption data") + try { + // Clear encryption service persistent identity (includes Ed25519 signing keys) + encryptionService.clearPersistentIdentity() + Log.d(TAG, "βœ… Cleared all encryption data") + } catch (e: Exception) { + Log.e(TAG, "❌ Error clearing encryption data: ${e.message}") + } + } } /** diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt index ab7e4660..202c64ee 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt @@ -20,8 +20,19 @@ import kotlinx.coroutines.channels.actor /** * Handles packet broadcasting to connected devices using actor pattern for serialization * - * SERIALIZATION FIX: Uses Kotlin coroutine actor to serialize all packet broadcasting - * This prevents race conditions when multiple threads try to broadcast simultaneously + * In Bluetooth Low Energy (BLE): + * + * Peripheral (server): + * Advertises. + * Accepts connections. + * Hosts a GATT server. + * Remote devices read/write/subscribe to characteristics. + * + * Central (client): + * Scans. + * Initiates connections. + * Hosts a GATT client. + * Reads/writes to the peripheral’s characteristics. */ class BluetoothPacketBroadcaster( private val connectionScope: CoroutineScope, @@ -52,9 +63,7 @@ class BluetoothPacketBroadcaster( Log.d(TAG, "🎭 Created packet broadcaster actor") try { for (request in channel) { - Log.d(TAG, "Processing broadcast for packet type ${request.routed.packet.type} (serialized)") broadcastSinglePacketInternal(request.routed, request.gattServer, request.characteristic) - Log.d(TAG, "Completed broadcast for packet type ${request.routed.packet.type}") } } finally { Log.d(TAG, "🎭 Packet broadcaster actor terminated") @@ -158,25 +167,25 @@ class BluetoothPacketBroadcaster( // Send to server connections (devices connected to our GATT server) subscribedDevices.forEach { device -> if (device.address == routed.relayAddress) { - Log.d(TAG, "Skipping broadcast back to relayer: ${device.address}") + Log.d(TAG, "Skipping broadcast to client back to relayer: ${device.address}") return@forEach } if (connectionTracker.addressPeerMap[device.address] == senderID) { - Log.d(TAG, "Skipping broadcast back to sender: ${device.address}") + Log.d(TAG, "Skipping broadcast to client back to sender: ${device.address}") return@forEach } notifyDevice(device, data, gattServer, characteristic) } - // Send to client connections + // Send to client connections (GATT servers we are connected to) connectedDevices.values.forEach { deviceConn -> if (deviceConn.isClient && deviceConn.gatt != null && deviceConn.characteristic != null) { if (deviceConn.device.address == routed.relayAddress) { - Log.d(TAG, "Skipping broadcast back to relayer: ${deviceConn.device.address}") + Log.d(TAG, "Skipping broadcast to server back to relayer: ${deviceConn.device.address}") return@forEach } if (connectionTracker.addressPeerMap[deviceConn.device.address] == senderID) { - Log.d(TAG, "Skipping broadcast back to sender: ${deviceConn.device.address}") + Log.d(TAG, "Skipping roadcast to server back to sender: ${deviceConn.device.address}") return@forEach } writeToDeviceConn(deviceConn, data) @@ -185,7 +194,7 @@ class BluetoothPacketBroadcaster( } /** - * Send data to a single device (server side) + * Send data to a single device (server->client) */ private fun notifyDevice( device: BluetoothDevice, @@ -211,7 +220,7 @@ class BluetoothPacketBroadcaster( } /** - * Send data to a single device (client side) + * Send data to a single device (client->server) */ private fun writeToDeviceConn( deviceConn: BluetoothConnectionTracker.DeviceConnection, diff --git a/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt b/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt index 87e266cc..6e4e6ff6 100644 --- a/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt +++ b/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt @@ -120,13 +120,27 @@ class MessageHandler(private val myPeerID: String) { Log.d(TAG, "Parsed identity announcement: peerID=${announcement.peerID}, " + "nickname=${announcement.nickname}, fingerprint=${announcement.fingerprint?.take(16)}...") - // Verify the announcement signature (basic validation) - // In a full implementation, this would use cryptographic verification + // Verify the announcement signature using Ed25519 (iOS compatibility) if (announcement.signature.isEmpty()) { - Log.w(TAG, "Identity announcement from $peerID has no signature") + Log.w(TAG, "❌ Identity announcement from $peerID has no signature - rejecting") return } + // Verify signature using the same format as iOS + val timestampMs = announcement.timestamp.time + val bindingData = announcement.peerID.toByteArray(Charsets.UTF_8) + + announcement.publicKey + + timestampMs.toString().toByteArray(Charsets.UTF_8) + + val isSignatureValid = delegate?.verifyEd25519Signature(announcement.signature, bindingData, announcement.signingPublicKey) ?: false + + if (!isSignatureValid) { + Log.w(TAG, "❌ Signature verification failed for identity announcement from $peerID - rejecting") + return + } + + Log.d(TAG, "βœ… Signature verification successful for identity announcement from $peerID") + // Update peer binding in the delegate (ChatViewModel/BluetoothMeshService) delegate?.updatePeerIDBinding( newPeerID = announcement.peerID, @@ -377,6 +391,7 @@ interface MessageHandlerDelegate { fun verifySignature(packet: BitchatPacket, peerID: String): Boolean fun encryptForPeer(data: ByteArray, recipientPeerID: String): ByteArray? fun decryptFromPeer(encryptedData: ByteArray, senderPeerID: String): ByteArray? + fun verifyEd25519Signature(signature: ByteArray, data: ByteArray, publicKey: ByteArray): Boolean // Noise protocol operations fun hasNoiseSession(peerID: String): Boolean diff --git a/app/src/main/java/com/bitchat/android/mesh/PacketProcessor.kt b/app/src/main/java/com/bitchat/android/mesh/PacketProcessor.kt index 8cadeddd..63e647b8 100644 --- a/app/src/main/java/com/bitchat/android/mesh/PacketProcessor.kt +++ b/app/src/main/java/com/bitchat/android/mesh/PacketProcessor.kt @@ -70,7 +70,15 @@ class PacketProcessor(private val myPeerID: String) { * SURGICAL FIX: Route to per-peer actor for serialized processing */ fun processPacket(routed: RoutedPacket) { - val peerID = routed.peerID ?: "unknown" + Log.d(TAG, "processPacket ${routed.packet.type}") + val peerID = routed.peerID + + if (peerID == null) { + Log.w(TAG, "Received packet with no peer ID, skipping") + return + } + + // Get or create actor for this peer val actor = actors.getOrPut(peerID) { getOrCreateActorForPeer(peerID) } diff --git a/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt b/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt index f66c38f6..0a5f9a99 100644 --- a/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt @@ -135,7 +135,7 @@ class SecurityManager(private val encryptionService: EncryptionService, private // Check if session is now established (handshake complete) if (encryptionService.hasEstablishedSession(peerID)) { Log.d(TAG, "βœ… Noise handshake completed with $peerID") - delegate?.onKeyExchangeCompleted(peerID, packet.payload, routed.relayAddress) + delegate?.onKeyExchangeCompleted(peerID, packet.payload) } return true @@ -145,48 +145,7 @@ class SecurityManager(private val encryptionService: EncryptionService, private return false } } - -// /** -// * Handle key exchange packet -// */ -// suspend fun handleKeyExchange(routed: RoutedPacket): Boolean { -// val packet = routed.packet -// val peerID = routed.peerID ?: "unknown" -// -// if (peerID == myPeerID) return false -// -// if (packet.payload.isEmpty()) { -// Log.w(TAG, "Key exchange packet has empty payload") -// return false -// } -// -// // Prevent duplicate key exchange processing -// val exchangeKey = "$peerID-${packet.payload.sliceArray(0 until minOf(16, packet.payload.size)).contentHashCode()}" -// -// if (processedKeyExchanges.contains(exchangeKey)) { -// Log.d(TAG, "Already processed key exchange: $exchangeKey") -// return false -// } -// -// processedKeyExchanges.add(exchangeKey) -// -// try { -// // Process the key exchange -// encryptionService.addPeerPublicKey(peerID, packet.payload) -// -// Log.d(TAG, "Successfully processed key exchange from $peerID") -// -// // Notify delegate -// delegate?.onKeyExchangeCompleted(peerID, packet.payload, routed.relayAddress) -// -// return true -// -// } catch (e: Exception) { -// Log.e(TAG, "Failed to process key exchange from $peerID: ${e.message}") -// return false -// } -// } - + /** * Verify packet signature */ @@ -377,6 +336,6 @@ class SecurityManager(private val encryptionService: EncryptionService, private * Delegate interface for security manager callbacks */ interface SecurityManagerDelegate { - fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray, receivedAddress: String?) + fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray) fun sendHandshakeResponse(peerID: String, response: ByteArray) } diff --git a/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt b/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt index 21c872f0..2a657bc5 100644 --- a/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt +++ b/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt @@ -57,7 +57,7 @@ class NoiseEncryptionService(private val context: Context) { if (loadedKeyPair != null) { staticIdentityPrivateKey = loadedKeyPair.first staticIdentityPublicKey = loadedKeyPair.second - Log.d(TAG, "Loaded existing static identity key") + Log.d(TAG, "Loaded existing static identity key: ${calculateFingerprint(staticIdentityPublicKey)}") } else { // Generate new identity key pair val keyPair = generateKeyPair() diff --git a/app/src/main/java/com/bitchat/android/noise/NoiseSessionManager.kt b/app/src/main/java/com/bitchat/android/noise/NoiseSessionManager.kt index 27f1847a..a7ba2a36 100644 --- a/app/src/main/java/com/bitchat/android/noise/NoiseSessionManager.kt +++ b/app/src/main/java/com/bitchat/android/noise/NoiseSessionManager.kt @@ -36,7 +36,7 @@ class NoiseSessionManager( */ fun getSession(peerID: String): NoiseSession? { val session = sessions[peerID] - Log.d(TAG, "getSession($peerID): ${if (session?.isEstablished() == true) "ESTABLISHED" else "NOT_FOUND"}") + // Log.d(TAG, "getSession($peerID): ${if (session?.isEstablished() == true) "ESTABLISHED" else "NOT_FOUND"}") return session } diff --git a/app/src/main/java/com/bitchat/android/protocol/CompressionUtil.kt b/app/src/main/java/com/bitchat/android/protocol/CompressionUtil.kt index 6e3ea80a..1fe02bc0 100644 --- a/app/src/main/java/com/bitchat/android/protocol/CompressionUtil.kt +++ b/app/src/main/java/com/bitchat/android/protocol/CompressionUtil.kt @@ -15,6 +15,8 @@ object CompressionUtil { * Helper to check if compression is worth it - exact same logic as iOS */ fun shouldCompress(data: ByteArray): Boolean { + // TODO: COMPRESSION DOESN'T WORK WITH IOS YET + return false // Don't compress if: // 1. Data is too small // 2. Data appears to be already compressed (high entropy) diff --git a/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt b/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt index 21f7e5ed..a5f90f84 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt @@ -303,18 +303,19 @@ private fun PrivateChatHeader( modifier = Modifier.align(Alignment.Center) ) { - // Reactive Noise session status icon - NoiseSessionIcon( - sessionState = sessionState, - modifier = Modifier.size(14.dp) - ) - - Spacer(modifier = Modifier.width(4.dp)) Text( text = peerNickname, style = MaterialTheme.typography.titleMedium, color = Color(0xFFFF9500) // Orange ) + + Spacer(modifier = Modifier.width(4.dp)) + + NoiseSessionIcon( + sessionState = sessionState, + modifier = Modifier.size(14.dp) + ) + } // Favorite button - positioned on the right diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt index 874694da..ea9f3b5a 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -25,6 +25,10 @@ class ChatViewModel( val meshService: BluetoothMeshService ) : AndroidViewModel(application), BluetoothMeshDelegate { + companion object { + private const val TAG = "ChatViewModel" + } + // State management private val state = ChatState() @@ -37,7 +41,7 @@ class ChatViewModel( private val noiseSessionDelegate = object : NoiseSessionDelegate { override fun hasEstablishedSession(peerID: String): Boolean = meshService.hasEstablishedSession(peerID) override fun initiateHandshake(peerID: String) = meshService.initiateNoiseHandshake(peerID) - override fun sendIdentityAnnouncement() = meshService.sendKeyExchangeToDevice() + override fun broadcastNoiseIdentityAnnouncement() = meshService.broadcastNoiseIdentityAnnouncement() override fun sendHandshakeRequest(targetPeerID: String, pendingCount: UByte) = meshService.sendHandshakeRequest(targetPeerID, pendingCount) override fun getMyPeerID(): String = meshService.myPeerID } @@ -394,21 +398,71 @@ class ChatViewModel( // MARK: - Emergency Clear fun panicClearAllData() { - // Clear all managers + Log.w(TAG, "🚨 PANIC MODE ACTIVATED - Clearing all sensitive data") + + // Clear all UI managers messageManager.clearAllMessages() channelManager.clearAllChannels() privateChatManager.clearAllPrivateChats() dataManager.clearAllData() + // Clear all mesh service data + clearAllMeshServiceData() + + // Clear all cryptographic data + clearAllCryptographicData() + + // Clear all notifications + notificationManager.clearAllNotifications() + // Reset nickname val newNickname = "anon${Random.nextInt(1000, 9999)}" state.setNickname(newNickname) dataManager.saveNickname(newNickname) + Log.w(TAG, "🚨 PANIC MODE COMPLETED - All sensitive data cleared") + // Note: Mesh service restart is now handled by MainActivity // This method now only clears data, not mesh service lifecycle } + /** + * Clear all mesh service related data + */ + private fun clearAllMeshServiceData() { + try { + // Request mesh service to clear all its internal data + meshService.clearAllInternalData() + + Log.d(TAG, "βœ… Cleared all mesh service data") + } catch (e: Exception) { + Log.e(TAG, "❌ Error clearing mesh service data: ${e.message}") + } + } + + /** + * Clear all cryptographic data including persistent identity + */ + private fun clearAllCryptographicData() { + try { + // Clear encryption service persistent identity (Ed25519 signing keys) + meshService.clearAllEncryptionData() + + // Clear secure identity state (if used) + try { + val identityManager = com.bitchat.android.identity.SecureIdentityStateManager(getApplication()) + identityManager.clearIdentityData() + Log.d(TAG, "βœ… Cleared secure identity state") + } catch (e: Exception) { + Log.d(TAG, "SecureIdentityStateManager not available or already cleared: ${e.message}") + } + + Log.d(TAG, "βœ… Cleared all cryptographic data") + } catch (e: Exception) { + Log.e(TAG, "❌ Error clearing cryptographic data: ${e.message}") + } + } + // MARK: - Navigation Management fun showAppInfo() { diff --git a/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt b/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt index 0a79ee13..20fb30b2 100644 --- a/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt +++ b/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt @@ -13,7 +13,7 @@ import android.util.Log interface NoiseSessionDelegate { fun hasEstablishedSession(peerID: String): Boolean fun initiateHandshake(peerID: String) - fun sendIdentityAnnouncement() + fun broadcastNoiseIdentityAnnouncement() fun sendHandshakeRequest(targetPeerID: String, pendingCount: UByte) fun getMyPeerID(): String } @@ -342,7 +342,7 @@ class PrivateChatManager( } else { // They should initiate, we send a Noise identity announcement Log.d(TAG, "Our peer ID lexicographically >= target peer ID, sending Noise identity announcement to prompt handshake from $peerID") - noiseSessionDelegate.sendIdentityAnnouncement() + noiseSessionDelegate.broadcastNoiseIdentityAnnouncement() // Also send handshake request to this peer noiseSessionDelegate.sendHandshakeRequest(peerID, 1u) // 1 pending message (the chat we're trying to start) } @@ -414,12 +414,12 @@ class PrivateChatManager( /** * Send Noise identity announcement to prompt other peer to initiate handshake - * This follows the same pattern as sendKeyExchangeToDevice() in BluetoothMeshService + * This follows the same pattern as broadcastNoiseIdentityAnnouncement() in BluetoothMeshService */ private fun sendNoiseIdentityAnnouncement(meshService: Any) { try { - // Call sendKeyExchangeToDevice which sends a NoiseIdentityAnnouncement - val method = meshService::class.java.getDeclaredMethod("sendKeyExchangeToDevice") + // Call broadcastNoiseIdentityAnnouncement which sends a NoiseIdentityAnnouncement + val method = meshService::class.java.getDeclaredMethod("broadcastNoiseIdentityAnnouncement") method.invoke(meshService) Log.d(TAG, "Successfully sent Noise identity announcement") } catch (e: Exception) {