From 4aa27c41001c87c137c05a4e53159ecbea274362 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 16 Jul 2025 14:20:54 +0200 Subject: [PATCH] works? --- .../android/noise/NoiseEncryptionService.kt | 35 +- .../com/bitchat/android/noise/NoiseSession.kt | 387 ++++++++++-------- 2 files changed, 242 insertions(+), 180 deletions(-) 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 f4a830e4..8f403c63 100644 --- a/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt +++ b/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt @@ -3,6 +3,7 @@ package com.bitchat.android.noise import android.content.Context import android.util.Log import com.bitchat.android.identity.SecureIdentityStateManager +import com.southernstorm.noise.protocol.Noise import java.security.MessageDigest import java.security.SecureRandom import java.util.concurrent.ConcurrentHashMap @@ -290,25 +291,27 @@ class NoiseEncryptionService(private val context: Context) { // MARK: - Private Helpers /** - * Generate a new Curve25519 key pair + * Generate a new Curve25519 key pair using the real Noise library * Returns (privateKey, publicKey) as 32-byte arrays */ private fun generateKeyPair(): Pair { - val random = SecureRandom() - val privateKey = ByteArray(32) - random.nextBytes(privateKey) - - // Clamp private key for Curve25519 - privateKey[0] = (privateKey[0].toInt() and 248).toByte() - privateKey[31] = (privateKey[31].toInt() and 127).toByte() - privateKey[31] = (privateKey[31].toInt() or 64).toByte() - - // For now, we'll compute public key using the Noise library - // This will be properly implemented when we integrate the actual Noise library - val publicKey = ByteArray(32) - random.nextBytes(publicKey) // Temporary - will be replaced with proper curve25519 scalar multiplication - - return Pair(privateKey, publicKey) + try { + val dhState = com.southernstorm.noise.protocol.Noise.createDH("25519") + dhState.generateKeyPair() + + val privateKey = ByteArray(32) + val publicKey = ByteArray(32) + + dhState.getPrivateKey(privateKey, 0) + dhState.getPublicKey(publicKey, 0) + + dhState.destroy() + + return Pair(privateKey, publicKey) + } catch (e: Exception) { + Log.e(TAG, "Failed to generate key pair: ${e.message}") + throw e + } } /** diff --git a/app/src/main/java/com/bitchat/android/noise/NoiseSession.kt b/app/src/main/java/com/bitchat/android/noise/NoiseSession.kt index 97f99760..b57b3eda 100644 --- a/app/src/main/java/com/bitchat/android/noise/NoiseSession.kt +++ b/app/src/main/java/com/bitchat/android/noise/NoiseSession.kt @@ -1,16 +1,12 @@ package com.bitchat.android.noise import android.util.Log +import com.southernstorm.noise.protocol.* import java.security.SecureRandom /** - * Individual Noise session for a specific peer - 100% compatible with iOS implementation - * - * Represents a single Noise XX protocol session with the following states: - * - UNINITIALIZED: Session created but handshake not started - * - HANDSHAKING: Handshake in progress (messages 1, 2, or 3) - * - ESTABLISHED: Handshake complete, ready for transport encryption - * - FAILED: Session failed and needs to be recreated + * Individual Noise session for a specific peer - REAL IMPLEMENTATION with noise-java + * 100% compatible with iOS bitchat Noise Protocol */ class NoiseSession( private val peerID: String, @@ -22,33 +18,31 @@ class NoiseSession( companion object { private const val TAG = "NoiseSession" + // Noise Protocol Configuration (exactly matching iOS) + private const val PROTOCOL_NAME = "Noise_XX_25519_ChaChaPoly_SHA256" + // Rekey thresholds (same as iOS) private const val REKEY_TIME_LIMIT = 3600000L // 1 hour private const val REKEY_MESSAGE_LIMIT = 10000L // 10k messages } + // Real Noise Protocol objects + private var handshakeState: HandshakeState? = null + private var sendCipher: CipherState? = null + private var receiveCipher: CipherState? = null + // Session state private var state: NoiseSessionState = NoiseSessionState.Uninitialized private val creationTime = System.currentTimeMillis() - // Handshake state (will be replaced with actual Noise implementation) - private var handshakeStep = 0 - private var handshakeMessages = mutableListOf() - - // Transport state (after handshake completion) - private var sendKey: ByteArray? = null - private var receiveKey: ByteArray? = null - private var sendNonce = 0L - private var receiveNonce = 0L - - // Remote peer information - private var remoteStaticPublicKey: ByteArray? = null - private var handshakeHash: ByteArray? = null - - // Message counting for rekey + // Session counters private var messagesSent = 0L private var messagesReceived = 0L + // Remote peer information + private var remoteStaticPublicKey: ByteArray? = null + private var handshakeHash: ByteArray? = null + // MARK: - Session State /** @@ -73,12 +67,46 @@ class NoiseSession( fun isHandshaking(): Boolean = state is NoiseSessionState.Handshaking fun getCreationTime(): Long = creationTime - // MARK: - Handshake Implementation + init { + try { + initializeNoiseHandshake() + Log.d(TAG, "Created ${if (isInitiator) "initiator" else "responder"} session for $peerID") + } catch (e: Exception) { + state = NoiseSessionState.Failed(e) + Log.e(TAG, "Failed to initialize Noise session: ${e.message}") + } + } /** - * Start handshake (initiator only) - * Returns the first handshake message (32 bytes - ephemeral public key) + * Initialize the real Noise handshake state using noise-java */ + private fun initializeNoiseHandshake() { + val role = if (isInitiator) HandshakeState.INITIATOR else HandshakeState.RESPONDER + handshakeState = HandshakeState(PROTOCOL_NAME, role) + + // Check if we need to set a local key pair + if (handshakeState?.needsLocalKeyPair() == true) { + val staticKeyPair = Noise.createDH("25519") + staticKeyPair.setPrivateKey(localStaticPrivateKey, 0) + + // Set the local key pair on the handshake state + val localKeyPair = handshakeState?.getLocalKeyPair() + localKeyPair?.setPrivateKey(localStaticPrivateKey, 0) + } + + // Start the handshake + handshakeState?.start() + + Log.d(TAG, "Initialized real Noise handshake state for $peerID") + } + + // MARK: - Real Handshake Implementation + + /** + * Start handshake (initiator only) using real Noise Protocol + * Returns the first handshake message for XX pattern + */ + @Synchronized fun startHandshake(): ByteArray { if (!isInitiator) { throw IllegalStateException("Only initiator can start handshake") @@ -88,170 +116,184 @@ class NoiseSession( throw IllegalStateException("Handshake already started") } - state = NoiseSessionState.Handshaking - handshakeStep = 1 + Log.d(TAG, "Starting real XX handshake with $peerID as initiator") - // TODO: Replace with actual Noise implementation - // For now, generate a mock 32-byte ephemeral key message - val ephemeralKey = ByteArray(32) - SecureRandom().nextBytes(ephemeralKey) - - handshakeMessages.add(ephemeralKey) - - Log.d(TAG, "Started handshake with $peerID (step 1)") - return ephemeralKey + try { + state = NoiseSessionState.Handshaking + + val messageBuffer = ByteArray(256) // Max handshake message size + val messageLength = handshakeState?.writeMessage(ByteArray(0), 0, messageBuffer, 0, 0) ?: 0 + val firstMessage = messageBuffer.copyOf(messageLength) + + Log.d(TAG, "Sent real XX handshake message 1 to $peerID (${firstMessage.size} bytes)") + return firstMessage + } catch (e: Exception) { + state = NoiseSessionState.Failed(e) + Log.e(TAG, "Failed to start handshake: ${e.message}") + throw e + } } /** - * Process incoming handshake message + * Process incoming handshake message using real Noise Protocol * Returns response message if needed, null if handshake complete */ + @Synchronized fun processHandshakeMessage(message: ByteArray): ByteArray? { - Log.d(TAG, "Processing handshake message from $peerID (${message.size} bytes)") + Log.d(TAG, "Processing real handshake message from $peerID (${message.size} bytes)") - // Initialize if we're a responder receiving the first message - if (state == NoiseSessionState.Uninitialized && !isInitiator) { - state = NoiseSessionState.Handshaking - handshakeStep = 1 - } - - if (state != NoiseSessionState.Handshaking) { - throw IllegalStateException("Invalid state for handshake message: $state") - } - - handshakeMessages.add(message) - - // TODO: Replace with actual Noise XX implementation - // This is a mock implementation that follows the XX pattern steps - - return when (handshakeStep) { - 1 -> { - // Received first message (e) - if (isInitiator) { - throw IllegalStateException("Initiator should not receive first message") - } - - handshakeStep = 2 - - // Generate response message (e, ee, s, es) - mock implementation - val responseMessage = ByteArray(96) // 32 (e) + 48 (encrypted s) + 16 (tag) - SecureRandom().nextBytes(responseMessage) - - Log.d(TAG, "Sending handshake response to $peerID (step 2)") - responseMessage + try { + // Initialize as responder if receiving first message + if (state == NoiseSessionState.Uninitialized && !isInitiator) { + state = NoiseSessionState.Handshaking + Log.d(TAG, "Initialized as responder for real XX handshake with $peerID") } - 2 -> { - // Received second message (e, ee, s, es) - if (!isInitiator) { - throw IllegalStateException("Responder should not receive second message") - } - - handshakeStep = 3 - - // Generate final message (s, se) - mock implementation - val finalMessage = ByteArray(48) // 48 bytes (encrypted s) + 16 (tag) - SecureRandom().nextBytes(finalMessage) - - Log.d(TAG, "Sending final handshake message to $peerID (step 3)") - finalMessage + if (state != NoiseSessionState.Handshaking) { + throw IllegalStateException("Invalid state for handshake: $state") } - 3 -> { - // Received final message (s, se) - if (isInitiator) { - throw IllegalStateException("Initiator should not receive third message") + val payloadBuffer = ByteArray(256) // Buffer for any payload data + + // Read the incoming message + val payloadLength = handshakeState?.readMessage(message, 0, message.size, payloadBuffer, 0) ?: 0 + Log.d(TAG, "Read handshake message, payload length: $payloadLength") + + // Check the handshake action state + val action = handshakeState?.getAction() ?: HandshakeState.FAILED + + return when (action) { + HandshakeState.WRITE_MESSAGE -> { + // Need to send a response + val responseBuffer = ByteArray(256) + val responseLength = handshakeState?.writeMessage(ByteArray(0), 0, responseBuffer, 0, 0) ?: 0 + responseBuffer.copyOf(responseLength).also { + Log.d(TAG, "Generated handshake response: ${it.size} bytes") + } } - // Complete handshake - completeHandshake() - null // No response needed + HandshakeState.SPLIT -> { + // Handshake complete, split into transport keys + completeHandshake() + Log.d(TAG, "Real XX handshake completed with $peerID") + null + } + + HandshakeState.FAILED -> { + throw Exception("Handshake failed") + } + + else -> { + Log.d(TAG, "Handshake action: $action") + null + } } - else -> { - throw IllegalStateException("Invalid handshake step: $handshakeStep") - } - }.also { - // Check if we (as initiator) completed handshake after sending final message - if (isInitiator && handshakeStep == 3) { - completeHandshake() - } + } catch (e: Exception) { + state = NoiseSessionState.Failed(e) + Log.e(TAG, "Real handshake failed with $peerID: ${e.message}") + throw e } } /** - * Complete handshake and derive transport keys + * Complete handshake and derive real transport keys */ private fun completeHandshake() { - Log.d(TAG, "Completing handshake with $peerID") + Log.d(TAG, "Completing real XX handshake with $peerID") - // TODO: Replace with actual Noise session split - // For now, generate mock transport keys - sendKey = ByteArray(32) - receiveKey = ByteArray(32) - remoteStaticPublicKey = ByteArray(32) - handshakeHash = ByteArray(32) - - SecureRandom().apply { - nextBytes(sendKey!!) - nextBytes(receiveKey!!) - nextBytes(remoteStaticPublicKey!!) - nextBytes(handshakeHash!!) + try { + // Split handshake state into transport ciphers + val cipherPair = handshakeState?.split() + sendCipher = cipherPair?.getSender() + receiveCipher = cipherPair?.getReceiver() + + // Extract remote static key if available + if (handshakeState?.hasRemotePublicKey() == true) { + val remoteDH = handshakeState?.getRemotePublicKey() + if (remoteDH != null) { + remoteStaticPublicKey = ByteArray(32) + remoteDH.getPublicKey(remoteStaticPublicKey!!, 0) + } + } + + // Extract handshake hash for channel binding + handshakeHash = handshakeState?.getHandshakeHash() + + // Clean up handshake state + handshakeState?.destroy() + handshakeState = null + + messagesSent = 0 + messagesReceived = 0 + + state = NoiseSessionState.Established + Log.d(TAG, "Real XX handshake completed with $peerID - transport keys derived") + } catch (e: Exception) { + state = NoiseSessionState.Failed(e) + Log.e(TAG, "Failed to complete handshake: ${e.message}") + throw e } - - // Reset nonces for transport mode - sendNonce = 0 - receiveNonce = 0 - - state = NoiseSessionState.Established - Log.d(TAG, "Handshake completed with $peerID") } - // MARK: - Transport Encryption + // MARK: - Real Transport Encryption /** - * Encrypt data in transport mode + * Encrypt data in transport mode using real ChaCha20-Poly1305 */ + @Synchronized fun encrypt(data: ByteArray): ByteArray { if (!isEstablished()) { throw IllegalStateException("Session not established") } - // TODO: Replace with actual ChaCha20-Poly1305 encryption - // For now, return mock encrypted data - val encryptedData = ByteArray(data.size + 16) // Add 16-byte MAC tag - System.arraycopy(data, 0, encryptedData, 0, data.size) + if (sendCipher == null) { + throw IllegalStateException("Send cipher not available") + } - // Fill MAC tag area with mock data - SecureRandom().nextBytes(encryptedData.sliceArray(data.size until encryptedData.size)) - - messagesSent++ - sendNonce++ - - return encryptedData + try { + val ciphertext = ByteArray(data.size + 16) // Add space for MAC tag + val ciphertextLength = sendCipher!!.encryptWithAd(null, data, 0, ciphertext, 0, data.size) + + messagesSent++ + + val result = ciphertext.copyOf(ciphertextLength) + Log.d(TAG, "Real encrypted ${data.size} bytes to ${result.size} bytes for $peerID") + return result + + } catch (e: Exception) { + Log.e(TAG, "Real encryption failed: ${e.message}") + throw SessionError.EncryptionFailed + } } /** - * Decrypt data in transport mode + * Decrypt data in transport mode using real ChaCha20-Poly1305 */ + @Synchronized fun decrypt(encryptedData: ByteArray): ByteArray { if (!isEstablished()) { throw IllegalStateException("Session not established") } - if (encryptedData.size < 16) { - throw IllegalArgumentException("Encrypted data too short") + if (receiveCipher == null) { + throw IllegalStateException("Receive cipher not available") } - // TODO: Replace with actual ChaCha20-Poly1305 decryption - // For now, return mock decrypted data (remove 16-byte MAC tag) - val decryptedData = ByteArray(encryptedData.size - 16) - System.arraycopy(encryptedData, 0, decryptedData, 0, decryptedData.size) - - messagesReceived++ - receiveNonce++ - - return decryptedData + try { + val plaintext = ByteArray(encryptedData.size) // Over-allocate for safety + val plaintextLength = receiveCipher!!.decryptWithAd(null, encryptedData, 0, plaintext, 0, encryptedData.size) + + messagesReceived++ + + val result = plaintext.copyOf(plaintextLength) + Log.d(TAG, "Real decrypted ${encryptedData.size} bytes to ${result.size} bytes from $peerID") + return result + + } catch (e: Exception) { + Log.e(TAG, "Real decryption failed: ${e.message}") + throw SessionError.DecryptionFailed + } } // MARK: - Session Information @@ -286,50 +328,67 @@ class NoiseSession( * Get session statistics */ fun getSessionStats(): String = buildString { - appendLine("Session with $peerID:") + appendLine("NoiseSession with $peerID:") appendLine(" State: $state") appendLine(" Role: ${if (isInitiator) "initiator" else "responder"}") - appendLine(" Handshake step: $handshakeStep") appendLine(" Messages sent: $messagesSent") appendLine(" Messages received: $messagesReceived") appendLine(" Session age: ${(System.currentTimeMillis() - creationTime) / 1000}s") appendLine(" Needs rekey: ${needsRekey()}") appendLine(" Has remote key: ${remoteStaticPublicKey != null}") + appendLine(" Has send cipher: ${sendCipher != null}") + appendLine(" Has receive cipher: ${receiveCipher != null}") } /** * Reset session state */ + @Synchronized fun reset() { - state = NoiseSessionState.Uninitialized - handshakeStep = 0 - handshakeMessages.clear() - sendKey = null - receiveKey = null - sendNonce = 0 - receiveNonce = 0 - remoteStaticPublicKey = null - handshakeHash = null - messagesSent = 0 - messagesReceived = 0 + try { + // Destroy existing state + destroy() + + // Reinitialize + initializeNoiseHandshake() + state = NoiseSessionState.Uninitialized + messagesSent = 0 + messagesReceived = 0 + remoteStaticPublicKey = null + handshakeHash = null + } catch (e: Exception) { + state = NoiseSessionState.Failed(e) + Log.e(TAG, "Failed to reset session: ${e.message}") + } } /** - * Clean up session resources + * Clean up session resources securely */ + @Synchronized fun destroy() { try { - // Clear sensitive keys - sendKey?.fill(0) - receiveKey?.fill(0) + // Destroy Noise objects + sendCipher?.destroy() + receiveCipher?.destroy() + handshakeState?.destroy() + + // Clear sensitive data remoteStaticPublicKey?.fill(0) handshakeHash?.fill(0) - // Clear handshake messages - handshakeMessages.forEach { it.fill(0) } - handshakeMessages.clear() + // Null out references + sendCipher = null + receiveCipher = null + handshakeState = null + remoteStaticPublicKey = null + handshakeHash = null - state = NoiseSessionState.Failed(Exception("Session destroyed")) + if (state !is NoiseSessionState.Failed) { + state = NoiseSessionState.Failed(Exception("Session destroyed")) + } + + Log.d(TAG, "Session destroyed for $peerID") } catch (e: Exception) { Log.w(TAG, "Error during session cleanup: ${e.message}")