mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 23:25:19 +00:00
improve identity announcements (#198)
* improve identity announcements * touches * signing noise id * panic mode delete signing key
This commit is contained in:
@@ -3,6 +3,12 @@ package com.bitchat.android.crypto
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.bitchat.android.noise.NoiseEncryptionService
|
||||
import org.bouncycastle.crypto.AsymmetricCipherKeyPair
|
||||
import org.bouncycastle.crypto.generators.Ed25519KeyPairGenerator
|
||||
import org.bouncycastle.crypto.params.Ed25519KeyGenerationParameters
|
||||
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters
|
||||
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters
|
||||
import org.bouncycastle.crypto.signers.Ed25519Signer
|
||||
import java.security.SecureRandom
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
@@ -17,6 +23,7 @@ class EncryptionService(private val context: Context) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "EncryptionService"
|
||||
private const val ED25519_PRIVATE_KEY_PREF = "ed25519_signing_private_key"
|
||||
}
|
||||
|
||||
// Core Noise encryption service
|
||||
@@ -25,12 +32,23 @@ class EncryptionService(private val context: Context) {
|
||||
// Session tracking for established connections
|
||||
private val establishedSessions = ConcurrentHashMap<String, String>() // peerID -> fingerprint
|
||||
|
||||
// Ed25519 signing keys (separate from Noise static keys)
|
||||
private val ed25519PrivateKey: Ed25519PrivateKeyParameters
|
||||
private val ed25519PublicKey: Ed25519PublicKeyParameters
|
||||
|
||||
// Callbacks for UI state updates
|
||||
var onSessionEstablished: ((String) -> Unit)? = null // peerID
|
||||
var onSessionLost: ((String) -> Unit)? = null // peerID
|
||||
var onHandshakeRequired: ((String) -> Unit)? = null // peerID
|
||||
|
||||
init {
|
||||
// Initialize or load Ed25519 signing keys
|
||||
val keyPair = loadOrCreateEd25519KeyPair()
|
||||
ed25519PrivateKey = keyPair.private as Ed25519PrivateKeyParameters
|
||||
ed25519PublicKey = keyPair.public as Ed25519PublicKeyParameters
|
||||
|
||||
Log.d(TAG, "✅ Ed25519 signing keys initialized")
|
||||
|
||||
// Set up NoiseEncryptionService callbacks
|
||||
noiseService.onPeerAuthenticated = { peerID, fingerprint ->
|
||||
Log.d(TAG, "✅ Noise session established with $peerID, fingerprint: ${fingerprint.take(16)}...")
|
||||
@@ -63,24 +81,26 @@ class EncryptionService(private val context: Context) {
|
||||
|
||||
/**
|
||||
* Get our signing public key for Ed25519 signatures (for identity announcements)
|
||||
* Note: In the current implementation, this returns the same as static key
|
||||
* In a full implementation, this would be a separate Ed25519 key
|
||||
*/
|
||||
fun getSigningPublicKey(): ByteArray? {
|
||||
// For now, return the static public key as placeholder
|
||||
// In a full implementation, this would be a separate Ed25519 signing key
|
||||
return noiseService.getStaticPublicKeyData()
|
||||
return ed25519PublicKey.encoded
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign data using our signing key (for identity announcements)
|
||||
* Note: In the current simplified implementation, this returns empty signature
|
||||
* In a full implementation, this would use Ed25519 signing
|
||||
* Sign data using our Ed25519 signing key (for identity announcements)
|
||||
*/
|
||||
fun signData(data: ByteArray): ByteArray? {
|
||||
// For now, return empty signature as placeholder
|
||||
// In a full implementation, this would use Ed25519 to sign the data
|
||||
return ByteArray(64) // Ed25519 signature length placeholder
|
||||
return try {
|
||||
val signer = Ed25519Signer()
|
||||
signer.init(true, ed25519PrivateKey)
|
||||
signer.update(data, 0, data.size)
|
||||
val signature = signer.generateSignature()
|
||||
Log.d(TAG, "✅ Generated Ed25519 signature (${signature.size} bytes)")
|
||||
signature
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ Failed to sign data with Ed25519: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,6 +132,15 @@ class EncryptionService(private val context: Context) {
|
||||
fun clearPersistentIdentity() {
|
||||
noiseService.clearPersistentIdentity()
|
||||
establishedSessions.clear()
|
||||
|
||||
// Clear Ed25519 signing key from preferences
|
||||
try {
|
||||
val prefs = context.getSharedPreferences("bitchat_crypto", Context.MODE_PRIVATE)
|
||||
prefs.edit().remove(ED25519_PRIVATE_KEY_PREF).apply()
|
||||
Log.d(TAG, "🗑️ Cleared Ed25519 signing keys from preferences")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ Failed to clear Ed25519 keys: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -321,4 +350,67 @@ class EncryptionService(private val context: Context) {
|
||||
noiseService.shutdown()
|
||||
Log.d(TAG, "🔌 EncryptionService shut down")
|
||||
}
|
||||
|
||||
// MARK: - Ed25519 Signature Verification
|
||||
|
||||
/**
|
||||
* Verify Ed25519 signature against data using a public key
|
||||
*/
|
||||
fun verifyEd25519Signature(signature: ByteArray, data: ByteArray, publicKeyBytes: ByteArray): Boolean {
|
||||
return try {
|
||||
val publicKey = Ed25519PublicKeyParameters(publicKeyBytes, 0)
|
||||
val verifier = Ed25519Signer()
|
||||
verifier.init(false, publicKey)
|
||||
verifier.update(data, 0, data.size)
|
||||
val isValid = verifier.verifySignature(signature)
|
||||
Log.d(TAG, "✅ Ed25519 signature verification: $isValid")
|
||||
isValid
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ Failed to verify Ed25519 signature: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private Key Management
|
||||
|
||||
/**
|
||||
* Load existing Ed25519 key pair from preferences or create a new one
|
||||
*/
|
||||
private fun loadOrCreateEd25519KeyPair(): AsymmetricCipherKeyPair {
|
||||
try {
|
||||
val prefs = context.getSharedPreferences("bitchat_crypto", Context.MODE_PRIVATE)
|
||||
val storedKey = prefs.getString(ED25519_PRIVATE_KEY_PREF, null)
|
||||
|
||||
if (storedKey != null) {
|
||||
// Load existing key
|
||||
val privateKeyBytes = android.util.Base64.decode(storedKey, android.util.Base64.DEFAULT)
|
||||
val privateKey = Ed25519PrivateKeyParameters(privateKeyBytes, 0)
|
||||
val publicKey = privateKey.generatePublicKey()
|
||||
Log.d(TAG, "✅ Loaded existing Ed25519 signing key pair")
|
||||
return AsymmetricCipherKeyPair(publicKey, privateKey)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "⚠️ Failed to load existing Ed25519 key, creating new one: ${e.message}")
|
||||
}
|
||||
|
||||
// Create new key pair
|
||||
val keyGen = Ed25519KeyPairGenerator()
|
||||
keyGen.init(Ed25519KeyGenerationParameters(SecureRandom()))
|
||||
val keyPair = keyGen.generateKeyPair()
|
||||
|
||||
// Store private key in preferences
|
||||
try {
|
||||
val privateKey = keyPair.private as Ed25519PrivateKeyParameters
|
||||
val privateKeyBytes = privateKey.encoded
|
||||
val encodedKey = android.util.Base64.encodeToString(privateKeyBytes, android.util.Base64.DEFAULT)
|
||||
|
||||
val prefs = context.getSharedPreferences("bitchat_crypto", Context.MODE_PRIVATE)
|
||||
prefs.edit().putString(ED25519_PRIVATE_KEY_PREF, encodedKey).apply()
|
||||
Log.d(TAG, "✅ Created and stored new Ed25519 signing key pair")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ Failed to store Ed25519 private key: ${e.message}")
|
||||
}
|
||||
|
||||
return keyPair
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,13 +41,23 @@ class BluetoothConnectionManager(
|
||||
// Delegate for component managers to call back to main manager
|
||||
private val componentDelegate = object : BluetoothConnectionManagerDelegate {
|
||||
override fun onPacketReceived(packet: BitchatPacket, peerID: String, device: BluetoothDevice?) {
|
||||
Log.d(TAG, "onPacketReceived: Packet received from ${device?.address} ($peerID)")
|
||||
device?.let { bluetoothDevice ->
|
||||
// if connection does not have a peerID yet, we assume that the first package
|
||||
// we receive from that connection is from the peer
|
||||
if (!connectionTracker.addressPeerMap.containsKey(device.address)) {
|
||||
Log.d(TAG, "First packet received from new device: ${bluetoothDevice.address}, assuming peerID: $peerID")
|
||||
connectionTracker.addressPeerMap[device.address] = peerID
|
||||
}
|
||||
// Get current RSSI for this device and update if available
|
||||
val currentRSSI = connectionTracker.getBestRSSI(bluetoothDevice.address)
|
||||
if (currentRSSI != null) {
|
||||
delegate?.onRSSIUpdated(bluetoothDevice.address, currentRSSI)
|
||||
}
|
||||
}
|
||||
|
||||
if (peerID == myPeerID) return // Ignore messages from self
|
||||
|
||||
delegate?.onPacketReceived(packet, peerID, device)
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ class BluetoothConnectionTracker(
|
||||
* Add a device connection
|
||||
*/
|
||||
fun addDeviceConnection(deviceAddress: String, deviceConn: DeviceConnection) {
|
||||
Log.d(TAG, "Tracker: Adding device connection for $deviceAddress")
|
||||
Log.d(TAG, "Tracker: Adding device connection for $deviceAddress (isClient: ${deviceConn.isClient}")
|
||||
connectedDevices[deviceAddress] = deviceConn
|
||||
pendingConnections.remove(deviceAddress)
|
||||
}
|
||||
|
||||
@@ -133,6 +133,13 @@ class BluetoothGattServerManager(
|
||||
isClient = false
|
||||
)
|
||||
connectionTracker.addDeviceConnection(device.address, deviceConn)
|
||||
|
||||
connectionScope.launch {
|
||||
delay(1000)
|
||||
if (isActive) { // Check if still active
|
||||
delegate?.onDeviceConnected(device)
|
||||
}
|
||||
}
|
||||
}
|
||||
BluetoothProfile.STATE_DISCONNECTED -> {
|
||||
Log.i(TAG, "Server: Device disconnected ${device.address}")
|
||||
@@ -204,9 +211,9 @@ class BluetoothGattServerManager(
|
||||
}
|
||||
|
||||
if (BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE.contentEquals(value)) {
|
||||
Log.d(TAG, "Device ${device.address} subscribed to notifications")
|
||||
connectionTracker.addSubscribedDevice(device)
|
||||
|
||||
|
||||
Log.d(TAG, "Server: Connection setup complete for ${device.address}")
|
||||
connectionScope.launch {
|
||||
delay(100)
|
||||
if (isActive) { // Check if still active
|
||||
@@ -272,7 +279,9 @@ class BluetoothGattServerManager(
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
private fun startAdvertising() {
|
||||
if (!permissionManager.hasBluetoothPermissions() || bleAdvertiser == null || !isActive) return
|
||||
if (!permissionManager.hasBluetoothPermissions() || bleAdvertiser == null || !isActive || bluetoothAdapter == null || !bluetoothAdapter.isMultipleAdvertisementSupported()) {
|
||||
throw Exception("Missing Bluetooth permissions or BLE advertiser not available")
|
||||
}
|
||||
|
||||
val settings = powerManager.getAdvertiseSettings()
|
||||
|
||||
|
||||
@@ -63,11 +63,8 @@ class BluetoothMeshService(private val context: Context) {
|
||||
|
||||
init {
|
||||
setupDelegates()
|
||||
|
||||
// Wire up PacketProcessor reference for recursive handling in MessageHandler
|
||||
messageHandler.packetProcessor = packetProcessor
|
||||
sendPeriodicBroadcastAnnounce()
|
||||
// startPeriodicDebugLogging()
|
||||
startPeriodicDebugLogging()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,6 +95,7 @@ class BluetoothMeshService(private val context: Context) {
|
||||
try {
|
||||
delay(30000) // 30 seconds
|
||||
sendBroadcastAnnounce()
|
||||
broadcastNoiseIdentityAnnouncement()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in periodic broadcast announce: ${e.message}")
|
||||
}
|
||||
@@ -126,11 +124,7 @@ class BluetoothMeshService(private val context: Context) {
|
||||
|
||||
// SecurityManager delegate for key exchange notifications
|
||||
securityManager.delegate = object : SecurityManagerDelegate {
|
||||
override fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray, receivedAddress: String?) {
|
||||
receivedAddress?.let { address ->
|
||||
connectionManager.addressPeerMap[address] = peerID
|
||||
}
|
||||
|
||||
override fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray) {
|
||||
// Send announcement and cached messages after key exchange
|
||||
serviceScope.launch {
|
||||
delay(100)
|
||||
@@ -150,7 +144,7 @@ class BluetoothMeshService(private val context: Context) {
|
||||
recipientID = hexStringToByteArray(peerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = response,
|
||||
ttl = 1u
|
||||
ttl = MAX_TTL
|
||||
)
|
||||
connectionManager.broadcastPacket(RoutedPacket(responsePacket))
|
||||
Log.d(TAG, "Sent Noise handshake response to $peerID (${response.size} bytes)")
|
||||
@@ -225,6 +219,10 @@ class BluetoothMeshService(private val context: Context) {
|
||||
return securityManager.decryptFromPeer(encryptedData, senderPeerID)
|
||||
}
|
||||
|
||||
override fun verifyEd25519Signature(signature: ByteArray, data: ByteArray, publicKey: ByteArray): Boolean {
|
||||
return encryptionService.verifyEd25519Signature(signature, data, publicKey)
|
||||
}
|
||||
|
||||
// Noise protocol operations
|
||||
override fun hasNoiseSession(peerID: String): Boolean {
|
||||
return encryptionService.hasEstablishedSession(peerID)
|
||||
@@ -243,7 +241,7 @@ class BluetoothMeshService(private val context: Context) {
|
||||
recipientID = hexStringToByteArray(peerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = handshakeData,
|
||||
ttl = 1u
|
||||
ttl = MAX_TTL
|
||||
)
|
||||
|
||||
connectionManager.broadcastPacket(RoutedPacket(packet))
|
||||
@@ -387,13 +385,13 @@ class BluetoothMeshService(private val context: Context) {
|
||||
override fun onDeviceConnected(device: android.bluetooth.BluetoothDevice) {
|
||||
// Send initial announcements after services are ready
|
||||
serviceScope.launch {
|
||||
delay(100)
|
||||
delay(200)
|
||||
sendBroadcastAnnounce()
|
||||
}
|
||||
// Send key exchange to newly connected device
|
||||
serviceScope.launch {
|
||||
delay(100) // Ensure connection is stable
|
||||
sendKeyExchangeToDevice()
|
||||
delay(400) // Ensure connection is stable
|
||||
broadcastNoiseIdentityAnnouncement()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -685,7 +683,7 @@ class BluetoothMeshService(private val context: Context) {
|
||||
|
||||
val announcePacket = BitchatPacket(
|
||||
type = MessageType.ANNOUNCE.value,
|
||||
ttl = 3u,
|
||||
ttl = MAX_TTL,
|
||||
senderID = myPeerID,
|
||||
payload = nickname.toByteArray()
|
||||
)
|
||||
@@ -703,7 +701,7 @@ class BluetoothMeshService(private val context: Context) {
|
||||
val nickname = delegate?.getNickname() ?: myPeerID
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.ANNOUNCE.value,
|
||||
ttl = 3u,
|
||||
ttl = MAX_TTL,
|
||||
senderID = myPeerID,
|
||||
payload = nickname.toByteArray()
|
||||
)
|
||||
@@ -715,7 +713,7 @@ class BluetoothMeshService(private val context: Context) {
|
||||
/**
|
||||
* Send key exchange to newly connected device
|
||||
*/
|
||||
fun sendKeyExchangeToDevice() {
|
||||
fun broadcastNoiseIdentityAnnouncement() {
|
||||
serviceScope.launch {
|
||||
try {
|
||||
val nickname = delegate?.getNickname() ?: myPeerID
|
||||
@@ -726,13 +724,11 @@ class BluetoothMeshService(private val context: Context) {
|
||||
val announcementData = announcement.toBinaryData()
|
||||
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_IDENTITY_ANNOUNCE.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = SpecialRecipients.BROADCAST,
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
ttl = MAX_TTL,
|
||||
senderID = myPeerID,
|
||||
payload = announcementData,
|
||||
ttl = 1u
|
||||
|
||||
)
|
||||
|
||||
connectionManager.broadcastPacket(RoutedPacket(packet))
|
||||
@@ -838,7 +834,7 @@ class BluetoothMeshService(private val context: Context) {
|
||||
val nickname = delegate?.getNickname() ?: myPeerID
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.LEAVE.value,
|
||||
ttl = 1u,
|
||||
ttl = MAX_TTL,
|
||||
senderID = myPeerID,
|
||||
payload = nickname.toByteArray()
|
||||
)
|
||||
@@ -984,6 +980,40 @@ class BluetoothMeshService(private val context: Context) {
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// MARK: - Panic Mode Support
|
||||
|
||||
/**
|
||||
* Clear all internal mesh service data (for panic mode)
|
||||
*/
|
||||
fun clearAllInternalData() {
|
||||
Log.w(TAG, "🚨 Clearing all mesh service internal data")
|
||||
try {
|
||||
// Clear all managers
|
||||
fragmentManager.clearAllFragments()
|
||||
storeForwardManager.clearAllCache()
|
||||
securityManager.clearAllData()
|
||||
peerManager.clearAllPeers()
|
||||
peerManager.clearAllFingerprints()
|
||||
Log.d(TAG, "✅ Cleared all mesh service internal data")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ Error clearing mesh service internal data: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all encryption and cryptographic data (for panic mode)
|
||||
*/
|
||||
fun clearAllEncryptionData() {
|
||||
Log.w(TAG, "🚨 Clearing all encryption data")
|
||||
try {
|
||||
// Clear encryption service persistent identity (includes Ed25519 signing keys)
|
||||
encryptionService.clearPersistentIdentity()
|
||||
Log.d(TAG, "✅ Cleared all encryption data")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ Error clearing encryption data: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,8 +20,19 @@ import kotlinx.coroutines.channels.actor
|
||||
/**
|
||||
* Handles packet broadcasting to connected devices using actor pattern for serialization
|
||||
*
|
||||
* SERIALIZATION FIX: Uses Kotlin coroutine actor to serialize all packet broadcasting
|
||||
* This prevents race conditions when multiple threads try to broadcast simultaneously
|
||||
* In Bluetooth Low Energy (BLE):
|
||||
*
|
||||
* Peripheral (server):
|
||||
* Advertises.
|
||||
* Accepts connections.
|
||||
* Hosts a GATT server.
|
||||
* Remote devices read/write/subscribe to characteristics.
|
||||
*
|
||||
* Central (client):
|
||||
* Scans.
|
||||
* Initiates connections.
|
||||
* Hosts a GATT client.
|
||||
* Reads/writes to the peripheral’s characteristics.
|
||||
*/
|
||||
class BluetoothPacketBroadcaster(
|
||||
private val connectionScope: CoroutineScope,
|
||||
@@ -52,9 +63,7 @@ class BluetoothPacketBroadcaster(
|
||||
Log.d(TAG, "🎭 Created packet broadcaster actor")
|
||||
try {
|
||||
for (request in channel) {
|
||||
Log.d(TAG, "Processing broadcast for packet type ${request.routed.packet.type} (serialized)")
|
||||
broadcastSinglePacketInternal(request.routed, request.gattServer, request.characteristic)
|
||||
Log.d(TAG, "Completed broadcast for packet type ${request.routed.packet.type}")
|
||||
}
|
||||
} finally {
|
||||
Log.d(TAG, "🎭 Packet broadcaster actor terminated")
|
||||
@@ -158,25 +167,25 @@ class BluetoothPacketBroadcaster(
|
||||
// Send to server connections (devices connected to our GATT server)
|
||||
subscribedDevices.forEach { device ->
|
||||
if (device.address == routed.relayAddress) {
|
||||
Log.d(TAG, "Skipping broadcast back to relayer: ${device.address}")
|
||||
Log.d(TAG, "Skipping broadcast to client back to relayer: ${device.address}")
|
||||
return@forEach
|
||||
}
|
||||
if (connectionTracker.addressPeerMap[device.address] == senderID) {
|
||||
Log.d(TAG, "Skipping broadcast back to sender: ${device.address}")
|
||||
Log.d(TAG, "Skipping broadcast to client back to sender: ${device.address}")
|
||||
return@forEach
|
||||
}
|
||||
notifyDevice(device, data, gattServer, characteristic)
|
||||
}
|
||||
|
||||
// Send to client connections
|
||||
// Send to client connections (GATT servers we are connected to)
|
||||
connectedDevices.values.forEach { deviceConn ->
|
||||
if (deviceConn.isClient && deviceConn.gatt != null && deviceConn.characteristic != null) {
|
||||
if (deviceConn.device.address == routed.relayAddress) {
|
||||
Log.d(TAG, "Skipping broadcast back to relayer: ${deviceConn.device.address}")
|
||||
Log.d(TAG, "Skipping broadcast to server back to relayer: ${deviceConn.device.address}")
|
||||
return@forEach
|
||||
}
|
||||
if (connectionTracker.addressPeerMap[deviceConn.device.address] == senderID) {
|
||||
Log.d(TAG, "Skipping broadcast back to sender: ${deviceConn.device.address}")
|
||||
Log.d(TAG, "Skipping roadcast to server back to sender: ${deviceConn.device.address}")
|
||||
return@forEach
|
||||
}
|
||||
writeToDeviceConn(deviceConn, data)
|
||||
@@ -185,7 +194,7 @@ class BluetoothPacketBroadcaster(
|
||||
}
|
||||
|
||||
/**
|
||||
* Send data to a single device (server side)
|
||||
* Send data to a single device (server->client)
|
||||
*/
|
||||
private fun notifyDevice(
|
||||
device: BluetoothDevice,
|
||||
@@ -211,7 +220,7 @@ class BluetoothPacketBroadcaster(
|
||||
}
|
||||
|
||||
/**
|
||||
* Send data to a single device (client side)
|
||||
* Send data to a single device (client->server)
|
||||
*/
|
||||
private fun writeToDeviceConn(
|
||||
deviceConn: BluetoothConnectionTracker.DeviceConnection,
|
||||
|
||||
@@ -120,13 +120,27 @@ class MessageHandler(private val myPeerID: String) {
|
||||
Log.d(TAG, "Parsed identity announcement: peerID=${announcement.peerID}, " +
|
||||
"nickname=${announcement.nickname}, fingerprint=${announcement.fingerprint?.take(16)}...")
|
||||
|
||||
// Verify the announcement signature (basic validation)
|
||||
// In a full implementation, this would use cryptographic verification
|
||||
// Verify the announcement signature using Ed25519 (iOS compatibility)
|
||||
if (announcement.signature.isEmpty()) {
|
||||
Log.w(TAG, "Identity announcement from $peerID has no signature")
|
||||
Log.w(TAG, "❌ Identity announcement from $peerID has no signature - rejecting")
|
||||
return
|
||||
}
|
||||
|
||||
// Verify signature using the same format as iOS
|
||||
val timestampMs = announcement.timestamp.time
|
||||
val bindingData = announcement.peerID.toByteArray(Charsets.UTF_8) +
|
||||
announcement.publicKey +
|
||||
timestampMs.toString().toByteArray(Charsets.UTF_8)
|
||||
|
||||
val isSignatureValid = delegate?.verifyEd25519Signature(announcement.signature, bindingData, announcement.signingPublicKey) ?: false
|
||||
|
||||
if (!isSignatureValid) {
|
||||
Log.w(TAG, "❌ Signature verification failed for identity announcement from $peerID - rejecting")
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "✅ Signature verification successful for identity announcement from $peerID")
|
||||
|
||||
// Update peer binding in the delegate (ChatViewModel/BluetoothMeshService)
|
||||
delegate?.updatePeerIDBinding(
|
||||
newPeerID = announcement.peerID,
|
||||
@@ -377,6 +391,7 @@ interface MessageHandlerDelegate {
|
||||
fun verifySignature(packet: BitchatPacket, peerID: String): Boolean
|
||||
fun encryptForPeer(data: ByteArray, recipientPeerID: String): ByteArray?
|
||||
fun decryptFromPeer(encryptedData: ByteArray, senderPeerID: String): ByteArray?
|
||||
fun verifyEd25519Signature(signature: ByteArray, data: ByteArray, publicKey: ByteArray): Boolean
|
||||
|
||||
// Noise protocol operations
|
||||
fun hasNoiseSession(peerID: String): Boolean
|
||||
|
||||
@@ -70,7 +70,15 @@ class PacketProcessor(private val myPeerID: String) {
|
||||
* SURGICAL FIX: Route to per-peer actor for serialized processing
|
||||
*/
|
||||
fun processPacket(routed: RoutedPacket) {
|
||||
val peerID = routed.peerID ?: "unknown"
|
||||
Log.d(TAG, "processPacket ${routed.packet.type}")
|
||||
val peerID = routed.peerID
|
||||
|
||||
if (peerID == null) {
|
||||
Log.w(TAG, "Received packet with no peer ID, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Get or create actor for this peer
|
||||
val actor = actors.getOrPut(peerID) { getOrCreateActorForPeer(peerID) }
|
||||
|
||||
@@ -135,7 +135,7 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
||||
// Check if session is now established (handshake complete)
|
||||
if (encryptionService.hasEstablishedSession(peerID)) {
|
||||
Log.d(TAG, "✅ Noise handshake completed with $peerID")
|
||||
delegate?.onKeyExchangeCompleted(peerID, packet.payload, routed.relayAddress)
|
||||
delegate?.onKeyExchangeCompleted(peerID, packet.payload)
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -145,48 +145,7 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Handle key exchange packet
|
||||
// */
|
||||
// suspend fun handleKeyExchange(routed: RoutedPacket): Boolean {
|
||||
// val packet = routed.packet
|
||||
// val peerID = routed.peerID ?: "unknown"
|
||||
//
|
||||
// if (peerID == myPeerID) return false
|
||||
//
|
||||
// if (packet.payload.isEmpty()) {
|
||||
// Log.w(TAG, "Key exchange packet has empty payload")
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// // Prevent duplicate key exchange processing
|
||||
// val exchangeKey = "$peerID-${packet.payload.sliceArray(0 until minOf(16, packet.payload.size)).contentHashCode()}"
|
||||
//
|
||||
// if (processedKeyExchanges.contains(exchangeKey)) {
|
||||
// Log.d(TAG, "Already processed key exchange: $exchangeKey")
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// processedKeyExchanges.add(exchangeKey)
|
||||
//
|
||||
// try {
|
||||
// // Process the key exchange
|
||||
// encryptionService.addPeerPublicKey(peerID, packet.payload)
|
||||
//
|
||||
// Log.d(TAG, "Successfully processed key exchange from $peerID")
|
||||
//
|
||||
// // Notify delegate
|
||||
// delegate?.onKeyExchangeCompleted(peerID, packet.payload, routed.relayAddress)
|
||||
//
|
||||
// return true
|
||||
//
|
||||
// } catch (e: Exception) {
|
||||
// Log.e(TAG, "Failed to process key exchange from $peerID: ${e.message}")
|
||||
// return false
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
/**
|
||||
* Verify packet signature
|
||||
*/
|
||||
@@ -377,6 +336,6 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
||||
* Delegate interface for security manager callbacks
|
||||
*/
|
||||
interface SecurityManagerDelegate {
|
||||
fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray, receivedAddress: String?)
|
||||
fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray)
|
||||
fun sendHandshakeResponse(peerID: String, response: ByteArray)
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ class NoiseEncryptionService(private val context: Context) {
|
||||
if (loadedKeyPair != null) {
|
||||
staticIdentityPrivateKey = loadedKeyPair.first
|
||||
staticIdentityPublicKey = loadedKeyPair.second
|
||||
Log.d(TAG, "Loaded existing static identity key")
|
||||
Log.d(TAG, "Loaded existing static identity key: ${calculateFingerprint(staticIdentityPublicKey)}")
|
||||
} else {
|
||||
// Generate new identity key pair
|
||||
val keyPair = generateKeyPair()
|
||||
|
||||
@@ -36,7 +36,7 @@ class NoiseSessionManager(
|
||||
*/
|
||||
fun getSession(peerID: String): NoiseSession? {
|
||||
val session = sessions[peerID]
|
||||
Log.d(TAG, "getSession($peerID): ${if (session?.isEstablished() == true) "ESTABLISHED" else "NOT_FOUND"}")
|
||||
// Log.d(TAG, "getSession($peerID): ${if (session?.isEstablished() == true) "ESTABLISHED" else "NOT_FOUND"}")
|
||||
return session
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ object CompressionUtil {
|
||||
* Helper to check if compression is worth it - exact same logic as iOS
|
||||
*/
|
||||
fun shouldCompress(data: ByteArray): Boolean {
|
||||
// TODO: COMPRESSION DOESN'T WORK WITH IOS YET
|
||||
return false
|
||||
// Don't compress if:
|
||||
// 1. Data is too small
|
||||
// 2. Data appears to be already compressed (high entropy)
|
||||
|
||||
@@ -303,18 +303,19 @@ private fun PrivateChatHeader(
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
) {
|
||||
|
||||
// Reactive Noise session status icon
|
||||
NoiseSessionIcon(
|
||||
sessionState = sessionState,
|
||||
modifier = Modifier.size(14.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = peerNickname,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = Color(0xFFFF9500) // Orange
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
|
||||
NoiseSessionIcon(
|
||||
sessionState = sessionState,
|
||||
modifier = Modifier.size(14.dp)
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
// Favorite button - positioned on the right
|
||||
|
||||
@@ -25,6 +25,10 @@ class ChatViewModel(
|
||||
val meshService: BluetoothMeshService
|
||||
) : AndroidViewModel(application), BluetoothMeshDelegate {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ChatViewModel"
|
||||
}
|
||||
|
||||
// State management
|
||||
private val state = ChatState()
|
||||
|
||||
@@ -37,7 +41,7 @@ class ChatViewModel(
|
||||
private val noiseSessionDelegate = object : NoiseSessionDelegate {
|
||||
override fun hasEstablishedSession(peerID: String): Boolean = meshService.hasEstablishedSession(peerID)
|
||||
override fun initiateHandshake(peerID: String) = meshService.initiateNoiseHandshake(peerID)
|
||||
override fun sendIdentityAnnouncement() = meshService.sendKeyExchangeToDevice()
|
||||
override fun broadcastNoiseIdentityAnnouncement() = meshService.broadcastNoiseIdentityAnnouncement()
|
||||
override fun sendHandshakeRequest(targetPeerID: String, pendingCount: UByte) = meshService.sendHandshakeRequest(targetPeerID, pendingCount)
|
||||
override fun getMyPeerID(): String = meshService.myPeerID
|
||||
}
|
||||
@@ -394,21 +398,71 @@ class ChatViewModel(
|
||||
// MARK: - Emergency Clear
|
||||
|
||||
fun panicClearAllData() {
|
||||
// Clear all managers
|
||||
Log.w(TAG, "🚨 PANIC MODE ACTIVATED - Clearing all sensitive data")
|
||||
|
||||
// Clear all UI managers
|
||||
messageManager.clearAllMessages()
|
||||
channelManager.clearAllChannels()
|
||||
privateChatManager.clearAllPrivateChats()
|
||||
dataManager.clearAllData()
|
||||
|
||||
// Clear all mesh service data
|
||||
clearAllMeshServiceData()
|
||||
|
||||
// Clear all cryptographic data
|
||||
clearAllCryptographicData()
|
||||
|
||||
// Clear all notifications
|
||||
notificationManager.clearAllNotifications()
|
||||
|
||||
// Reset nickname
|
||||
val newNickname = "anon${Random.nextInt(1000, 9999)}"
|
||||
state.setNickname(newNickname)
|
||||
dataManager.saveNickname(newNickname)
|
||||
|
||||
Log.w(TAG, "🚨 PANIC MODE COMPLETED - All sensitive data cleared")
|
||||
|
||||
// Note: Mesh service restart is now handled by MainActivity
|
||||
// This method now only clears data, not mesh service lifecycle
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all mesh service related data
|
||||
*/
|
||||
private fun clearAllMeshServiceData() {
|
||||
try {
|
||||
// Request mesh service to clear all its internal data
|
||||
meshService.clearAllInternalData()
|
||||
|
||||
Log.d(TAG, "✅ Cleared all mesh service data")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ Error clearing mesh service data: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cryptographic data including persistent identity
|
||||
*/
|
||||
private fun clearAllCryptographicData() {
|
||||
try {
|
||||
// Clear encryption service persistent identity (Ed25519 signing keys)
|
||||
meshService.clearAllEncryptionData()
|
||||
|
||||
// Clear secure identity state (if used)
|
||||
try {
|
||||
val identityManager = com.bitchat.android.identity.SecureIdentityStateManager(getApplication())
|
||||
identityManager.clearIdentityData()
|
||||
Log.d(TAG, "✅ Cleared secure identity state")
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "SecureIdentityStateManager not available or already cleared: ${e.message}")
|
||||
}
|
||||
|
||||
Log.d(TAG, "✅ Cleared all cryptographic data")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ Error clearing cryptographic data: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Navigation Management
|
||||
|
||||
fun showAppInfo() {
|
||||
|
||||
@@ -13,7 +13,7 @@ import android.util.Log
|
||||
interface NoiseSessionDelegate {
|
||||
fun hasEstablishedSession(peerID: String): Boolean
|
||||
fun initiateHandshake(peerID: String)
|
||||
fun sendIdentityAnnouncement()
|
||||
fun broadcastNoiseIdentityAnnouncement()
|
||||
fun sendHandshakeRequest(targetPeerID: String, pendingCount: UByte)
|
||||
fun getMyPeerID(): String
|
||||
}
|
||||
@@ -342,7 +342,7 @@ class PrivateChatManager(
|
||||
} else {
|
||||
// They should initiate, we send a Noise identity announcement
|
||||
Log.d(TAG, "Our peer ID lexicographically >= target peer ID, sending Noise identity announcement to prompt handshake from $peerID")
|
||||
noiseSessionDelegate.sendIdentityAnnouncement()
|
||||
noiseSessionDelegate.broadcastNoiseIdentityAnnouncement()
|
||||
// Also send handshake request to this peer
|
||||
noiseSessionDelegate.sendHandshakeRequest(peerID, 1u) // 1 pending message (the chat we're trying to start)
|
||||
}
|
||||
@@ -414,12 +414,12 @@ class PrivateChatManager(
|
||||
|
||||
/**
|
||||
* Send Noise identity announcement to prompt other peer to initiate handshake
|
||||
* This follows the same pattern as sendKeyExchangeToDevice() in BluetoothMeshService
|
||||
* This follows the same pattern as broadcastNoiseIdentityAnnouncement() in BluetoothMeshService
|
||||
*/
|
||||
private fun sendNoiseIdentityAnnouncement(meshService: Any) {
|
||||
try {
|
||||
// Call sendKeyExchangeToDevice which sends a NoiseIdentityAnnouncement
|
||||
val method = meshService::class.java.getDeclaredMethod("sendKeyExchangeToDevice")
|
||||
// Call broadcastNoiseIdentityAnnouncement which sends a NoiseIdentityAnnouncement
|
||||
val method = meshService::class.java.getDeclaredMethod("broadcastNoiseIdentityAnnouncement")
|
||||
method.invoke(meshService)
|
||||
Log.d(TAG, "Successfully sent Noise identity announcement")
|
||||
} catch (e: Exception) {
|
||||
|
||||
Reference in New Issue
Block a user