Files
bitchat-android/app/src/main/java/com/bitchat/android/nostr/NostrEmbeddedBitChat.kt
T
callebtcandGitHub 1486121b77 Extract constants (#469)
* extract constants

* refactor constants
2025-10-12 20:59:37 +02:00

235 lines
8.4 KiB
Kotlin

package com.bitchat.android.nostr
import android.util.Base64
import android.util.Log
import com.bitchat.android.model.PrivateMessagePacket
import com.bitchat.android.model.NoisePayloadType
import com.bitchat.android.protocol.BitchatPacket
import com.bitchat.android.protocol.MessageType
import java.util.*
/**
* BitChat-over-Nostr Adapter
* Direct port from iOS implementation for 100% compatibility
*/
object NostrEmbeddedBitChat {
private const val TAG = "NostrEmbeddedBitChat"
/**
* Build a `bitchat1:` base64url-encoded BitChat packet carrying a private message for Nostr DMs.
*/
fun encodePMForNostr(
content: String,
messageID: String,
recipientPeerID: String,
senderPeerID: String
): String? {
try {
// TLV-encode the private message
val pm = PrivateMessagePacket(messageID = messageID, content = content)
val tlv = pm.encode() ?: return null
// Prefix with NoisePayloadType
val payload = ByteArray(1 + tlv.size)
payload[0] = NoisePayloadType.PRIVATE_MESSAGE.value.toByte()
System.arraycopy(tlv, 0, payload, 1, tlv.size)
// Determine 8-byte recipient ID to embed
val recipientIDHex = normalizeRecipientPeerID(recipientPeerID)
val packet = BitchatPacket(
version = 1u,
type = MessageType.NOISE_ENCRYPTED.value,
senderID = hexStringToByteArray(senderPeerID),
recipientID = hexStringToByteArray(recipientIDHex),
timestamp = System.currentTimeMillis().toULong(),
payload = payload,
signature = null,
ttl = com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS
)
val data = packet.toBinaryData() ?: return null
return "bitchat1:" + base64URLEncode(data)
} catch (e: Exception) {
Log.e(TAG, "Failed to encode PM for Nostr: ${e.message}")
return null
}
}
/**
* Build a `bitchat1:` base64url-encoded BitChat packet carrying a delivery/read ack for Nostr DMs.
*/
fun encodeAckForNostr(
type: NoisePayloadType,
messageID: String,
recipientPeerID: String,
senderPeerID: String
): String? {
if (type != NoisePayloadType.DELIVERED && type != NoisePayloadType.READ_RECEIPT) {
return null
}
try {
val payload = ByteArray(1 + messageID.toByteArray(Charsets.UTF_8).size)
payload[0] = type.value.toByte()
val messageIDBytes = messageID.toByteArray(Charsets.UTF_8)
System.arraycopy(messageIDBytes, 0, payload, 1, messageIDBytes.size)
val recipientIDHex = normalizeRecipientPeerID(recipientPeerID)
val packet = BitchatPacket(
version = 1u,
type = MessageType.NOISE_ENCRYPTED.value,
senderID = hexStringToByteArray(senderPeerID),
recipientID = hexStringToByteArray(recipientIDHex),
timestamp = System.currentTimeMillis().toULong(),
payload = payload,
signature = null,
ttl = com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS
)
val data = packet.toBinaryData() ?: return null
return "bitchat1:" + base64URLEncode(data)
} catch (e: Exception) {
Log.e(TAG, "Failed to encode ACK for Nostr: ${e.message}")
return null
}
}
/**
* Build a `bitchat1:` ACK (delivered/read) without an embedded recipient peer ID (geohash DMs).
*/
fun encodeAckForNostrNoRecipient(
type: NoisePayloadType,
messageID: String,
senderPeerID: String
): String? {
if (type != NoisePayloadType.DELIVERED && type != NoisePayloadType.READ_RECEIPT) {
return null
}
try {
val payload = ByteArray(1 + messageID.toByteArray(Charsets.UTF_8).size)
payload[0] = type.value.toByte()
val messageIDBytes = messageID.toByteArray(Charsets.UTF_8)
System.arraycopy(messageIDBytes, 0, payload, 1, messageIDBytes.size)
val packet = BitchatPacket(
version = 1u,
type = MessageType.NOISE_ENCRYPTED.value,
senderID = hexStringToByteArray(senderPeerID),
recipientID = null, // No recipient for geohash DMs
timestamp = System.currentTimeMillis().toULong(),
payload = payload,
signature = null,
ttl = com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS
)
val data = packet.toBinaryData() ?: return null
return "bitchat1:" + base64URLEncode(data)
} catch (e: Exception) {
Log.e(TAG, "Failed to encode ACK for Nostr (no recipient): ${e.message}")
return null
}
}
/**
* Build a `bitchat1:` payload without an embedded recipient peer ID (used for geohash DMs).
*/
fun encodePMForNostrNoRecipient(
content: String,
messageID: String,
senderPeerID: String
): String? {
try {
val pm = PrivateMessagePacket(messageID = messageID, content = content)
val tlv = pm.encode() ?: return null
val payload = ByteArray(1 + tlv.size)
payload[0] = NoisePayloadType.PRIVATE_MESSAGE.value.toByte()
System.arraycopy(tlv, 0, payload, 1, tlv.size)
val packet = BitchatPacket(
version = 1u,
type = MessageType.NOISE_ENCRYPTED.value,
senderID = hexStringToByteArray(senderPeerID),
recipientID = null, // No recipient for geohash DMs
timestamp = System.currentTimeMillis().toULong(),
payload = payload,
signature = null,
ttl = com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS
)
val data = packet.toBinaryData() ?: return null
return "bitchat1:" + base64URLEncode(data)
} catch (e: Exception) {
Log.e(TAG, "Failed to encode PM for Nostr (no recipient): ${e.message}")
return null
}
}
/**
* Normalize recipient peer ID (matches iOS implementation)
*/
private fun normalizeRecipientPeerID(recipientPeerID: String): String {
try {
val maybeData = hexStringToByteArray(recipientPeerID)
return when (maybeData.size) {
32 -> {
// Treat as Noise static public key; derive peerID from fingerprint
// For now, return first 8 bytes as hex (simplified)
maybeData.take(8).joinToString("") { "%02x".format(it) }
}
8 -> {
// Already an 8-byte peer ID
recipientPeerID
}
else -> {
// Fallback: return as-is (expecting 16 hex chars)
recipientPeerID
}
}
} catch (e: Exception) {
// Fallback: return as-is
return recipientPeerID
}
}
/**
* Base64url encode without padding (matches iOS implementation)
*/
private fun base64URLEncode(data: ByteArray): String {
val b64 = Base64.encodeToString(data, Base64.NO_WRAP)
return b64
.replace("+", "-")
.replace("/", "_")
.replace("=", "")
}
/**
* Convert hex string to byte array
*/
private fun hexStringToByteArray(hexString: String): ByteArray {
if (hexString.length % 2 != 0) {
return ByteArray(8) // Return 8-byte array filled with zeros
}
val result = ByteArray(8) { 0 } // Exactly 8 bytes like iOS
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
}
}