mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 20:05:21 +00:00
works?
This commit is contained in:
@@ -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<ByteArray, ByteArray> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<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
|
||||
// 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}")
|
||||
|
||||
Reference in New Issue
Block a user