mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 03:45:21 +00:00
409 lines
14 KiB
Kotlin
409 lines
14 KiB
Kotlin
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 - REAL IMPLEMENTATION with noise-java
|
|
* 100% compatible with iOS bitchat Noise Protocol
|
|
*/
|
|
class NoiseSession(
|
|
private val peerID: String,
|
|
private val isInitiator: Boolean,
|
|
private val localStaticPrivateKey: ByteArray,
|
|
private val localStaticPublicKey: ByteArray
|
|
) {
|
|
|
|
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()
|
|
|
|
// 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
|
|
|
|
/**
|
|
* Session states matching iOS implementation
|
|
*/
|
|
sealed class NoiseSessionState {
|
|
object Uninitialized : NoiseSessionState()
|
|
object Handshaking : NoiseSessionState()
|
|
object Established : NoiseSessionState()
|
|
data class Failed(val error: Throwable) : NoiseSessionState()
|
|
|
|
override fun toString(): String = when (this) {
|
|
is Uninitialized -> "uninitialized"
|
|
is Handshaking -> "handshaking"
|
|
is Established -> "established"
|
|
is Failed -> "failed: ${error.message}"
|
|
}
|
|
}
|
|
|
|
fun getState(): NoiseSessionState = state
|
|
fun isEstablished(): Boolean = state is NoiseSessionState.Established
|
|
fun isHandshaking(): Boolean = state is NoiseSessionState.Handshaking
|
|
fun getCreationTime(): Long = creationTime
|
|
|
|
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}")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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")
|
|
}
|
|
|
|
if (state != NoiseSessionState.Uninitialized) {
|
|
throw IllegalStateException("Handshake already started")
|
|
}
|
|
|
|
Log.d(TAG, "Starting real XX handshake with $peerID as initiator")
|
|
|
|
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 using real Noise Protocol
|
|
* Returns response message if needed, null if handshake complete
|
|
*/
|
|
@Synchronized
|
|
fun processHandshakeMessage(message: ByteArray): ByteArray? {
|
|
Log.d(TAG, "Processing real handshake message from $peerID (${message.size} bytes)")
|
|
|
|
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")
|
|
}
|
|
|
|
if (state != NoiseSessionState.Handshaking) {
|
|
throw IllegalStateException("Invalid state for handshake: $state")
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
} catch (e: Exception) {
|
|
state = NoiseSessionState.Failed(e)
|
|
Log.e(TAG, "Real handshake failed with $peerID: ${e.message}")
|
|
throw e
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Complete handshake and derive real transport keys
|
|
*/
|
|
private fun completeHandshake() {
|
|
Log.d(TAG, "Completing real XX handshake with $peerID")
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
// MARK: - Real Transport Encryption
|
|
|
|
/**
|
|
* Encrypt data in transport mode using real ChaCha20-Poly1305
|
|
*/
|
|
@Synchronized
|
|
fun encrypt(data: ByteArray): ByteArray {
|
|
if (!isEstablished()) {
|
|
throw IllegalStateException("Session not established")
|
|
}
|
|
|
|
if (sendCipher == null) {
|
|
throw IllegalStateException("Send cipher not available")
|
|
}
|
|
|
|
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 using real ChaCha20-Poly1305
|
|
*/
|
|
@Synchronized
|
|
fun decrypt(encryptedData: ByteArray): ByteArray {
|
|
if (!isEstablished()) {
|
|
throw IllegalStateException("Session not established")
|
|
}
|
|
|
|
if (receiveCipher == null) {
|
|
throw IllegalStateException("Receive cipher not available")
|
|
}
|
|
|
|
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
|
|
|
|
/**
|
|
* Get remote static public key (available after handshake completion)
|
|
*/
|
|
fun getRemoteStaticPublicKey(): ByteArray? {
|
|
return remoteStaticPublicKey?.clone()
|
|
}
|
|
|
|
/**
|
|
* Get handshake hash for channel binding
|
|
*/
|
|
fun getHandshakeHash(): ByteArray? {
|
|
return handshakeHash?.clone()
|
|
}
|
|
|
|
/**
|
|
* Check if session needs rekeying
|
|
*/
|
|
fun needsRekey(): Boolean {
|
|
if (!isEstablished()) return false
|
|
|
|
val timeLimit = System.currentTimeMillis() - creationTime > REKEY_TIME_LIMIT
|
|
val messageLimit = (messagesSent + messagesReceived) > REKEY_MESSAGE_LIMIT
|
|
|
|
return timeLimit || messageLimit
|
|
}
|
|
|
|
/**
|
|
* Get session statistics
|
|
*/
|
|
fun getSessionStats(): String = buildString {
|
|
appendLine("NoiseSession with $peerID:")
|
|
appendLine(" State: $state")
|
|
appendLine(" Role: ${if (isInitiator) "initiator" else "responder"}")
|
|
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() {
|
|
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 securely
|
|
*/
|
|
@Synchronized
|
|
fun destroy() {
|
|
try {
|
|
// Destroy Noise objects
|
|
sendCipher?.destroy()
|
|
receiveCipher?.destroy()
|
|
handshakeState?.destroy()
|
|
|
|
// Clear sensitive data
|
|
remoteStaticPublicKey?.fill(0)
|
|
handshakeHash?.fill(0)
|
|
|
|
// Null out references
|
|
sendCipher = null
|
|
receiveCipher = null
|
|
handshakeState = null
|
|
remoteStaticPublicKey = null
|
|
handshakeHash = null
|
|
|
|
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}")
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Session-specific errors
|
|
*/
|
|
sealed class SessionError(message: String, cause: Throwable? = null) : Exception(message, cause) {
|
|
object InvalidState : SessionError("Session in invalid state")
|
|
object NotEstablished : SessionError("Session not established")
|
|
object HandshakeFailed : SessionError("Handshake failed")
|
|
object EncryptionFailed : SessionError("Encryption failed")
|
|
object DecryptionFailed : SessionError("Decryption failed")
|
|
}
|