mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 16:25:22 +00:00
Sign announcements (#267)
* wip * announcements WIP * works * restore mainnet
This commit is contained in:
@@ -24,6 +24,8 @@ class SecureIdentityStateManager(private val context: Context) {
|
|||||||
private const val PREFS_NAME = "bitchat_identity"
|
private const val PREFS_NAME = "bitchat_identity"
|
||||||
private const val KEY_STATIC_PRIVATE_KEY = "static_private_key"
|
private const val KEY_STATIC_PRIVATE_KEY = "static_private_key"
|
||||||
private const val KEY_STATIC_PUBLIC_KEY = "static_public_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_LAST_ROTATION = "last_rotation"
|
||||||
private const val KEY_NEXT_ROTATION_INTERVAL = "next_rotation_interval"
|
private const val KEY_NEXT_ROTATION_INTERVAL = "next_rotation_interval"
|
||||||
|
|
||||||
@@ -108,6 +110,64 @@ class SecureIdentityStateManager(private val context: Context) {
|
|||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Signing Key Management
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load saved signing key pair
|
||||||
|
* Returns (privateKey, publicKey) or null if none exists
|
||||||
|
*/
|
||||||
|
fun loadSigningKey(): Pair<ByteArray, ByteArray>? {
|
||||||
|
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
|
// MARK: - Fingerprint Generation
|
||||||
|
|
||||||
|
|||||||
@@ -181,6 +181,14 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
return delegate?.getNickname()
|
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
|
// Packet operations
|
||||||
override fun sendPacket(packet: BitchatPacket) {
|
override fun sendPacket(packet: BitchatPacket) {
|
||||||
connectionManager.broadcastPacket(RoutedPacket(packet))
|
connectionManager.broadcastPacket(RoutedPacket(packet))
|
||||||
@@ -575,8 +583,15 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
return@launch
|
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
|
// Create iOS-compatible IdentityAnnouncement with TLV encoding
|
||||||
val announcement = IdentityAnnouncement(nickname, staticKey)
|
val announcement = IdentityAnnouncement(nickname, staticKey, signingKey)
|
||||||
val tlvPayload = announcement.encode()
|
val tlvPayload = announcement.encode()
|
||||||
if (tlvPayload == null) {
|
if (tlvPayload == null) {
|
||||||
Log.e(TAG, "Failed to encode announcement as TLV")
|
Log.e(TAG, "Failed to encode announcement as TLV")
|
||||||
@@ -590,8 +605,13 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
payload = tlvPayload
|
payload = tlvPayload
|
||||||
)
|
)
|
||||||
|
|
||||||
connectionManager.broadcastPacket(RoutedPacket(announcePacket))
|
// Sign the packet using our signing key (exactly like iOS)
|
||||||
Log.d(TAG, "Sent iOS-compatible TLV announce (${tlvPayload.size} bytes)")
|
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
|
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
|
// Create iOS-compatible IdentityAnnouncement with TLV encoding
|
||||||
val announcement = IdentityAnnouncement(nickname, staticKey)
|
val announcement = IdentityAnnouncement(nickname, staticKey, signingKey)
|
||||||
val tlvPayload = announcement.encode()
|
val tlvPayload = announcement.encode()
|
||||||
if (tlvPayload == null) {
|
if (tlvPayload == null) {
|
||||||
Log.e(TAG, "Failed to encode peer announcement as TLV")
|
Log.e(TAG, "Failed to encode peer announcement as TLV")
|
||||||
@@ -625,9 +652,14 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
payload = tlvPayload
|
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)
|
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? {
|
fun getPeerFingerprint(peerID: String): String? {
|
||||||
return peerManager.getFingerprintForPeer(peerID)
|
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
|
* Get our identity fingerprint
|
||||||
|
|||||||
@@ -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 {
|
suspend fun handleAnnounce(routed: RoutedPacket): Boolean {
|
||||||
val packet = routed.packet
|
val packet = routed.packet
|
||||||
@@ -179,26 +179,59 @@ class MessageHandler(private val myPeerID: String) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Successfully decoded TLV format exactly like iOS
|
// Verify packet signature using the announced signing public key
|
||||||
Log.d(TAG, "Received iOS-compatible announce from $peerID: nickname=${announcement.nickname}, " +
|
var verified = false
|
||||||
"publicKey=${announcement.publicKey.joinToString("") { "%02x".format(it) }.take(16)}...")
|
if (packet.signature != null) {
|
||||||
|
// Verify that the packet was signed by the signing private key corresponding to the announced signing public key
|
||||||
// Extract nickname and public key from TLV data
|
verified = delegate?.verifyEd25519Signature(packet.signature!!, packet.toBinaryDataForSigning()!!, announcement.signingPublicKey) ?: false
|
||||||
val nickname = announcement.nickname
|
if (!verified) {
|
||||||
val publicKey = announcement.publicKey
|
Log.w(TAG, "⚠️ Signature verification for announce failed ${peerID.take(8)}")
|
||||||
|
}
|
||||||
// Notify delegate to handle peer management with nickname
|
}
|
||||||
val isFirstAnnounce = delegate?.addOrUpdatePeer(peerID, nickname) ?: false
|
|
||||||
|
|
||||||
// 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(
|
delegate?.updatePeerIDBinding(
|
||||||
newPeerID = peerID,
|
newPeerID = peerID,
|
||||||
nickname = nickname,
|
nickname = nickname,
|
||||||
publicKey = publicKey,
|
publicKey = noisePublicKey,
|
||||||
previousPeerID = null
|
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
|
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) {
|
private suspend fun handleBroadcastMessage(routed: RoutedPacket) {
|
||||||
val packet = routed.packet
|
val packet = routed.packet
|
||||||
val peerID = routed.peerID ?: "unknown"
|
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 {
|
try {
|
||||||
// Parse message
|
// Parse message
|
||||||
val message = BitchatMessage(
|
val message = BitchatMessage(
|
||||||
@@ -398,6 +439,8 @@ interface MessageHandlerDelegate {
|
|||||||
fun getPeerNickname(peerID: String): String?
|
fun getPeerNickname(peerID: String): String?
|
||||||
fun getNetworkSize(): Int
|
fun getNetworkSize(): Int
|
||||||
fun getMyNickname(): String?
|
fun getMyNickname(): String?
|
||||||
|
fun getPeerInfo(peerID: String): PeerInfo?
|
||||||
|
fun updatePeerInfo(peerID: String, nickname: String, noisePublicKey: ByteArray, signingPublicKey: ByteArray, isVerified: Boolean): Boolean
|
||||||
|
|
||||||
// Packet operations
|
// Packet operations
|
||||||
fun sendPacket(packet: BitchatPacket)
|
fun sendPacket(packet: BitchatPacket)
|
||||||
|
|||||||
@@ -5,11 +5,60 @@ import kotlinx.coroutines.*
|
|||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
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
|
* Manages active peers, nicknames, RSSI tracking, and peer fingerprints
|
||||||
* Extracted from BluetoothMeshService for better separation of concerns
|
* Extracted from BluetoothMeshService for better separation of concerns
|
||||||
*
|
*
|
||||||
* Now includes centralized peer fingerprint management via PeerFingerprintManager singleton
|
* Now includes centralized peer fingerprint management via PeerFingerprintManager singleton
|
||||||
|
* and support for signed announcement verification
|
||||||
*/
|
*/
|
||||||
class PeerManager {
|
class PeerManager {
|
||||||
|
|
||||||
@@ -19,13 +68,18 @@ class PeerManager {
|
|||||||
private const val CLEANUP_INTERVAL = 60000L // 1 minute
|
private const val CLEANUP_INTERVAL = 60000L // 1 minute
|
||||||
}
|
}
|
||||||
|
|
||||||
// Peer tracking data
|
// Peer tracking data - enhanced with verification status
|
||||||
private val peerNicknames = ConcurrentHashMap<String, String>()
|
private val peers = ConcurrentHashMap<String, PeerInfo>() // peerID -> PeerInfo
|
||||||
private val activePeers = ConcurrentHashMap<String, Long>() // peerID -> lastSeen timestamp
|
|
||||||
private val peerRSSI = ConcurrentHashMap<String, Int>()
|
private val peerRSSI = ConcurrentHashMap<String, Int>()
|
||||||
private val announcedPeers = CopyOnWriteArrayList<String>()
|
private val announcedPeers = CopyOnWriteArrayList<String>()
|
||||||
private val announcedToPeers = CopyOnWriteArrayList<String>()
|
private val announcedToPeers = CopyOnWriteArrayList<String>()
|
||||||
|
|
||||||
|
// Legacy support for existing code
|
||||||
|
@Deprecated("Use PeerInfo structure instead")
|
||||||
|
private val peerNicknames = ConcurrentHashMap<String, String>()
|
||||||
|
@Deprecated("Use PeerInfo structure instead")
|
||||||
|
private val activePeers = ConcurrentHashMap<String, Long>() // peerID -> lastSeen timestamp
|
||||||
|
|
||||||
// Centralized fingerprint management
|
// Centralized fingerprint management
|
||||||
private val fingerprintManager = PeerFingerprintManager.getInstance()
|
private val fingerprintManager = PeerFingerprintManager.getInstance()
|
||||||
|
|
||||||
@@ -38,13 +92,90 @@ class PeerManager {
|
|||||||
init {
|
init {
|
||||||
startPeriodicCleanup()
|
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<String, PeerInfo> {
|
||||||
|
return peers.filterValues { it.isVerifiedNickname }
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Legacy Methods (maintained for compatibility)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update peer last seen timestamp
|
* Update peer last seen timestamp
|
||||||
*/
|
*/
|
||||||
fun updatePeerLastSeen(peerID: String) {
|
fun updatePeerLastSeen(peerID: String) {
|
||||||
if (peerID != "unknown") {
|
if (peerID != "unknown") {
|
||||||
activePeers[peerID] = System.currentTimeMillis()
|
activePeers[peerID] = System.currentTimeMillis()
|
||||||
|
// Also update PeerInfo if it exists
|
||||||
|
peers[peerID]?.let { info ->
|
||||||
|
peers[peerID] = info.copy(lastSeen = System.currentTimeMillis())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import com.bitchat.android.util.*
|
|||||||
@Parcelize
|
@Parcelize
|
||||||
data class IdentityAnnouncement(
|
data class IdentityAnnouncement(
|
||||||
val nickname: String,
|
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 {
|
) : Parcelable {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,7 +20,8 @@ data class IdentityAnnouncement(
|
|||||||
*/
|
*/
|
||||||
private enum class TLVType(val value: UByte) {
|
private enum class TLVType(val value: UByte) {
|
||||||
NICKNAME(0x01u),
|
NICKNAME(0x01u),
|
||||||
NOISE_PUBLIC_KEY(0x02u);
|
NOISE_PUBLIC_KEY(0x02u),
|
||||||
|
SIGNING_PUBLIC_KEY(0x03u); // NEW: Ed25519 signing public key
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromValue(value: UByte): TLVType? {
|
fun fromValue(value: UByte): TLVType? {
|
||||||
@@ -35,7 +37,7 @@ data class IdentityAnnouncement(
|
|||||||
val nicknameData = nickname.toByteArray(Charsets.UTF_8)
|
val nicknameData = nickname.toByteArray(Charsets.UTF_8)
|
||||||
|
|
||||||
// Check size limits
|
// Check size limits
|
||||||
if (nicknameData.size > 255 || publicKey.size > 255) {
|
if (nicknameData.size > 255 || noisePublicKey.size > 255 || signingPublicKey.size > 255) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,10 +48,15 @@ data class IdentityAnnouncement(
|
|||||||
result.add(nicknameData.size.toByte())
|
result.add(nicknameData.size.toByte())
|
||||||
result.addAll(nicknameData.toList())
|
result.addAll(nicknameData.toList())
|
||||||
|
|
||||||
// TLV for public key
|
// TLV for noise public key
|
||||||
result.add(TLVType.NOISE_PUBLIC_KEY.value.toByte())
|
result.add(TLVType.NOISE_PUBLIC_KEY.value.toByte())
|
||||||
result.add(publicKey.size.toByte())
|
result.add(noisePublicKey.size.toByte())
|
||||||
result.addAll(publicKey.toList())
|
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()
|
return result.toByteArray()
|
||||||
}
|
}
|
||||||
@@ -64,12 +71,13 @@ data class IdentityAnnouncement(
|
|||||||
|
|
||||||
var offset = 0
|
var offset = 0
|
||||||
var nickname: String? = null
|
var nickname: String? = null
|
||||||
var publicKey: ByteArray? = null
|
var noisePublicKey: ByteArray? = null
|
||||||
|
var signingPublicKey: ByteArray? = null
|
||||||
|
|
||||||
while (offset + 2 <= dataCopy.size) {
|
while (offset + 2 <= dataCopy.size) {
|
||||||
// Read TLV type
|
// Read TLV type
|
||||||
val typeValue = dataCopy[offset].toUByte()
|
val typeValue = dataCopy[offset].toUByte()
|
||||||
val type = TLVType.fromValue(typeValue) ?: return null
|
val type = TLVType.fromValue(typeValue)
|
||||||
offset += 1
|
offset += 1
|
||||||
|
|
||||||
// Read TLV length
|
// Read TLV length
|
||||||
@@ -83,19 +91,27 @@ data class IdentityAnnouncement(
|
|||||||
val value = dataCopy.sliceArray(offset until offset + length)
|
val value = dataCopy.sliceArray(offset until offset + length)
|
||||||
offset += length
|
offset += length
|
||||||
|
|
||||||
|
// Process known TLV types, skip unknown ones for forward compatibility
|
||||||
when (type) {
|
when (type) {
|
||||||
TLVType.NICKNAME -> {
|
TLVType.NICKNAME -> {
|
||||||
nickname = String(value, Charsets.UTF_8)
|
nickname = String(value, Charsets.UTF_8)
|
||||||
}
|
}
|
||||||
TLVType.NOISE_PUBLIC_KEY -> {
|
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
|
// All three fields are required
|
||||||
return if (nickname != null && publicKey != null) {
|
return if (nickname != null && noisePublicKey != null && signingPublicKey != null) {
|
||||||
IdentityAnnouncement(nickname, publicKey)
|
IdentityAnnouncement(nickname, noisePublicKey, signingPublicKey)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -110,18 +126,20 @@ data class IdentityAnnouncement(
|
|||||||
other as IdentityAnnouncement
|
other as IdentityAnnouncement
|
||||||
|
|
||||||
if (nickname != other.nickname) return false
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = nickname.hashCode()
|
var result = nickname.hashCode()
|
||||||
result = 31 * result + publicKey.contentHashCode()
|
result = 31 * result + noisePublicKey.contentHashCode()
|
||||||
|
result = 31 * result + signingPublicKey.contentHashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
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)}...)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ class NoiseEncryptionService(private val context: Context) {
|
|||||||
private val staticIdentityPrivateKey: ByteArray
|
private val staticIdentityPrivateKey: ByteArray
|
||||||
private val staticIdentityPublicKey: 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
|
// Session management
|
||||||
private val sessionManager: NoiseSessionManager
|
private val sessionManager: NoiseSessionManager
|
||||||
|
|
||||||
@@ -69,6 +73,23 @@ class NoiseEncryptionService(private val context: Context) {
|
|||||||
Log.d(TAG, "Generated and saved new static identity key")
|
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
|
// Initialize session manager
|
||||||
sessionManager = NoiseSessionManager(staticIdentityPrivateKey, staticIdentityPublicKey)
|
sessionManager = NoiseSessionManager(staticIdentityPrivateKey, staticIdentityPublicKey)
|
||||||
|
|
||||||
@@ -86,6 +107,13 @@ class NoiseEncryptionService(private val context: Context) {
|
|||||||
fun getStaticPublicKeyData(): ByteArray {
|
fun getStaticPublicKeyData(): ByteArray {
|
||||||
return staticIdentityPublicKey.clone()
|
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)
|
* 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) }
|
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<ByteArray, ByteArray> {
|
||||||
|
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
|
* Clean shutdown
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ data class BitchatPacket(
|
|||||||
val recipientID: ByteArray? = null,
|
val recipientID: ByteArray? = null,
|
||||||
val timestamp: ULong,
|
val timestamp: ULong,
|
||||||
val payload: ByteArray,
|
val payload: ByteArray,
|
||||||
val signature: ByteArray? = null,
|
var signature: ByteArray? = null, // Changed from val to var for packet signing
|
||||||
var ttl: UByte
|
var ttl: UByte
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
@@ -80,6 +80,26 @@ data class BitchatPacket(
|
|||||||
return BinaryProtocol.encode(this)
|
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 {
|
companion object {
|
||||||
fun fromBinaryData(data: ByteArray): BitchatPacket? {
|
fun fromBinaryData(data: ByteArray): BitchatPacket? {
|
||||||
return BinaryProtocol.decode(data)
|
return BinaryProtocol.decode(data)
|
||||||
|
|||||||
Reference in New Issue
Block a user