diff --git a/app/src/main/java/com/bitchat/android/crypto/EncryptionService.kt b/app/src/main/java/com/bitchat/android/crypto/EncryptionService.kt index c615d62b..f3d9d68b 100644 --- a/app/src/main/java/com/bitchat/android/crypto/EncryptionService.kt +++ b/app/src/main/java/com/bitchat/android/crypto/EncryptionService.kt @@ -145,6 +145,12 @@ open class EncryptionService(private val context: Context) { 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") + + // Generate new keys immediately + val keyPair = loadOrCreateEd25519KeyPair() + ed25519PrivateKey = keyPair.private as Ed25519PrivateKeyParameters + ed25519PublicKey = keyPair.public as Ed25519PublicKeyParameters + Log.d(TAG, "✅ Rotated Ed25519 signing keys in memory") } catch (e: Exception) { Log.e(TAG, "❌ Failed to clear Ed25519 keys: ${e.message}") } 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 4e8b3a43..a0ce15c3 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -1375,6 +1375,9 @@ class BluetoothMeshService(private val context: Context) { fun clearAllInternalData() { Log.w(TAG, "🚨 Clearing all mesh service internal data") try { + // Stop services to cease broadcasting old ID immediately + stopServices() + // Clear all managers fragmentManager.clearAllFragments() storeForwardManager.clearAllCache() 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 f9e969d3..58dc810e 100644 --- a/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt +++ b/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt @@ -29,15 +29,15 @@ class NoiseEncryptionService(private val context: Context) { } // Static identity key (persistent across app restarts) - loaded from secure storage - private val staticIdentityPrivateKey: ByteArray - private val staticIdentityPublicKey: ByteArray + private var staticIdentityPrivateKey: ByteArray + private var staticIdentityPublicKey: ByteArray // Ed25519 signing key (persistent across app restarts) - loaded from secure storage - private val signingPrivateKey: ByteArray - private val signingPublicKey: ByteArray + private var signingPrivateKey: ByteArray + private var signingPublicKey: ByteArray // Session management - private val sessionManager: NoiseSessionManager + private lateinit var sessionManager: NoiseSessionManager // Channel encryption for password-protected channels private val channelEncryption = NoiseChannelEncryption() @@ -56,6 +56,32 @@ class NoiseEncryptionService(private val context: Context) { // Initialize identity state manager for persistent storage identityStateManager = SecureIdentityStateManager(context) + // Load or create keys - temporary placeholders + staticIdentityPrivateKey = ByteArray(32) + staticIdentityPublicKey = ByteArray(32) + signingPrivateKey = ByteArray(32) + signingPublicKey = ByteArray(32) + + loadOrGenerateKeys() + + // Initialize session manager + initializeSessionManager() + } + + private fun initializeSessionManager() { + // Create new session manager with current keys + sessionManager = NoiseSessionManager(staticIdentityPrivateKey, staticIdentityPublicKey) + + // Set up session callbacks + sessionManager.onSessionEstablished = { peerID, remoteStaticKey -> + handleSessionEstablished(peerID, remoteStaticKey) + } + + // Ensure any other callbacks are wired if needed + // sessionManager.onSessionFailed could be wired if we exposed it + } + + private fun loadOrGenerateKeys() { // Load or create static identity key (persistent across sessions) val loadedKeyPair = identityStateManager.loadStaticKey() if (loadedKeyPair != null) { @@ -89,16 +115,8 @@ class NoiseEncryptionService(private val context: Context) { identityStateManager.saveSigningKey(signingPrivateKey, signingPublicKey) Log.d(TAG, "Generated and saved new Ed25519 signing key") } - - // Initialize session manager - sessionManager = NoiseSessionManager(staticIdentityPrivateKey, staticIdentityPublicKey) - - // Set up session callbacks - sessionManager.onSessionEstablished = { peerID, remoteStaticKey -> - handleSessionEstablished(peerID, remoteStaticKey) - } } - + // MARK: - Public Interface /** @@ -135,7 +153,23 @@ class NoiseEncryptionService(private val context: Context) { * Clear persistent identity (for panic mode) */ fun clearPersistentIdentity() { + Log.w(TAG, "🚨 Panic Mode: Clearing persistent identity and rotating in-memory keys") + + // 1. Clear storage identityStateManager.clearIdentityData() + + // 2. Clear all sessions immediately + if (::sessionManager.isInitialized) { + sessionManager.shutdown() + } + + // 3. Regenerate keys immediately (in-memory rotation) + loadOrGenerateKeys() + + // 4. Re-initialize SessionManager with new keys + initializeSessionManager() + + Log.d(TAG, "✅ Identity cleared and keys rotated") } // MARK: - Handshake Management @@ -478,7 +512,9 @@ class NoiseEncryptionService(private val context: Context) { * Clean shutdown */ fun shutdown() { - sessionManager.shutdown() + if (::sessionManager.isInitialized) { + sessionManager.shutdown() + } channelEncryption.clear() // No need to clear fingerprints here - they are managed centrally } diff --git a/app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt b/app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt index bf1f236f..aa9e6fc3 100644 --- a/app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt +++ b/app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt @@ -10,6 +10,7 @@ import android.content.Intent import android.content.pm.ServiceInfo import android.os.Build import android.os.IBinder +import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import com.bitchat.android.MainActivity @@ -112,7 +113,8 @@ class MeshForegroundService : Service() { private lateinit var notificationManager: NotificationManagerCompat private var updateJob: Job? = null - private var meshService: BluetoothMeshService? = null + private val meshService: BluetoothMeshService? + get() = MeshServiceHolder.meshService private val serviceJob = Job() private val scope = CoroutineScope(Dispatchers.Default + serviceJob) private var isInForeground: Boolean = false @@ -123,15 +125,15 @@ class MeshForegroundService : Service() { notificationManager = NotificationManagerCompat.from(this) createChannel() - // Adopt or create the mesh service + // Ensure mesh service exists in holder (create if needed) val existing = MeshServiceHolder.meshService - meshService = existing ?: MeshServiceHolder.getOrCreate(applicationContext) if (existing != null) { - android.util.Log.d("MeshForegroundService", "Adopted existing BluetoothMeshService from holder") + Log.d("MeshForegroundService", "Using existing BluetoothMeshService from holder") } else { - android.util.Log.i("MeshForegroundService", "Created/adopted new BluetoothMeshService via holder") + val created = MeshServiceHolder.getOrCreate(applicationContext) + Log.i("MeshForegroundService", "Created new BluetoothMeshService via holder") + MeshServiceHolder.attach(created) } - MeshServiceHolder.attach(meshService!!) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { @@ -227,7 +229,8 @@ class MeshForegroundService : Service() { if (!hasBluetoothPermissions()) return try { android.util.Log.d("MeshForegroundService", "Ensuring mesh service is started") - meshService?.startServices() + val service = MeshServiceHolder.getOrCreate(applicationContext) + service.startServices() } catch (e: Exception) { android.util.Log.e("MeshForegroundService", "Failed to start mesh service: ${e.message}") } diff --git a/app/src/main/java/com/bitchat/android/services/SeenMessageStore.kt b/app/src/main/java/com/bitchat/android/services/SeenMessageStore.kt index a6cd2378..15e1c7ca 100644 --- a/app/src/main/java/com/bitchat/android/services/SeenMessageStore.kt +++ b/app/src/main/java/com/bitchat/android/services/SeenMessageStore.kt @@ -50,6 +50,12 @@ class SeenMessageStore private constructor(private val context: Context) { persist() } + @Synchronized fun clear() { + delivered.clear() + read.clear() + persist() + } + private fun trim(set: LinkedHashSet) { if (set.size <= MAX_IDS) return val it = set.iterator() diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt index 9fccff3c..ca33a6a9 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import com.bitchat.android.mesh.BluetoothMeshDelegate import com.bitchat.android.mesh.BluetoothMeshService +import com.bitchat.android.service.MeshServiceHolder import com.bitchat.android.model.BitchatMessage import com.bitchat.android.model.BitchatMessageType import com.bitchat.android.nostr.NostrIdentityBridge @@ -37,8 +38,12 @@ import java.security.MessageDigest */ class ChatViewModel( application: Application, - val meshService: BluetoothMeshService + initialMeshService: BluetoothMeshService ) : AndroidViewModel(application), BluetoothMeshDelegate { + + // Made var to support mesh service replacement after panic clear + var meshService: BluetoothMeshService = initialMeshService + private set private val debugManager by lazy { try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance() } catch (e: Exception) { null } } companion object { @@ -104,7 +109,7 @@ class ChatViewModel( private val verificationHandler = VerificationHandler( context = application.applicationContext, scope = viewModelScope, - meshService = meshService, + getMeshService = { meshService }, identityManager = identityManager, state = state, notificationManager = notificationManager, @@ -113,7 +118,7 @@ class ChatViewModel( val verifiedFingerprints = verificationHandler.verifiedFingerprints // Media file sending manager - private val mediaSendingManager = MediaSendingManager(state, messageManager, channelManager, meshService) + private val mediaSendingManager = MediaSendingManager(state, messageManager, channelManager) { meshService } // Delegate handler for mesh callbacks private val meshDelegateHandler = MeshDelegateHandler( @@ -908,6 +913,11 @@ class ChatViewModel( privateChatManager.clearAllPrivateChats() dataManager.clearAllData() + // Clear seen message store + try { + com.bitchat.android.services.SeenMessageStore.getInstance(getApplication()).clear() + } catch (_: Exception) { } + // Clear all mesh service data clearAllMeshServiceData() @@ -935,10 +945,37 @@ class ChatViewModel( 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 + // Recreate mesh service with fresh identity + recreateMeshServiceAfterPanic() + + Log.w(TAG, "🚨 PANIC MODE COMPLETED - New identity: ${meshService.myPeerID}") + } + + /** + * Recreate the mesh service with a fresh identity after panic clear. + * This ensures the new cryptographic keys are used for a new peer ID. + */ + private fun recreateMeshServiceAfterPanic() { + val oldPeerID = meshService.myPeerID + + // Clear the holder so getOrCreate() returns a fresh instance + MeshServiceHolder.clear() + + // Create fresh mesh service with new identity (keys were regenerated in clearAllCryptographicData) + val freshMeshService = MeshServiceHolder.getOrCreate(getApplication()) + + // Replace our reference and set up the new service + meshService = freshMeshService + meshService.delegate = this + + // Restart mesh operations with new identity + meshService.startServices() + meshService.sendBroadcastAnnounce() + + Log.d( + TAG, + "✅ Mesh service recreated. Old peerID: $oldPeerID, New peerID: ${meshService.myPeerID}" + ) } /** diff --git a/app/src/main/java/com/bitchat/android/ui/MediaSendingManager.kt b/app/src/main/java/com/bitchat/android/ui/MediaSendingManager.kt index a3def523..8de53239 100644 --- a/app/src/main/java/com/bitchat/android/ui/MediaSendingManager.kt +++ b/app/src/main/java/com/bitchat/android/ui/MediaSendingManager.kt @@ -16,8 +16,11 @@ class MediaSendingManager( private val state: ChatState, private val messageManager: MessageManager, private val channelManager: ChannelManager, - private val meshService: BluetoothMeshService + private val getMeshService: () -> BluetoothMeshService ) { + // Helper to get current mesh service (may change after panic clear) + private val meshService: BluetoothMeshService + get() = getMeshService() companion object { private const val TAG = "MediaSendingManager" private const val MAX_FILE_SIZE = com.bitchat.android.util.AppConstants.Media.MAX_FILE_SIZE_BYTES // 50MB limit diff --git a/app/src/main/java/com/bitchat/android/ui/VerificationHandler.kt b/app/src/main/java/com/bitchat/android/ui/VerificationHandler.kt index 32620281..2b0d503f 100644 --- a/app/src/main/java/com/bitchat/android/ui/VerificationHandler.kt +++ b/app/src/main/java/com/bitchat/android/ui/VerificationHandler.kt @@ -26,12 +26,15 @@ import java.util.concurrent.ConcurrentHashMap class VerificationHandler( private val context: Context, private val scope: CoroutineScope, - private val meshService: BluetoothMeshService, + private val getMeshService: () -> BluetoothMeshService, private val identityManager: SecureIdentityStateManager, private val state: ChatState, private val notificationManager: NotificationManager, private val messageManager: MessageManager ) { + // Helper to get current mesh service (may change after panic clear) + private val meshService: BluetoothMeshService + get() = getMeshService() private val _verifiedFingerprints = MutableStateFlow>(emptySet()) val verifiedFingerprints: StateFlow> = _verifiedFingerprints.asStateFlow() diff --git a/app/src/test/java/com/bitchat/android/crypto/EncryptionServiceTest.kt b/app/src/test/java/com/bitchat/android/crypto/EncryptionServiceTest.kt new file mode 100644 index 00000000..7e610d32 --- /dev/null +++ b/app/src/test/java/com/bitchat/android/crypto/EncryptionServiceTest.kt @@ -0,0 +1,55 @@ +package com.bitchat.android.crypto + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.bitchat.android.noise.NoiseEncryptionService +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNotNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import java.util.Arrays + +@RunWith(RobolectricTestRunner::class) +class EncryptionServiceTest { + + private lateinit var context: Context + private lateinit var encryptionService: EncryptionService + + @Before + fun setup() { + context = ApplicationProvider.getApplicationContext() + encryptionService = EncryptionService(context) + } + + @Test + fun `test clearPersistentIdentity changes keys`() { + // 1. Get initial keys + val initialStaticKey = encryptionService.getStaticPublicKey() + val initialSigningKey = encryptionService.getSigningPublicKey() + val initialFingerprint = encryptionService.getIdentityFingerprint() + + assertNotNull("Initial static key should not be null", initialStaticKey) + assertNotNull("Initial signing key should not be null", initialSigningKey) + + // 2. Call clearPersistentIdentity (Panic Mode) + encryptionService.clearPersistentIdentity() + + // 3. Get keys again. + val afterStaticKey = encryptionService.getStaticPublicKey() + val afterSigningKey = encryptionService.getSigningPublicKey() + val afterFingerprint = encryptionService.getIdentityFingerprint() + + // 4. Verify keys are different (Panic Mode should clear/rotate in-memory keys) + // Note: We use string comparison for byte arrays to be safe in assertion messages + assertNotEquals("Static key should change after panic", + Arrays.toString(initialStaticKey), Arrays.toString(afterStaticKey)) + + assertNotEquals("Signing key should change after panic", + Arrays.toString(initialSigningKey), Arrays.toString(afterSigningKey)) + + assertNotEquals("Fingerprint should change after panic", + initialFingerprint, afterFingerprint) + } +}