mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 07:05:21 +00:00
Add QR verification payloads and mesh wiring
This commit is contained in:
@@ -7,12 +7,15 @@ import com.bitchat.android.model.BitchatMessage
|
||||
import com.bitchat.android.protocol.MessagePadding
|
||||
import com.bitchat.android.model.RoutedPacket
|
||||
import com.bitchat.android.model.IdentityAnnouncement
|
||||
import com.bitchat.android.model.NoisePayload
|
||||
import com.bitchat.android.model.NoisePayloadType
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
import com.bitchat.android.protocol.SpecialRecipients
|
||||
import com.bitchat.android.model.RequestSyncPacket
|
||||
import com.bitchat.android.sync.GossipSyncManager
|
||||
import com.bitchat.android.util.toHexString
|
||||
import com.bitchat.android.services.VerificationService
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.*
|
||||
import kotlin.math.sign
|
||||
@@ -72,6 +75,7 @@ class BluetoothMeshService(private val context: Context) {
|
||||
|
||||
init {
|
||||
Log.i(TAG, "Initializing BluetoothMeshService for peer=$myPeerID")
|
||||
VerificationService.configure(encryptionService)
|
||||
setupDelegates()
|
||||
messageHandler.packetProcessor = packetProcessor
|
||||
//startPeriodicDebugLogging()
|
||||
@@ -414,6 +418,14 @@ class BluetoothMeshService(private val context: Context) {
|
||||
override fun onReadReceiptReceived(messageID: String, peerID: String) {
|
||||
delegate?.didReceiveReadReceipt(messageID, peerID)
|
||||
}
|
||||
|
||||
override fun onVerifyChallengeReceived(peerID: String, payload: ByteArray, timestampMs: Long) {
|
||||
delegate?.didReceiveVerifyChallenge(peerID, payload, timestampMs)
|
||||
}
|
||||
|
||||
override fun onVerifyResponseReceived(peerID: String, payload: ByteArray, timestampMs: Long) {
|
||||
delegate?.didReceiveVerifyResponse(peerID, payload, timestampMs)
|
||||
}
|
||||
}
|
||||
|
||||
// PacketProcessor delegates
|
||||
@@ -939,6 +951,50 @@ class BluetoothMeshService(private val context: Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: QR Verification over Noise
|
||||
|
||||
fun sendVerifyChallenge(peerID: String, noiseKeyHex: String, nonceA: ByteArray) {
|
||||
val tlv = VerificationService.buildVerifyChallenge(noiseKeyHex, nonceA)
|
||||
val payload = NoisePayload(
|
||||
type = NoisePayloadType.VERIFY_CHALLENGE,
|
||||
data = tlv
|
||||
)
|
||||
sendNoisePayloadToPeer(payload, peerID, "verify challenge")
|
||||
}
|
||||
|
||||
fun sendVerifyResponse(peerID: String, noiseKeyHex: String, nonceA: ByteArray) {
|
||||
val tlv = VerificationService.buildVerifyResponse(noiseKeyHex, nonceA) ?: return
|
||||
val payload = NoisePayload(
|
||||
type = NoisePayloadType.VERIFY_RESPONSE,
|
||||
data = tlv
|
||||
)
|
||||
sendNoisePayloadToPeer(payload, peerID, "verify response")
|
||||
}
|
||||
|
||||
private fun sendNoisePayloadToPeer(payload: NoisePayload, recipientPeerID: String, label: String) {
|
||||
serviceScope.launch {
|
||||
try {
|
||||
val encrypted = encryptionService.encrypt(payload.encode(), recipientPeerID)
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_ENCRYPTED.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = hexStringToByteArray(recipientPeerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = encrypted,
|
||||
signature = null,
|
||||
ttl = com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS
|
||||
)
|
||||
|
||||
val signedPacket = signPacketBeforeBroadcast(packet)
|
||||
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
|
||||
Log.d(TAG, "📤 Sent $label to $recipientPeerID (${payload.data.size} bytes)")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to send $label to $recipientPeerID: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send broadcast announce with TLV-encoded identity announcement - exactly like iOS
|
||||
@@ -1127,6 +1183,10 @@ class BluetoothMeshService(private val context: Context) {
|
||||
fun getIdentityFingerprint(): String {
|
||||
return encryptionService.getIdentityFingerprint()
|
||||
}
|
||||
|
||||
fun getStaticNoisePublicKey(): ByteArray? {
|
||||
return encryptionService.getStaticPublicKey()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if encryption icon should be shown for a peer
|
||||
@@ -1283,6 +1343,8 @@ interface BluetoothMeshDelegate {
|
||||
fun didReceiveChannelLeave(channel: String, fromPeer: String)
|
||||
fun didReceiveDeliveryAck(messageID: String, recipientPeerID: String)
|
||||
fun didReceiveReadReceipt(messageID: String, recipientPeerID: String)
|
||||
fun didReceiveVerifyChallenge(peerID: String, payload: ByteArray, timestampMs: Long)
|
||||
fun didReceiveVerifyResponse(peerID: String, payload: ByteArray, timestampMs: Long)
|
||||
fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String?
|
||||
fun getNickname(): String?
|
||||
fun isFavorite(peerID: String): Boolean
|
||||
|
||||
@@ -157,6 +157,14 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
||||
// Simplified: Call delegate with messageID and peerID directly
|
||||
delegate?.onReadReceiptReceived(messageID, peerID)
|
||||
}
|
||||
com.bitchat.android.model.NoisePayloadType.VERIFY_CHALLENGE -> {
|
||||
Log.d(TAG, "🔐 Verify challenge received from $peerID (${noisePayload.data.size} bytes)")
|
||||
delegate?.onVerifyChallengeReceived(peerID, noisePayload.data, packet.timestamp.toLong())
|
||||
}
|
||||
com.bitchat.android.model.NoisePayloadType.VERIFY_RESPONSE -> {
|
||||
Log.d(TAG, "🔐 Verify response received from $peerID (${noisePayload.data.size} bytes)")
|
||||
delegate?.onVerifyResponseReceived(peerID, noisePayload.data, packet.timestamp.toLong())
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
@@ -611,4 +619,6 @@ interface MessageHandlerDelegate {
|
||||
fun onChannelLeave(channel: String, fromPeer: String)
|
||||
fun onDeliveryAckReceived(messageID: String, peerID: String)
|
||||
fun onReadReceiptReceived(messageID: String, peerID: String)
|
||||
fun onVerifyChallengeReceived(peerID: String, payload: ByteArray, timestampMs: Long)
|
||||
fun onVerifyResponseReceived(peerID: String, payload: ByteArray, timestampMs: Long)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ enum class NoisePayloadType(val value: UByte) {
|
||||
PRIVATE_MESSAGE(0x01u), // Private chat message with TLV encoding
|
||||
READ_RECEIPT(0x02u), // Message was read
|
||||
DELIVERED(0x03u), // Message was delivered
|
||||
VERIFY_CHALLENGE(0x10u), // Verification challenge
|
||||
VERIFY_RESPONSE(0x11u), // Verification response
|
||||
FILE_TRANSFER(0x20u);
|
||||
|
||||
|
||||
|
||||
@@ -2,8 +2,12 @@ package com.bitchat.android.nostr
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import com.bitchat.android.model.BitchatFilePacket
|
||||
import com.bitchat.android.model.BitchatMessage
|
||||
import com.bitchat.android.model.DeliveryStatus
|
||||
import com.bitchat.android.model.NoisePayload
|
||||
import com.bitchat.android.model.NoisePayloadType
|
||||
import com.bitchat.android.model.PrivateMessagePacket
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.services.SeenMessageStore
|
||||
import com.bitchat.android.ui.ChatState
|
||||
@@ -71,7 +75,7 @@ class NostrDirectMessageHandler(
|
||||
|
||||
if (packet.type != com.bitchat.android.protocol.MessageType.NOISE_ENCRYPTED.value) return@launch
|
||||
|
||||
val noisePayload = com.bitchat.android.model.NoisePayload.decode(packet.payload) ?: return@launch
|
||||
val noisePayload = NoisePayload.decode(packet.payload) ?: return@launch
|
||||
val messageTimestamp = Date(giftWrap.createdAt * 1000L)
|
||||
val convKey = "nostr_${senderPubkey.take(16)}"
|
||||
repo.putNostrKeyMapping(convKey, senderPubkey)
|
||||
@@ -104,7 +108,7 @@ class NostrDirectMessageHandler(
|
||||
}
|
||||
|
||||
private suspend fun processNoisePayload(
|
||||
payload: com.bitchat.android.model.NoisePayload,
|
||||
payload: NoisePayload,
|
||||
convKey: String,
|
||||
senderNickname: String,
|
||||
timestamp: Date,
|
||||
@@ -112,8 +116,8 @@ class NostrDirectMessageHandler(
|
||||
recipientIdentity: NostrIdentity
|
||||
) {
|
||||
when (payload.type) {
|
||||
com.bitchat.android.model.NoisePayloadType.PRIVATE_MESSAGE -> {
|
||||
val pm = com.bitchat.android.model.PrivateMessagePacket.decode(payload.data) ?: return
|
||||
NoisePayloadType.PRIVATE_MESSAGE -> {
|
||||
val pm = PrivateMessagePacket.decode(payload.data) ?: return
|
||||
val existingMessages = state.getPrivateChatsValue()[convKey] ?: emptyList()
|
||||
if (existingMessages.any { it.id == pm.messageID }) return
|
||||
|
||||
@@ -148,21 +152,21 @@ class NostrDirectMessageHandler(
|
||||
seenStore.markRead(pm.messageID)
|
||||
}
|
||||
}
|
||||
com.bitchat.android.model.NoisePayloadType.DELIVERED -> {
|
||||
NoisePayloadType.DELIVERED -> {
|
||||
val messageId = String(payload.data, Charsets.UTF_8)
|
||||
withContext(Dispatchers.Main) {
|
||||
meshDelegateHandler.didReceiveDeliveryAck(messageId, convKey)
|
||||
}
|
||||
}
|
||||
com.bitchat.android.model.NoisePayloadType.READ_RECEIPT -> {
|
||||
NoisePayloadType.READ_RECEIPT -> {
|
||||
val messageId = String(payload.data, Charsets.UTF_8)
|
||||
withContext(Dispatchers.Main) {
|
||||
meshDelegateHandler.didReceiveReadReceipt(messageId, convKey)
|
||||
}
|
||||
}
|
||||
com.bitchat.android.model.NoisePayloadType.FILE_TRANSFER -> {
|
||||
NoisePayloadType.FILE_TRANSFER -> {
|
||||
// Properly handle encrypted file transfer
|
||||
val file = com.bitchat.android.model.BitchatFilePacket.decode(payload.data)
|
||||
val file = BitchatFilePacket.decode(payload.data)
|
||||
if (file != null) {
|
||||
val uniqueMsgId = java.util.UUID.randomUUID().toString().uppercase()
|
||||
val savedPath = com.bitchat.android.features.file.FileUtils.saveIncomingFile(application, file)
|
||||
@@ -185,6 +189,8 @@ class NostrDirectMessageHandler(
|
||||
Log.w(TAG, "⚠️ Failed to decode Nostr file transfer from $convKey")
|
||||
}
|
||||
}
|
||||
NoisePayloadType.VERIFY_CHALLENGE,
|
||||
NoisePayloadType.VERIFY_RESPONSE -> Unit // Ignore verification payloads in Nostr direct messages
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
package com.bitchat.android.services
|
||||
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import com.bitchat.android.crypto.EncryptionService
|
||||
import com.bitchat.android.util.AppConstants
|
||||
import com.bitchat.android.util.dataFromHexString
|
||||
import com.bitchat.android.util.hexEncodedString
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.security.SecureRandom
|
||||
import androidx.core.net.toUri
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
/**
|
||||
* QR verification helpers: schema, signing, and basic challenge/response helpers.
|
||||
*/
|
||||
object VerificationService {
|
||||
private const val CONTEXT = "bitchat-verify-v1"
|
||||
private const val RESPONSE_CONTEXT = "bitchat-verify-resp-v1"
|
||||
|
||||
private var encryptionServiceRef: WeakReference<EncryptionService>? = null
|
||||
|
||||
fun configure(encryptionService: EncryptionService) {
|
||||
this.encryptionServiceRef = WeakReference(encryptionService)
|
||||
}
|
||||
|
||||
data class VerificationQR(
|
||||
val v: Int,
|
||||
val noiseKeyHex: String,
|
||||
val signKeyHex: String,
|
||||
val npub: String?,
|
||||
val nickname: String,
|
||||
val ts: Long,
|
||||
val nonceB64: String,
|
||||
val sigHex: String
|
||||
) {
|
||||
fun canonicalBytes(): ByteArray {
|
||||
val out = ByteArrayOutputStream()
|
||||
|
||||
fun appendField(value: String) {
|
||||
val data = value.toByteArray(Charsets.UTF_8)
|
||||
val len = minOf(data.size, 255)
|
||||
out.write(len)
|
||||
out.write(data, 0, len)
|
||||
}
|
||||
|
||||
appendField(CONTEXT)
|
||||
appendField(v.toString())
|
||||
appendField(noiseKeyHex.lowercase())
|
||||
appendField(signKeyHex.lowercase())
|
||||
appendField(npub ?: "")
|
||||
appendField(nickname)
|
||||
appendField(ts.toString())
|
||||
appendField(nonceB64)
|
||||
return out.toByteArray()
|
||||
}
|
||||
|
||||
fun toUrlString(): String {
|
||||
val builder = Uri.Builder()
|
||||
.scheme("bitchat")
|
||||
.authority("verify")
|
||||
.appendQueryParameter("v", v.toString())
|
||||
.appendQueryParameter("noise", noiseKeyHex)
|
||||
.appendQueryParameter("sign", signKeyHex)
|
||||
.appendQueryParameter("nick", nickname)
|
||||
.appendQueryParameter("ts", ts.toString())
|
||||
.appendQueryParameter("nonce", nonceB64)
|
||||
.appendQueryParameter("sig", sigHex)
|
||||
if (npub != null) {
|
||||
builder.appendQueryParameter("npub", npub)
|
||||
}
|
||||
return builder.build().toString()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromUrlString(urlString: String): VerificationQR? {
|
||||
val uri = runCatching { urlString.toUri() }.getOrNull() ?: return null
|
||||
if (uri.scheme != "bitchat" || uri.host != "verify") return null
|
||||
|
||||
val vStr = uri.getQueryParameter("v") ?: return null
|
||||
val v = vStr.toIntOrNull() ?: return null
|
||||
val noise = uri.getQueryParameter("noise") ?: return null
|
||||
val sign = uri.getQueryParameter("sign") ?: return null
|
||||
val nick = uri.getQueryParameter("nick") ?: return null
|
||||
val tsStr = uri.getQueryParameter("ts") ?: return null
|
||||
val ts = tsStr.toLongOrNull() ?: return null
|
||||
val nonce = uri.getQueryParameter("nonce") ?: return null
|
||||
val sig = uri.getQueryParameter("sig") ?: return null
|
||||
val npub = uri.getQueryParameter("npub")
|
||||
|
||||
return VerificationQR(
|
||||
v = v,
|
||||
noiseKeyHex = noise,
|
||||
signKeyHex = sign,
|
||||
npub = npub,
|
||||
nickname = nick,
|
||||
ts = ts,
|
||||
nonceB64 = nonce,
|
||||
sigHex = sig
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun buildMyQRString(nickname: String, npub: String?): String? {
|
||||
val service = encryptionServiceRef?.get() ?: return null
|
||||
val cache = Cache.last
|
||||
if (cache != null && cache.nickname == nickname && cache.npub == npub) {
|
||||
if (System.currentTimeMillis() - cache.builtAtMs < 60_000L) {
|
||||
return cache.value
|
||||
}
|
||||
}
|
||||
|
||||
val noiseKey = service.getStaticPublicKey()?.hexEncodedString() ?: return null
|
||||
val signKey = service.getSigningPublicKey()?.hexEncodedString() ?: return null
|
||||
val ts = System.currentTimeMillis() / 1000L
|
||||
val nonce = ByteArray(16)
|
||||
SecureRandom().nextBytes(nonce)
|
||||
val nonceB64 = Base64.encodeToString(
|
||||
nonce,
|
||||
Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING
|
||||
)
|
||||
|
||||
val payload = VerificationQR(
|
||||
v = 1,
|
||||
noiseKeyHex = noiseKey,
|
||||
signKeyHex = signKey,
|
||||
npub = npub,
|
||||
nickname = nickname,
|
||||
ts = ts,
|
||||
nonceB64 = nonceB64,
|
||||
sigHex = ""
|
||||
)
|
||||
|
||||
val signature = service.signData(payload.canonicalBytes()) ?: return null
|
||||
val signed = payload.copy(sigHex = signature.hexEncodedString())
|
||||
val out = signed.toUrlString()
|
||||
Cache.last = CacheEntry(nickname, npub, System.currentTimeMillis(), out)
|
||||
return out
|
||||
}
|
||||
|
||||
fun verifyScannedQR(
|
||||
urlString: String,
|
||||
maxAgeSeconds: Long = AppConstants.Verification.QR_MAX_AGE_SECONDS
|
||||
): VerificationQR? {
|
||||
val service = encryptionServiceRef?.get() ?: return null
|
||||
val qr = VerificationQR.fromUrlString(urlString) ?: return null
|
||||
val now = System.currentTimeMillis() / 1000L
|
||||
if (now - qr.ts > maxAgeSeconds) return null
|
||||
|
||||
val sig = qr.sigHex.dataFromHexString() ?: return null
|
||||
val signKey = qr.signKeyHex.dataFromHexString() ?: return null
|
||||
val ok = service.verifyEd25519Signature(sig, qr.canonicalBytes(), signKey)
|
||||
return if (ok) qr else null
|
||||
}
|
||||
|
||||
fun buildVerifyChallenge(noiseKeyHex: String, nonceA: ByteArray): ByteArray {
|
||||
val noiseData = noiseKeyHex.toByteArray(Charsets.UTF_8)
|
||||
val out = ByteArrayOutputStream()
|
||||
out.write(0x01)
|
||||
out.write(minOf(noiseData.size, 255))
|
||||
out.write(noiseData, 0, minOf(noiseData.size, 255))
|
||||
out.write(0x02)
|
||||
out.write(minOf(nonceA.size, 255))
|
||||
out.write(nonceA, 0, minOf(nonceA.size, 255))
|
||||
return out.toByteArray()
|
||||
}
|
||||
|
||||
fun buildVerifyResponse(noiseKeyHex: String, nonceA: ByteArray): ByteArray? {
|
||||
val service = encryptionServiceRef?.get() ?: return null
|
||||
val noiseData = noiseKeyHex.toByteArray(Charsets.UTF_8)
|
||||
val msg = ByteArrayOutputStream()
|
||||
msg.write(RESPONSE_CONTEXT.toByteArray(Charsets.UTF_8))
|
||||
msg.write(minOf(noiseData.size, 255))
|
||||
msg.write(noiseData, 0, minOf(noiseData.size, 255))
|
||||
msg.write(nonceA)
|
||||
val sig = service.signData(msg.toByteArray()) ?: return null
|
||||
|
||||
val out = ByteArrayOutputStream()
|
||||
out.write(0x01)
|
||||
out.write(minOf(noiseData.size, 255))
|
||||
out.write(noiseData, 0, minOf(noiseData.size, 255))
|
||||
out.write(0x02)
|
||||
out.write(minOf(nonceA.size, 255))
|
||||
out.write(nonceA, 0, minOf(nonceA.size, 255))
|
||||
out.write(0x03)
|
||||
out.write(minOf(sig.size, 255))
|
||||
out.write(sig, 0, minOf(sig.size, 255))
|
||||
return out.toByteArray()
|
||||
}
|
||||
|
||||
fun parseVerifyChallenge(data: ByteArray): Pair<String, ByteArray>? {
|
||||
var idx = 0
|
||||
|
||||
fun take(n: Int): ByteArray? {
|
||||
if (idx + n > data.size) return null
|
||||
val out = data.copyOfRange(idx, idx + n)
|
||||
idx += n
|
||||
return out
|
||||
}
|
||||
|
||||
val t1 = take(1) ?: return null
|
||||
if (t1[0].toInt() != 0x01) return null
|
||||
val l1 = take(1)?.get(0)?.toInt() ?: return null
|
||||
val noiseBytes = take(l1) ?: return null
|
||||
val noise = noiseBytes.toString(Charsets.UTF_8)
|
||||
|
||||
val t2 = take(1) ?: return null
|
||||
if (t2[0].toInt() != 0x02) return null
|
||||
val l2 = take(1)?.get(0)?.toInt() ?: return null
|
||||
val nonce = take(l2) ?: return null
|
||||
|
||||
return noise to nonce
|
||||
}
|
||||
|
||||
data class VerifyResponse(val noiseKeyHex: String, val nonceA: ByteArray, val signature: ByteArray) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as VerifyResponse
|
||||
|
||||
if (noiseKeyHex != other.noiseKeyHex) return false
|
||||
if (!nonceA.contentEquals(other.nonceA)) return false
|
||||
if (!signature.contentEquals(other.signature)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = noiseKeyHex.hashCode()
|
||||
result = 31 * result + nonceA.contentHashCode()
|
||||
result = 31 * result + signature.contentHashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
fun parseVerifyResponse(data: ByteArray): VerifyResponse? {
|
||||
var idx = 0
|
||||
|
||||
fun take(n: Int): ByteArray? {
|
||||
if (idx + n > data.size) return null
|
||||
val out = data.copyOfRange(idx, idx + n)
|
||||
idx += n
|
||||
return out
|
||||
}
|
||||
|
||||
val t1 = take(1) ?: return null
|
||||
if (t1[0].toInt() != 0x01) return null
|
||||
val l1 = take(1)?.get(0)?.toInt() ?: return null
|
||||
val noiseBytes = take(l1) ?: return null
|
||||
val noise = noiseBytes.toString(Charsets.UTF_8)
|
||||
|
||||
val t2 = take(1) ?: return null
|
||||
if (t2[0].toInt() != 0x02) return null
|
||||
val l2 = take(1)?.get(0)?.toInt() ?: return null
|
||||
val nonce = take(l2) ?: return null
|
||||
|
||||
val t3 = take(1) ?: return null
|
||||
if (t3[0].toInt() != 0x03) return null
|
||||
val l3 = take(1)?.get(0)?.toInt() ?: return null
|
||||
val sig = take(l3) ?: return null
|
||||
|
||||
return VerifyResponse(noise, nonce, sig)
|
||||
}
|
||||
|
||||
fun verifyResponseSignature(
|
||||
noiseKeyHex: String,
|
||||
nonceA: ByteArray,
|
||||
signature: ByteArray,
|
||||
signerPublicKeyHex: String
|
||||
): Boolean {
|
||||
val service = encryptionServiceRef?.get() ?: return false
|
||||
val noiseData = noiseKeyHex.toByteArray(Charsets.UTF_8)
|
||||
val msg = ByteArrayOutputStream()
|
||||
msg.write(RESPONSE_CONTEXT.toByteArray(Charsets.UTF_8))
|
||||
msg.write(minOf(noiseData.size, 255))
|
||||
msg.write(noiseData, 0, minOf(noiseData.size, 255))
|
||||
msg.write(nonceA)
|
||||
val signerKey = signerPublicKeyHex.dataFromHexString() ?: return false
|
||||
return service.verifyEd25519Signature(signature, msg.toByteArray(), signerKey)
|
||||
}
|
||||
|
||||
private data class CacheEntry(
|
||||
val nickname: String,
|
||||
val npub: String?,
|
||||
val builtAtMs: Long,
|
||||
val value: String
|
||||
)
|
||||
|
||||
private object Cache {
|
||||
var last: CacheEntry? = null
|
||||
}
|
||||
}
|
||||
@@ -216,6 +216,14 @@ class MeshDelegateHandler(
|
||||
messageManager.updateMessageDeliveryStatus(messageID, DeliveryStatus.Read(recipientPeerID, Date()))
|
||||
}
|
||||
}
|
||||
|
||||
override fun didReceiveVerifyChallenge(peerID: String, payload: ByteArray, timestampMs: Long) {
|
||||
// Handled by ChatViewModel for verification flow
|
||||
}
|
||||
|
||||
override fun didReceiveVerifyResponse(peerID: String, payload: ByteArray, timestampMs: Long) {
|
||||
// Handled by ChatViewModel for verification flow
|
||||
}
|
||||
|
||||
override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? {
|
||||
return channelManager.decryptChannelMessage(encryptedContent, channel)
|
||||
|
||||
@@ -58,6 +58,10 @@ object AppConstants {
|
||||
const val HIGH_NONCE_WARNING_THRESHOLD: Long = 1_000_000_000L
|
||||
}
|
||||
|
||||
object Verification {
|
||||
const val QR_MAX_AGE_SECONDS: Long = 300L // 5 minutes
|
||||
}
|
||||
|
||||
object Protocol {
|
||||
const val COMPRESSION_THRESHOLD_BYTES: Int = 100
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user