Implement Noise XX Handshake Protocol for Direct Messages (#180)

* noise

* works?

* noise

* temporary

* better

* wip: use subnet

* better

* barely working

* werk

* subnet

* fix peer ID

* 8 byte peer ID

* wip noise

* wip fixes for noise

* std lib for noise

* noise handshake one step further

* buffers

* use fork

* fix imports

* simplify counter

* remove trash

* hashing

* no prologue

* nice

* wip, use noise encryption

* peer ID hex

* simplify session manager

* heavy logging

* use singleton

* Fix Noise session race condition with elegant per-peer actor serialization

- Use Kotlin coroutine actors for per-peer packet processing
- Each peer gets dedicated actor that processes packets sequentially
- Eliminates race conditions in session management without complex locking
- Single surgical change in PacketProcessor - minimal, maintainable
- Leverages Kotlin's native concurrency primitives

* decrypt correctly

* iniator works now

* clean code and fix signature to null

* better

* no signature in private message

* small fixes

* refactor ack

* refactor but untested

* messages working

* wip ack

* wip fix ack

* more logging

* pending tracker

* keep pending connections on errors

* less logging

* refactor model

* refactor frombinarydata

* idendityannouncement refactor and update to new binary protocol

* fix keys

* refix keys

* dms work

* revert to mainnet

* do not change bluetooth adapter name

* keep code but uncomment

* clean up comments

* cleanup comments
This commit is contained in:
callebtc
2025-07-24 12:01:46 +02:00
committed by GitHub
parent 9e9231353b
commit c3c395832c
58 changed files with 15209 additions and 517 deletions
@@ -1,13 +1,14 @@
package com.bitchat.android.mesh
import android.util.Log
import com.bitchat.android.crypto.MessagePadding
import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.model.DeliveryAck
import com.bitchat.android.model.NoiseIdentityAnnouncement
import com.bitchat.android.model.ReadReceipt
import com.bitchat.android.model.RoutedPacket
import com.bitchat.android.protocol.BitchatPacket
import com.bitchat.android.protocol.MessageType
import com.bitchat.android.util.toHexString
import kotlinx.coroutines.*
import java.util.*
import kotlin.random.Random
@@ -25,9 +26,142 @@ class MessageHandler(private val myPeerID: String) {
// Delegate for callbacks
var delegate: MessageHandlerDelegate? = null
// Reference to PacketProcessor for recursive packet handling
var packetProcessor: PacketProcessor? = null
// Coroutines
private val handlerScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
/**
* Handle Noise encrypted transport message
*/
suspend fun handleNoiseEncrypted(routed: RoutedPacket) {
val packet = routed.packet
val peerID = routed.peerID ?: "unknown"
Log.d(TAG, "Processing Noise encrypted message from $peerID (${packet.payload.size} bytes)")
// Skip our own messages
if (peerID == myPeerID) return
try {
// Decrypt the message using the Noise service
val decryptedData = delegate?.decryptFromPeer(packet.payload, peerID)
if (decryptedData == null) {
Log.w(TAG, "Failed to decrypt Noise message from $peerID - may need handshake")
return
}
// Check if it's a special format message (type marker + payload)
if (decryptedData.size > 1) {
val typeMarker = decryptedData[0].toUByte()
// Check if this is a delivery ACK with the new format
if (typeMarker == MessageType.DELIVERY_ACK.value) {
// Extract the ACK JSON data (skip the type marker)
val ackData = decryptedData.sliceArray(1 until decryptedData.size)
// Decode the delivery ACK
val ack = DeliveryAck.decode(ackData)
if (ack != null) {
delegate?.onDeliveryAckReceived(ack)
Log.d(TAG, "Processed delivery ACK from $peerID")
return
}
}
// Check for read receipt with type marker
if (typeMarker == MessageType.READ_RECEIPT.value) {
val receiptData = decryptedData.sliceArray(1 until decryptedData.size)
val receipt = ReadReceipt.decode(receiptData)
if (receipt != null) {
delegate?.onReadReceiptReceived(receipt)
Log.d(TAG, "Processed read receipt from $peerID")
return
}
}
}
// Try to parse as a full inner packet (for compatibility with other message types)
val innerPacket = BitchatPacket.fromBinaryData(decryptedData)
if (innerPacket != null) {
Log.d(TAG, "Decrypted inner packet type ${innerPacket.type} from $peerID")
// Create a new routed packet with the decrypted inner packet
val innerRouted = RoutedPacket(innerPacket, peerID, routed.relayAddress)
// Use PacketProcessor to handle the inner packet recursively
if (packetProcessor != null) {
packetProcessor!!.processPacket(innerRouted)
} else {
Log.w(TAG, "PacketProcessor reference is null; cannot recursively process inner packet.")
}
} else {
Log.w(TAG, "Failed to parse decrypted data as packet from $peerID")
}
} catch (e: Exception) {
Log.e(TAG, "Error processing Noise encrypted message from $peerID: ${e.message}")
}
}
/**
* Handle Noise identity announcement - supports peer ID rotation
*/
suspend fun handleNoiseIdentityAnnouncement(routed: RoutedPacket) {
val packet = routed.packet
val peerID = routed.peerID ?: "unknown"
Log.d(TAG, "Processing Noise identity announcement from $peerID (${packet.payload.size} bytes)")
// Skip our own announcements
if (peerID == myPeerID) return
try {
// Parse the identity announcement
val announcement = NoiseIdentityAnnouncement.fromBinaryData(packet.payload)
if (announcement == null) {
Log.w(TAG, "Failed to parse Noise identity announcement from $peerID")
return
}
Log.d(TAG, "Parsed identity announcement: peerID=${announcement.peerID}, " +
"nickname=${announcement.nickname}, fingerprint=${announcement.fingerprint?.take(16)}...")
// Verify the announcement signature (basic validation)
// In a full implementation, this would use cryptographic verification
if (announcement.signature.isEmpty()) {
Log.w(TAG, "Identity announcement from $peerID has no signature")
return
}
// Update peer binding in the delegate (ChatViewModel/BluetoothMeshService)
delegate?.updatePeerIDBinding(
newPeerID = announcement.peerID,
fingerprint = announcement.fingerprint ?: "",
nickname = announcement.nickname,
publicKey = announcement.publicKey,
previousPeerID = announcement.previousPeerID
)
// Check if we need to initiate a handshake with this peer
val hasSession = delegate?.hasNoiseSession(announcement.peerID) ?: false
if (!hasSession) {
Log.d(TAG, "No session with ${announcement.peerID}, may need handshake")
// Use lexicographic comparison to decide who initiates (prevents both sides from initiating)
if (myPeerID < announcement.peerID) {
delegate?.initiateNoiseHandshake(announcement.peerID)
}
}
Log.d(TAG, "Successfully processed identity announcement from $peerID")
} catch (e: Exception) {
Log.e(TAG, "Error processing Noise identity announcement from $peerID: ${e.message}")
}
}
/**
* Handle announce message
*/
@@ -62,11 +196,14 @@ class MessageHandler(private val myPeerID: String) {
if (peerID == myPeerID) return
val recipientID = packet.recipientID?.takeIf { !it.contentEquals(delegate?.getBroadcastRecipient()) }
var recipientIDString = ""
if (recipientID != null) {
recipientIDString = recipientID.toHexString()
}
if (recipientID == null) {
// BROADCAST MESSAGE
handleBroadcastMessage(routed)
} else if (String(recipientID).replace("\u0000", "") == myPeerID) {
} else if (recipientID.toHexString() == myPeerID) {
// PRIVATE MESSAGE FOR US
handlePrivateMessage(packet, peerID)
} else if (packet.ttl > 0u) {
@@ -120,7 +257,7 @@ class MessageHandler(private val myPeerID: String) {
}
/**
* Handle private message addressed to us
* Handle (decrypted) private message addressed to us
*/
private suspend fun handlePrivateMessage(packet: BitchatPacket, peerID: String) {
try {
@@ -129,18 +266,9 @@ class MessageHandler(private val myPeerID: String) {
Log.w(TAG, "Invalid signature for private message from $peerID")
return
}
// Decrypt message
val decryptedData = delegate?.decryptFromPeer(packet.payload, peerID)
if (decryptedData == null) {
Log.e(TAG, "Failed to decrypt private message from $peerID")
return
}
val unpaddedData = MessagePadding.unpad(decryptedData)
// Parse message
val message = BitchatMessage.fromBinaryPayload(unpaddedData)
val message = BitchatMessage.fromBinaryPayload(packet.payload)
if (message != null) {
// Check for cover traffic (dummy messages)
if (message.content.startsWith("☂DUMMY☂")) {
@@ -149,17 +277,10 @@ class MessageHandler(private val myPeerID: String) {
}
delegate?.updatePeerNickname(peerID, message.sender)
// Replace timestamp with current time (same as iOS)
val messageWithCurrentTime = message.copy(
senderPeerID = peerID,
timestamp = Date() // Use current time instead of original timestamp
)
delegate?.onMessageReceived(messageWithCurrentTime)
delegate?.onMessageReceived(message)
// Send delivery ACK
sendDeliveryAck(message, peerID)
// delegate?.sendDeliveryAck(message, peerID)
}
} catch (e: Exception) {
@@ -272,42 +393,6 @@ class MessageHandler(private val myPeerID: String) {
}
}
/**
* Send delivery acknowledgment for a received private message
*/
private fun sendDeliveryAck(message: BitchatMessage, senderPeerID: String) {
handlerScope.launch {
val nickname = delegate?.getMyNickname() ?: myPeerID
val ack = DeliveryAck(
originalMessageID = message.id,
recipientID = myPeerID,
recipientNickname = nickname,
hopCount = 0u // Will be calculated during relay
)
try {
val ackData = ack.encode() ?: return@launch
val encryptedPayload = delegate?.encryptForPeer(ackData, senderPeerID)
if (encryptedPayload != null) {
val packet = BitchatPacket(
type = MessageType.DELIVERY_ACK.value,
senderID = myPeerID.toByteArray(),
recipientID = senderPeerID.toByteArray(),
timestamp = System.currentTimeMillis().toULong(),
payload = encryptedPayload,
signature = null,
ttl = 3u
)
delegate?.sendPacket(packet)
}
} catch (e: Exception) {
Log.e(TAG, "Failed to send delivery ACK: ${e.message}")
}
}
}
/**
* Get debug information
*/
@@ -319,6 +404,27 @@ class MessageHandler(private val myPeerID: String) {
}
}
/**
* Convert hex string peer ID to binary data (8 bytes) - same as iOS implementation
*/
private fun hexStringToByteArray(hexString: String): ByteArray {
val result = ByteArray(8) { 0 } // Initialize with zeros, exactly 8 bytes
var tempID = hexString
var index = 0
while (tempID.length >= 2 && index < 8) {
val hexByte = tempID.substring(0, 2)
val byte = hexByte.toIntOrNull(16)?.toByte()
if (byte != null) {
result[index] = byte
}
tempID = tempID.substring(2)
index++
}
return result
}
/**
* Shutdown the handler
*/
@@ -349,8 +455,15 @@ interface MessageHandlerDelegate {
fun encryptForPeer(data: ByteArray, recipientPeerID: String): ByteArray?
fun decryptFromPeer(encryptedData: ByteArray, senderPeerID: String): ByteArray?
// Noise protocol operations
fun hasNoiseSession(peerID: String): Boolean
fun initiateNoiseHandshake(peerID: String)
fun updatePeerIDBinding(newPeerID: String, fingerprint: String, nickname: String,
publicKey: ByteArray, previousPeerID: String?)
// Message operations
fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String?
fun sendDeliveryAck(message: BitchatMessage, senderPeerID: String)
// Callbacks
fun onMessageReceived(message: BitchatMessage)