mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 11:25:21 +00:00
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:
@@ -1,6 +1,7 @@
|
||||
package com.bitchat.android.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.GsonBuilder
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
@@ -341,67 +342,4 @@ data class BitchatMessage(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delivery acknowledgment structure - exact same as iOS version
|
||||
*/
|
||||
@Parcelize
|
||||
data class DeliveryAck(
|
||||
val originalMessageID: String,
|
||||
val ackID: String = UUID.randomUUID().toString(),
|
||||
val recipientID: String,
|
||||
val recipientNickname: String,
|
||||
val timestamp: Date = Date(),
|
||||
val hopCount: UByte
|
||||
) : Parcelable {
|
||||
|
||||
fun encode(): ByteArray? {
|
||||
return try {
|
||||
com.google.gson.Gson().toJson(this).toByteArray(Charsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun decode(data: ByteArray): DeliveryAck? {
|
||||
return try {
|
||||
val json = String(data, Charsets.UTF_8)
|
||||
com.google.gson.Gson().fromJson(json, DeliveryAck::class.java)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read receipt structure - exact same as iOS version
|
||||
*/
|
||||
@Parcelize
|
||||
data class ReadReceipt(
|
||||
val originalMessageID: String,
|
||||
val receiptID: String = UUID.randomUUID().toString(),
|
||||
val readerID: String,
|
||||
val readerNickname: String,
|
||||
val timestamp: Date = Date()
|
||||
) : Parcelable {
|
||||
|
||||
fun encode(): ByteArray? {
|
||||
return try {
|
||||
com.google.gson.Gson().toJson(this).toByteArray(Charsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun decode(data: ByteArray): ReadReceipt? {
|
||||
return try {
|
||||
val json = String(data, Charsets.UTF_8)
|
||||
com.google.gson.Gson().fromJson(json, ReadReceipt::class.java)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.bitchat.android.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.GsonBuilder
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Delivery acknowledgment structure - exact same as iOS version
|
||||
*/
|
||||
@Parcelize
|
||||
data class DeliveryAck(
|
||||
val originalMessageID: String,
|
||||
val ackID: String = UUID.randomUUID().toString(),
|
||||
val recipientID: String,
|
||||
val recipientNickname: String,
|
||||
val timestamp: Date = Date(),
|
||||
val hopCount: UInt
|
||||
) : Parcelable {
|
||||
|
||||
private val gson = GsonBuilder()
|
||||
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
|
||||
.create()
|
||||
|
||||
fun encode(): ByteArray? {
|
||||
return try {
|
||||
gson.toJson(this).toByteArray(Charsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun decode(data: ByteArray): DeliveryAck? {
|
||||
return try {
|
||||
val json = String(data, Charsets.UTF_8)
|
||||
com.google.gson.Gson().fromJson(json, DeliveryAck::class.java)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package com.bitchat.android.model
|
||||
|
||||
import android.util.Log
|
||||
import com.bitchat.android.util.*
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Noise Identity Announcement data class (compatible with iOS version)
|
||||
* Enhanced identity announcement with rotation support and binary protocol
|
||||
*/
|
||||
data class NoiseIdentityAnnouncement(
|
||||
val peerID: String, // Current ephemeral peer ID
|
||||
val publicKey: ByteArray, // Noise static public key
|
||||
val signingPublicKey: ByteArray, // Ed25519 signing public key
|
||||
val nickname: String, // Current nickname
|
||||
val timestamp: Date, // When this binding was created
|
||||
val previousPeerID: String?, // Previous peer ID (for smooth transition)
|
||||
val signature: ByteArray // Signature proving ownership
|
||||
) {
|
||||
|
||||
// Computed fingerprint from public key
|
||||
val fingerprint: String by lazy {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
val hash = digest.digest(publicKey)
|
||||
hash.joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as NoiseIdentityAnnouncement
|
||||
|
||||
if (peerID != other.peerID) return false
|
||||
if (nickname != other.nickname) return false
|
||||
if (!publicKey.contentEquals(other.publicKey)) return false
|
||||
if (!signingPublicKey.contentEquals(other.signingPublicKey)) return false
|
||||
if (timestamp != other.timestamp) return false
|
||||
if (!signature.contentEquals(other.signature)) return false
|
||||
if (previousPeerID != other.previousPeerID) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = peerID.hashCode()
|
||||
result = 31 * result + nickname.hashCode()
|
||||
result = 31 * result + publicKey.contentHashCode()
|
||||
result = 31 * result + signingPublicKey.contentHashCode()
|
||||
result = 31 * result + timestamp.hashCode()
|
||||
result = 31 * result + signature.contentHashCode()
|
||||
result = 31 * result + (previousPeerID?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
// MARK: - Binary Encoding
|
||||
|
||||
fun toBinaryData(): ByteArray {
|
||||
val builder = BinaryDataBuilder()
|
||||
|
||||
// Flags byte: bit 0 = hasPreviousPeerID
|
||||
var flags: UByte = 0u
|
||||
if (previousPeerID != null) flags = flags or 0x01u
|
||||
builder.appendUInt8(flags)
|
||||
|
||||
// PeerID as 8-byte hex string
|
||||
val peerData = hexStringToByteArray(peerID)
|
||||
// Directly append the 8 bytes without length prefix since this is a fixed field
|
||||
for (byte in peerData) {
|
||||
builder.buffer.add(byte)
|
||||
}
|
||||
|
||||
builder.appendData(publicKey)
|
||||
builder.appendData(signingPublicKey)
|
||||
builder.appendString(nickname)
|
||||
builder.appendDate(timestamp)
|
||||
|
||||
if (previousPeerID != null) {
|
||||
// Previous PeerID as 8-byte hex string
|
||||
val prevData = hexStringToByteArray(previousPeerID)
|
||||
// Directly append the 8 bytes without length prefix since this is a fixed field
|
||||
for (byte in prevData) {
|
||||
builder.buffer.add(byte)
|
||||
}
|
||||
}
|
||||
|
||||
builder.appendData(signature)
|
||||
|
||||
return builder.toByteArray()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "NoiseIdentityAnnouncement"
|
||||
|
||||
/**
|
||||
* Parse Noise identity announcement from binary payload with proper iOS compatibility
|
||||
*/
|
||||
fun fromBinaryData(data: ByteArray): NoiseIdentityAnnouncement? {
|
||||
return try {
|
||||
// Create defensive copy
|
||||
val dataCopy = data.copyOf()
|
||||
|
||||
// Minimum size check: flags(1) + peerID(8) + min data lengths
|
||||
if (dataCopy.size < 20) {
|
||||
Log.w(TAG, "Data too small for NoiseIdentityAnnouncement: ${dataCopy.size} bytes")
|
||||
return null
|
||||
}
|
||||
|
||||
val offsetArray = intArrayOf(0)
|
||||
|
||||
val flags = dataCopy.readUInt8(offsetArray) ?: run {
|
||||
Log.w(TAG, "Failed to read flags")
|
||||
return null
|
||||
}
|
||||
val hasPreviousPeerID = (flags.toInt() and 0x01) != 0
|
||||
|
||||
// Read peerID using safe method
|
||||
val peerIDBytes = dataCopy.readFixedBytes(offsetArray, 8) ?: run {
|
||||
Log.w(TAG, "Failed to read peerID bytes")
|
||||
return null
|
||||
}
|
||||
val peerID = peerIDBytes.hexEncodedString()
|
||||
|
||||
val publicKey = dataCopy.readData(offsetArray) ?: run {
|
||||
Log.w(TAG, "Failed to read public key")
|
||||
return null
|
||||
}
|
||||
|
||||
val signingPublicKey = dataCopy.readData(offsetArray) ?: run {
|
||||
Log.w(TAG, "Failed to read signing public key")
|
||||
return null
|
||||
}
|
||||
|
||||
val nickname = dataCopy.readString(offsetArray) ?: run {
|
||||
Log.w(TAG, "Failed to read nickname")
|
||||
return null
|
||||
}
|
||||
|
||||
val timestamp = dataCopy.readDate(offsetArray) ?: run {
|
||||
Log.w(TAG, "Failed to read timestamp")
|
||||
return null
|
||||
}
|
||||
|
||||
var previousPeerID: String? = null
|
||||
if (hasPreviousPeerID) {
|
||||
// Read previousPeerID using safe method
|
||||
val prevIDBytes = dataCopy.readFixedBytes(offsetArray, 8) ?: run {
|
||||
Log.w(TAG, "Failed to read previousPeerID bytes")
|
||||
return null
|
||||
}
|
||||
previousPeerID = prevIDBytes.hexEncodedString()
|
||||
}
|
||||
|
||||
val signature = dataCopy.readData(offsetArray) ?: run {
|
||||
Log.w(TAG, "Failed to read signature")
|
||||
return null
|
||||
}
|
||||
|
||||
Log.d(TAG, "Successfully parsed NoiseIdentityAnnouncement: peerID=$peerID, nickname=$nickname")
|
||||
|
||||
return NoiseIdentityAnnouncement(
|
||||
peerID = peerID,
|
||||
publicKey = publicKey,
|
||||
signingPublicKey = signingPublicKey,
|
||||
nickname = nickname,
|
||||
timestamp = timestamp,
|
||||
previousPeerID = previousPeerID,
|
||||
signature = signature
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to parse Noise identity announcement: ${e.message}", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert hex string peer ID to binary data (8 bytes) - exactly same as iOS
|
||||
*/
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.bitchat.android.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Read receipt structure - exact same as iOS version
|
||||
*/
|
||||
@Parcelize
|
||||
data class ReadReceipt(
|
||||
val originalMessageID: String,
|
||||
val receiptID: String = UUID.randomUUID().toString(),
|
||||
val readerID: String,
|
||||
val readerNickname: String,
|
||||
val timestamp: Date = Date()
|
||||
) : Parcelable {
|
||||
|
||||
fun encode(): ByteArray? {
|
||||
return try {
|
||||
com.google.gson.Gson().toJson(this).toByteArray(Charsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun decode(data: ByteArray): ReadReceipt? {
|
||||
return try {
|
||||
val json = String(data, Charsets.UTF_8)
|
||||
com.google.gson.Gson().fromJson(json, ReadReceipt::class.java)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user