Init handshake on dm (#187)

* kinda working

* establish handshake if not done yet

* add handshakerequest packet

* read receipt
This commit is contained in:
callebtc
2025-07-25 19:11:45 +02:00
committed by GitHub
parent 565e4ab33c
commit 34b3d8dd82
7 changed files with 338 additions and 30 deletions
@@ -5,6 +5,7 @@ import android.util.Log
import com.bitchat.android.crypto.EncryptionService import com.bitchat.android.crypto.EncryptionService
import com.bitchat.android.protocol.MessagePadding import com.bitchat.android.protocol.MessagePadding
import com.bitchat.android.model.BitchatMessage import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.model.HandshakeRequest
import com.bitchat.android.model.RoutedPacket import com.bitchat.android.model.RoutedPacket
import com.bitchat.android.model.DeliveryAck import com.bitchat.android.model.DeliveryAck
import com.bitchat.android.model.ReadReceipt import com.bitchat.android.model.ReadReceipt
@@ -556,7 +557,6 @@ class BluetoothMeshService(private val context: Context) {
) )
try { try {
// TODO: THIS FORMAT FOR DELIVERY ACKS SHOULD BE DEPRECATED
val ackData = ack.encode() ?: return val ackData = ack.encode() ?: return
val typeMarker = MessageType.DELIVERY_ACK.value.toByte() val typeMarker = MessageType.DELIVERY_ACK.value.toByte()
val payloadWithMarker = byteArrayOf(typeMarker) + ackData val payloadWithMarker = byteArrayOf(typeMarker) + ackData
@@ -591,36 +591,41 @@ class BluetoothMeshService(private val context: Context) {
*/ */
fun sendReadReceipt(messageID: String, recipientPeerID: String, readerNickname: String) { fun sendReadReceipt(messageID: String, recipientPeerID: String, readerNickname: String) {
serviceScope.launch { serviceScope.launch {
// Create the read receipt
val receipt = ReadReceipt(
originalMessageID = messageID,
readerID = myPeerID,
readerNickname = readerNickname
)
try { try {
Log.d(TAG, "Sending read receipt for message $messageID to $recipientPeerID")
// Create the read receipt
val receipt = ReadReceipt(
originalMessageID = messageID,
readerID = myPeerID,
readerNickname = readerNickname
)
// Encode the receipt // Encode the receipt
val receiptData = receipt.encode() val receiptData = receipt.encode()
val typeMarker = MessageType.READ_RECEIPT.value.toByte()
// Create inner read receipt packet val payloadWithMarker = byteArrayOf(typeMarker) + receiptData
val innerPacket = BitchatPacket( val encryptedPayload = securityManager.encryptForPeer(payloadWithMarker, recipientPeerID)
version = 1u,
type = MessageType.READ_RECEIPT.value, if (encryptedPayload == null) {
Log.w(TAG, "Failed to encrypt delivery ACK for $recipientPeerID")
return@launch
}
// Create inner packet with the delivery ACK data
val packet = BitchatPacket(
type = MessageType.NOISE_ENCRYPTED.value,
senderID = hexStringToByteArray(myPeerID), senderID = hexStringToByteArray(myPeerID),
recipientID = hexStringToByteArray(recipientPeerID), recipientID = hexStringToByteArray(recipientPeerID),
timestamp = System.currentTimeMillis().toULong(), timestamp = System.currentTimeMillis().toULong(),
payload = receiptData, payload = encryptedPayload,
signature = null, signature = null,
ttl = 3u ttl = 3u
) )
// Encrypt the entire inner packet and send as NOISE_ENCRYPTED Log.d(TAG, "Sending read receipt for message $messageID to $recipientPeerID")
encryptAndBroadcastNoisePacket(innerPacket, recipientPeerID)
// Use the new encrypt and broadcast function
Log.d(TAG, "Sent read receipt for message $messageID to $recipientPeerID") connectionManager.broadcastPacket(RoutedPacket(packet))
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to send read receipt for message $messageID: ${e.message}") Log.e(TAG, "Failed to send read receipt for message $messageID: ${e.message}")
} }
@@ -708,10 +713,9 @@ class BluetoothMeshService(private val context: Context) {
} }
/** /**
* Send Noise identity announcement (broadcast our static public key and signing key) * Send key exchange to newly connected device
* Now properly formatted as NoiseIdentityAnnouncement to match iOS
*/ */
private fun sendKeyExchangeToDevice() { fun sendKeyExchangeToDevice() {
serviceScope.launch { serviceScope.launch {
try { try {
val nickname = delegate?.getNickname() ?: myPeerID val nickname = delegate?.getNickname() ?: myPeerID
@@ -743,6 +747,43 @@ class BluetoothMeshService(private val context: Context) {
} }
} }
/**
* Send handshake request to target peer for pending messages
*/
fun sendHandshakeRequest(targetPeerID: String, pendingCount: UByte) {
serviceScope.launch {
try {
// Create handshake request
val request = HandshakeRequest(
requesterID = myPeerID,
requesterNickname = delegate?.getNickname() ?: myPeerID,
targetID = targetPeerID,
pendingMessageCount = pendingCount
)
val requestData = request.toBinaryData()
// Create packet for handshake request
val packet = BitchatPacket(
version = 1u,
type = MessageType.HANDSHAKE_REQUEST.value,
senderID = hexStringToByteArray(myPeerID),
recipientID = hexStringToByteArray(targetPeerID),
timestamp = System.currentTimeMillis().toULong(),
payload = requestData,
ttl = 6u
)
// Broadcast the packet (Android equivalent of both direct and relay attempts)
connectionManager.broadcastPacket(RoutedPacket(packet))
Log.d(TAG, "Sent handshake request to $targetPeerID (pending: $pendingCount, ${requestData.size} bytes)")
} catch (e: Exception) {
Log.e(TAG, "Failed to send handshake request to $targetPeerID: ${e.message}")
}
}
}
/** /**
* Create a properly formatted NoiseIdentityAnnouncement exactly like iOS * Create a properly formatted NoiseIdentityAnnouncement exactly like iOS
*/ */
@@ -822,6 +863,14 @@ class BluetoothMeshService(private val context: Context) {
return encryptionService.hasEstablishedSession(peerID) return encryptionService.hasEstablishedSession(peerID)
} }
/**
* Initiate Noise handshake with a specific peer (public API)
*/
fun initiateNoiseHandshake(peerID: String) {
// Delegate to the existing implementation in the MessageHandler delegate
messageHandler.delegate?.initiateNoiseHandshake(peerID)
}
/** /**
* Get peer fingerprint for identity management * Get peer fingerprint for identity management
*/ */
@@ -63,7 +63,6 @@ class MessageHandler(private val myPeerID: String) {
} }
// Check for read receipt with type marker // Check for read receipt with type marker
// NOTE: THIS DOESN'T WORK WITH IOS, IT SENDS AN INNER PACKET INSTEAD
if (typeMarker == MessageType.READ_RECEIPT.value) { if (typeMarker == MessageType.READ_RECEIPT.value) {
val receiptData = decryptedData.sliceArray(1 until decryptedData.size) val receiptData = decryptedData.sliceArray(1 until decryptedData.size)
val receipt = ReadReceipt.decode(receiptData) val receipt = ReadReceipt.decode(receiptData)
@@ -139,8 +139,8 @@ class PacketProcessor(private val myPeerID: String) {
MessageType.NOISE_HANDSHAKE_INIT -> handleNoiseHandshake(routed, 1) MessageType.NOISE_HANDSHAKE_INIT -> handleNoiseHandshake(routed, 1)
MessageType.NOISE_HANDSHAKE_RESP -> handleNoiseHandshake(routed, 2) MessageType.NOISE_HANDSHAKE_RESP -> handleNoiseHandshake(routed, 2)
MessageType.NOISE_ENCRYPTED -> handleNoiseEncrypted(routed) MessageType.NOISE_ENCRYPTED -> handleNoiseEncrypted(routed)
//MessageType.DELIVERY_ACK -> handleDeliveryAck(routed) // custom packet type... // MessageType.DELIVERY_ACK -> handleDeliveryAck(routed) // custom packet type...
MessageType.READ_RECEIPT -> handleReadReceipt(routed) // MessageType.READ_RECEIPT -> handleReadReceipt(routed)
else -> { else -> {
validPacket = false validPacket = false
Log.w(TAG, "Unknown message type: ${packet.type}") Log.w(TAG, "Unknown message type: ${packet.type}")
@@ -0,0 +1,122 @@
package com.bitchat.android.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import com.bitchat.android.util.*
import java.util.*
/**
* Handshake request for pending messages - exact same as iOS version
* Uses binary encoding for efficient protocol communication
*/
@Parcelize
data class HandshakeRequest(
val requestID: String,
val requesterID: String, // Who needs the handshake
val requesterNickname: String, // Nickname of requester
val targetID: String, // Who should initiate handshake
val pendingMessageCount: UByte, // Number of messages queued
val timestamp: Date
) : Parcelable {
// Primary constructor for creating new requests
constructor(requesterID: String, requesterNickname: String, targetID: String, pendingMessageCount: UByte) : this(
requestID = UUID.randomUUID().toString(),
requesterID = requesterID,
requesterNickname = requesterNickname,
targetID = targetID,
pendingMessageCount = pendingMessageCount,
timestamp = Date()
)
/**
* Encode to binary data matching iOS toBinaryData implementation
*/
fun toBinaryData(): ByteArray {
val builder = BinaryDataBuilder()
// Append request ID UUID
builder.appendUUID(requestID)
// RequesterID as 8-byte hex string
val requesterData = ByteArray(8) { 0 }
var tempID = requesterID
var index = 0
while (tempID.length >= 2 && index < 8) {
val hexByte = tempID.substring(0, 2)
val byte = hexByte.toIntOrNull(16)?.toByte()
if (byte != null) {
requesterData[index] = byte
}
tempID = tempID.substring(2)
index++
}
builder.buffer.addAll(requesterData.toList())
// TargetID as 8-byte hex string
val targetData = ByteArray(8) { 0 }
tempID = targetID
index = 0
while (tempID.length >= 2 && index < 8) {
val hexByte = tempID.substring(0, 2)
val byte = hexByte.toIntOrNull(16)?.toByte()
if (byte != null) {
targetData[index] = byte
}
tempID = tempID.substring(2)
index++
}
builder.buffer.addAll(targetData.toList())
// Append pending message count (UInt8)
builder.appendUInt8(pendingMessageCount)
// Append timestamp
builder.appendDate(timestamp)
// Append requester nickname as string
builder.appendString(requesterNickname)
return builder.toByteArray()
}
companion object {
/**
* Decode from binary data matching iOS fromBinaryData implementation
*/
fun fromBinaryData(data: ByteArray): HandshakeRequest? {
// Create defensive copy
val dataCopy = data.copyOf()
// Minimum size: UUID (16) + requesterID (8) + targetID (8) + count (1) + timestamp (8) + min nickname
if (dataCopy.size < 42) return null
val offset = intArrayOf(0)
val requestID = dataCopy.readUUID(offset) ?: return null
val requesterIDData = dataCopy.readFixedBytes(offset, 8) ?: return null
val requesterID = requesterIDData.hexEncodedString()
val targetIDData = dataCopy.readFixedBytes(offset, 8) ?: return null
val targetID = targetIDData.hexEncodedString()
val pendingMessageCount = dataCopy.readUInt8(offset) ?: return null
val timestamp = dataCopy.readDate(offset) ?: return null
val requesterNickname = dataCopy.readString(offset) ?: return null
return HandshakeRequest(
requestID = requestID,
requesterID = requesterID,
requesterNickname = requesterNickname,
targetID = targetID,
pendingMessageCount = pendingMessageCount,
timestamp = timestamp
)
}
}
}
@@ -32,6 +32,7 @@ enum class MessageType(val value: UByte) {
CHANNEL_KEY_VERIFY_RESPONSE(0x15u), // Response to key verification request CHANNEL_KEY_VERIFY_RESPONSE(0x15u), // Response to key verification request
CHANNEL_PASSWORD_UPDATE(0x16u), // Distribute new password to channel members CHANNEL_PASSWORD_UPDATE(0x16u), // Distribute new password to channel members
CHANNEL_METADATA(0x17u), // Announce channel creator and metadata CHANNEL_METADATA(0x17u), // Announce channel creator and metadata
HANDSHAKE_REQUEST(0x25u), // Request handshake initiation for pending messages
// Protocol version negotiation // Protocol version negotiation
VERSION_HELLO(0x20u), // Initial version announcement VERSION_HELLO(0x20u), // Initial version announcement
@@ -32,7 +32,17 @@ class ChatViewModel(
private val dataManager = DataManager(application.applicationContext) private val dataManager = DataManager(application.applicationContext)
private val messageManager = MessageManager(state) private val messageManager = MessageManager(state)
private val channelManager = ChannelManager(state, messageManager, dataManager, viewModelScope) private val channelManager = ChannelManager(state, messageManager, dataManager, viewModelScope)
val privateChatManager = PrivateChatManager(state, messageManager, dataManager)
// Create Noise session delegate for clean dependency injection
private val noiseSessionDelegate = object : NoiseSessionDelegate {
override fun hasEstablishedSession(peerID: String): Boolean = meshService.hasEstablishedSession(peerID)
override fun initiateHandshake(peerID: String) = meshService.initiateNoiseHandshake(peerID)
override fun sendIdentityAnnouncement() = meshService.sendKeyExchangeToDevice()
override fun sendHandshakeRequest(targetPeerID: String, pendingCount: UByte) = meshService.sendHandshakeRequest(targetPeerID, pendingCount)
override fun getMyPeerID(): String = meshService.myPeerID
}
val privateChatManager = PrivateChatManager(state, messageManager, dataManager, noiseSessionDelegate)
private val commandProcessor = CommandProcessor(state, messageManager, channelManager, privateChatManager) private val commandProcessor = CommandProcessor(state, messageManager, channelManager, privateChatManager)
private val notificationManager = NotificationManager(application.applicationContext) private val notificationManager = NotificationManager(application.applicationContext)
@@ -6,6 +6,18 @@ import com.bitchat.android.mesh.PeerFingerprintManager
import java.util.* import java.util.*
import android.util.Log import android.util.Log
/**
* Interface for Noise session operations needed by PrivateChatManager
* This avoids reflection and makes dependencies explicit
*/
interface NoiseSessionDelegate {
fun hasEstablishedSession(peerID: String): Boolean
fun initiateHandshake(peerID: String)
fun sendIdentityAnnouncement()
fun sendHandshakeRequest(targetPeerID: String, pendingCount: UByte)
fun getMyPeerID(): String
}
/** /**
* Handles private chat functionality including peer management and blocking * Handles private chat functionality including peer management and blocking
* Now uses centralized PeerFingerprintManager for all fingerprint operations * Now uses centralized PeerFingerprintManager for all fingerprint operations
@@ -13,7 +25,8 @@ import android.util.Log
class PrivateChatManager( class PrivateChatManager(
private val state: ChatState, private val state: ChatState,
private val messageManager: MessageManager, private val messageManager: MessageManager,
private val dataManager: DataManager private val dataManager: DataManager,
private val noiseSessionDelegate: NoiseSessionDelegate? = null
) { ) {
companion object { companion object {
@@ -41,6 +54,9 @@ class PrivateChatManager(
return false return false
} }
// Establish Noise session if needed before starting the chat
establishNoiseSessionIfNeeded(peerID, meshService)
state.setSelectedPrivateChatPeer(peerID) state.setSelectedPrivateChatPeer(peerID)
// Clear unread // Clear unread
@@ -300,6 +316,117 @@ class PrivateChatManager(
Log.d(TAG, "Cleaned up unread messages for disconnected peer $peerID") Log.d(TAG, "Cleaned up unread messages for disconnected peer $peerID")
} }
// MARK: - Noise Session Management
/**
* Establish Noise session if needed before starting private chat
* Uses same lexicographical logic as MessageHandler.handleNoiseIdentityAnnouncement
*/
private fun establishNoiseSessionIfNeeded(peerID: String, meshService: Any) {
// If we have a clean delegate, use it; otherwise fall back to reflection for backward compatibility
if (noiseSessionDelegate != null) {
if (noiseSessionDelegate.hasEstablishedSession(peerID)) {
Log.d(TAG, "Noise session already established with $peerID")
return
}
Log.d(TAG, "No Noise session with $peerID, determining who should initiate handshake")
val myPeerID = noiseSessionDelegate.getMyPeerID()
// Use lexicographical comparison to decide who initiates (same logic as MessageHandler)
if (myPeerID < peerID) {
// We should initiate the handshake
Log.d(TAG, "Our peer ID lexicographically < target peer ID, initiating Noise handshake with $peerID")
noiseSessionDelegate.initiateHandshake(peerID)
} else {
// They should initiate, we send a Noise identity announcement
Log.d(TAG, "Our peer ID lexicographically >= target peer ID, sending Noise identity announcement to prompt handshake from $peerID")
noiseSessionDelegate.sendIdentityAnnouncement()
// Also send handshake request to this peer
noiseSessionDelegate.sendHandshakeRequest(peerID, 1u) // 1 pending message (the chat we're trying to start)
}
} else {
// Fallback to reflection-based approach for backward compatibility
establishNoiseSessionIfNeededLegacy(peerID, meshService)
}
}
/**
* Legacy reflection-based implementation for backward compatibility
*/
private fun establishNoiseSessionIfNeededLegacy(peerID: String, meshService: Any) {
try {
// Check if we already have an established Noise session with this peer
val hasSessionMethod = meshService::class.java.getDeclaredMethod("hasEstablishedSession", String::class.java)
val hasSession = hasSessionMethod.invoke(meshService, peerID) as Boolean
if (hasSession) {
Log.d(TAG, "Noise session already established with $peerID")
return
}
Log.d(TAG, "No Noise session with $peerID, determining who should initiate handshake")
// Get our peer ID from mesh service for lexicographical comparison
val myPeerIDField = meshService::class.java.getField("myPeerID")
val myPeerID = myPeerIDField.get(meshService) as String
// Use lexicographical comparison to decide who initiates (same logic as MessageHandler)
if (myPeerID < peerID) {
// We should initiate the handshake
Log.d(TAG, "Our peer ID lexicographically < target peer ID, initiating Noise handshake with $peerID")
initiateHandshakeWithPeer(peerID, meshService)
} else {
// They should initiate, we send a Noise identity announcement
Log.d(TAG, "Our peer ID lexicographically >= target peer ID, sending Noise identity announcement to prompt handshake from $peerID")
sendNoiseIdentityAnnouncement(meshService)
}
} catch (e: Exception) {
Log.e(TAG, "Failed to establish Noise session with $peerID: ${e.message}")
}
}
/**
* Initiate handshake with specific peer using the existing delegate pattern
*/
private fun initiateHandshakeWithPeer(peerID: String, meshService: Any) {
try {
// Use the existing MessageHandler delegate approach to initiate handshake
// This calls the same code that's in MessageHandler's delegate.initiateNoiseHandshake()
val messageHandler = meshService::class.java.getDeclaredField("messageHandler")
messageHandler.isAccessible = true
val handler = messageHandler.get(meshService)
val delegate = handler::class.java.getDeclaredField("delegate")
delegate.isAccessible = true
val handlerDelegate = delegate.get(handler)
val method = handlerDelegate::class.java.getMethod("initiateNoiseHandshake", String::class.java)
method.invoke(handlerDelegate, peerID)
Log.d(TAG, "Successfully initiated Noise handshake with $peerID using delegate pattern")
} catch (e: Exception) {
Log.e(TAG, "Failed to initiate Noise handshake with $peerID: ${e.message}")
}
}
/**
* Send Noise identity announcement to prompt other peer to initiate handshake
* This follows the same pattern as sendKeyExchangeToDevice() in BluetoothMeshService
*/
private fun sendNoiseIdentityAnnouncement(meshService: Any) {
try {
// Call sendKeyExchangeToDevice which sends a NoiseIdentityAnnouncement
val method = meshService::class.java.getDeclaredMethod("sendKeyExchangeToDevice")
method.invoke(meshService)
Log.d(TAG, "Successfully sent Noise identity announcement")
} catch (e: Exception) {
Log.e(TAG, "Failed to send Noise identity announcement: ${e.message}")
}
}
// MARK: - Utility Functions // MARK: - Utility Functions
private fun getPeerIDForNickname(nickname: String, meshService: Any): String? { private fun getPeerIDForNickname(nickname: String, meshService: Any): String? {