fix(security): Clear in-memory keys during panic mode (#596)

* fix(security): Clear in-memory keys during panic mode #588

* feat: Recreate mesh service after panic clear (#602)

* feat: Recreate mesh service after panic clear

This commit refactors the panic clear process to ensure a new mesh identity is immediately created and applied.

Previously, the `ChatViewModel` would clear sensitive data, but the recreation of the `BluetoothMeshService` was handled externally. This could lead to a delay or failure in adopting the new identity.

Key changes:
- Introduces `MeshServiceHolder` to manage the lifecycle of the `BluetoothMeshService` instance.
- Adds `recreateMeshServiceAfterPanic()` to `ChatViewModel`, which now explicitly clears the old service instance and creates a new one with a regenerated identity.
- The `meshService` property in `ChatViewModel` is now a `var` to allow it to be replaced with the fresh instance post-panic.
- The new service is started, and a broadcast announcement is sent immediately, ensuring the new peer ID is used on the network.

* fix: Ensure mesh service is properly managed in foreground service

* refactor: Decouple handlers from direct service reference

This commit updates the `VerificationHandler` and `MediaSendingManager` to receive the `meshService` via a lambda function (`getMeshService`) instead of a direct reference.

This change decouples the handlers from the service instance, preventing them from holding a stale reference if the service reconnects or changes. By invoking the lambda to get the current service instance when needed, it ensures they always interact with the active `meshService`.

* fix: restart bluetooth

---------

Co-authored-by: Moe Hamade <69801237+moehamade@users.noreply.github.com>
This commit is contained in:
callebtc
2026-01-15 15:11:10 +07:00
committed by GitHub
co-authored by Moe Hamade
parent 9dfebd73db
commit ae67fa4344
9 changed files with 183 additions and 31 deletions
@@ -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}")
}
@@ -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()
@@ -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
}
@@ -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}")
}
@@ -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<String>) {
if (set.size <= MAX_IDS) return
val it = set.iterator()
@@ -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}"
)
}
/**
@@ -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
@@ -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<Set<String>>(emptySet())
val verifiedFingerprints: StateFlow<Set<String>> = _verifiedFingerprints.asStateFlow()