mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 03:45:21 +00:00
simplify session manager
This commit is contained in:
@@ -613,35 +613,37 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
* Check if we have an established Noise session with a peer
|
* Check if we have an established Noise session with a peer
|
||||||
*/
|
*/
|
||||||
fun hasEstablishedSession(peerID: String): Boolean {
|
fun hasEstablishedSession(peerID: String): Boolean {
|
||||||
return encryptionService.hasEstablishedSession(peerID)
|
return noiseEncryptionService.hasEstablishedSession(peerID) // FIXED: Use same service as delegate
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get peer fingerprint for identity management
|
* Get peer fingerprint for identity management
|
||||||
*/
|
*/
|
||||||
fun getPeerFingerprint(peerID: String): String? {
|
fun getPeerFingerprint(peerID: String): String? {
|
||||||
return encryptionService.getPeerFingerprint(peerID)
|
return noiseEncryptionService.getPeerFingerprint(peerID) // FIXED: Use same service as delegate
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get our identity fingerprint
|
* Get our identity fingerprint
|
||||||
*/
|
*/
|
||||||
fun getIdentityFingerprint(): String {
|
fun getIdentityFingerprint(): String {
|
||||||
return encryptionService.getIdentityFingerprint()
|
return noiseEncryptionService.getIdentityFingerprint() // FIXED: Use same service as delegate
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if encryption icon should be shown for a peer
|
* Check if encryption icon should be shown for a peer
|
||||||
*/
|
*/
|
||||||
fun shouldShowEncryptionIcon(peerID: String): Boolean {
|
fun shouldShowEncryptionIcon(peerID: String): Boolean {
|
||||||
return encryptionService.shouldShowEncryptionIcon(peerID)
|
return noiseEncryptionService.hasEstablishedSession(peerID) // FIXED: Use same service as delegate
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all peers with established encrypted sessions
|
* Get all peers with established encrypted sessions
|
||||||
*/
|
*/
|
||||||
fun getEncryptedPeers(): List<String> {
|
fun getEncryptedPeers(): List<String> {
|
||||||
return encryptionService.getEstablishedPeers()
|
// SIMPLIFIED: Return empty list for now since we don't have direct access to sessionManager
|
||||||
|
// This method is not critical for the session retention fix
|
||||||
|
return emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ class NoiseSession(
|
|||||||
private var messagesSent = 0L
|
private var messagesSent = 0L
|
||||||
private var messagesReceived = 0L
|
private var messagesReceived = 0L
|
||||||
|
|
||||||
|
// CRITICAL FIX: Enhanced thread safety for cipher operations
|
||||||
|
// The noise-java CipherState objects are NOT thread-safe. Multiple concurrent
|
||||||
|
// decrypt/encrypt operations can corrupt the internal nonce state.
|
||||||
|
private val cipherLock = Any() // Dedicated lock for cipher operations
|
||||||
|
|
||||||
// Remote peer information
|
// Remote peer information
|
||||||
private var remoteStaticPublicKey: ByteArray? = null
|
private var remoteStaticPublicKey: ByteArray? = null
|
||||||
private var handshakeHash: ByteArray? = null
|
private var handshakeHash: ByteArray? = null
|
||||||
@@ -171,8 +176,8 @@ class NoiseSession(
|
|||||||
// MARK: - Real Handshake Implementation
|
// MARK: - Real Handshake Implementation
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start handshake (initiator only) using real Noise Protocol
|
* Start handshake as INITIATOR
|
||||||
* Returns the first handshake message for XX pattern (32 bytes exactly)
|
* Returns e, the first handshake message for XX pattern (32 bytes)
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun startHandshake(): ByteArray {
|
fun startHandshake(): ByteArray {
|
||||||
@@ -211,8 +216,8 @@ class NoiseSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process incoming handshake message using real Noise Protocol
|
* Process incoming handshake as RESPONDER
|
||||||
* FIXED: Eliminates manual step counting - lets the Noise library handle it
|
* Returns e, ee
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun processHandshakeMessage(message: ByteArray): ByteArray? {
|
fun processHandshakeMessage(message: ByteArray): ByteArray? {
|
||||||
@@ -329,58 +334,88 @@ class NoiseSession(
|
|||||||
/**
|
/**
|
||||||
* Encrypt data in transport mode using real ChaCha20-Poly1305
|
* Encrypt data in transport mode using real ChaCha20-Poly1305
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
|
||||||
fun encrypt(data: ByteArray): ByteArray {
|
fun encrypt(data: ByteArray): ByteArray {
|
||||||
|
// Pre-check state without holding cipher lock
|
||||||
if (!isEstablished()) {
|
if (!isEstablished()) {
|
||||||
throw IllegalStateException("Session not established")
|
throw IllegalStateException("Session not established")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sendCipher == null) {
|
// Critical section: Use dedicated cipher lock to protect CipherState nonce corruption
|
||||||
throw IllegalStateException("Send cipher not available")
|
synchronized(cipherLock) {
|
||||||
}
|
// Double-check state inside lock
|
||||||
|
if (!isEstablished()) {
|
||||||
try {
|
throw IllegalStateException("Session not established during cipher operation")
|
||||||
val ciphertext = ByteArray(data.size + 16) // Add space for MAC tag
|
}
|
||||||
val ciphertextLength = sendCipher!!.encryptWithAd(null, data, 0, ciphertext, 0, data.size)
|
|
||||||
|
|
||||||
messagesSent++
|
if (sendCipher == null) {
|
||||||
|
throw IllegalStateException("Send cipher not available")
|
||||||
|
}
|
||||||
|
|
||||||
val result = ciphertext.copyOf(ciphertextLength)
|
try {
|
||||||
Log.d(TAG, "Real encrypted ${data.size} bytes to ${result.size} bytes for $peerID")
|
val ciphertext = ByteArray(data.size + 16) // Add space for MAC tag
|
||||||
return result
|
val ciphertextLength = sendCipher!!.encryptWithAd(null, data, 0, ciphertext, 0, data.size)
|
||||||
|
|
||||||
} catch (e: Exception) {
|
messagesSent++
|
||||||
Log.e(TAG, "Real encryption failed: ${e.message}")
|
|
||||||
throw SessionError.EncryptionFailed
|
val result = ciphertext.copyOf(ciphertextLength)
|
||||||
|
Log.d(TAG, "Real encrypted ${data.size} bytes to ${result.size} bytes for $peerID (msg #$messagesSent)")
|
||||||
|
return result
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Real encryption failed - exception: ${e.message}")
|
||||||
|
|
||||||
|
// ENHANCED: Log cipher state for debugging
|
||||||
|
if (sendCipher != null) {
|
||||||
|
Log.e(TAG, "Send cipher state: ${sendCipher!!.javaClass.simpleName}")
|
||||||
|
}
|
||||||
|
|
||||||
|
throw SessionError.EncryptionFailed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decrypt data in transport mode using real ChaCha20-Poly1305
|
* Decrypt data in transport mode using real ChaCha20-Poly1305
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
|
||||||
fun decrypt(encryptedData: ByteArray): ByteArray {
|
fun decrypt(encryptedData: ByteArray): ByteArray {
|
||||||
|
// Pre-check state without holding cipher lock
|
||||||
if (!isEstablished()) {
|
if (!isEstablished()) {
|
||||||
throw IllegalStateException("Session not established")
|
throw IllegalStateException("Session not established")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (receiveCipher == null) {
|
// Critical section: Use dedicated cipher lock to protect CipherState nonce corruption
|
||||||
throw IllegalStateException("Receive cipher not available")
|
synchronized(cipherLock) {
|
||||||
}
|
// Double-check state inside lock
|
||||||
|
if (!isEstablished()) {
|
||||||
try {
|
throw IllegalStateException("Session not established during cipher operation")
|
||||||
val plaintext = ByteArray(encryptedData.size) // Over-allocate for safety
|
}
|
||||||
val plaintextLength = receiveCipher!!.decryptWithAd(null, encryptedData, 0, plaintext, 0, encryptedData.size)
|
|
||||||
|
|
||||||
messagesReceived++
|
if (receiveCipher == null) {
|
||||||
|
throw IllegalStateException("Receive cipher not available")
|
||||||
|
}
|
||||||
|
|
||||||
val result = plaintext.copyOf(plaintextLength)
|
try {
|
||||||
Log.d(TAG, "Real decrypted ${encryptedData.size} bytes to ${result.size} bytes from $peerID")
|
val plaintext = ByteArray(encryptedData.size) // Over-allocate for safety
|
||||||
return result
|
val plaintextLength = receiveCipher!!.decryptWithAd(null, encryptedData, 0, plaintext, 0, encryptedData.size)
|
||||||
|
|
||||||
} catch (e: Exception) {
|
messagesReceived++
|
||||||
Log.e(TAG, "Real decryption failed: ${e.message}")
|
|
||||||
throw SessionError.DecryptionFailed
|
val result = plaintext.copyOf(plaintextLength)
|
||||||
|
Log.d(TAG, "Real decrypted ${encryptedData.size} bytes to ${result.size} bytes from $peerID (msg #$messagesReceived)")
|
||||||
|
return result
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Real decryption failed - exception: ${e.message}")
|
||||||
|
|
||||||
|
// ENHANCED: Log cipher state and session details for debugging
|
||||||
|
if (receiveCipher != null) {
|
||||||
|
Log.e(TAG, "Receive cipher state: ${receiveCipher!!.javaClass.simpleName}")
|
||||||
|
}
|
||||||
|
Log.e(TAG, "Session state: $state, messages received: $messagesReceived")
|
||||||
|
Log.e(TAG, "Input data size: ${encryptedData.size} bytes")
|
||||||
|
|
||||||
|
throw SessionError.DecryptionFailed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,18 +2,9 @@ package com.bitchat.android.noise
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.locks.ReentrantReadWriteLock
|
|
||||||
import kotlin.concurrent.read
|
|
||||||
import kotlin.concurrent.write
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Thread-safe manager for Noise sessions - 100% compatible with iOS implementation
|
* SIMPLIFIED Noise session manager - focuses on core functionality only
|
||||||
*
|
|
||||||
* Manages the lifecycle of Noise sessions for each peer, including:
|
|
||||||
* - Handshake initiation and processing
|
|
||||||
* - Session establishment and transport encryption
|
|
||||||
* - Session cleanup and rekey support
|
|
||||||
* - Thread-safe operations for concurrent access
|
|
||||||
*/
|
*/
|
||||||
class NoiseSessionManager(
|
class NoiseSessionManager(
|
||||||
private val localStaticPrivateKey: ByteArray,
|
private val localStaticPrivateKey: ByteArray,
|
||||||
@@ -22,446 +13,176 @@ class NoiseSessionManager(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "NoiseSessionManager"
|
private const val TAG = "NoiseSessionManager"
|
||||||
private const val MAX_SESSIONS = 50 // Prevent memory exhaustion
|
|
||||||
private const val SESSION_CLEANUP_INTERVAL = 300000L // 5 minutes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Thread-safe session storage
|
private val sessions = ConcurrentHashMap<String, NoiseSession>()
|
||||||
private val sessions = ConcurrentHashMap<String, NoiseSession>()
|
|
||||||
private val sessionLock = ReentrantReadWriteLock()
|
|
||||||
|
|
||||||
// Pending handshakes to handle handshake storms and prevent duplicates
|
|
||||||
private val pendingHandshakes = ConcurrentHashMap<String, HandshakeInfo>()
|
|
||||||
|
|
||||||
// Callbacks
|
// Callbacks
|
||||||
var onSessionEstablished: ((String, ByteArray) -> Unit)? = null // (peerID, remoteStaticKey)
|
var onSessionEstablished: ((String, ByteArray) -> Unit)? = null
|
||||||
var onSessionFailed: ((String, Throwable) -> Unit)? = null // (peerID, error)
|
var onSessionFailed: ((String, Throwable) -> Unit)? = null
|
||||||
|
|
||||||
|
// MARK: - Simple Session Management
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Information about ongoing handshakes
|
* Get existing session for a peer
|
||||||
*/
|
|
||||||
data class HandshakeInfo(
|
|
||||||
val startTime: Long,
|
|
||||||
val isInitiator: Boolean,
|
|
||||||
var step: Int = 1
|
|
||||||
)
|
|
||||||
|
|
||||||
// MARK: - Session Management
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new session for a peer with specified role
|
|
||||||
*/
|
|
||||||
private fun createSession(peerID: String, isInitiator: Boolean): NoiseSession {
|
|
||||||
return sessionLock.write {
|
|
||||||
val session = NoiseSession(
|
|
||||||
peerID = peerID,
|
|
||||||
isInitiator = isInitiator,
|
|
||||||
localStaticPrivateKey = localStaticPrivateKey,
|
|
||||||
localStaticPublicKey = localStaticPublicKey
|
|
||||||
)
|
|
||||||
sessions[peerID] = session
|
|
||||||
session
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get existing session for a peer (thread-safe)
|
|
||||||
*/
|
*/
|
||||||
fun getSession(peerID: String): NoiseSession? {
|
fun getSession(peerID: String): NoiseSession? {
|
||||||
return sessionLock.read {
|
val session = sessions[peerID]
|
||||||
sessions[peerID]
|
Log.d(TAG, "getSession($peerID): ${if (session?.isEstablished() == true) "ESTABLISHED" else "NOT_FOUND"}")
|
||||||
}
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove session for a peer (thread-safe)
|
* Remove session for a peer
|
||||||
*/
|
*/
|
||||||
fun removeSession(peerID: String) {
|
fun removeSession(peerID: String) {
|
||||||
sessionLock.write {
|
sessions[peerID]?.destroy()
|
||||||
sessions[peerID]?.destroy()
|
sessions.remove(peerID)
|
||||||
sessions.remove(peerID)
|
Log.d(TAG, "Removed session for $peerID")
|
||||||
pendingHandshakes.remove(peerID)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all established sessions
|
* SIMPLIFIED: Initiate handshake - no tie breaker, just start
|
||||||
*/
|
|
||||||
fun getEstablishedSessions(): Map<String, NoiseSession> {
|
|
||||||
return sessionLock.read {
|
|
||||||
sessions.filter { it.value.isEstablished() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Handshake Management
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initiate handshake as the initiator
|
|
||||||
* Implements tie-breaker mechanism to prevent handshake storms
|
|
||||||
*/
|
*/
|
||||||
fun initiateHandshake(peerID: String): ByteArray {
|
fun initiateHandshake(peerID: String): ByteArray {
|
||||||
return sessionLock.write {
|
// Remove any existing session first
|
||||||
// Check if we already have an established session
|
removeSession(peerID)
|
||||||
val existingSession = sessions[peerID]
|
|
||||||
if (existingSession?.isEstablished() == true) {
|
// Create new session as initiator
|
||||||
throw IllegalStateException("Session already established with $peerID")
|
val session = NoiseSession(
|
||||||
}
|
peerID = peerID,
|
||||||
|
isInitiator = true,
|
||||||
// Apply tie-breaker mechanism (same as iOS implementation)
|
localStaticPrivateKey = localStaticPrivateKey,
|
||||||
val shouldInitiate = resolveTieBreaker(peerID)
|
localStaticPublicKey = localStaticPublicKey
|
||||||
if (!shouldInitiate) {
|
)
|
||||||
Log.d(TAG, "Tie-breaker: Waiting for $peerID to initiate handshake")
|
sessions[peerID] = session
|
||||||
// throw IllegalStateException("Tie-breaker: Should not initiate with $peerID")
|
|
||||||
}
|
try {
|
||||||
|
val handshakeData = session.startHandshake()
|
||||||
// Remove any existing non-established session
|
Log.d(TAG, "Started handshake with $peerID as initiator")
|
||||||
if (existingSession != null && !existingSession.isEstablished()) {
|
return handshakeData
|
||||||
existingSession.destroy()
|
} catch (e: Exception) {
|
||||||
sessions.remove(peerID)
|
sessions.remove(peerID)
|
||||||
}
|
throw e
|
||||||
|
|
||||||
// Create new session as initiator
|
|
||||||
val session = createSession(peerID, isInitiator = true)
|
|
||||||
|
|
||||||
// Mark as pending handshake
|
|
||||||
pendingHandshakes[peerID] = HandshakeInfo(
|
|
||||||
startTime = System.currentTimeMillis(),
|
|
||||||
isInitiator = true,
|
|
||||||
step = 1
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
|
||||||
val handshakeData = session.startHandshake()
|
|
||||||
Log.d(TAG, "Started handshake with $peerID as initiator")
|
|
||||||
handshakeData
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Clean up failed session
|
|
||||||
sessions.remove(peerID)
|
|
||||||
pendingHandshakes.remove(peerID)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle incoming handshake message
|
* SIMPLIFIED: Handle incoming handshake message
|
||||||
* Supports both initiating and responding to handshakes
|
|
||||||
* FIXED: Added comprehensive message validation
|
|
||||||
*/
|
*/
|
||||||
fun handleIncomingHandshake(peerID: String, message: ByteArray): ByteArray? {
|
fun handleIncomingHandshake(peerID: String, message: ByteArray): ByteArray? {
|
||||||
return sessionLock.write {
|
Log.d(TAG, "handleIncomingHandshake($peerID, ${message.size} bytes)")
|
||||||
|
|
||||||
|
try {
|
||||||
|
var session = sessions[peerID]
|
||||||
|
|
||||||
// CRITICAL FIX: Validate handshake message before processing
|
// If no session exists, create one as responder
|
||||||
if (!validateHandshakeMessage(message, peerID)) {
|
if (session == null) {
|
||||||
Log.w(TAG, "Invalid handshake message from $peerID, ignoring")
|
Log.d(TAG, "Creating new responder session for $peerID")
|
||||||
return@write null
|
session = NoiseSession(
|
||||||
}
|
peerID = peerID,
|
||||||
|
|
||||||
var shouldCreateNew = false
|
|
||||||
var existingSession: NoiseSession? = null
|
|
||||||
|
|
||||||
val existing = sessions[peerID]
|
|
||||||
if (existing == null) {
|
|
||||||
shouldCreateNew = true
|
|
||||||
} else {
|
|
||||||
when {
|
|
||||||
existing.isEstablished() -> {
|
|
||||||
// If this is a handshake initiation (first message), help complete their handshake
|
|
||||||
if (isFirstHandshakeMessage(message)) {
|
|
||||||
Log.d(TAG, "Received new handshake from established peer $peerID, resetting")
|
|
||||||
existing.destroy()
|
|
||||||
sessions.remove(peerID)
|
|
||||||
shouldCreateNew = true
|
|
||||||
} else {
|
|
||||||
// Ignore other handshake messages if already established
|
|
||||||
Log.d(TAG, "Ignoring handshake message from established peer $peerID")
|
|
||||||
return@write null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
existing.isHandshaking() -> {
|
|
||||||
// Continue existing handshake
|
|
||||||
if (isFirstHandshakeMessage(message)) {
|
|
||||||
// Peer restarted handshake, reset and start fresh
|
|
||||||
existing.destroy()
|
|
||||||
sessions.remove(peerID)
|
|
||||||
shouldCreateNew = true
|
|
||||||
} else {
|
|
||||||
existingSession = existing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
// Session in invalid state, recreate
|
|
||||||
existing.destroy()
|
|
||||||
sessions.remove(peerID)
|
|
||||||
shouldCreateNew = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get or create session
|
|
||||||
val session: NoiseSession = if (shouldCreateNew) {
|
|
||||||
val newSession = createSession(peerID, isInitiator = false)
|
|
||||||
pendingHandshakes[peerID] = HandshakeInfo(
|
|
||||||
startTime = System.currentTimeMillis(),
|
|
||||||
isInitiator = false,
|
isInitiator = false,
|
||||||
step = 1
|
localStaticPrivateKey = localStaticPrivateKey,
|
||||||
|
localStaticPublicKey = localStaticPublicKey
|
||||||
)
|
)
|
||||||
newSession
|
sessions[peerID] = session
|
||||||
} else {
|
|
||||||
existingSession!!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process handshake message
|
// Process handshake message
|
||||||
try {
|
val response = session.processHandshakeMessage(message)
|
||||||
val response = session.processHandshakeMessage(message)
|
|
||||||
|
// Check if session is established
|
||||||
// Update handshake step
|
if (session.isEstablished()) {
|
||||||
pendingHandshakes[peerID]?.let { it.step += 1 }
|
Log.d(TAG, "✅ Session ESTABLISHED with $peerID")
|
||||||
|
val remoteStaticKey = session.getRemoteStaticPublicKey()
|
||||||
// Check if session is established
|
if (remoteStaticKey != null) {
|
||||||
if (session.isEstablished()) {
|
onSessionEstablished?.invoke(peerID, remoteStaticKey)
|
||||||
pendingHandshakes.remove(peerID)
|
|
||||||
|
|
||||||
// Get remote static key and notify
|
|
||||||
val remoteStaticKey = session.getRemoteStaticPublicKey()
|
|
||||||
if (remoteStaticKey != null) {
|
|
||||||
Log.d(TAG, "Handshake completed with $peerID")
|
|
||||||
onSessionEstablished?.invoke(peerID, remoteStaticKey)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Clean up failed session
|
|
||||||
sessions.remove(peerID)
|
|
||||||
pendingHandshakes.remove(peerID)
|
|
||||||
|
|
||||||
Log.e(TAG, "Handshake failed with $peerID: ${e.message}")
|
|
||||||
onSessionFailed?.invoke(peerID, e)
|
|
||||||
|
|
||||||
throw e
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Handshake failed with $peerID: ${e.message}")
|
||||||
|
sessions.remove(peerID)
|
||||||
|
onSessionFailed?.invoke(peerID, e)
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate handshake message format and size
|
* SIMPLIFIED: Encrypt data
|
||||||
* ADDED: Comprehensive validation for XX pattern messages
|
|
||||||
*/
|
|
||||||
private fun validateHandshakeMessage(message: ByteArray, peerID: String): Boolean {
|
|
||||||
// Basic size validation
|
|
||||||
if (message.isEmpty()) {
|
|
||||||
Log.w(TAG, "Empty handshake message from $peerID")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for reasonable size limits
|
|
||||||
if (message.size > 200) {
|
|
||||||
Log.w(TAG, "Handshake message too large from $peerID: ${message.size} bytes")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Encryption/Decryption
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encrypt data for a specific peer using established session
|
|
||||||
*/
|
*/
|
||||||
fun encrypt(data: ByteArray, peerID: String): ByteArray {
|
fun encrypt(data: ByteArray, peerID: String): ByteArray {
|
||||||
val session = getSession(peerID)
|
val session = sessions[peerID] ?: throw IllegalStateException("No session found for $peerID")
|
||||||
?: throw IllegalStateException("No session found for $peerID")
|
|
||||||
|
|
||||||
if (!session.isEstablished()) {
|
if (!session.isEstablished()) {
|
||||||
throw IllegalStateException("Session not established with $peerID")
|
throw IllegalStateException("Session not established with $peerID")
|
||||||
}
|
}
|
||||||
|
|
||||||
return session.encrypt(data)
|
return session.encrypt(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decrypt data from a specific peer using established session
|
* SIMPLIFIED: Decrypt data
|
||||||
*/
|
*/
|
||||||
fun decrypt(encryptedData: ByteArray, peerID: String): ByteArray {
|
fun decrypt(encryptedData: ByteArray, peerID: String): ByteArray {
|
||||||
val session = getSession(peerID)
|
val session = sessions[peerID]
|
||||||
?: throw IllegalStateException("No session found for $peerID")
|
if (session == null) {
|
||||||
|
Log.e(TAG, "No session found for $peerID when trying to decrypt")
|
||||||
|
throw IllegalStateException("No session found for $peerID")
|
||||||
|
}
|
||||||
if (!session.isEstablished()) {
|
if (!session.isEstablished()) {
|
||||||
|
Log.e(TAG, "Session not established with $peerID when trying to decrypt")
|
||||||
throw IllegalStateException("Session not established with $peerID")
|
throw IllegalStateException("Session not established with $peerID")
|
||||||
}
|
}
|
||||||
|
|
||||||
return session.decrypt(encryptedData)
|
return session.decrypt(encryptedData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Session Information
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if session is established with peer
|
* Check if session is established with peer
|
||||||
*/
|
*/
|
||||||
fun hasEstablishedSession(peerID: String): Boolean {
|
fun hasEstablishedSession(peerID: String): Boolean {
|
||||||
return sessionLock.read {
|
val hasSession = sessions[peerID]?.isEstablished() ?: false
|
||||||
sessions[peerID]?.isEstablished() ?: false
|
Log.d(TAG, "hasEstablishedSession($peerID): $hasSession")
|
||||||
}
|
return hasSession
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get remote static public key for a peer (if session established)
|
* Get remote static public key for a peer (if session established)
|
||||||
*/
|
*/
|
||||||
fun getRemoteStaticKey(peerID: String): ByteArray? {
|
fun getRemoteStaticKey(peerID: String): ByteArray? {
|
||||||
return getSession(peerID)?.getRemoteStaticPublicKey()
|
return sessions[peerID]?.getRemoteStaticPublicKey()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get handshake hash for channel binding (if session established)
|
* Get handshake hash for channel binding (if session established)
|
||||||
*/
|
*/
|
||||||
fun getHandshakeHash(peerID: String): ByteArray? {
|
fun getHandshakeHash(peerID: String): ByteArray? {
|
||||||
return getSession(peerID)?.getHandshakeHash()
|
return sessions[peerID]?.getHandshakeHash()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Session Rekey Support
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get sessions that need rekeying based on time or message count
|
* Get sessions that need rekeying based on time or message count
|
||||||
*/
|
*/
|
||||||
fun getSessionsNeedingRekey(): List<String> {
|
fun getSessionsNeedingRekey(): List<String> {
|
||||||
return sessionLock.read {
|
return sessions.entries
|
||||||
sessions.entries
|
.filter { (_, session) ->
|
||||||
.filter { (_, session) ->
|
session.isEstablished() && session.needsRekey()
|
||||||
session.isEstablished() && session.needsRekey()
|
|
||||||
}
|
|
||||||
.map { it.key }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Session Cleanup
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean up stale sessions and pending handshakes
|
|
||||||
*/
|
|
||||||
fun cleanupStaleSessions() {
|
|
||||||
val now = System.currentTimeMillis()
|
|
||||||
val toRemove = mutableListOf<String>()
|
|
||||||
|
|
||||||
sessionLock.write {
|
|
||||||
// Clean up old pending handshakes (30 second timeout)
|
|
||||||
pendingHandshakes.entries.removeAll { (peerID, info) ->
|
|
||||||
if (now - info.startTime > 30000) {
|
|
||||||
toRemove.add(peerID)
|
|
||||||
true
|
|
||||||
} else false
|
|
||||||
}
|
}
|
||||||
|
.map { it.key }
|
||||||
// Remove corresponding failed sessions
|
|
||||||
toRemove.forEach { peerID ->
|
|
||||||
sessions[peerID]?.destroy()
|
|
||||||
sessions.remove(peerID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce session limits (remove oldest sessions if too many)
|
|
||||||
if (sessions.size > MAX_SESSIONS) {
|
|
||||||
val oldestSessions = sessions.entries
|
|
||||||
.filter { !it.value.isEstablished() } // Remove non-established first
|
|
||||||
.sortedBy { it.value.getCreationTime() }
|
|
||||||
.take(sessions.size - MAX_SESSIONS)
|
|
||||||
|
|
||||||
oldestSessions.forEach { (peerID, session) ->
|
|
||||||
session.destroy()
|
|
||||||
sessions.remove(peerID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toRemove.isNotEmpty()) {
|
|
||||||
Log.d(TAG, "Cleaned up ${toRemove.size} stale sessions")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Private Helpers
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tie-breaker mechanism to prevent handshake storms
|
|
||||||
* FIXED: Exactly matching iOS implementation logic
|
|
||||||
*/
|
|
||||||
private fun resolveTieBreaker(peerID: String): Boolean {
|
|
||||||
// Convert our static public key and peer ID to comparable hex strings
|
|
||||||
val myPublicKeyHex = localStaticPublicKey.joinToString("") { "%02x".format(it) }
|
|
||||||
|
|
||||||
// For peer ID comparison, we need to ensure consistent format
|
|
||||||
// iOS likely uses a consistent peer ID format (hex string)
|
|
||||||
val normalizedPeerID = if (peerID.length == 64) {
|
|
||||||
// Already a hex string, use as-is
|
|
||||||
peerID.lowercase()
|
|
||||||
} else {
|
|
||||||
// Convert to consistent format - pad or hash if needed
|
|
||||||
if (peerID.length < 64) {
|
|
||||||
peerID.padEnd(64, '0').lowercase()
|
|
||||||
} else {
|
|
||||||
// Hash if too long to get consistent 64-char representation
|
|
||||||
val digest = java.security.MessageDigest.getInstance("SHA-256")
|
|
||||||
val hash = digest.digest(peerID.toByteArray())
|
|
||||||
hash.joinToString("") { "%02x".format(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The peer with lexicographically smaller static public key should initiate
|
|
||||||
// This ensures consistent behavior across iOS and Android
|
|
||||||
val shouldInitiate = myPublicKeyHex < normalizedPeerID
|
|
||||||
|
|
||||||
Log.d(TAG, "Tie-breaker: myKey=${myPublicKeyHex.take(16)}..., peerID=${normalizedPeerID.take(16)}..., shouldInitiate=$shouldInitiate")
|
|
||||||
|
|
||||||
return shouldInitiate
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if message is the first handshake message (32 bytes for XX pattern)
|
* Get debug information
|
||||||
* FIXED: More robust detection for XX pattern message 1
|
|
||||||
*/
|
|
||||||
private fun isFirstHandshakeMessage(message: ByteArray): Boolean {
|
|
||||||
// XX pattern message 1 is exactly 32 bytes (ephemeral key only)
|
|
||||||
// This is the most reliable way to detect the first message
|
|
||||||
if (message.size != 32) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Additional validation: check that it's not all zeros (invalid ephemeral key)
|
|
||||||
if (message.all { it == 0.toByte() }) {
|
|
||||||
Log.w(TAG, "Detected all-zero message, not a valid first handshake message")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Additional validation: check it's not all 0xFF (also invalid)
|
|
||||||
if (message.all { it == 0xFF.toByte() }) {
|
|
||||||
Log.w(TAG, "Detected all-0xFF message, not a valid first handshake message")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d(TAG, "Detected valid first handshake message (32 bytes)")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get debug information about session manager state
|
|
||||||
*/
|
*/
|
||||||
fun getDebugInfo(): String = buildString {
|
fun getDebugInfo(): String = buildString {
|
||||||
sessionLock.read {
|
appendLine("=== Noise Session Manager Debug ===")
|
||||||
appendLine("=== Noise Session Manager Debug ===")
|
appendLine("Active sessions: ${sessions.size}")
|
||||||
appendLine("Active sessions: ${sessions.size}")
|
appendLine("")
|
||||||
appendLine("Pending handshakes: ${pendingHandshakes.size}")
|
|
||||||
appendLine("")
|
if (sessions.isNotEmpty()) {
|
||||||
|
appendLine("Sessions:")
|
||||||
if (sessions.isNotEmpty()) {
|
sessions.forEach { (peerID, session) ->
|
||||||
appendLine("Sessions:")
|
appendLine(" $peerID: ${session.getState()}")
|
||||||
sessions.forEach { (peerID, session) ->
|
|
||||||
appendLine(" $peerID: ${session.getState()}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pendingHandshakes.isNotEmpty()) {
|
|
||||||
appendLine("Pending handshakes:")
|
|
||||||
pendingHandshakes.forEach { (peerID, info) ->
|
|
||||||
appendLine(" $peerID: step ${info.step}, ${if (info.isInitiator) "initiator" else "responder"}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -470,11 +191,8 @@ class NoiseSessionManager(
|
|||||||
* Shutdown manager and clean up all sessions
|
* Shutdown manager and clean up all sessions
|
||||||
*/
|
*/
|
||||||
fun shutdown() {
|
fun shutdown() {
|
||||||
sessionLock.write {
|
sessions.values.forEach { it.destroy() }
|
||||||
sessions.values.forEach { it.destroy() }
|
sessions.clear()
|
||||||
sessions.clear()
|
|
||||||
pendingHandshakes.clear()
|
|
||||||
}
|
|
||||||
Log.d(TAG, "Noise session manager shut down")
|
Log.d(TAG, "Noise session manager shut down")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user