From c5a3368b9f0772c27f43747df4895a9098d64075 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 20 Aug 2025 13:24:52 +0200 Subject: [PATCH] Sign announcements (#267) * wip * announcements WIP * works * restore mainnet --- .../identity/SecureIdentityStateManager.kt | 60 ++++++++ .../android/mesh/BluetoothMeshService.kt | 64 +++++++- .../bitchat/android/mesh/MessageHandler.kt | 73 +++++++-- .../com/bitchat/android/mesh/PeerManager.kt | 139 +++++++++++++++++- .../android/model/IdentityAnnouncement.kt | 48 ++++-- .../android/noise/NoiseEncryptionService.kt | 136 +++++++++++++++++ .../android/protocol/BinaryProtocol.kt | 22 ++- 7 files changed, 501 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/identity/SecureIdentityStateManager.kt b/app/src/main/java/com/bitchat/android/identity/SecureIdentityStateManager.kt index b3a53da4..667cfc78 100644 --- a/app/src/main/java/com/bitchat/android/identity/SecureIdentityStateManager.kt +++ b/app/src/main/java/com/bitchat/android/identity/SecureIdentityStateManager.kt @@ -24,6 +24,8 @@ class SecureIdentityStateManager(private val context: Context) { private const val PREFS_NAME = "bitchat_identity" private const val KEY_STATIC_PRIVATE_KEY = "static_private_key" private const val KEY_STATIC_PUBLIC_KEY = "static_public_key" + private const val KEY_SIGNING_PRIVATE_KEY = "signing_private_key" + private const val KEY_SIGNING_PUBLIC_KEY = "signing_public_key" private const val KEY_LAST_ROTATION = "last_rotation" private const val KEY_NEXT_ROTATION_INTERVAL = "next_rotation_interval" @@ -108,6 +110,64 @@ class SecureIdentityStateManager(private val context: Context) { throw e } } + + // MARK: - Signing Key Management + + /** + * Load saved signing key pair + * Returns (privateKey, publicKey) or null if none exists + */ + fun loadSigningKey(): Pair? { + return try { + val privateKeyString = prefs.getString(KEY_SIGNING_PRIVATE_KEY, null) + val publicKeyString = prefs.getString(KEY_SIGNING_PUBLIC_KEY, null) + + if (privateKeyString != null && publicKeyString != null) { + val privateKey = android.util.Base64.decode(privateKeyString, android.util.Base64.DEFAULT) + val publicKey = android.util.Base64.decode(publicKeyString, android.util.Base64.DEFAULT) + + // Validate key sizes + if (privateKey.size == 32 && publicKey.size == 32) { + Log.d(TAG, "Loaded Ed25519 signing key from secure storage") + Pair(privateKey, publicKey) + } else { + Log.w(TAG, "Invalid signing key sizes in storage, returning null") + null + } + } else { + Log.d(TAG, "No Ed25519 signing key found in storage") + null + } + } catch (e: Exception) { + Log.e(TAG, "Failed to load signing key: ${e.message}") + null + } + } + + /** + * Save signing key pair to secure storage + */ + fun saveSigningKey(privateKey: ByteArray, publicKey: ByteArray) { + try { + // Validate key sizes + if (privateKey.size != 32 || publicKey.size != 32) { + throw IllegalArgumentException("Invalid signing key sizes: private=${privateKey.size}, public=${publicKey.size}") + } + + val privateKeyString = android.util.Base64.encodeToString(privateKey, android.util.Base64.DEFAULT) + val publicKeyString = android.util.Base64.encodeToString(publicKey, android.util.Base64.DEFAULT) + + prefs.edit() + .putString(KEY_SIGNING_PRIVATE_KEY, privateKeyString) + .putString(KEY_SIGNING_PUBLIC_KEY, publicKeyString) + .apply() + + Log.d(TAG, "Saved Ed25519 signing key to secure storage") + } catch (e: Exception) { + Log.e(TAG, "Failed to save signing key: ${e.message}") + throw e + } + } // MARK: - Fingerprint Generation diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt index ac841498..45b20d52 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -181,6 +181,14 @@ class BluetoothMeshService(private val context: Context) { return delegate?.getNickname() } + override fun getPeerInfo(peerID: String): PeerInfo? { + return peerManager.getPeerInfo(peerID) + } + + override fun updatePeerInfo(peerID: String, nickname: String, noisePublicKey: ByteArray, signingPublicKey: ByteArray, isVerified: Boolean): Boolean { + return peerManager.updatePeerInfo(peerID, nickname, noisePublicKey, signingPublicKey, isVerified) + } + // Packet operations override fun sendPacket(packet: BitchatPacket) { connectionManager.broadcastPacket(RoutedPacket(packet)) @@ -575,8 +583,15 @@ class BluetoothMeshService(private val context: Context) { return@launch } + // Get the signing public key for the announcement + val signingKey = encryptionService.getSigningPublicKey() + if (signingKey == null) { + Log.e(TAG, "No signing public key available for announcement") + return@launch + } + // Create iOS-compatible IdentityAnnouncement with TLV encoding - val announcement = IdentityAnnouncement(nickname, staticKey) + val announcement = IdentityAnnouncement(nickname, staticKey, signingKey) val tlvPayload = announcement.encode() if (tlvPayload == null) { Log.e(TAG, "Failed to encode announcement as TLV") @@ -590,8 +605,13 @@ class BluetoothMeshService(private val context: Context) { payload = tlvPayload ) - connectionManager.broadcastPacket(RoutedPacket(announcePacket)) - Log.d(TAG, "Sent iOS-compatible TLV announce (${tlvPayload.size} bytes)") + // Sign the packet using our signing key (exactly like iOS) + val signedPacket = encryptionService.signData(announcePacket.toBinaryDataForSigning()!!)?.let { signature -> + announcePacket.copy(signature = signature) + } ?: announcePacket + + connectionManager.broadcastPacket(RoutedPacket(signedPacket)) + Log.d(TAG, "Sent iOS-compatible signed TLV announce (${tlvPayload.size} bytes)") } } @@ -610,8 +630,15 @@ class BluetoothMeshService(private val context: Context) { return } + // Get the signing public key for the announcement + val signingKey = encryptionService.getSigningPublicKey() + if (signingKey == null) { + Log.e(TAG, "No signing public key available for peer announcement") + return + } + // Create iOS-compatible IdentityAnnouncement with TLV encoding - val announcement = IdentityAnnouncement(nickname, staticKey) + val announcement = IdentityAnnouncement(nickname, staticKey, signingKey) val tlvPayload = announcement.encode() if (tlvPayload == null) { Log.e(TAG, "Failed to encode peer announcement as TLV") @@ -625,9 +652,14 @@ class BluetoothMeshService(private val context: Context) { payload = tlvPayload ) - connectionManager.broadcastPacket(RoutedPacket(packet)) + // Sign the packet using our signing key (exactly like iOS) + val signedPacket = encryptionService.signData(packet.toBinaryDataForSigning()!!)?.let { signature -> + packet.copy(signature = signature) + } ?: packet + + connectionManager.broadcastPacket(RoutedPacket(signedPacket)) peerManager.markPeerAsAnnouncedTo(peerID) - Log.d(TAG, "Sent iOS-compatible TLV peer announce to $peerID (${tlvPayload.size} bytes)") + Log.d(TAG, "Sent iOS-compatible signed TLV peer announce to $peerID (${tlvPayload.size} bytes)") } /** @@ -683,6 +715,26 @@ class BluetoothMeshService(private val context: Context) { fun getPeerFingerprint(peerID: String): String? { return peerManager.getFingerprintForPeer(peerID) } + + /** + * Get peer info for verification purposes + */ + fun getPeerInfo(peerID: String): PeerInfo? { + return peerManager.getPeerInfo(peerID) + } + + /** + * Update peer information with verification data + */ + fun updatePeerInfo( + peerID: String, + nickname: String, + noisePublicKey: ByteArray, + signingPublicKey: ByteArray, + isVerified: Boolean + ): Boolean { + return peerManager.updatePeerInfo(peerID, nickname, noisePublicKey, signingPublicKey, isVerified) + } /** * Get our identity fingerprint diff --git a/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt b/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt index 6a947a64..91de7ec0 100644 --- a/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt +++ b/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt @@ -164,7 +164,7 @@ class MessageHandler(private val myPeerID: String) { } /** - * Handle announce message with TLV decoding support - exactly like iOS + * Handle announce message with TLV decoding and signature verification - exactly like iOS */ suspend fun handleAnnounce(routed: RoutedPacket): Boolean { val packet = routed.packet @@ -179,26 +179,59 @@ class MessageHandler(private val myPeerID: String) { return false } - // Successfully decoded TLV format exactly like iOS - Log.d(TAG, "Received iOS-compatible announce from $peerID: nickname=${announcement.nickname}, " + - "publicKey=${announcement.publicKey.joinToString("") { "%02x".format(it) }.take(16)}...") - - // Extract nickname and public key from TLV data - val nickname = announcement.nickname - val publicKey = announcement.publicKey - - // Notify delegate to handle peer management with nickname - val isFirstAnnounce = delegate?.addOrUpdatePeer(peerID, nickname) ?: false + // Verify packet signature using the announced signing public key + var verified = false + if (packet.signature != null) { + // Verify that the packet was signed by the signing private key corresponding to the announced signing public key + verified = delegate?.verifyEd25519Signature(packet.signature!!, packet.toBinaryDataForSigning()!!, announcement.signingPublicKey) ?: false + if (!verified) { + Log.w(TAG, "⚠️ Signature verification for announce failed ${peerID.take(8)}") + } + } - // Update peer ID binding with public key for identity management + // Check for existing peer with different noise public key + // If existing peer has a different noise public key, do not consider this verified + val existingPeer = delegate?.getPeerInfo(peerID) + + if (existingPeer != null && existingPeer.noisePublicKey != null && !existingPeer.noisePublicKey!!.contentEquals(announcement.noisePublicKey)) { + Log.w(TAG, "⚠️ Announce key mismatch for ${peerID.take(8)}... — keeping unverified") + verified = false + } + + // Require verified announce; ignore otherwise (no backward compatibility) + if (!verified) { + Log.w(TAG, "❌ Ignoring unverified announce from ${peerID.take(8)}...") + return false + } + + // Successfully decoded TLV format exactly like iOS + Log.d(TAG, "✅ Verified announce from $peerID: nickname=${announcement.nickname}, " + + "noisePublicKey=${announcement.noisePublicKey.joinToString("") { "%02x".format(it) }.take(16)}..., " + + "signingPublicKey=${announcement.signingPublicKey.joinToString("") { "%02x".format(it) }.take(16)}...") + + // Extract nickname and public keys from TLV data + val nickname = announcement.nickname + val noisePublicKey = announcement.noisePublicKey + val signingPublicKey = announcement.signingPublicKey + + // Update peer info with verification status through new method + val isFirstAnnounce = delegate?.updatePeerInfo( + peerID = peerID, + nickname = nickname, + noisePublicKey = noisePublicKey, + signingPublicKey = signingPublicKey, + isVerified = true + ) ?: false + + // Update peer ID binding with noise public key for identity management delegate?.updatePeerIDBinding( newPeerID = peerID, nickname = nickname, - publicKey = publicKey, + publicKey = noisePublicKey, previousPeerID = null ) - Log.d(TAG, "Processed iOS-compatible TLV announce: stored public key for $peerID") + Log.d(TAG, "✅ Processed verified TLV announce: stored identity for $peerID") return isFirstAnnounce } @@ -282,11 +315,19 @@ class MessageHandler(private val myPeerID: String) { } /** - * Handle broadcast message + * Handle broadcast message with verification enforcement */ private suspend fun handleBroadcastMessage(routed: RoutedPacket) { val packet = routed.packet val peerID = routed.peerID ?: "unknown" + + // Enforce: only accept public messages from verified peers we know + val peerInfo = delegate?.getPeerInfo(peerID) + if (peerInfo == null || !peerInfo.isVerifiedNickname) { + Log.w(TAG, "🚫 Dropping public message from unverified or unknown peer ${peerID.take(8)}...") + return + } + try { // Parse message val message = BitchatMessage( @@ -398,6 +439,8 @@ interface MessageHandlerDelegate { fun getPeerNickname(peerID: String): String? fun getNetworkSize(): Int fun getMyNickname(): String? + fun getPeerInfo(peerID: String): PeerInfo? + fun updatePeerInfo(peerID: String, nickname: String, noisePublicKey: ByteArray, signingPublicKey: ByteArray, isVerified: Boolean): Boolean // Packet operations fun sendPacket(packet: BitchatPacket) diff --git a/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt b/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt index b383df63..26a0ef87 100644 --- a/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt @@ -5,11 +5,60 @@ import kotlinx.coroutines.* import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList +/** + * Peer information structure with verification status + * Compatible with iOS PeerInfo structure + */ +data class PeerInfo( + val id: String, + var nickname: String, + var isConnected: Boolean, + var noisePublicKey: ByteArray?, + var signingPublicKey: ByteArray?, // NEW: Ed25519 public key for verification + var isVerifiedNickname: Boolean, // NEW: Verification status flag + var lastSeen: Long // Using Long instead of Date for simplicity +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PeerInfo + + if (id != other.id) return false + if (nickname != other.nickname) return false + if (isConnected != other.isConnected) return false + if (noisePublicKey != null) { + if (other.noisePublicKey == null) return false + if (!noisePublicKey.contentEquals(other.noisePublicKey)) return false + } else if (other.noisePublicKey != null) return false + if (signingPublicKey != null) { + if (other.signingPublicKey == null) return false + if (!signingPublicKey.contentEquals(other.signingPublicKey)) return false + } else if (other.signingPublicKey != null) return false + if (isVerifiedNickname != other.isVerifiedNickname) return false + if (lastSeen != other.lastSeen) return false + + return true + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + nickname.hashCode() + result = 31 * result + isConnected.hashCode() + result = 31 * result + (noisePublicKey?.contentHashCode() ?: 0) + result = 31 * result + (signingPublicKey?.contentHashCode() ?: 0) + result = 31 * result + isVerifiedNickname.hashCode() + result = 31 * result + lastSeen.hashCode() + return result + } +} + /** * Manages active peers, nicknames, RSSI tracking, and peer fingerprints * Extracted from BluetoothMeshService for better separation of concerns * * Now includes centralized peer fingerprint management via PeerFingerprintManager singleton + * and support for signed announcement verification */ class PeerManager { @@ -19,13 +68,18 @@ class PeerManager { private const val CLEANUP_INTERVAL = 60000L // 1 minute } - // Peer tracking data - private val peerNicknames = ConcurrentHashMap() - private val activePeers = ConcurrentHashMap() // peerID -> lastSeen timestamp + // Peer tracking data - enhanced with verification status + private val peers = ConcurrentHashMap() // peerID -> PeerInfo private val peerRSSI = ConcurrentHashMap() private val announcedPeers = CopyOnWriteArrayList() private val announcedToPeers = CopyOnWriteArrayList() + // Legacy support for existing code + @Deprecated("Use PeerInfo structure instead") + private val peerNicknames = ConcurrentHashMap() + @Deprecated("Use PeerInfo structure instead") + private val activePeers = ConcurrentHashMap() // peerID -> lastSeen timestamp + // Centralized fingerprint management private val fingerprintManager = PeerFingerprintManager.getInstance() @@ -38,13 +92,90 @@ class PeerManager { init { startPeriodicCleanup() } - + + // MARK: - New PeerInfo-based methods + + /** + * Update peer information with verification data + * Similar to iOS updatePeer method + */ + fun updatePeerInfo( + peerID: String, + nickname: String, + noisePublicKey: ByteArray, + signingPublicKey: ByteArray, + isVerified: Boolean + ): Boolean { + if (peerID == "unknown") return false + + val now = System.currentTimeMillis() + val existingPeer = peers[peerID] + val isNewPeer = existingPeer == null + + // Update or create peer info + val peerInfo = PeerInfo( + id = peerID, + nickname = nickname, + isConnected = true, + noisePublicKey = noisePublicKey, + signingPublicKey = signingPublicKey, + isVerifiedNickname = isVerified, + lastSeen = now + ) + + peers[peerID] = peerInfo + + // Update legacy structures for compatibility + peerNicknames[peerID] = nickname + activePeers[peerID] = now + + if (isNewPeer && isVerified) { + announcedPeers.add(peerID) + notifyPeerListUpdate() + Log.d(TAG, "🆕 New verified peer: $nickname ($peerID)") + return true + } else if (isVerified) { + Log.d(TAG, "🔄 Updated verified peer: $nickname ($peerID)") + } else { + Log.d(TAG, "⚠️ Unverified peer announcement from: $nickname ($peerID)") + } + + return false + } + + /** + * Get peer info + */ + fun getPeerInfo(peerID: String): PeerInfo? { + return peers[peerID] + } + + /** + * Check if peer is verified + */ + fun isPeerVerified(peerID: String): Boolean { + return peers[peerID]?.isVerifiedNickname == true + } + + /** + * Get all verified peers + */ + fun getVerifiedPeers(): Map { + return peers.filterValues { it.isVerifiedNickname } + } + + // MARK: - Legacy Methods (maintained for compatibility) + /** * Update peer last seen timestamp */ fun updatePeerLastSeen(peerID: String) { if (peerID != "unknown") { activePeers[peerID] = System.currentTimeMillis() + // Also update PeerInfo if it exists + peers[peerID]?.let { info -> + peers[peerID] = info.copy(lastSeen = System.currentTimeMillis()) + } } } diff --git a/app/src/main/java/com/bitchat/android/model/IdentityAnnouncement.kt b/app/src/main/java/com/bitchat/android/model/IdentityAnnouncement.kt index 7b9b1935..2dfbe9c2 100644 --- a/app/src/main/java/com/bitchat/android/model/IdentityAnnouncement.kt +++ b/app/src/main/java/com/bitchat/android/model/IdentityAnnouncement.kt @@ -11,7 +11,8 @@ import com.bitchat.android.util.* @Parcelize data class IdentityAnnouncement( val nickname: String, - val publicKey: ByteArray // FIXED: Made non-nullable to match iOS and AnnouncementPacket + val noisePublicKey: ByteArray, // Noise static public key (Curve25519.KeyAgreement) + val signingPublicKey: ByteArray // Ed25519 public key for signing ) : Parcelable { /** @@ -19,7 +20,8 @@ data class IdentityAnnouncement( */ private enum class TLVType(val value: UByte) { NICKNAME(0x01u), - NOISE_PUBLIC_KEY(0x02u); + NOISE_PUBLIC_KEY(0x02u), + SIGNING_PUBLIC_KEY(0x03u); // NEW: Ed25519 signing public key companion object { fun fromValue(value: UByte): TLVType? { @@ -35,7 +37,7 @@ data class IdentityAnnouncement( val nicknameData = nickname.toByteArray(Charsets.UTF_8) // Check size limits - if (nicknameData.size > 255 || publicKey.size > 255) { + if (nicknameData.size > 255 || noisePublicKey.size > 255 || signingPublicKey.size > 255) { return null } @@ -46,10 +48,15 @@ data class IdentityAnnouncement( result.add(nicknameData.size.toByte()) result.addAll(nicknameData.toList()) - // TLV for public key + // TLV for noise public key result.add(TLVType.NOISE_PUBLIC_KEY.value.toByte()) - result.add(publicKey.size.toByte()) - result.addAll(publicKey.toList()) + result.add(noisePublicKey.size.toByte()) + result.addAll(noisePublicKey.toList()) + + // TLV for signing public key + result.add(TLVType.SIGNING_PUBLIC_KEY.value.toByte()) + result.add(signingPublicKey.size.toByte()) + result.addAll(signingPublicKey.toList()) return result.toByteArray() } @@ -64,12 +71,13 @@ data class IdentityAnnouncement( var offset = 0 var nickname: String? = null - var publicKey: ByteArray? = null + var noisePublicKey: ByteArray? = null + var signingPublicKey: ByteArray? = null while (offset + 2 <= dataCopy.size) { // Read TLV type val typeValue = dataCopy[offset].toUByte() - val type = TLVType.fromValue(typeValue) ?: return null + val type = TLVType.fromValue(typeValue) offset += 1 // Read TLV length @@ -83,19 +91,27 @@ data class IdentityAnnouncement( val value = dataCopy.sliceArray(offset until offset + length) offset += length + // Process known TLV types, skip unknown ones for forward compatibility when (type) { TLVType.NICKNAME -> { nickname = String(value, Charsets.UTF_8) } TLVType.NOISE_PUBLIC_KEY -> { - publicKey = value + noisePublicKey = value + } + TLVType.SIGNING_PUBLIC_KEY -> { + signingPublicKey = value + } + null -> { + // Unknown TLV; skip (tolerant decoder for forward compatibility) + continue } } } - // Both fields are required - return if (nickname != null && publicKey != null) { - IdentityAnnouncement(nickname, publicKey) + // All three fields are required + return if (nickname != null && noisePublicKey != null && signingPublicKey != null) { + IdentityAnnouncement(nickname, noisePublicKey, signingPublicKey) } else { null } @@ -110,18 +126,20 @@ data class IdentityAnnouncement( other as IdentityAnnouncement if (nickname != other.nickname) return false - if (!publicKey.contentEquals(other.publicKey)) return false + if (!noisePublicKey.contentEquals(other.noisePublicKey)) return false + if (!signingPublicKey.contentEquals(other.signingPublicKey)) return false return true } override fun hashCode(): Int { var result = nickname.hashCode() - result = 31 * result + publicKey.contentHashCode() + result = 31 * result + noisePublicKey.contentHashCode() + result = 31 * result + signingPublicKey.contentHashCode() return result } override fun toString(): String { - return "IdentityAnnouncement(nickname='$nickname', publicKey=${publicKey.joinToString("") { "%02x".format(it) }.take(16)}...)" + return "IdentityAnnouncement(nickname='$nickname', noisePublicKey=${noisePublicKey.joinToString("") { "%02x".format(it) }.take(16)}..., signingPublicKey=${signingPublicKey.joinToString("") { "%02x".format(it) }.take(16)}...)" } } diff --git a/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt b/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt index 2a657bc5..a3d469be 100644 --- a/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt +++ b/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt @@ -32,6 +32,10 @@ class NoiseEncryptionService(private val context: Context) { private val staticIdentityPrivateKey: ByteArray private val staticIdentityPublicKey: ByteArray + // Ed25519 signing key (persistent across app restarts) - loaded from secure storage + private val signingPrivateKey: ByteArray + private val signingPublicKey: ByteArray + // Session management private val sessionManager: NoiseSessionManager @@ -69,6 +73,23 @@ class NoiseEncryptionService(private val context: Context) { Log.d(TAG, "Generated and saved new static identity key") } + // Load or create Ed25519 signing key (persistent across sessions) + val loadedSigningKeyPair = identityStateManager.loadSigningKey() + if (loadedSigningKeyPair != null) { + signingPrivateKey = loadedSigningKeyPair.first + signingPublicKey = loadedSigningKeyPair.second + Log.d(TAG, "Loaded existing Ed25519 signing key") + } else { + // Generate new Ed25519 signing key pair + val signingKeyPair = generateEd25519KeyPair() + signingPrivateKey = signingKeyPair.first + signingPublicKey = signingKeyPair.second + + // Save to secure storage + identityStateManager.saveSigningKey(signingPrivateKey, signingPublicKey) + Log.d(TAG, "Generated and saved new Ed25519 signing key") + } + // Initialize session manager sessionManager = NoiseSessionManager(staticIdentityPrivateKey, staticIdentityPublicKey) @@ -86,6 +107,13 @@ class NoiseEncryptionService(private val context: Context) { fun getStaticPublicKeyData(): ByteArray { return staticIdentityPublicKey.clone() } + + /** + * Get our signing public key data for sharing (32 bytes) + */ + fun getSigningPublicKeyData(): ByteArray { + return signingPublicKey.clone() + } /** * Get our identity fingerprint (SHA-256 hash of static public key) @@ -338,6 +366,114 @@ class NoiseEncryptionService(private val context: Context) { return hash.joinToString("") { "%02x".format(it) } } + // MARK: - Packet Signing/Verification + + /** + * Sign a BitchatPacket using our Ed25519 signing key + */ + fun signPacket(packet: com.bitchat.android.protocol.BitchatPacket): com.bitchat.android.protocol.BitchatPacket? { + // Create canonical packet bytes for signing + val packetData = packet.toBinaryDataForSigning() ?: return null + + // Sign with our Ed25519 signing private key + val signature = signData(packetData) ?: return null + + // Return new packet with signature + return packet.copy(signature = signature) + } + + /** + * Verify a BitchatPacket signature using the provided public key + */ + fun verifyPacketSignature(packet: com.bitchat.android.protocol.BitchatPacket, publicKey: ByteArray): Boolean { + val signature = packet.signature ?: return false + + // Create canonical packet bytes for verification (without signature) + val packetData = packet.toBinaryDataForSigning() ?: return false + + // Verify signature using the provided Ed25519 public key + return verifySignature(signature, packetData, publicKey) + } + + /** + * Sign data with our Ed25519 signing key + */ + fun signData(data: ByteArray): ByteArray? { + return try { + // For simplicity, we'll implement this using BouncyCastle which should be available + // In a production system, you might want to use the Android Keystore + signWithEd25519(data, signingPrivateKey) + } catch (e: Exception) { + Log.e(TAG, "Failed to sign data: ${e.message}") + null + } + } + + /** + * Verify signature with a public key + */ + fun verifySignature(signature: ByteArray, data: ByteArray, publicKey: ByteArray): Boolean { + return try { + verifyWithEd25519(signature, data, publicKey) + } catch (e: Exception) { + Log.e(TAG, "Failed to verify signature: ${e.message}") + false + } + } + + /** + * Generate a new Ed25519 key pair for signing using BouncyCastle + * Returns (privateKey, publicKey) as 32-byte arrays + */ + private fun generateEd25519KeyPair(): Pair { + try { + // Use BouncyCastle for proper Ed25519 key generation + val keyGen = org.bouncycastle.crypto.generators.Ed25519KeyPairGenerator() + keyGen.init(org.bouncycastle.crypto.params.Ed25519KeyGenerationParameters(SecureRandom())) + val keyPair = keyGen.generateKeyPair() + + val privateKey = (keyPair.private as org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters).encoded + val publicKey = (keyPair.public as org.bouncycastle.crypto.params.Ed25519PublicKeyParameters).encoded + + return Pair(privateKey, publicKey) + } catch (e: Exception) { + Log.e(TAG, "Failed to generate Ed25519 key pair: ${e.message}") + throw e + } + } + + /** + * Sign data with Ed25519 private key using BouncyCastle + */ + private fun signWithEd25519(data: ByteArray, privateKey: ByteArray): ByteArray { + try { + val privateKeyParams = org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters(privateKey, 0) + val signer = org.bouncycastle.crypto.signers.Ed25519Signer() + signer.init(true, privateKeyParams) + signer.update(data, 0, data.size) + return signer.generateSignature() + } catch (e: Exception) { + Log.e(TAG, "Failed to sign data with Ed25519: ${e.message}") + throw e + } + } + + /** + * Verify Ed25519 signature using BouncyCastle + */ + private fun verifyWithEd25519(signature: ByteArray, data: ByteArray, publicKey: ByteArray): Boolean { + try { + val publicKeyParams = org.bouncycastle.crypto.params.Ed25519PublicKeyParameters(publicKey, 0) + val verifier = org.bouncycastle.crypto.signers.Ed25519Signer() + verifier.init(false, publicKeyParams) + verifier.update(data, 0, data.size) + return verifier.verifySignature(signature) + } catch (e: Exception) { + Log.e(TAG, "Failed to verify Ed25519 signature: ${e.message}") + return false + } + } + /** * Clean shutdown */ diff --git a/app/src/main/java/com/bitchat/android/protocol/BinaryProtocol.kt b/app/src/main/java/com/bitchat/android/protocol/BinaryProtocol.kt index b5b17008..3dcc47a2 100644 --- a/app/src/main/java/com/bitchat/android/protocol/BinaryProtocol.kt +++ b/app/src/main/java/com/bitchat/android/protocol/BinaryProtocol.kt @@ -56,7 +56,7 @@ data class BitchatPacket( val recipientID: ByteArray? = null, val timestamp: ULong, val payload: ByteArray, - val signature: ByteArray? = null, + var signature: ByteArray? = null, // Changed from val to var for packet signing var ttl: UByte ) : Parcelable { @@ -80,6 +80,26 @@ data class BitchatPacket( return BinaryProtocol.encode(this) } + /** + * Create binary representation for signing (without signature and TTL fields) + * TTL is excluded because it changes during packet relay operations + */ + fun toBinaryDataForSigning(): ByteArray? { + // Create a copy without signature and with fixed TTL for signing + // TTL must be excluded because it changes during relay + val unsignedPacket = BitchatPacket( + version = version, + type = type, + senderID = senderID, + recipientID = recipientID, + timestamp = timestamp, + payload = payload, + signature = null, // Remove signature for signing + ttl = 0u // Use fixed TTL=0 for signing to ensure relay compatibility + ) + return BinaryProtocol.encode(unsignedPacket) + } + companion object { fun fromBinaryData(data: ByteArray): BitchatPacket? { return BinaryProtocol.decode(data)