This commit is contained in:
callebtc
2025-07-16 14:20:54 +02:00
parent 57f870fff9
commit 4aa27c4100
2 changed files with 242 additions and 180 deletions
@@ -3,6 +3,7 @@ package com.bitchat.android.noise
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import com.bitchat.android.identity.SecureIdentityStateManager import com.bitchat.android.identity.SecureIdentityStateManager
import com.southernstorm.noise.protocol.Noise
import java.security.MessageDigest import java.security.MessageDigest
import java.security.SecureRandom import java.security.SecureRandom
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@@ -290,25 +291,27 @@ class NoiseEncryptionService(private val context: Context) {
// MARK: - Private Helpers // 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 * Returns (privateKey, publicKey) as 32-byte arrays
*/ */
private fun generateKeyPair(): Pair<ByteArray, ByteArray> { private fun generateKeyPair(): Pair<ByteArray, ByteArray> {
val random = SecureRandom() try {
val privateKey = ByteArray(32) val dhState = com.southernstorm.noise.protocol.Noise.createDH("25519")
random.nextBytes(privateKey) dhState.generateKeyPair()
// Clamp private key for Curve25519 val privateKey = ByteArray(32)
privateKey[0] = (privateKey[0].toInt() and 248).toByte() val publicKey = ByteArray(32)
privateKey[31] = (privateKey[31].toInt() and 127).toByte()
privateKey[31] = (privateKey[31].toInt() or 64).toByte() dhState.getPrivateKey(privateKey, 0)
dhState.getPublicKey(publicKey, 0)
// For now, we'll compute public key using the Noise library
// This will be properly implemented when we integrate the actual Noise library dhState.destroy()
val publicKey = ByteArray(32)
random.nextBytes(publicKey) // Temporary - will be replaced with proper curve25519 scalar multiplication return Pair(privateKey, publicKey)
} catch (e: Exception) {
return Pair(privateKey, publicKey) Log.e(TAG, "Failed to generate key pair: ${e.message}")
throw e
}
} }
/** /**
@@ -1,16 +1,12 @@
package com.bitchat.android.noise package com.bitchat.android.noise
import android.util.Log import android.util.Log
import com.southernstorm.noise.protocol.*
import java.security.SecureRandom import java.security.SecureRandom
/** /**
* Individual Noise session for a specific peer - 100% compatible with iOS implementation * Individual Noise session for a specific peer - REAL IMPLEMENTATION with noise-java
* * 100% compatible with iOS bitchat Noise Protocol
* 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
*/ */
class NoiseSession( class NoiseSession(
private val peerID: String, private val peerID: String,
@@ -22,33 +18,31 @@ class NoiseSession(
companion object { companion object {
private const val TAG = "NoiseSession" 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) // Rekey thresholds (same as iOS)
private const val REKEY_TIME_LIMIT = 3600000L // 1 hour private const val REKEY_TIME_LIMIT = 3600000L // 1 hour
private const val REKEY_MESSAGE_LIMIT = 10000L // 10k messages 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 // Session state
private var state: NoiseSessionState = NoiseSessionState.Uninitialized private var state: NoiseSessionState = NoiseSessionState.Uninitialized
private val creationTime = System.currentTimeMillis() private val creationTime = System.currentTimeMillis()
// Handshake state (will be replaced with actual Noise implementation) // Session counters
private var handshakeStep = 0
private var handshakeMessages = mutableListOf<ByteArray>()
// 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
private var messagesSent = 0L private var messagesSent = 0L
private var messagesReceived = 0L private var messagesReceived = 0L
// Remote peer information
private var remoteStaticPublicKey: ByteArray? = null
private var handshakeHash: ByteArray? = null
// MARK: - Session State // MARK: - Session State
/** /**
@@ -73,12 +67,46 @@ class NoiseSession(
fun isHandshaking(): Boolean = state is NoiseSessionState.Handshaking fun isHandshaking(): Boolean = state is NoiseSessionState.Handshaking
fun getCreationTime(): Long = creationTime 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) * Initialize the real Noise handshake state using noise-java
* Returns the first handshake message (32 bytes - ephemeral public key)
*/ */
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 { fun startHandshake(): ByteArray {
if (!isInitiator) { if (!isInitiator) {
throw IllegalStateException("Only initiator can start handshake") throw IllegalStateException("Only initiator can start handshake")
@@ -88,170 +116,184 @@ class NoiseSession(
throw IllegalStateException("Handshake already started") throw IllegalStateException("Handshake already started")
} }
state = NoiseSessionState.Handshaking Log.d(TAG, "Starting real XX handshake with $peerID as initiator")
handshakeStep = 1
// TODO: Replace with actual Noise implementation try {
// For now, generate a mock 32-byte ephemeral key message state = NoiseSessionState.Handshaking
val ephemeralKey = ByteArray(32)
SecureRandom().nextBytes(ephemeralKey) val messageBuffer = ByteArray(256) // Max handshake message size
val messageLength = handshakeState?.writeMessage(ByteArray(0), 0, messageBuffer, 0, 0) ?: 0
handshakeMessages.add(ephemeralKey) val firstMessage = messageBuffer.copyOf(messageLength)
Log.d(TAG, "Started handshake with $peerID (step 1)") Log.d(TAG, "Sent real XX handshake message 1 to $peerID (${firstMessage.size} bytes)")
return ephemeralKey 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 * Returns response message if needed, null if handshake complete
*/ */
@Synchronized
fun processHandshakeMessage(message: ByteArray): ByteArray? { 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 try {
if (state == NoiseSessionState.Uninitialized && !isInitiator) { // Initialize as responder if receiving first message
state = NoiseSessionState.Handshaking if (state == NoiseSessionState.Uninitialized && !isInitiator) {
handshakeStep = 1 state = NoiseSessionState.Handshaking
} Log.d(TAG, "Initialized as responder for real XX handshake with $peerID")
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
} }
2 -> { if (state != NoiseSessionState.Handshaking) {
// Received second message (e, ee, s, es) throw IllegalStateException("Invalid state for handshake: $state")
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
} }
3 -> { val payloadBuffer = ByteArray(256) // Buffer for any payload data
// Received final message (s, se)
if (isInitiator) { // Read the incoming message
throw IllegalStateException("Initiator should not receive third 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 HandshakeState.SPLIT -> {
completeHandshake() // Handshake complete, split into transport keys
null // No response needed 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 -> { } catch (e: Exception) {
throw IllegalStateException("Invalid handshake step: $handshakeStep") state = NoiseSessionState.Failed(e)
} Log.e(TAG, "Real handshake failed with $peerID: ${e.message}")
}.also { throw e
// Check if we (as initiator) completed handshake after sending final message
if (isInitiator && handshakeStep == 3) {
completeHandshake()
}
} }
} }
/** /**
* Complete handshake and derive transport keys * Complete handshake and derive real transport keys
*/ */
private fun completeHandshake() { 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 try {
// For now, generate mock transport keys // Split handshake state into transport ciphers
sendKey = ByteArray(32) val cipherPair = handshakeState?.split()
receiveKey = ByteArray(32) sendCipher = cipherPair?.getSender()
remoteStaticPublicKey = ByteArray(32) receiveCipher = cipherPair?.getReceiver()
handshakeHash = ByteArray(32)
// Extract remote static key if available
SecureRandom().apply { if (handshakeState?.hasRemotePublicKey() == true) {
nextBytes(sendKey!!) val remoteDH = handshakeState?.getRemotePublicKey()
nextBytes(receiveKey!!) if (remoteDH != null) {
nextBytes(remoteStaticPublicKey!!) remoteStaticPublicKey = ByteArray(32)
nextBytes(handshakeHash!!) 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 { fun encrypt(data: ByteArray): ByteArray {
if (!isEstablished()) { if (!isEstablished()) {
throw IllegalStateException("Session not established") throw IllegalStateException("Session not established")
} }
// TODO: Replace with actual ChaCha20-Poly1305 encryption if (sendCipher == null) {
// For now, return mock encrypted data throw IllegalStateException("Send cipher not available")
val encryptedData = ByteArray(data.size + 16) // Add 16-byte MAC tag }
System.arraycopy(data, 0, encryptedData, 0, data.size)
// Fill MAC tag area with mock data try {
SecureRandom().nextBytes(encryptedData.sliceArray(data.size until encryptedData.size)) val ciphertext = ByteArray(data.size + 16) // Add space for MAC tag
val ciphertextLength = sendCipher!!.encryptWithAd(null, data, 0, ciphertext, 0, data.size)
messagesSent++
sendNonce++ messagesSent++
return encryptedData 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 { fun decrypt(encryptedData: ByteArray): ByteArray {
if (!isEstablished()) { if (!isEstablished()) {
throw IllegalStateException("Session not established") throw IllegalStateException("Session not established")
} }
if (encryptedData.size < 16) { if (receiveCipher == null) {
throw IllegalArgumentException("Encrypted data too short") throw IllegalStateException("Receive cipher not available")
} }
// TODO: Replace with actual ChaCha20-Poly1305 decryption try {
// For now, return mock decrypted data (remove 16-byte MAC tag) val plaintext = ByteArray(encryptedData.size) // Over-allocate for safety
val decryptedData = ByteArray(encryptedData.size - 16) val plaintextLength = receiveCipher!!.decryptWithAd(null, encryptedData, 0, plaintext, 0, encryptedData.size)
System.arraycopy(encryptedData, 0, decryptedData, 0, decryptedData.size)
messagesReceived++
messagesReceived++
receiveNonce++ val result = plaintext.copyOf(plaintextLength)
Log.d(TAG, "Real decrypted ${encryptedData.size} bytes to ${result.size} bytes from $peerID")
return decryptedData return result
} catch (e: Exception) {
Log.e(TAG, "Real decryption failed: ${e.message}")
throw SessionError.DecryptionFailed
}
} }
// MARK: - Session Information // MARK: - Session Information
@@ -286,50 +328,67 @@ class NoiseSession(
* Get session statistics * Get session statistics
*/ */
fun getSessionStats(): String = buildString { fun getSessionStats(): String = buildString {
appendLine("Session with $peerID:") appendLine("NoiseSession with $peerID:")
appendLine(" State: $state") appendLine(" State: $state")
appendLine(" Role: ${if (isInitiator) "initiator" else "responder"}") appendLine(" Role: ${if (isInitiator) "initiator" else "responder"}")
appendLine(" Handshake step: $handshakeStep")
appendLine(" Messages sent: $messagesSent") appendLine(" Messages sent: $messagesSent")
appendLine(" Messages received: $messagesReceived") appendLine(" Messages received: $messagesReceived")
appendLine(" Session age: ${(System.currentTimeMillis() - creationTime) / 1000}s") appendLine(" Session age: ${(System.currentTimeMillis() - creationTime) / 1000}s")
appendLine(" Needs rekey: ${needsRekey()}") appendLine(" Needs rekey: ${needsRekey()}")
appendLine(" Has remote key: ${remoteStaticPublicKey != null}") appendLine(" Has remote key: ${remoteStaticPublicKey != null}")
appendLine(" Has send cipher: ${sendCipher != null}")
appendLine(" Has receive cipher: ${receiveCipher != null}")
} }
/** /**
* Reset session state * Reset session state
*/ */
@Synchronized
fun reset() { fun reset() {
state = NoiseSessionState.Uninitialized try {
handshakeStep = 0 // Destroy existing state
handshakeMessages.clear() destroy()
sendKey = null
receiveKey = null // Reinitialize
sendNonce = 0 initializeNoiseHandshake()
receiveNonce = 0 state = NoiseSessionState.Uninitialized
remoteStaticPublicKey = null messagesSent = 0
handshakeHash = null messagesReceived = 0
messagesSent = 0 remoteStaticPublicKey = null
messagesReceived = 0 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() { fun destroy() {
try { try {
// Clear sensitive keys // Destroy Noise objects
sendKey?.fill(0) sendCipher?.destroy()
receiveKey?.fill(0) receiveCipher?.destroy()
handshakeState?.destroy()
// Clear sensitive data
remoteStaticPublicKey?.fill(0) remoteStaticPublicKey?.fill(0)
handshakeHash?.fill(0) handshakeHash?.fill(0)
// Clear handshake messages // Null out references
handshakeMessages.forEach { it.fill(0) } sendCipher = null
handshakeMessages.clear() 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) { } catch (e: Exception) {
Log.w(TAG, "Error during session cleanup: ${e.message}") Log.w(TAG, "Error during session cleanup: ${e.message}")