From bc49c71ea0bc23ea9855229aa2f4134af3c2a642 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 27 Jul 2026 10:33:29 +0200 Subject: [PATCH] security: stop logging Noise key material, reduce noisy logging app-wide (#775) C1 from security review: SymmetricState/HandshakeState logged raw X25519 shared secrets, chaining keys, and handshake hashes in hex to logcat on every handshake, in release builds. A logcat transcript of a handshake allowed full session decryption. Both classes no longer log at all. Also reduces excessive logging across the app (~50% fewer log calls in the noisiest files): - NoiseSession emits one line per completed handshake; per-message encrypt/decrypt and per-handshake-step debug logs removed - Removes all content/key logging: decrypted DM content, file names, payload hex dumps, pubkeys, event IDs, lat/lon, peer IPs, arti log forwarding - Collapses multi-line banner/emoji log sequences into single factual lifecycle lines (connect/disconnect, relay/Tor state transitions) - Keeps security-relevant warnings (signature failures, replay detection, key mismatches, panic wipe) in compact form No logic changes. Includes the full security review report in docs/security-review-jul-27.md. --- .../java/com/bitchat/android/MainActivity.kt | 57 +----- .../android/geohash/LocationChannelManager.kt | 38 +--- .../mesh/BluetoothConnectionManager.kt | 22 +-- .../mesh/BluetoothGattClientManager.kt | 83 +++----- .../mesh/BluetoothGattServerManager.kt | 55 ++---- .../android/mesh/BluetoothMeshService.kt | 71 ++----- .../mesh/BluetoothPacketBroadcaster.kt | 38 +--- .../bitchat/android/mesh/FragmentManager.kt | 52 +---- .../bitchat/android/mesh/MessageHandler.kt | 56 +----- .../bitchat/android/mesh/PacketProcessor.kt | 34 +--- .../bitchat/android/mesh/SecurityManager.kt | 54 ++---- .../com/bitchat/android/net/ArtiTorManager.kt | 27 +-- .../android/noise/NoiseEncryptionService.kt | 11 +- .../com/bitchat/android/noise/NoiseSession.kt | 133 +++---------- .../android/noise/NoiseSessionManager.kt | 37 +--- .../protocol/HandshakeState.java | 34 +--- .../protocol/SymmetricState.java | 50 +---- .../android/nostr/LocationNotesManager.kt | 2 +- .../bitchat/android/nostr/NostrProtocol.kt | 2 - .../android/nostr/NostrRelayManager.kt | 131 +++++-------- .../bitchat/android/nostr/NostrTestManager.kt | 2 +- .../bitchat/android/nostr/NostrTransport.kt | 38 +--- .../bitchat/android/ui/MediaSendingManager.kt | 55 ++---- .../wifi-aware/WifiAwareMeshService.kt | 153 ++++----------- docs/security-review-jul-27.md | 182 ++++++++++++++++++ 25 files changed, 440 insertions(+), 977 deletions(-) create mode 100644 docs/security-review-jul-27.md diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index ce3beb24..e6344efa 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -221,7 +221,6 @@ class MainActivity : OrientationAwareActivity() { onDispose { try { context.unregisterReceiver(receiver) - Log.d("BluetoothStatusUI", "BroadcastReceiver unregistered") } catch (e: IllegalStateException) { Log.w("BluetoothStatusUI", "Receiver was not registered") } @@ -354,7 +353,7 @@ class MainActivity : OrientationAwareActivity() { when (state) { OnboardingState.COMPLETE -> { // App is fully initialized, mesh service is running - android.util.Log.d("MainActivity", "Onboarding completed - app ready") + android.util.Log.i("MainActivity", "Onboarding completed - app ready") } OnboardingState.ERROR -> { android.util.Log.e("MainActivity", "Onboarding error state reached") @@ -364,8 +363,6 @@ class MainActivity : OrientationAwareActivity() { } private fun checkOnboardingStatus() { - Log.d("MainActivity", "Checking onboarding status") - lifecycleScope.launch { // Small delay to show the checking state delay(500) @@ -379,19 +376,15 @@ class MainActivity : OrientationAwareActivity() { * Check Bluetooth status and proceed with onboarding flow */ private fun checkBluetoothAndProceed() { - // Log.d("MainActivity", "Checking Bluetooth status") - // Check if user has skipped Bluetooth check for this session if (mainViewModel.isBluetoothCheckSkipped.value) { - Log.d("MainActivity", "Bluetooth check skipped by user, proceeding to location check") checkLocationAndProceed() return } - + // For first-time users, skip Bluetooth check and go straight to permissions // We'll check Bluetooth after permissions are granted if (permissionManager.isFirstTimeLaunch()) { - Log.d("MainActivity", "First-time launch, skipping Bluetooth check - will check after permissions") proceedWithPermissionCheck() return } @@ -413,7 +406,6 @@ class MainActivity : OrientationAwareActivity() { } BluetoothStatus.DISABLED -> { // Show Bluetooth enable screen (should have permissions as existing user) - Log.d("MainActivity", "Bluetooth disabled, showing enable screen") mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK) mainViewModel.updateBluetoothLoading(false) } @@ -430,16 +422,12 @@ class MainActivity : OrientationAwareActivity() { * Proceed with permission checking */ private fun proceedWithPermissionCheck() { - Log.d("MainActivity", "Proceeding with permission check") - lifecycleScope.launch { delay(200) // Small delay for smooth transition - + if (permissionManager.isFirstTimeLaunch()) { - Log.d("MainActivity", "First time launch, showing permission explanation") mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION) } else if (permissionManager.areRequiredPermissionsGranted()) { - Log.d("MainActivity", "Existing user with required permissions") if (permissionManager.needsBackgroundLocationPermission() && !permissionManager.isBackgroundLocationGranted() && !com.bitchat.android.onboarding.BackgroundLocationPreferenceManager.isSkipped(this@MainActivity) @@ -450,7 +438,6 @@ class MainActivity : OrientationAwareActivity() { initializeApp() } } else { - Log.d("MainActivity", "Existing user missing permissions, showing explanation") mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION) } } @@ -460,7 +447,6 @@ class MainActivity : OrientationAwareActivity() { * Handle Bluetooth enabled callback */ private fun handleBluetoothEnabled() { - Log.d("MainActivity", "Bluetooth enabled by user") mainViewModel.updateBluetoothLoading(false) mainViewModel.updateBluetoothStatus(BluetoothStatus.ENABLED) checkLocationAndProceed() @@ -470,12 +456,9 @@ class MainActivity : OrientationAwareActivity() { * Check Location services status and proceed with onboarding flow */ private fun checkLocationAndProceed() { - Log.d("MainActivity", "Checking location services status") - // For first-time users, skip location check and go straight to permissions // We'll check location after permissions are granted if (permissionManager.isFirstTimeLaunch()) { - Log.d("MainActivity", "First-time launch, skipping location check - will check after permissions") proceedWithPermissionCheck() return } @@ -491,7 +474,6 @@ class MainActivity : OrientationAwareActivity() { } LocationStatus.DISABLED -> { // Show location enable screen (should have permissions as existing user) - Log.d("MainActivity", "Location services disabled, showing enable screen") mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK) mainViewModel.updateLocationLoading(false) } @@ -508,7 +490,6 @@ class MainActivity : OrientationAwareActivity() { * Handle Location enabled callback */ private fun handleLocationEnabled() { - Log.d("MainActivity", "Location services enabled by user") mainViewModel.updateLocationLoading(false) mainViewModel.updateLocationStatus(LocationStatus.ENABLED) // Ensure Wi-Fi Aware starts now that location is enabled @@ -554,12 +535,10 @@ class MainActivity : OrientationAwareActivity() { message.contains("Permission") && permissionManager.isFirstTimeLaunch() -> { // During first-time onboarding, if Bluetooth enable fails due to permissions, // proceed to permission explanation screen where user will grant permissions first - Log.d("MainActivity", "Bluetooth enable requires permissions, proceeding to permission explanation") proceedWithPermissionCheck() } message.contains("Permission") -> { // For existing users, redirect to permission explanation to grant missing permissions - Log.d("MainActivity", "Bluetooth enable requires permissions, showing permission explanation") mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION) } else -> { @@ -570,8 +549,6 @@ class MainActivity : OrientationAwareActivity() { } private fun handleOnboardingComplete() { - Log.d("MainActivity", "Onboarding completed, checking Bluetooth and Location before initializing app") - // After permissions are granted, re-check Bluetooth, Location, and Battery Optimization status val currentBluetoothStatus = bluetoothStatusManager.checkBluetoothStatus() val currentLocationStatus = locationStatusManager.checkLocationStatus() @@ -585,28 +562,24 @@ class MainActivity : OrientationAwareActivity() { when { bleRequired2 && currentBluetoothStatus != BluetoothStatus.ENABLED -> { // Bluetooth still disabled, but now we have permissions to enable it - Log.d("MainActivity", "Permissions granted, but Bluetooth still disabled. Showing Bluetooth enable screen.") mainViewModel.updateBluetoothStatus(currentBluetoothStatus) mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK) mainViewModel.updateBluetoothLoading(false) } currentLocationStatus != LocationStatus.ENABLED -> { // Location services still disabled, but now we have permissions to enable it - Log.d("MainActivity", "Permissions granted, but Location services still disabled. Showing Location enable screen.") mainViewModel.updateLocationStatus(currentLocationStatus) mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK) mainViewModel.updateLocationLoading(false) } currentBatteryOptimizationStatus == BatteryOptimizationStatus.ENABLED -> { // Battery optimization still enabled, show battery optimization screen - android.util.Log.d("MainActivity", "Permissions granted, but battery optimization still enabled. Showing battery optimization screen.") mainViewModel.updateBatteryOptimizationStatus(currentBatteryOptimizationStatus) mainViewModel.updateOnboardingState(OnboardingState.BATTERY_OPTIMIZATION_CHECK) mainViewModel.updateBatteryOptimizationLoading(false) } else -> { // Both are enabled, proceed to app initialization - Log.d("MainActivity", "Both Bluetooth and Location services are enabled, proceeding to initialization") mainViewModel.updateOnboardingState(OnboardingState.INITIALIZING) initializeApp() } @@ -639,19 +612,15 @@ class MainActivity : OrientationAwareActivity() { * Check Battery Optimization status and proceed with onboarding flow */ private fun checkBatteryOptimizationAndProceed() { - android.util.Log.d("MainActivity", "Checking battery optimization status") - // For first-time users, skip battery optimization check and go straight to permissions // We'll check battery optimization after permissions are granted if (permissionManager.isFirstTimeLaunch()) { - android.util.Log.d("MainActivity", "First-time launch, skipping battery optimization check - will check after permissions") proceedWithPermissionCheck() return } - + // Check if user has previously skipped battery optimization if (BatteryOptimizationPreferenceManager.isSkipped(this)) { - android.util.Log.d("MainActivity", "User previously skipped battery optimization, proceeding to permissions") proceedWithPermissionCheck() return } @@ -672,7 +641,6 @@ class MainActivity : OrientationAwareActivity() { } BatteryOptimizationStatus.ENABLED -> { // Show battery optimization disable screen - android.util.Log.d("MainActivity", "Battery optimization enabled, showing disable screen") mainViewModel.updateOnboardingState(OnboardingState.BATTERY_OPTIMIZATION_CHECK) mainViewModel.updateBatteryOptimizationLoading(false) } @@ -683,7 +651,6 @@ class MainActivity : OrientationAwareActivity() { * Handle Battery Optimization disabled callback */ private fun handleBatteryOptimizationDisabled() { - android.util.Log.d("MainActivity", "Battery optimization disabled by user") mainViewModel.updateBatteryOptimizationLoading(false) mainViewModel.updateBatteryOptimizationStatus(BatteryOptimizationStatus.DISABLED) proceedWithPermissionCheck() @@ -707,19 +674,14 @@ class MainActivity : OrientationAwareActivity() { } private fun initializeApp() { - Log.d("MainActivity", "Starting app initialization") - lifecycleScope.launch { try { // Initialize the app with a proper delay to ensure Bluetooth stack is ready // This solves the issue where app needs restart to work on first install delay(1000) // Give the system time to process permission grants - - Log.d("MainActivity", "Permissions verified, initializing chat system") - + // Initialize PoW preferences early in the initialization process PoWPreferenceManager.init(this@MainActivity) - Log.d("MainActivity", "PoW preferences initialized") // Initialize Location Notes Manager (extracted to separate file) com.bitchat.android.nostr.LocationNotesInitializer.initialize(this@MainActivity) @@ -736,16 +698,14 @@ class MainActivity : OrientationAwareActivity() { unifiedMeshService.delegate = chatViewModel unifiedMeshService.startServices() startMeshForegroundServiceBestEffort() - - Log.d("MainActivity", "Mesh service started successfully") - + // Handle any notification intent handleNotificationIntent(intent) handleVerificationIntent(intent) - + // Small delay to ensure mesh service is fully initialized delay(500) - Log.d("MainActivity", "App initialization complete") + Log.i("MainActivity", "App initialization complete") mainViewModel.updateOnboardingState(OnboardingState.COMPLETE) } catch (e: Exception) { Log.e("MainActivity", "Failed to initialize app", e) @@ -902,7 +862,6 @@ class MainActivity : OrientationAwareActivity() { // Cleanup location status manager try { locationStatusManager.cleanup() - Log.d("MainActivity", "Location status manager cleaned up successfully") } catch (e: Exception) { Log.w("MainActivity", "Error cleaning up location status manager: ${e.message}") } diff --git a/app/src/main/java/com/bitchat/android/geohash/LocationChannelManager.kt b/app/src/main/java/com/bitchat/android/geohash/LocationChannelManager.kt index ea595697..6c91c32e 100644 --- a/app/src/main/java/com/bitchat/android/geohash/LocationChannelManager.kt +++ b/app/src/main/java/com/bitchat/android/geohash/LocationChannelManager.kt @@ -145,20 +145,16 @@ class LocationChannelManager private constructor(private val context: Context) { * UNIFIED: Only requests location if location services are enabled by user */ fun enableLocationChannels() { - Log.d(TAG, "enableLocationChannels() called") - if (!_locationServicesEnabled.value || !_systemLocationEnabled.value) { Log.w(TAG, "Location services disabled (app or system) - not requesting location") return } - + if (getCurrentPermissionStatus() == PermissionState.AUTHORIZED) { - Log.d(TAG, "Permission authorized - requesting location") _permissionState.value = PermissionState.AUTHORIZED requestOneShotLocation() } else { - Log.d(TAG, "Permission not granted") _permissionState.value = PermissionState.DENIED } } @@ -177,8 +173,6 @@ class LocationChannelManager private constructor(private val context: Context) { * Uses requestLocationUpdates for continuous updates, plus a one-shot to prime state immediately */ fun beginLiveRefresh(interval: Long = 5000L) { - Log.d(TAG, "Beginning live refresh (continuous updates)") - if (_permissionState.value != PermissionState.AUTHORIZED) { Log.w(TAG, "Cannot start live refresh - permission not authorized") return @@ -204,7 +198,6 @@ class LocationChannelManager private constructor(private val context: Context) { * Stop periodic refreshes when selector UI is dismissed */ fun endLiveRefresh() { - Log.d(TAG, "Ending live refresh") locationProvider.removeLocationUpdates(locationUpdateCallback) } @@ -231,7 +224,6 @@ class LocationChannelManager private constructor(private val context: Context) { ) val isTeleportedNow = currentGeohash != channel.channel.geohash _teleported.value = isTeleportedNow - Log.d(TAG, "Teleported (immediate recompute): $isTeleportedNow (current: $currentGeohash, selected: ${channel.channel.geohash})") } } } @@ -241,7 +233,6 @@ class LocationChannelManager private constructor(private val context: Context) { * Set teleported status (for manual geohash teleportation) */ fun setTeleported(teleported: Boolean) { - Log.d(TAG, "Setting teleported status: $teleported") _teleported.value = teleported } @@ -249,7 +240,6 @@ class LocationChannelManager private constructor(private val context: Context) { * Enable location services (user-controlled toggle) */ fun enableLocationServices() { - Log.d(TAG, "enableLocationServices() called by user") _locationServicesEnabled.value = true saveLocationServicesState(true) @@ -263,7 +253,6 @@ class LocationChannelManager private constructor(private val context: Context) { * Disable location services (user-controlled toggle) */ fun disableLocationServices() { - Log.d(TAG, "disableLocationServices() called by user") _locationServicesEnabled.value = false saveLocationServicesState(false) @@ -298,21 +287,17 @@ class LocationChannelManager private constructor(private val context: Context) { return } - Log.d(TAG, "Requesting one-shot location") // Set loading state initially _isLoadingLocation.value = true - + locationProvider.getLastKnownLocation { cached -> // If we have a cached location and it's reasonably recent (e.g. < 5 mins), use it // For now, we just use it if it exists, similar to previous logic if (cached != null) { - Log.d(TAG, "Using last known location: ${cached.latitude}, ${cached.longitude}") onLocationUpdated(cached) } else { - Log.d(TAG, "No last known location available, requesting fresh...") locationProvider.requestFreshLocation { fresh -> if (fresh != null) { - Log.d(TAG, "Fresh location received: ${fresh.latitude}, ${fresh.longitude}") onLocationUpdated(fresh) } else { Log.w(TAG, "Failed to get fresh location") @@ -348,7 +333,6 @@ class LocationChannelManager private constructor(private val context: Context) { val newState = if (hasPermission) PermissionState.AUTHORIZED else PermissionState.DENIED if (_permissionState.value != newState) { - Log.d(TAG, "Permission state updated to: $newState") _permissionState.value = newState } @@ -356,11 +340,9 @@ class LocationChannelManager private constructor(private val context: Context) { } private fun computeChannels(location: Location) { - Log.d(TAG, "Computing channels for location: ${location.latitude}, ${location.longitude}") - val levels = GeohashChannelLevel.allCases() val result = mutableListOf() - + for (level in levels) { val geohash = Geohash.encode( latitude = location.latitude, @@ -368,8 +350,6 @@ class LocationChannelManager private constructor(private val context: Context) { precision = level.precision ) result.add(GeohashChannel(level = level, geohash = geohash)) - - Log.v(TAG, "Generated ${level.displayName}: $geohash") } _availableChannels.value = result @@ -388,7 +368,6 @@ class LocationChannelManager private constructor(private val context: Context) { ) val isTeleported = currentGeohash != selectedChannelValue.channel.geohash _teleported.value = isTeleported - Log.d(TAG, "Teleported status: $isTeleported (current: $currentGeohash, selected: ${selectedChannelValue.channel.geohash})") } } } @@ -399,8 +378,6 @@ class LocationChannelManager private constructor(private val context: Context) { geocodingJob = scope.launch(Dispatchers.IO) { try { - Log.d(TAG, "Starting reverse geocoding") - val addresses = geocoderProvider.getFromLocation(location.latitude, location.longitude, 1) if (!isActive) return@launch @@ -408,7 +385,6 @@ class LocationChannelManager private constructor(private val context: Context) { if (addresses.isNotEmpty()) { val address = addresses[0] val names = namesByLevel(address) - Log.d(TAG, "Reverse geocoding result: $names") _locationNames.value = names } else { Log.w(TAG, "No reverse geocoding results") @@ -482,7 +458,6 @@ class LocationChannelManager private constructor(private val context: Context) { ) } dataManager?.saveLastGeohashChannel(channelData) - Log.d(TAG, "Saved channel selection: ${channel.displayName}") } catch (e: Exception) { Log.e(TAG, "Failed to save channel selection: ${e.message}") } @@ -499,13 +474,10 @@ class LocationChannelManager private constructor(private val context: Context) { val channel = persisted?.toChannel() if (channel != null) { _selectedChannel.value = channel - Log.d(TAG, "Restored persisted channel: ${channel.displayName}") } else { - Log.d(TAG, "Could not restore persisted channel, defaulting to Mesh") _selectedChannel.value = ChannelID.Mesh } } else { - Log.d(TAG, "No persisted channel found, defaulting to Mesh") _selectedChannel.value = ChannelID.Mesh } } catch (e: JsonSyntaxException) { @@ -540,7 +512,6 @@ class LocationChannelManager private constructor(private val context: Context) { dataManager?.clearLastGeohashChannel() _selectedChannel.value = ChannelID.Mesh _teleported.value = false - Log.d(TAG, "Cleared persisted channel selection") } // MARK: - Location Services State Persistence @@ -551,7 +522,6 @@ class LocationChannelManager private constructor(private val context: Context) { private fun saveLocationServicesState(enabled: Boolean) { try { dataManager?.saveLocationServicesEnabled(enabled) - Log.d(TAG, "Saved location services state: $enabled") } catch (e: Exception) { Log.e(TAG, "Failed to save location services state: ${e.message}") } @@ -564,7 +534,6 @@ class LocationChannelManager private constructor(private val context: Context) { try { val enabled = dataManager?.isLocationServicesEnabled() ?: false _locationServicesEnabled.value = enabled - Log.d(TAG, "Loaded location services state: $enabled") } catch (e: Exception) { Log.e(TAG, "Failed to load location services state: ${e.message}") _locationServicesEnabled.value = false @@ -575,7 +544,6 @@ class LocationChannelManager private constructor(private val context: Context) { * Cleanup resources */ fun cleanup() { - Log.d(TAG, "Cleaning up LocationChannelManager") endLiveRefresh() locationProvider.cancel() diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt index 58923847..17e4ce28 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt @@ -48,7 +48,6 @@ class BluetoothConnectionManager( device: BluetoothDevice?, ingressLinkID: String ) { - Log.d(TAG, "onPacketReceived: Packet received from ${device?.address} ($peerID)") device?.let { bluetoothDevice -> // Get current RSSI for this device and update if available val currentRSSI = connectionTracker.getBestRSSI(bluetoothDevice.address) @@ -183,10 +182,8 @@ class BluetoothConnectionManager( toEvict.forEach { conn -> if (conn.isClient) { - Log.d(TAG, "Evicting client ${conn.device.address}") try { conn.gatt?.disconnect() } catch (_: Exception) { } } else { - Log.d(TAG, "Evicting server ${conn.device.address}") serverManager.disconnectDevice(conn.device) } } @@ -200,8 +197,6 @@ class BluetoothConnectionManager( * Start all Bluetooth services with power optimization */ fun startServices(): Boolean { - Log.i(TAG, "Starting power-optimized Bluetooth services...") - if (!isBleTransportEnabled()) { Log.i(TAG, "BLE transport disabled by debug settings; not starting Bluetooth services") disableTransport() @@ -220,7 +215,6 @@ class BluetoothConnectionManager( try { isActive = true - Log.d(TAG, "ConnectionManager activated (permissions and adapter OK)") // set the adapter's name to our 8-character peerID for iOS privacy, TODO: Make this configurable // try { @@ -250,7 +244,6 @@ class BluetoothConnectionManager( this@BluetoothConnectionManager.isActive = false return@launch } - Log.d(TAG, "GATT Server started") } else { Log.i(TAG, "GATT Server disabled by debug settings; not starting") } @@ -261,7 +254,6 @@ class BluetoothConnectionManager( this@BluetoothConnectionManager.isActive = false return@launch } - Log.d(TAG, "GATT Client started") } else { Log.i(TAG, "GATT Client disabled by debug settings; not starting") } @@ -295,12 +287,9 @@ class BluetoothConnectionManager( * Stop all Bluetooth services with proper cleanup */ fun stopServices() { - Log.i(TAG, "Stopping power-optimized Bluetooth services") - isActive = false - + connectionScope.launch { - Log.d(TAG, "Stopping client/server and power components...") // Stop component managers clientManager.stop() serverManager.stop() @@ -323,11 +312,7 @@ class BluetoothConnectionManager( * Returns false if its coroutine scope has been cancelled. */ fun isReusable(): Boolean { - val active = connectionScope.isActive - if (!active) { - Log.d(TAG, "BluetoothConnectionManager isReusable=false (scope cancelled)") - } - return active + return connectionScope.isActive } /** @@ -492,15 +477,12 @@ class BluetoothConnectionManager( // Only restart scanning if the duty cycle behavior changed val nowUsingDutyCycle = powerManager.shouldUseDutyCycle() if (wasUsingDutyCycle != nowUsingDutyCycle) { - Log.d(TAG, "Duty cycle behavior changed (${wasUsingDutyCycle} -> ${nowUsingDutyCycle}), restarting scan") val clientEnabled = isGattClientEnabled() if (clientEnabled) { clientManager.restartScanning() } else { clientManager.stop() } - } else { - Log.d(TAG, "Duty cycle behavior unchanged, keeping existing scan state") } // Enforce connection limits diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt index 96a351ec..66971ea4 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt @@ -63,7 +63,7 @@ class BluetoothGattClientManager( */ fun connectToAddress(deviceAddress: String): Boolean { if (!isClientRoleEnabled()) { - Log.i(TAG, "connectToAddress skipped: BLE client disabled") + Log.d(TAG, "connectToAddress skipped: BLE client disabled") return false } val device = bluetoothAdapter?.getRemoteDevice(deviceAddress) @@ -111,7 +111,6 @@ class BluetoothGattClientManager( } if (isActive) { - Log.d(TAG, "GATT client already active; start is a no-op") return true } if (!permissionManager.hasBluetoothPermissions()) { @@ -159,7 +158,6 @@ class BluetoothGattClientManager( // Idempotent stop stopScanning() stopRSSIMonitoring() - Log.i(TAG, "GATT client manager stopped (already inactive)") return } @@ -205,10 +203,9 @@ class BluetoothGattClientManager( val connectedDevices = connectionTracker.getConnectedDevices() connectedDevices.values.filter { it.isClient && it.gatt != null }.forEach { deviceConn -> try { - Log.d(TAG, "Requesting RSSI from ${deviceConn.device.address}") deviceConn.gatt?.readRemoteRssi() } catch (e: Exception) { - Log.w(TAG, "Failed to request RSSI from ${deviceConn.device.address}: ${e.message}") + Log.d(TAG, "Failed to request RSSI from ${deviceConn.device.address}: ${e.message}") } } delay(AppConstants.Mesh.RSSI_UPDATE_INTERVAL_MS) @@ -240,14 +237,13 @@ class BluetoothGattClientManager( // Rate limit scan starts to prevent "scanning too frequently" errors val currentTime = System.currentTimeMillis() if (isCurrentlyScanning) { - Log.d(TAG, "Scan already in progress, skipping start request") return } - + val timeSinceLastStart = currentTime - lastScanStartTime if (timeSinceLastStart < scanRateLimit) { val remainingWait = scanRateLimit - timeSinceLastStart - Log.w(TAG, "Scan rate limited: need to wait ${remainingWait}ms before starting scan") + Log.d(TAG, "Scan rate limited: waiting ${remainingWait}ms before starting scan") // Schedule delayed scan start connectionScope.launch { @@ -263,63 +259,52 @@ class BluetoothGattClientManager( .setServiceUuid(ParcelUuid(AppConstants.Mesh.Gatt.SERVICE_UUID)) .build() - val scanFilters = listOf(scanFilter) - - Log.d(TAG, "Starting BLE scan with target service UUID: ${AppConstants.Mesh.Gatt.SERVICE_UUID}") - + val scanFilters = listOf(scanFilter) + scanCallback = object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { - // Log.d(TAG, "Scan result received: ${result.device.address}") handleScanResult(result) } - + override fun onBatchScanResults(results: MutableList) { - Log.d(TAG, "Batch scan results received: ${results.size} devices") results.forEach { result -> handleScanResult(result) } } - + override fun onScanFailed(errorCode: Int) { - Log.e(TAG, "Scan failed: $errorCode") isCurrentlyScanning = false lastScanStopTime = System.currentTimeMillis() - + when (errorCode) { 1 -> { // Already started: the stack thinks a scan is running. Re-arm from a clean // state so we don't stay wedged (stop then restart with backoff). - Log.e(TAG, "SCAN_FAILED_ALREADY_STARTED") stopScanning() scheduleScanRestart("already-started", SCAN_RETRY_BASE_MS) } 2 -> { // App registration failed: common transient stack fault. Previously had NO // retry, which left discovery dead until a manual BLE toggle. - Log.e(TAG, "SCAN_FAILED_APPLICATION_REGISTRATION_FAILED") scheduleScanRestart("registration-failed", SCAN_RETRY_BASE_MS) } 3 -> { - Log.e(TAG, "SCAN_FAILED_INTERNAL_ERROR") scheduleScanRestart("internal-error", SCAN_RETRY_BASE_MS) } - 4 -> Log.e(TAG, "SCAN_FAILED_FEATURE_UNSUPPORTED") // permanent: don't retry + 4 -> Unit // permanent: don't retry 5 -> { // Out of hardware resources: back off longer so other scanners/connections // can free up before we try again. - Log.e(TAG, "SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES") scheduleScanRestart("out-of-resources", SCAN_RETRY_BASE_MS * 3) } 6 -> { - Log.e(TAG, "SCAN_FAILED_SCANNING_TOO_FREQUENTLY") - Log.w(TAG, "Scan failed due to rate limiting - will retry after delay") scheduleScanRestart("too-frequently", 10_000L) } else -> { - Log.e(TAG, "Unknown scan failure code: $errorCode") scheduleScanRestart("unknown-$errorCode", SCAN_RETRY_BASE_MS) } } + Log.e(TAG, "Scan failed: $errorCode") } } @@ -328,7 +313,7 @@ class BluetoothGattClientManager( isCurrentlyScanning = true bleScanner.startScan(scanFilters, powerManager.getScanSettings(), scanCallback) - Log.d(TAG, "BLE scan started successfully") + Log.i(TAG, "BLE scan started") } catch (e: Exception) { Log.e(TAG, "Exception starting scan: ${e.message}") isCurrentlyScanning = false @@ -344,9 +329,9 @@ class BluetoothGattClientManager( if (isCurrentlyScanning) { try { - scanCallback?.let { + scanCallback?.let { bleScanner.stopScan(it) - Log.d(TAG, "BLE scan stopped successfully") + Log.i(TAG, "BLE scan stopped") } } catch (e: Exception) { Log.w(TAG, "Error stopping scan: ${e.message}") @@ -455,15 +440,11 @@ class BluetoothGattClientManager( } if (peerID != null) { - // Log.v(TAG, "Found peerID $peerID in scan record for $deviceAddress") if (connectionTracker.isPeerConnected(peerID)) { - Log.d(TAG, "Deduplication: Peer $peerID is already connected (ignoring $deviceAddress)") return } } - // Log.d(TAG, "Received scan result from $deviceAddress - already connected: ${connectionTracker.isDeviceConnected(deviceAddress)}") - // Store RSSI from scan results for later use (especially for server connections) connectionTracker.updateScanRSSI(deviceAddress, rssi) @@ -481,7 +462,6 @@ class BluetoothGattClientManager( // Power-aware RSSI filtering if (rssi < powerManager.getRSSIThreshold()) { - Log.d(TAG, "Skipping device $deviceAddress due to weak signal: $rssi < ${powerManager.getRSSIThreshold()}") // Even if we skip connecting, still publish scan result to debug UI try { DebugSettingsManager.getInstance().addScanResult( @@ -503,7 +483,6 @@ class BluetoothGattClientManager( // Check if connection attempt is allowed if (!connectionTracker.isConnectionAttemptAllowed(deviceAddress)) { - Log.d(TAG, "Connection to $deviceAddress not allowed due to recent attempts") return } @@ -513,7 +492,6 @@ class BluetoothGattClientManager( val maxClient = dbg?.maxClientConnections?.value ?: maxOverall if (!connectionTracker.canConnectAsClient(maxOverall, maxClient)) { - Log.d(TAG, "Client connection limit reached (overall: $maxOverall, client: $maxClient)") return } @@ -533,14 +511,11 @@ class BluetoothGattClientManager( val deviceAddress = device.address val linkID = UUID.randomUUID().toString() - Log.i(TAG, "Connecting to bitchat device: $deviceAddress (peerID: $peerID)") - + Log.d(TAG, "Connecting to bitchat device: $deviceAddress (peerID: $peerID)") + val gattCallback = object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { - Log.d(TAG, "Client: Connection state change - Device: $deviceAddress, Status: $status, NewState: $newState") - if (newState == BluetoothProfile.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) { - Log.i(TAG, "Client: Successfully connected to $deviceAddress. Requesting MTU...") // Request a larger MTU. Must be done before any data transfer. connectionScope.launch { delay(200) // A small delay can improve reliability of MTU request. @@ -548,12 +523,9 @@ class BluetoothGattClientManager( } } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { if (status != BluetoothGatt.GATT_SUCCESS) { - Log.w(TAG, "Client: Disconnected from $deviceAddress with error status $status") - if (status == 147) { - Log.e(TAG, "Client: Connection establishment failed (status 147) for $deviceAddress") - } + Log.w(TAG, "Disconnected from $deviceAddress with error status $status (client)") } else { - Log.d(TAG, "Client: Cleanly disconnected from $deviceAddress") + Log.i(TAG, "Disconnected from $deviceAddress (client)") } connectionTracker.cleanupDeviceConnectionIfCurrent(deviceAddress, linkID) @@ -573,11 +545,8 @@ class BluetoothGattClientManager( override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) { val deviceAddress = gatt.device.address - Log.i(TAG, "Client: MTU changed for $deviceAddress to $mtu with status $status") if (status == BluetoothGatt.GATT_SUCCESS) { - Log.i(TAG, "MTU successfully negotiated for $deviceAddress. Discovering services.") - // Now that MTU is set, connection is fully ready. val deviceConn = BluetoothConnectionTracker.DeviceConnection( device = gatt.device, @@ -609,7 +578,7 @@ class BluetoothGattClientManager( linkID ) { it.copy(characteristic = characteristic) } ) { - Log.d(TAG, "Client: Updated device connection with characteristic for $deviceAddress") + // Characteristic stored on the current device connection } gatt.setCharacteristicNotification(characteristic, true) @@ -620,7 +589,7 @@ class BluetoothGattClientManager( connectionScope.launch { delay(200) - Log.i(TAG, "Client: Connection setup complete for $deviceAddress") + Log.i(TAG, "Connected to $deviceAddress (client)") delegate?.onDeviceConnected(device) } } else { @@ -643,42 +612,34 @@ class BluetoothGattClientManager( override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { val value = characteristic.value - Log.i(TAG, "Client: Received packet from ${gatt.device.address}, size: ${value.size} bytes") val packet = BitchatPacket.fromBinaryData(value) if (packet != null) { val peerID = packet.senderID.take(8).toByteArray().joinToString("") { "%02x".format(it) } - Log.d(TAG, "Client: Parsed packet type ${packet.type} from $peerID") delegate?.onPacketReceived(packet, peerID, gatt.device, linkID) } else { - Log.w(TAG, "Client: Failed to parse packet from ${gatt.device.address}, size: ${value.size} bytes") - Log.w(TAG, "Client: Packet data: ${value.joinToString(" ") { "%02x".format(it) }}") + Log.d(TAG, "Failed to parse packet from ${gatt.device.address}, size: ${value.size} bytes") } } override fun onReadRemoteRssi(gatt: BluetoothGatt, rssi: Int, status: Int) { val deviceAddress = gatt.device.address if (status == BluetoothGatt.GATT_SUCCESS) { - Log.d(TAG, "Client: RSSI updated for $deviceAddress: $rssi dBm") - // Update the connection tracker with new RSSI value connectionTracker.updateDeviceConnectionIfCurrent(deviceAddress, linkID) { it.copy(rssi = rssi) } } else { - Log.w(TAG, "Client: Failed to read RSSI for $deviceAddress, status: $status") + Log.d(TAG, "Failed to read RSSI for $deviceAddress, status: $status") } } } try { - Log.d(TAG, "Client: Attempting GATT connection to $deviceAddress with autoConnect=false") val gatt = device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE) if (gatt == null) { Log.e(TAG, "connectGatt returned null for $deviceAddress") // keep the pending connection so we can avoid too many reconnections attempts, TODO: needs testing // connectionTracker.removePendingConnection(deviceAddress) - } else { - Log.d(TAG, "Client: GATT connection initiated successfully for $deviceAddress") } } catch (e: Exception) { Log.e(TAG, "Client: Exception connecting to $deviceAddress: ${e.message}") diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt index d9ae8b17..53ae88ef 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt @@ -87,7 +87,6 @@ class BluetoothGattServerManager( } if (isActive) { - Log.d(TAG, "GATT server already active; start is a no-op") return true } if (!permissionManager.hasBluetoothPermissions()) { @@ -127,7 +126,6 @@ class BluetoothGattServerManager( gattServer?.close() gattServer = null serverLinkIDs.clear() - Log.i(TAG, "GATT server stopped (already inactive)") return } @@ -175,13 +173,12 @@ class BluetoothGattServerManager( override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) { // Guard against callbacks after service shutdown if (!isActive) { - Log.d(TAG, "Server: Ignoring connection state change after shutdown") return } - + when (newState) { BluetoothProfile.STATE_CONNECTED -> { - Log.i(TAG, "Server: Device connected ${device.address}") + Log.i(TAG, "Connected to ${device.address} (server)") val linkID = UUID.randomUUID().toString() serverLinkIDs[device.address] = linkID @@ -204,7 +201,7 @@ class BluetoothGattServerManager( } } BluetoothProfile.STATE_DISCONNECTED -> { - Log.i(TAG, "Server: Device disconnected ${device.address}") + Log.i(TAG, "Disconnected from ${device.address} (server)") val linkID = serverLinkIDs.remove(device.address) if (linkID != null) { connectionTracker.cleanupDeviceConnectionIfCurrent(device.address, linkID) @@ -218,13 +215,10 @@ class BluetoothGattServerManager( override fun onServiceAdded(status: Int, service: BluetoothGattService) { // Guard against callbacks after service shutdown if (!isActive) { - Log.d(TAG, "Server: Ignoring service added callback after shutdown") return } - - if (status == BluetoothGatt.GATT_SUCCESS) { - Log.d(TAG, "Server: Service added successfully: ${service.uuid}") - } else { + + if (status != BluetoothGatt.GATT_SUCCESS) { Log.e(TAG, "Server: Failed to add service: ${service.uuid}, status: $status") } } @@ -240,15 +234,13 @@ class BluetoothGattServerManager( ) { // Guard against callbacks after service shutdown if (!isActive) { - Log.d(TAG, "Server: Ignoring characteristic write after shutdown") return } - + if (characteristic.uuid == AppConstants.Mesh.Gatt.CHARACTERISTIC_UUID) { - Log.i(TAG, "Server: Received packet from ${device.address}, size: ${value.size} bytes") val linkID = serverLinkIDs[device.address] if (linkID == null) { - Log.w(TAG, "Server: Dropping packet from stale connection ${device.address}") + Log.d(TAG, "Server: Dropping packet from stale connection ${device.address}") if (responseNeeded) { gattServer?.sendResponse( device, @@ -263,11 +255,9 @@ class BluetoothGattServerManager( val packet = BitchatPacket.fromBinaryData(value) if (packet != null) { val peerID = packet.senderID.take(8).toByteArray().joinToString("") { "%02x".format(it) } - Log.d(TAG, "Server: Parsed packet type ${packet.type} from $peerID") delegate?.onPacketReceived(packet, peerID, device, linkID) } else { - Log.w(TAG, "Server: Failed to parse packet from ${device.address}, size: ${value.size} bytes") - Log.w(TAG, "Server: Packet data: ${value.joinToString(" ") { "%02x".format(it) }}") + Log.d(TAG, "Server: Failed to parse packet from ${device.address}, size: ${value.size} bytes") } if (responseNeeded) { @@ -287,14 +277,12 @@ class BluetoothGattServerManager( ) { // Guard against callbacks after service shutdown if (!isActive) { - Log.d(TAG, "Server: Ignoring descriptor write after shutdown") return } - + if (BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE.contentEquals(value)) { connectionTracker.addSubscribedDevice(device) - Log.d(TAG, "Server: Connection setup complete for ${device.address}") connectionScope.launch { delay(100) if (isActive) { // Check if still active @@ -311,19 +299,17 @@ class BluetoothGattServerManager( // Proper cleanup sequencing to prevent race conditions gattServer?.let { server -> - Log.d(TAG, "Cleaning up existing GATT server") try { server.close() } catch (e: Exception) { Log.w(TAG, "Error closing existing GATT server: ${e.message}") } } - + // Small delay to ensure cleanup is complete Thread.sleep(100) - + if (!isActive) { - Log.d(TAG, "Service inactive, skipping GATT server creation") return } @@ -373,11 +359,10 @@ class BluetoothGattServerManager( return } if (!isActive) { - Log.d(TAG, "Not starting advertising: manager not active") return } if (!enabled) { - Log.i(TAG, "Not starting advertising: GATT Server disabled via debug settings") + Log.d(TAG, "Not starting advertising: GATT Server disabled via debug settings") return } if (bleAdvertiser == null) { @@ -417,30 +402,24 @@ class BluetoothGattServerManager( val mode = try { powerManager.getPowerInfo().split("Current Mode: ")[1].split("\n")[0] } catch (_: Exception) { "unknown" } - Log.i(TAG, "Advertising started (power mode: $mode) with stable ID: ${peerIDBytes.joinToString("") { "%02x".format(it) }}") + Log.i(TAG, "Advertising started (power mode: $mode)") } - + override fun onStartFailure(errorCode: Int) { Log.e(TAG, "Advertising failed: $errorCode") // Previously this only logged, so if advertising failed this device became // undiscoverable until a manual BLE toggle. Retry transient failures with backoff. when (errorCode) { - ADVERTISE_FAILED_ALREADY_STARTED -> - Log.w(TAG, "ADVERTISE_FAILED_ALREADY_STARTED - already advertising, no retry") - ADVERTISE_FAILED_DATA_TOO_LARGE -> - Log.e(TAG, "ADVERTISE_FAILED_DATA_TOO_LARGE - config issue, not retrying") - ADVERTISE_FAILED_FEATURE_UNSUPPORTED -> - Log.e(TAG, "ADVERTISE_FAILED_FEATURE_UNSUPPORTED - unsupported, not retrying") + ADVERTISE_FAILED_ALREADY_STARTED -> Unit // already advertising, no retry + ADVERTISE_FAILED_DATA_TOO_LARGE -> Unit // config issue, not retrying + ADVERTISE_FAILED_FEATURE_UNSUPPORTED -> Unit // unsupported, not retrying ADVERTISE_FAILED_TOO_MANY_ADVERTISERS -> { - Log.w(TAG, "ADVERTISE_FAILED_TOO_MANY_ADVERTISERS - will retry after backoff") scheduleAdvertiseRestart("too-many-advertisers") } ADVERTISE_FAILED_INTERNAL_ERROR -> { - Log.w(TAG, "ADVERTISE_FAILED_INTERNAL_ERROR - will retry after backoff") scheduleAdvertiseRestart("internal-error") } else -> { - Log.w(TAG, "Unknown advertise failure $errorCode - will retry after backoff") scheduleAdvertiseRestart("unknown-$errorCode") } } 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 9a6dff60..55cdb6df 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -170,8 +170,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic peerManager.isPeerDirectlyConnected = { peerID -> connectionManager.addressPeerMap.containsValue(peerID) } - - Log.d(TAG, "Delegates set up; GossipSyncManager initialized") } override fun send(packet: RoutedPacket) { @@ -205,19 +203,17 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic */ private fun startPeriodicDebugLogging() { serviceScope.launch { - Log.d(TAG, "Starting periodic debug logging loop") while (isActive) { try { delay(10000) // 10 seconds if (isActive) { // Double-check before logging val debugInfo = getDebugStatus() - Log.d(TAG, "=== PERIODIC DEBUG STATUS ===\n$debugInfo\n=== END DEBUG STATUS ===") + Log.d(TAG, "Periodic debug status:\n$debugInfo") } } catch (e: Exception) { Log.e(TAG, "Error in periodic debug logging: ${e.message}") } } - Log.d(TAG, "Periodic debug logging loop ended (isActive=$isActive)") } } @@ -227,7 +223,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic private fun sendPeriodicBroadcastAnnounce() { announceJob?.cancel() announceJob = serviceScope.launch { - Log.d(TAG, "Starting periodic announce loop") while (isActive) { try { delay(30000) // 30 seconds @@ -236,7 +231,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic Log.e(TAG, "Error in periodic broadcast announce: ${e.message}") } } - Log.d(TAG, "Periodic announce loop ended (isActive=$isActive)") } } @@ -244,7 +238,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic * Setup delegate connections between components */ private fun setupDelegates() { - Log.d(TAG, "Setting up component delegates") // Provide nickname resolver to BLE broadcaster and debug manager try { val resolver: (String) -> String? = { pid -> peerManager.getPeerNickname(pid) } @@ -269,7 +262,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic // Also drop any Noise session state for this peer when they go offline try { encryptionService.removePeer(peerID) - Log.d(TAG, "Removed Noise session for offline peer $peerID") } catch (e: Exception) { Log.w(TAG, "Failed to remove Noise session for $peerID: ${e.message}") } @@ -308,7 +300,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic } // Send announcement and cached messages after key exchange serviceScope.launch { - Log.d(TAG, "Key exchange completed with $peerID; sending follow-ups") delay(100) sendAnnouncementToPeer(peerID) @@ -331,7 +322,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic // Sign the handshake response val signedPacket = signPacketBeforeBroadcast(responsePacket) broadcastRoutedPacket(RoutedPacket(signedPacket)) - Log.d(TAG, "Sent Noise handshake response to $peerID (${response.size} bytes)") } override fun getPeerInfo(peerID: String): PeerInfo? { @@ -461,7 +451,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic // Sign the handshake packet before broadcasting val signedPacket = signPacketBeforeBroadcast(packet) broadcastRoutedPacket(RoutedPacket(signedPacket)) - Log.d(TAG, "Initiated Noise handshake with $peerID (${handshakeData.size} bytes)") } else { Log.w(TAG, "Failed to generate Noise handshake data for $peerID") } @@ -702,7 +691,7 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic override fun onDeviceConnected(device: android.bluetooth.BluetoothDevice) { // Send initial announcements after services are ready serviceScope.launch { - Log.d(TAG, "Device connected: ${device.address}; scheduling announce") + Log.i(TAG, "Device connected: ${device.address}") delay(200) sendBroadcastAnnounce() } @@ -720,7 +709,7 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic device: android.bluetooth.BluetoothDevice, linkID: String? ) { - Log.d(TAG, "Device disconnected: ${device.address}") + Log.i(TAG, "Device disconnected: ${device.address}") val addr = device.address clearProvisionalBleClaimsForLink(addr, linkID) @@ -748,9 +737,7 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic provisionalBleClaims[peerID] = claim serviceScope.launch { delay(BLE_AUTHENTICATION_TIMEOUT_MS) - if (provisionalBleClaims.remove(peerID, claim)) { - Log.d(TAG, "Expired provisional BLE authentication claim for $peerID") - } + provisionalBleClaims.remove(peerID, claim) } } @@ -793,10 +780,8 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic // Start periodic announcements for peer discovery and connectivity sendPeriodicBroadcastAnnounce() - Log.d(TAG, "Started periodic broadcast announcements (every 30 seconds)") // Start periodic syncs com.bitchat.android.service.MeshServiceHolder.startSharedGossip("BLE") - Log.d(TAG, "GossipSyncManager started") } else { Log.e(TAG, "Failed to start Bluetooth services") } @@ -847,14 +832,11 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic sendLeaveAnnouncement() serviceScope.launch { - Log.d(TAG, "Stopping subcomponents and cancelling scope...") delay(200) // Give leave message time to send - + // Stop all components com.bitchat.android.service.MeshServiceHolder.stopSharedGossip("BLE") - Log.d(TAG, "GossipSyncManager stopped") connectionManager.stopServices() - Log.d(TAG, "BluetoothConnectionManager stop requested") peerManager.shutdown() fragmentManager.shutdown() securityManager.shutdown() @@ -875,9 +857,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic */ fun isReusable(): Boolean { val reusable = !terminated && serviceScope.isActive && connectionManager.isReusable() - if (!reusable) { - Log.d(TAG, "isReusable=false (terminated=$terminated, scopeActive=${serviceScope.isActive}, connReusable=${connectionManager.isReusable()})") - } return reusable } @@ -938,13 +917,11 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic fun sendFileBroadcast(file: com.bitchat.android.model.BitchatFilePacket) { try { - Log.d(TAG, "📤 sendFileBroadcast: name=${file.fileName}, size=${file.fileSize}") val payload = file.encode() if (payload == null) { - Log.e(TAG, "❌ Failed to encode file packet in sendFileBroadcast") + Log.e(TAG, "Failed to encode file packet in sendFileBroadcast") return } - Log.d(TAG, "📦 Encoded payload: ${payload.size} bytes") serviceScope.launch { val packet = BitchatPacket( version = 2u, // FILE_TRANSFER uses v2 for 4-byte payload length to support large files @@ -963,8 +940,7 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic try { gossipSyncManager.onPublicPacketSeen(signed) } catch (_: Exception) { } } } catch (e: Exception) { - Log.e(TAG, "❌ sendFileBroadcast failed: ${e.message}", e) - Log.e(TAG, "❌ File: name=${file.fileName}, size=${file.fileSize}") + Log.e(TAG, "sendFileBroadcast failed (size=${file.fileSize}): ${e.message}", e) } } @@ -981,7 +957,7 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic is PrivateMediaPreparation.RequiresLegacyConsent -> Log.w(TAG, "Private media requires explicit one-shot legacy consent") PrivateMediaPreparation.NeedsHandshake -> { - Log.i(TAG, "Private media needs a Noise handshake; initiating without sending") + Log.d(TAG, "Private media needs a Noise handshake; initiating without sending") initiateNoiseHandshake(recipientPeerID) } PrivateMediaPreparation.AwaitingPeerState -> Unit @@ -1051,9 +1027,7 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic serviceScope.launch { val finalMessageID = messageID ?: java.util.UUID.randomUUID().toString() - - Log.d(TAG, "📨 Sending PM to $recipientPeerID: ${content.take(30)}...") - + // Check if we have an established Noise session if (encryptionService.hasEstablishedSession(recipientPeerID)) { try { @@ -1093,16 +1067,14 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic // Sign the packet before broadcasting val signedPacket = signPacketBeforeBroadcast(packet) broadcastRoutedPacket(RoutedPacket(signedPacket)) - Log.d(TAG, "📤 Sent encrypted private message to $recipientPeerID (${encrypted.size} bytes)") - + // The UI handles sent messages through its own sending path. - + } catch (e: Exception) { Log.e(TAG, "Failed to encrypt private message for $recipientPeerID: ${e.message}") } } else { // Fire and forget - initiate handshake but don't queue exactly like iOS - Log.d(TAG, "🤝 No session with $recipientPeerID, initiating handshake") messageHandler.delegate?.initiateNoiseHandshake(recipientPeerID) // The UI handles sent messages through its own sending path. @@ -1116,8 +1088,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic */ fun sendReadReceipt(messageID: String, recipientPeerID: String, readerNickname: String) { serviceScope.launch { - Log.d(TAG, "📖 Sending read receipt for message $messageID to $recipientPeerID") - // Route geohash read receipts via MessageRouter instead of here val geo = runCatching { com.bitchat.android.services.MessageRouter.tryGetInstance() }.getOrNull() val isGeoAlias = try { @@ -1133,7 +1103,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic // Avoid duplicate read receipts: check persistent store first val seenStore = try { com.bitchat.android.services.SeenMessageStore.getInstance(context.applicationContext) } catch (_: Exception) { null } if (seenStore?.hasRead(messageID) == true) { - Log.d(TAG, "Skipping read receipt for $messageID - already marked read") return@launch } @@ -1161,7 +1130,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic // Sign the packet before broadcasting val signedPacket = signPacketBeforeBroadcast(packet) broadcastRoutedPacket(RoutedPacket(signedPacket)) - Log.d(TAG, "📤 Sent read receipt to $recipientPeerID for message $messageID") // Persist as read after successful send try { seenStore?.markRead(messageID) } catch (_: Exception) { } @@ -1209,7 +1177,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic val signedPacket = signPacketBeforeBroadcast(packet) broadcastRoutedPacket(RoutedPacket(signedPacket)) - Log.d(TAG, "📤 Sent $label to $recipientPeerID (${payload.data.size} bytes)") } catch (e: Exception) { Log.e(TAG, "Failed to send $label to $recipientPeerID: ${e.message}") } @@ -1220,7 +1187,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic * Send broadcast announce with TLV-encoded identity announcement - exactly like iOS */ fun sendBroadcastAnnounce() { - Log.d(TAG, "Sending broadcast announce") serviceScope.launch { val nickname = try { com.bitchat.android.services.NicknameProvider.getNickname(context, myPeerID) } catch (_: Exception) { myPeerID } @@ -1273,7 +1239,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic } ?: announcePacket broadcastRoutedPacket(RoutedPacket(signedPacket)) - Log.d(TAG, "Sent iOS-compatible signed TLV announce (${tlvPayload.size} bytes)") // Track announce for sync try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { } } @@ -1337,7 +1302,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic broadcastRoutedPacket(RoutedPacket(signedPacket)) peerManager.markPeerAsAnnouncedTo(peerID) - Log.d(TAG, "Sent iOS-compatible signed TLV peer announce to $peerID (${tlvPayload.size} bytes)") // Track announce for sync try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { } @@ -1592,7 +1556,6 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic // Sign the packet data using our signing key val signature = encryptionService.signData(packetDataForSigning) if (signature != null) { - Log.d(TAG, "✅ Signed packet type ${packet.type} (signature ${signature.size} bytes)") withRoute.copy(signature = signature) } else { Log.w(TAG, "Failed to sign packet type ${packet.type}, sending unsigned") @@ -1610,20 +1573,19 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic * Clear all internal mesh service data (for panic mode) */ fun clearAllInternalData() { - Log.w(TAG, "🚨 Clearing all mesh service internal data") + 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() 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}") + Log.e(TAG, "Error clearing mesh service internal data: ${e.message}") } } @@ -1631,13 +1593,12 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic * Clear all encryption and cryptographic data (for panic mode) */ fun clearAllEncryptionData() { - Log.w(TAG, "🚨 Clearing all encryption data") + 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}") + Log.e(TAG, "Error clearing encryption data: ${e.message}") } } } diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt index 3e1cf721..0e9b0c76 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt @@ -122,13 +122,8 @@ class BluetoothPacketBroadcaster( private val broadcasterActor = broadcasterScope.actor( capacity = Channel.UNLIMITED ) { - Log.d(TAG, "🎭 Created packet broadcaster actor") - try { - for (request in channel) { - broadcastSinglePacketInternal(request.routed, request.gattServer, request.characteristic) - } - } finally { - Log.d(TAG, "🎭 Packet broadcaster actor terminated") + for (request in channel) { + broadcastSinglePacketInternal(request.routed, request.gattServer, request.characteristic) } } @@ -195,10 +190,6 @@ class BluetoothPacketBroadcaster( // iOS-compatible: Use selective padding policy for BLE val padForBLE = BLEPacketPaddingPolicy.shouldPadForBLE(packet.type) val data = packet.toBinaryData(padding = padForBLE) ?: return false - val isFile = packet.type == MessageType.FILE_TRANSFER.value - if (isFile) { - Log.d(TAG, "📤 Broadcasting FILE_TRANSFER: ${packet.payload.size} bytes") - } val typeName = MessageType.fromValue(packet.type)?.name ?: packet.type.toString() val senderPeerID = routed.peerID ?: packet.senderID.toHexString() val incomingAddr = routed.relayAddress @@ -300,16 +291,14 @@ class BluetoothPacketBroadcaster( // If we are the sender and a source route is defined, we must send ONLY to the first hop. if (packet.senderID.toHexString() == myPeerID && !packet.route.isNullOrEmpty()) { val firstHop = packet.route!![0].toHexString() - Log.d(TAG, "Source Routing: Packet has explicit route, attempting to send to first hop: $firstHop") var sent = false // Try to find first hop in server connections (subscribedDevices) val serverTarget = connectionTracker.getSubscribedDevices() .firstOrNull { connectionTracker.addressPeerMap[it.address] == firstHop } - + if (serverTarget != null) { - Log.d(TAG, "Source Routing: sending directly to first hop (server conn) $firstHop: ${serverTarget.address}") if (notifyDevice(serverTarget, data, gattServer, characteristic)) { val toPeer = connectionTracker.addressPeerMap[serverTarget.address] logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, toPeer, serverTarget.address, packet.ttl, packet.version, routeInfo) @@ -323,7 +312,6 @@ class BluetoothPacketBroadcaster( .firstOrNull { connectionTracker.addressPeerMap[it.device.address] == firstHop } if (clientTarget != null) { - Log.d(TAG, "Source Routing: sending directly to first hop (client conn) $firstHop: ${clientTarget.device.address}") if (writeToDeviceConn(clientTarget, data)) { val toPeer = connectionTracker.addressPeerMap[clientTarget.device.address] logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, toPeer, clientTarget.device.address, packet.ttl, packet.version, routeInfo) @@ -333,8 +321,8 @@ class BluetoothPacketBroadcaster( } if (sent) return - - Log.w(TAG, "Source Routing: First hop $firstHop not connected. Falling back to standard broadcast logic.") + + Log.d(TAG, "Source Routing: First hop $firstHop not connected. Falling back to standard broadcast logic.") } if (packet.recipientID != SpecialRecipients.BROADCAST) { @@ -346,7 +334,6 @@ class BluetoothPacketBroadcaster( // If found, send directly if (targetDevice != null) { - Log.d(TAG, "Send packet type ${packet.type} directly to target device for recipient $recipientID: ${targetDevice.address}") if (notifyDevice(targetDevice, data, gattServer, characteristic)) { val toPeer = connectionTracker.addressPeerMap[targetDevice.address] logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, toPeer, targetDevice.address, packet.ttl, packet.version, routeInfo) @@ -360,7 +347,6 @@ class BluetoothPacketBroadcaster( // If found, send directly if (targetDeviceConn != null) { - Log.d(TAG, "Send packet type ${packet.type} directly to target client connection for recipient $recipientID: ${targetDeviceConn.device.address}") if (writeToDeviceConn(targetDeviceConn, data)) { val toPeer = connectionTracker.addressPeerMap[targetDeviceConn.device.address] logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, toPeer, targetDeviceConn.device.address, packet.ttl, packet.version, routeInfo) @@ -372,19 +358,15 @@ class BluetoothPacketBroadcaster( // Else, continue with broadcasting to all devices val subscribedDevices = connectionTracker.getSubscribedDevices() val connectedDevices = connectionTracker.getConnectedDevices() - - Log.i(TAG, "Broadcasting packet v${packet.version} type ${packet.type} to ${subscribedDevices.size} server + ${connectedDevices.size} client connections") val senderID = packet.senderID.toHexString() - + // Send to server connections (devices connected to our GATT server) subscribedDevices.forEach { device -> if (device.address == routed.relayAddress) { - 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 to client back to sender: ${device.address}") return@forEach } val sent = notifyDevice(device, data, gattServer, characteristic) @@ -398,11 +380,9 @@ class BluetoothPacketBroadcaster( connectedDevices.values.forEach { deviceConn -> if (deviceConn.isClient && deviceConn.gatt != null && deviceConn.characteristic != null) { if (deviceConn.device.address == routed.relayAddress) { - 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 roadcast to server back to sender: ${deviceConn.device.address}") return@forEach } val sent = writeToDeviceConn(deviceConn, data) @@ -479,14 +459,10 @@ class BluetoothPacketBroadcaster( * Shutdown the broadcaster actor gracefully */ fun shutdown() { - Log.d(TAG, "Shutting down BluetoothPacketBroadcaster actor") - // Close the actor gracefully broadcasterActor.close() - + // Cancel the broadcaster scope broadcasterScope.cancel() - - Log.d(TAG, "BluetoothPacketBroadcaster shutdown complete") } } diff --git a/app/src/main/java/com/bitchat/android/mesh/FragmentManager.kt b/app/src/main/java/com/bitchat/android/mesh/FragmentManager.kt index 489ead2a..3a10f998 100644 --- a/app/src/main/java/com/bitchat/android/mesh/FragmentManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/FragmentManager.kt @@ -66,22 +66,19 @@ class FragmentManager { Log.w(TAG, "Rejecting invalid outbound fragment limit: $maxFragments") return emptyList() } - Log.d(TAG, "🔀 Creating fragments for packet type ${packet.type}, payload: ${packet.payload.size} bytes") val encoded = packet.toBinaryData() if (encoded == null) { - Log.e(TAG, "❌ Failed to encode packet to binary data") + Log.e(TAG, "Failed to encode packet to binary data") return emptyList() } - Log.d(TAG, "📦 Encoded to ${encoded.size} bytes") // Fragment the unpadded frame; each fragment will be encoded (and padded) independently - iOS fix val fullData = try { MessagePadding.unpad(encoded) } catch (e: Exception) { - Log.e(TAG, "❌ Failed to unpad data: ${e.message}", e) + Log.e(TAG, "Failed to unpad data: ${e.message}", e) return emptyList() } - Log.d(TAG, "📏 Unpadded to ${fullData.size} bytes") // iOS logic: if data.count > 512 && packet.type != MessageType.fragment.rawValue if (fullData.size <= FRAGMENT_SIZE_THRESHOLD) { @@ -111,21 +108,15 @@ class FragmentManager { val maxDataSize = (512 - packetOverhead).coerceAtMost(MAX_FRAGMENT_SIZE) if (maxDataSize <= 0) { - Log.e(TAG, "❌ Calculated maxDataSize is non-positive ($maxDataSize). Route too large?") + Log.e(TAG, "Calculated maxDataSize is non-positive ($maxDataSize). Route too large?") return emptyList() } - Log.d(TAG, "📏 Dynamic fragment size: $maxDataSize (MAX: $MAX_FRAGMENT_SIZE, Overhead: $packetOverhead)") - val requiredFragments = ( (fullData.size.toLong() + maxDataSize.toLong() - 1L) / maxDataSize.toLong() ).toInt() if (requiredFragments > maxFragments) { - Log.w( - TAG, - "Rejecting outbound packet requiring $requiredFragments fragments " + - "(caller cap: $maxFragments)" - ) + Log.w(TAG, "Rejecting outbound packet requiring $requiredFragments fragments (caller cap: $maxFragments)") return emptyList() } @@ -135,8 +126,6 @@ class FragmentManager { fullData.sliceArray(offset.. 13 else { return } if (packet.payload.size < FragmentPayload.HEADER_SIZE) { - Log.w(TAG, "Fragment packet too small: ${packet.payload.size}") + Log.d(TAG, "Fragment packet too small: ${packet.payload.size}") return null } @@ -194,14 +181,12 @@ class FragmentManager { // Use FragmentPayload for type-safe decoding val fragmentPayload = FragmentPayload.decode(packet.payload) if (fragmentPayload == null || !fragmentPayload.isValid()) { - Log.w(TAG, "Invalid fragment payload") + Log.d(TAG, "Invalid fragment payload") return null } - + // iOS: let fragmentID = packet.payload[0..<8].map { String(format: "%02x", $0) }.joined() val fragmentIDString = fragmentPayload.getFragmentIDString() - - Log.d(TAG, "Received fragment ${fragmentPayload.index}/${fragmentPayload.total} for fragmentID: $fragmentIDString, originalType: ${fragmentPayload.originalType}") val maxFragments = com.bitchat.android.util.AppConstants.Fragmentation.MAX_FRAGMENTS_PER_ID if (fragmentPayload.total > maxFragments) { @@ -212,11 +197,7 @@ class FragmentManager { synchronized(fragmentStateLock) { fragmentMetadata[fragmentIDString]?.let { (expectedType, expectedTotal, _) -> if (expectedTotal != fragmentPayload.total || expectedType != fragmentPayload.originalType) { - Log.w( - TAG, - "Rejecting fragment for $fragmentIDString: inconsistent metadata " + - "(expected type=$expectedType total=$expectedTotal, got type=${fragmentPayload.originalType} total=${fragmentPayload.total})" - ) + Log.w(TAG, "Rejecting fragment for $fragmentIDString: inconsistent metadata") removeFragmentSetLocked(fragmentIDString) return null } @@ -265,10 +246,7 @@ class FragmentManager { val delta = (fragmentPayload.data.size - oldEntrySize).toLong() val maxGlobalBytes = com.bitchat.android.util.AppConstants.Fragmentation.MAX_GLOBAL_FRAGMENT_TOTAL_BYTES if (globalBufferedBytes + delta > maxGlobalBytes) { - Log.w( - TAG, - "Rejecting fragment for $fragmentIDString: global buffered bytes ${(globalBufferedBytes + delta)} exceeds cap $maxGlobalBytes" - ) + Log.w(TAG, "Rejecting fragment for $fragmentIDString: global buffered bytes exceed cap $maxGlobalBytes") if (isNewSet) { removeFragmentSetLocked(fragmentIDString) } @@ -281,8 +259,6 @@ class FragmentManager { val expectedTotal = fragmentMetadata[fragmentIDString]?.second ?: fragmentPayload.total if (fragmentMap.size == expectedTotal) { - Log.d(TAG, "All fragments received for $fragmentIDString, reassembling...") - // iOS reassembly logic: for i in 0..() for (i in 0 until expectedTotal) { @@ -296,15 +272,11 @@ class FragmentManager { removeFragmentSetLocked(fragmentIDString) val suppressedTtlPacket = originalPacket.copy(ttl = 0u.toUByte()) - Log.d(TAG, "Successfully reassembled original (${reassembledData.size} bytes); set TTL=0 to suppress relay") return suppressedTtlPacket } else { val metadata = fragmentMetadata[fragmentIDString] Log.e(TAG, "Failed to decode reassembled packet (type=${metadata?.first}, total=${metadata?.second})") } - } else { - val received = fragmentMap.size - Log.d(TAG, "Fragment ${fragmentPayload.index} stored, have $received/$expectedTotal fragments for $fragmentIDString") } } @@ -353,10 +325,6 @@ class FragmentManager { for (fragmentID in oldFragments) { removeFragmentSetLocked(fragmentID) } - - if (oldFragments.isNotEmpty()) { - Log.d(TAG, "Cleaned up ${oldFragments.size} old fragment sets (iOS compatible)") - } } } diff --git a/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt b/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt index beedd857..d3356e85 100644 --- a/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt +++ b/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt @@ -46,15 +46,12 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro val packet = routed.packet val peerID = routed.peerID ?: "unknown" - Log.d(TAG, "Processing Noise encrypted message from $peerID (${packet.payload.size} bytes)") - // Skip our own messages if (peerID == myPeerID) return // Check if this message is for us val recipientID = packet.recipientID?.toHexString() if (recipientID != myPeerID) { - Log.d(TAG, "🔐 Encrypted message not for me (for $recipientID, I am $myPeerID)") return } @@ -78,15 +75,11 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro return } - Log.d(TAG, "🔓 Decrypted NoisePayload type ${noisePayload.type} from $peerID") - when (noisePayload.type) { com.bitchat.android.model.NoisePayloadType.PRIVATE_MESSAGE -> { // Decode TLV private message exactly like iOS val privateMessage = com.bitchat.android.model.PrivateMessagePacket.decode(noisePayload.data) if (privateMessage != null) { - Log.d(TAG, "🔓 Decrypted TLV PM from $peerID: ${privateMessage.content.take(30)}...") - // Handle favorite/unfavorite notifications embedded as PMs val pmContent = privateMessage.content if (FavoriteControlMessage.parse(pmContent) != null) { @@ -122,7 +115,7 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro // Handle encrypted file transfer; generate unique message ID val file = com.bitchat.android.model.BitchatFilePacket.decode(noisePayload.data) if (file != null) { - Log.d(TAG, "🔓 Decrypted encrypted file from $peerID: name='${file.fileName}', size=${file.fileSize}, mime='${file.mimeType}'") + Log.d(TAG, "Encrypted file from $peerID: ${file.fileSize} bytes") val uniqueMsgId = java.util.UUID.randomUUID().toString().uppercase() val savedPath = com.bitchat.android.features.file.FileUtils.saveIncomingFile(appContext, file) val message = BitchatMessage( @@ -137,13 +130,12 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro senderPeerID = peerID ) - Log.d(TAG, "📄 Saved encrypted incoming file to $savedPath (msgId=$uniqueMsgId)") delegate?.onMessageReceived(message) // Send delivery ACK with generated message ID sendDeliveryAck(uniqueMsgId, peerID) } else { - Log.w(TAG, "⚠️ Failed to decode encrypted file transfer from $peerID") + Log.w(TAG, "Failed to decode encrypted file transfer from $peerID") } } @@ -163,7 +155,7 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro com.bitchat.android.model.NoisePayloadType.DELIVERED -> { // Handle delivery ACK exactly like iOS val messageID = String(noisePayload.data, Charsets.UTF_8) - Log.d(TAG, "📬 Delivery ACK received from $peerID for message $messageID") + Log.d(TAG, "Delivery ACK from $peerID for $messageID") // Simplified: Call delegate with messageID and peerID directly delegate?.onDeliveryAckReceived(messageID, peerID) @@ -172,17 +164,15 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro com.bitchat.android.model.NoisePayloadType.READ_RECEIPT -> { // Handle read receipt exactly like iOS val messageID = String(noisePayload.data, Charsets.UTF_8) - Log.d(TAG, "👁️ Read receipt received from $peerID for message $messageID") + Log.d(TAG, "Read receipt from $peerID for $messageID") // Simplified: Call delegate with messageID and peerID directly delegate?.onReadReceiptReceived(messageID, peerID) } com.bitchat.android.model.NoisePayloadType.VERIFY_CHALLENGE -> { - Log.d(TAG, "🔐 Verify challenge received from $peerID (${noisePayload.data.size} bytes)") delegate?.onVerifyChallengeReceived(peerID, noisePayload.data, packet.timestamp.toLong()) } com.bitchat.android.model.NoisePayloadType.VERIFY_RESPONSE -> { - Log.d(TAG, "🔐 Verify response received from $peerID (${noisePayload.data.size} bytes)") delegate?.onVerifyResponseReceived(peerID, noisePayload.data, packet.timestamp.toLong()) } } @@ -223,7 +213,6 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro ) delegate?.sendPacket(packet) - Log.d(TAG, "📤 Sent delivery ACK to $senderPeerID for message $messageID") } catch (e: Exception) { Log.e(TAG, "Failed to send delivery ACK to $senderPeerID: ${e.message}") @@ -250,8 +239,6 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro if (clockSkewMs > ANNOUNCE_CLOCK_SKEW_TOLERANCE_MS) { Log.w(TAG, "Ignoring ANNOUNCE from ${peerID.take(8)} with excessive clock skew (${clockSkewMs}ms > ${ANNOUNCE_CLOCK_SKEW_TOLERANCE_MS}ms)") return AnnounceHandlingResult.Rejected - } else if (clockSkewMs > com.bitchat.android.util.AppConstants.Mesh.STALE_PEER_TIMEOUT_MS) { - Log.w(TAG, "Accepting ANNOUNCE from ${peerID.take(8)} within clock skew tolerance (${clockSkewMs}ms)") } val announcement = AnnouncementIdentityValidator.verify(packet, peerID) { signature, data, key -> @@ -277,7 +264,7 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro 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") + Log.w(TAG, "Announce key mismatch for ${peerID.take(8)} - keeping unverified") verified = false } @@ -294,15 +281,9 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro // Require verified announce; ignore otherwise (no backward compatibility) if (!verified) { - Log.w(TAG, "❌ Ignoring unverified announce from ${peerID.take(8)}...") return AnnounceHandlingResult.Rejected } - // 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 @@ -325,7 +306,7 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro .updateFromAnnouncement(peerID, nickname, neighborsOrNull, packet.timestamp) } catch (_: Exception) { } - Log.d(TAG, "✅ Processed verified TLV announce: stored identity for $peerID") + Log.d(TAG, "Verified announce from $peerID (${announcement.nickname})") return AnnounceHandlingResult.Accepted(isFirstAnnounce) } @@ -337,15 +318,12 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro val packet = routed.packet val peerID = routed.peerID ?: "unknown" - Log.d(TAG, "Processing Noise handshake from $peerID (${packet.payload.size} bytes)") - // Skip our own handshake messages if (peerID == myPeerID) return // Check if handshake is addressed to us val recipientID = packet.recipientID?.toHexString() if (recipientID != myPeerID) { - Log.d(TAG, "Handshake not for me (for $recipientID, I am $myPeerID)") return } @@ -354,8 +332,6 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro val response = delegate?.processNoiseHandshakeMessage(packet.payload, peerID) if (response != null) { - Log.d(TAG, "Generated handshake response for $peerID (${response.size} bytes)") - // Send response using same packet type (simplified iOS approach) val responsePacket = BitchatPacket( version = 1u, @@ -369,13 +345,6 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro ) delegate?.sendPacket(responsePacket) - Log.d(TAG, "📤 Sent handshake response to $peerID") - } - - // Check if session is now established - val hasSession = delegate?.hasNoiseSession(peerID) ?: false - if (hasSession) { - Log.d(TAG, "✅ Noise session established with $peerID") } } catch (e: Exception) { @@ -418,7 +387,7 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro // 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)}...") + Log.w(TAG, "Dropping public message from unverified peer ${peerID.take(8)}") return } @@ -427,9 +396,7 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro val isFileTransfer = com.bitchat.android.protocol.MessageType.fromValue(packet.type) == com.bitchat.android.protocol.MessageType.FILE_TRANSFER val file = com.bitchat.android.model.BitchatFilePacket.decode(packet.payload) if (file != null) { - if (isFileTransfer) { - Log.d(TAG, "📥 FILE_TRANSFER decode success (broadcast): name='${file.fileName}', size=${file.fileSize}, mime='${file.mimeType}', from=${peerID.take(8)}") - } + val savedPath = com.bitchat.android.features.file.FileUtils.saveIncomingFile(appContext, file) val message = BitchatMessage( id = PacketIdUtil.computeIdHex(packet).uppercase(), @@ -439,11 +406,10 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro senderPeerID = peerID, timestamp = Date(packet.timestamp.toLong()) ) - Log.d(TAG, "📄 Saved incoming file to $savedPath") delegate?.onMessageReceived(message) return } else if (isFileTransfer) { - Log.w(TAG, "⚠️ FILE_TRANSFER decode failed (broadcast) from ${peerID.take(8)} payloadSize=${packet.payload.size}") + Log.w(TAG, "FILE_TRANSFER decode failed (broadcast) from ${peerID.take(8)}") } // Fallback: plain text @@ -487,9 +453,7 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro // Try file packet first (voice, image, etc.) and log outcome for FILE_TRANSFER val file = com.bitchat.android.model.BitchatFilePacket.decode(packet.payload) if (file != null) { - if (isFileTransfer) { - Log.d(TAG, "📥 FILE_TRANSFER decode success (private): name='${file.fileName}', size=${file.fileSize}, mime='${file.mimeType}', from=${peerID.take(8)}") - } + val savedPath = com.bitchat.android.features.file.FileUtils.saveIncomingFile(appContext, file) val message = BitchatMessage( id = java.util.UUID.randomUUID().toString().uppercase(), diff --git a/app/src/main/java/com/bitchat/android/mesh/PacketProcessor.kt b/app/src/main/java/com/bitchat/android/mesh/PacketProcessor.kt index 0fdae3b0..a5c59901 100644 --- a/app/src/main/java/com/bitchat/android/mesh/PacketProcessor.kt +++ b/app/src/main/java/com/bitchat/android/mesh/PacketProcessor.kt @@ -46,15 +46,8 @@ class PacketProcessor(private val myPeerID: String) { private fun getOrCreateActorForPeer(peerID: String) = processorScope.actor( capacity = Channel.UNLIMITED ) { - Log.d(TAG, "🎭 Created packet actor for peer: ${formatPeerForLog(peerID)}") - try { - for (packet in channel) { - Log.d(TAG, "📦 Processing packet type ${packet.packet.type} from ${formatPeerForLog(peerID)} (serialized)") - handleReceivedPacket(packet) - Log.d(TAG, "Completed packet type ${packet.packet.type} from ${formatPeerForLog(peerID)}") - } - } finally { - Log.d(TAG, "🎭 Packet actor for ${formatPeerForLog(peerID)} terminated") + for (packet in channel) { + handleReceivedPacket(packet) } } @@ -71,7 +64,6 @@ class PacketProcessor(private val myPeerID: String) { * SURGICAL FIX: Route to per-peer actor for serialized processing */ fun processPacket(routed: RoutedPacket) { - Log.d(TAG, "processPacket ${routed.packet.type}") val peerID = routed.peerID if (peerID == null) { @@ -125,13 +117,11 @@ class PacketProcessor(private val myPeerID: String) { // Basic validation and security checks if (!delegate?.validatePacketSecurity(packet, peerID)!!) { - Log.d(TAG, "Packet failed security validation from ${formatPeerForLog(peerID)}") return } var validPacket = true val messageType = MessageType.fromValue(packet.type) - Log.d(TAG, "Processing packet type ${messageType} from ${formatPeerForLog(peerID)}") // Verbose logging to debug manager (and chat via ChatViewModel observer) try { val mt = messageType?.name ?: packet.type.toString() @@ -162,7 +152,7 @@ class PacketProcessor(private val myPeerID: String) { } } } else { - Log.d(TAG, "Private packet type ${messageType} not addressed to us (from: ${formatPeerForLog(peerID)} to ${packet.recipientID?.let { it.joinToString("") { b -> "%02x".format(b) } }}), skipping") + // Not addressed to us; only relay handling below applies } } } @@ -180,8 +170,6 @@ class PacketProcessor(private val myPeerID: String) { * Handle Noise handshake message - SIMPLIFIED iOS-compatible version */ private suspend fun handleNoiseHandshake(routed: RoutedPacket): Boolean { - val peerID = routed.peerID ?: "unknown" - Log.d(TAG, "Processing Noise handshake from ${formatPeerForLog(peerID)}") return delegate?.handleNoiseHandshake(routed) ?: false } @@ -189,8 +177,6 @@ class PacketProcessor(private val myPeerID: String) { * Handle Noise encrypted transport message */ private suspend fun handleNoiseEncrypted(routed: RoutedPacket) { - val peerID = routed.peerID ?: "unknown" - Log.d(TAG, "Processing Noise encrypted message from ${formatPeerForLog(peerID)}") delegate?.handleNoiseEncrypted(routed) } @@ -198,8 +184,6 @@ class PacketProcessor(private val myPeerID: String) { * Handle announce message */ private suspend fun handleAnnounce(routed: RoutedPacket): Boolean { - val peerID = routed.peerID ?: "unknown" - Log.d(TAG, "Processing announce from ${formatPeerForLog(peerID)}") return delegate?.handleAnnounce(routed) ?: false } @@ -207,8 +191,6 @@ class PacketProcessor(private val myPeerID: String) { * Handle regular message */ private suspend fun handleMessage(routed: RoutedPacket) { - val peerID = routed.peerID ?: "unknown" - Log.d(TAG, "Processing message from ${formatPeerForLog(peerID)}") delegate?.handleMessage(routed) } @@ -216,8 +198,6 @@ class PacketProcessor(private val myPeerID: String) { * Handle leave message */ private suspend fun handleLeave(routed: RoutedPacket) { - val peerID = routed.peerID ?: "unknown" - Log.d(TAG, "Processing leave from ${formatPeerForLog(peerID)}") delegate?.handleLeave(routed) } @@ -225,12 +205,8 @@ class PacketProcessor(private val myPeerID: String) { * Handle message fragments */ private suspend fun handleFragment(routed: RoutedPacket) { - val peerID = routed.peerID ?: "unknown" - Log.d(TAG, "Processing fragment from ${formatPeerForLog(peerID)}") - val reassembledPacket = delegate?.handleFragment(routed.packet) if (reassembledPacket != null) { - Log.d(TAG, "Fragment reassembled, processing complete message") handleReceivedPacket( RoutedPacket( packet = reassembledPacket, @@ -248,8 +224,6 @@ class PacketProcessor(private val myPeerID: String) { * Handle REQUEST_SYNC packets (public, TTL=1) */ private suspend fun handleRequestSync(routed: RoutedPacket) { - val peerID = routed.peerID ?: "unknown" - Log.d(TAG, "Processing REQUEST_SYNC from ${formatPeerForLog(peerID)}") delegate?.handleRequestSync(routed) } @@ -298,8 +272,6 @@ class PacketProcessor(private val myPeerID: String) { // Cancel the main scope processorScope.cancel() - - Log.d(TAG, "PacketProcessor shutdown complete") } } diff --git a/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt b/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt index 45e668e8..5922ff5e 100644 --- a/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt @@ -48,7 +48,6 @@ class SecurityManager(private val encryptionService: EncryptionService, private fun validatePacket(packet: BitchatPacket, peerID: String): Boolean { // Skip validation for our own packets if (peerID == myPeerID) { - Log.d(TAG, "Skipping validation for our own packet") return false } @@ -83,15 +82,12 @@ class SecurityManager(private val encryptionService: EncryptionService, private packet.ttl >= com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS if (!isFreshAnnounce) { - Log.d(TAG, "Dropping duplicate packet: $messageID") return false } - Log.d(TAG, "Allowing duplicate ANNOUNCE from direct neighbor: $messageID") } // Enforce mandatory signature verification if (!verifyPacketSignature(packet, peerID)) { - Log.w(TAG, "Dropping packet from $peerID due to signature verification failure") return false } @@ -100,8 +96,7 @@ class SecurityManager(private val encryptionService: EncryptionService, private // later legitimate packet with the same timestamp and payload. processedMessages.add(messageID) messageTimestamps[messageID] = currentTime - - Log.d(TAG, "Packet validation passed for $peerID, messageID: $messageID") + return true } @@ -115,27 +110,24 @@ class SecurityManager(private val encryptionService: EncryptionService, private // Skip handshakes not addressed to us if (packet.recipientID?.toHexString() != myPeerID) { - Log.d(TAG, "Skipping handshake not addressed to us: $peerID") return false } - + // Skip our own handshake messages if (peerID == myPeerID) return false if (packet.payload.isEmpty()) { - Log.w(TAG, "Noise handshake packet has empty payload") + Log.d(TAG, "Noise handshake packet has empty payload") return false } - + // Prevent duplicate handshake processing val exchangeKey = "$peerID-${packet.payload.sliceArray(0 until minOf(16, packet.payload.size)).contentHashCode()}" - + if (processedKeyExchanges.contains(exchangeKey)) { - Log.d(TAG, "Already processed handshake: $exchangeKey") return false } - Log.d(TAG, "Processing Noise handshake from $peerID (${packet.payload.size} bytes)") - + try { // The session manager preserves an existing transport in a separate responder-candidate // flow and reports whether this exact frame completed authentication. Never infer that @@ -144,7 +136,6 @@ class SecurityManager(private val encryptionService: EncryptionService, private processedKeyExchanges.add(exchangeKey) if (result.response != null) { - Log.d(TAG, "Successfully processed Noise handshake from $peerID, sending response") // Send handshake response through delegate delegate?.sendHandshakeResponse(peerID, result.response) } @@ -162,7 +153,7 @@ class SecurityManager(private val encryptionService: EncryptionService, private return false } val isDirectIngress = packet.ttl == com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS - Log.d(TAG, "✅ Noise handshake completed with $peerID") + Log.i(TAG, "Noise handshake completed with $peerID") delegate?.onKeyExchangeCompleted( peerID = peerID, authenticatedRemoteStaticKey = authenticatedRemoteStaticKey, @@ -175,7 +166,7 @@ class SecurityManager(private val encryptionService: EncryptionService, private } catch (e: Exception) { - Log.e(TAG, "Failed to process Noise handshake from $peerID: ${e.message}") + Log.w(TAG, "Failed to process Noise handshake from $peerID: ${e.message}") return false } } @@ -315,7 +306,7 @@ class SecurityManager(private val encryptionService: EncryptionService, private // 1. Mandatory Signature Check if (packet.signature == null) { - Log.w(TAG, "❌ Signature check for $peerID: NO_SIGNATURE (packet type ${packet.type})") + Log.w(TAG, "Signature check for $peerID: NO_SIGNATURE (packet type ${packet.type})") return false } @@ -326,14 +317,14 @@ class SecurityManager(private val encryptionService: EncryptionService, private if (signingPublicKey == null) { // If we don't have a key (and it's not an announce), we can't verify. // For security, we must reject packets from unknown peers unless it's an announce. - Log.w(TAG, "❌ Signature check for $peerID: NO_SIGNING_KEY_AVAILABLE (packet type ${packet.type})") + Log.w(TAG, "Signature check for $peerID: NO_SIGNING_KEY_AVAILABLE (packet type ${packet.type})") return false } // 3. Get Canonical Data val packetDataForSigning = packet.toBinaryDataForSigning() if (packetDataForSigning == null) { - Log.w(TAG, "❌ Signature check for $peerID: ENCODING_ERROR (packet type ${packet.type})") + Log.w(TAG, "Signature check for $peerID: ENCODING_ERROR (packet type ${packet.type})") return false } @@ -346,15 +337,14 @@ class SecurityManager(private val encryptionService: EncryptionService, private ) if (isSignatureValid) { - // Log.v(TAG, "✅ Signature verified for $peerID (type ${packet.type})") return true } else { - Log.w(TAG, "❌ Signature INVALID for $peerID (type ${packet.type})") + Log.w(TAG, "Signature INVALID for $peerID (type ${packet.type})") return false } - + } catch (e: Exception) { - Log.e(TAG, "❌ Signature verification error for $peerID: ${e.message}") + Log.e(TAG, "Signature verification error for $peerID: ${e.message}") return false } } @@ -405,27 +395,23 @@ class SecurityManager(private val encryptionService: EncryptionService, private */ private fun cleanupOldData() { val cutoffTime = System.currentTimeMillis() - MESSAGE_TIMEOUT - var removedCount = 0 - + // Clean up old message timestamps and corresponding processed messages val messagesToRemove = messageTimestamps.entries.filter { (_, timestamp) -> timestamp < cutoffTime }.map { it.key } - + messagesToRemove.forEach { messageId -> messageTimestamps.remove(messageId) - if (processedMessages.remove(messageId)) { - removedCount++ - } + processedMessages.remove(messageId) } - + // Limit the size of processed messages set if (processedMessages.size > MAX_PROCESSED_MESSAGES) { val excess = processedMessages.size - MAX_PROCESSED_MESSAGES val toRemove = processedMessages.take(excess) processedMessages.removeAll(toRemove.toSet()) removeFromMessageTimestamps(toRemove) - removedCount += excess } // Limit the size of processed key exchanges set @@ -434,10 +420,6 @@ class SecurityManager(private val encryptionService: EncryptionService, private val toRemove = processedKeyExchanges.take(excess) processedKeyExchanges.removeAll(toRemove.toSet()) } - - if (removedCount > 0) { - Log.d(TAG, "Cleaned up $removedCount old processed messages") - } } /** diff --git a/app/src/main/java/com/bitchat/android/net/ArtiTorManager.kt b/app/src/main/java/com/bitchat/android/net/ArtiTorManager.kt index e3490576..d560c648 100644 --- a/app/src/main/java/com/bitchat/android/net/ArtiTorManager.kt +++ b/app/src/main/java/com/bitchat/android/net/ArtiTorManager.kt @@ -130,7 +130,6 @@ class ArtiTorManager private constructor() { val logListener = ArtiLogListener { logLine -> val text = logLine ?: return@ArtiLogListener val s = text - Log.i(TAG, "arti: $s") lastLogTime.set(System.currentTimeMillis()) _statusFlow.update { it.copy(lastLogLine = s) } handleArtiLogLine(s) @@ -198,10 +197,6 @@ class ArtiTorManager private constructor() { if (mode == s.mode && mode != TorMode.OFF && (lifecycleState == LifecycleState.STARTING || lifecycleState == LifecycleState.RUNNING) ) { - Log.i( - TAG, - "applyMode: already in progress/running mode=$mode, state=$lifecycleState; skip" - ) return } when (mode) { @@ -264,7 +259,6 @@ class ArtiTorManager private constructor() { private suspend fun startArti(application: Application, useDelay: Boolean = false) { try { stopArtiAndWait() - Log.i(TAG, "Starting Arti on port $currentSocksPort…") if (useDelay) { delay(RESTART_DELAY_MS) } @@ -296,10 +290,7 @@ class ArtiTorManager private constructor() { if (isBindError && bindRetryAttempts < MAX_RETRY_ATTEMPTS) { bindRetryAttempts++ currentSocksPort++ - Log.w( - TAG, - "Port bind failed (attempt $bindRetryAttempts/$MAX_RETRY_ATTEMPTS), retrying with port $currentSocksPort" - ) + Log.w(TAG, "Port bind failed (attempt $bindRetryAttempts/$MAX_RETRY_ATTEMPTS), retrying with port $currentSocksPort") socksAddr = InetSocketAddress("127.0.0.1", currentSocksPort) resetNetworkConnections() startArti(application, useDelay = false) @@ -347,7 +338,6 @@ class ArtiTorManager private constructor() { try { val proxy = artiProxy if (proxy != null) { - Log.i(TAG, "Stopping Arti…") try { proxy.stop() } catch (_: Throwable) { @@ -397,10 +387,7 @@ class ArtiTorManager private constructor() { if (currentMode == TorMode.ON) { val bootstrapPercent = _statusFlow.value.bootstrapPercent if (bootstrapPercent < 100) { - Log.w( - TAG, - "Inactivity detected (${timeSinceLastActivity}ms), restarting Arti" - ) + Log.w(TAG, "Inactivity detected (${timeSinceLastActivity}ms), restarting Arti") currentApplication?.let { app -> appScope.launch { restartArti(app) @@ -429,7 +416,6 @@ class ArtiTorManager private constructor() { delay(delayMs) val currentMode = _statusFlow.value.mode if (currentMode == TorMode.ON) { - Log.i(TAG, "Retrying Arti start (attempt $retryAttempts)") restartArti(application) } } @@ -465,7 +451,6 @@ class ArtiTorManager private constructor() { when { s.contains("AMEx: state changed to Initialized", ignoreCase = true) -> { if (currentLifecycle != LifecycleState.STARTING && currentLifecycle != LifecycleState.RUNNING) { - Log.w(TAG, "Ignoring stale 'Initialized' log (lifecycle: $currentLifecycle)") return } _statusFlow.update { it.copy(state = TorState.STARTING) } @@ -474,7 +459,6 @@ class ArtiTorManager private constructor() { s.contains("AMEx: state changed to Starting", ignoreCase = true) -> { if (currentLifecycle != LifecycleState.STARTING && currentLifecycle != LifecycleState.RUNNING) { - Log.w(TAG, "Ignoring stale 'Starting' log (lifecycle: $currentLifecycle)") return } _statusFlow.update { it.copy(state = TorState.STARTING) } @@ -486,7 +470,6 @@ class ArtiTorManager private constructor() { ignoreCase = true ) -> { if (currentLifecycle != LifecycleState.RUNNING) { - Log.w(TAG, "Ignoring bootstrap log (lifecycle: $currentLifecycle)") return } _statusFlow.update { @@ -502,7 +485,6 @@ class ArtiTorManager private constructor() { s.contains("We have found that guard [scrubbed] is usable.", ignoreCase = true) -> { if (currentLifecycle != LifecycleState.RUNNING) { - Log.w(TAG, "Ignoring guard discovery log (lifecycle: $currentLifecycle)") return } _statusFlow.update { @@ -517,7 +499,6 @@ class ArtiTorManager private constructor() { s.contains("AMEx: state changed to Stopping", ignoreCase = true) -> { if (currentLifecycle != LifecycleState.STOPPING) { - Log.w(TAG, "Ignoring stale 'Stopping' log (lifecycle: $currentLifecycle)") return } _statusFlow.update { @@ -530,10 +511,6 @@ class ArtiTorManager private constructor() { s.contains("AMEx: state changed to Stopped", ignoreCase = true) -> { if (currentLifecycle != LifecycleState.STOPPING && currentLifecycle != LifecycleState.STOPPED) { - Log.w( - TAG, - "Ignoring stale 'Stopped' log (lifecycle: $currentLifecycle, preventing state corruption)" - ) return } _statusFlow.update { 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 ecb25d45..909ca82b 100644 --- a/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt +++ b/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt @@ -88,7 +88,7 @@ class NoiseEncryptionService(private val context: Context) { if (loadedKeyPair != null) { staticIdentityPrivateKey = loadedKeyPair.first staticIdentityPublicKey = loadedKeyPair.second - Log.d(TAG, "Loaded existing static identity key: ${calculateFingerprint(staticIdentityPublicKey)}") + Log.d(TAG, "Identity loaded: ${calculateFingerprint(staticIdentityPublicKey).take(16)}") } else { // Generate new identity key pair val keyPair = generateKeyPair() @@ -97,7 +97,6 @@ class NoiseEncryptionService(private val context: Context) { // Save to secure storage identityStateManager.saveStaticKey(staticIdentityPrivateKey, staticIdentityPublicKey) - Log.d(TAG, "Generated and saved new static identity key") } // Load or create Ed25519 signing key (persistent across sessions) @@ -105,7 +104,6 @@ class NoiseEncryptionService(private val context: Context) { 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() @@ -114,7 +112,6 @@ class NoiseEncryptionService(private val context: Context) { // Save to secure storage identityStateManager.saveSigningKey(signingPrivateKey, signingPublicKey) - Log.d(TAG, "Generated and saved new Ed25519 signing key") } } @@ -163,7 +160,7 @@ 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") + Log.w(TAG, "Panic: clearing persistent identity and rotating in-memory keys") // 1. Clear storage identityStateManager.clearIdentityData() @@ -179,7 +176,6 @@ class NoiseEncryptionService(private val context: Context) { // 4. Re-initialize SessionManager with new keys initializeSessionManager() - Log.d(TAG, "✅ Identity cleared and keys rotated") } // MARK: - Handshake Management @@ -377,7 +373,7 @@ class NoiseEncryptionService(private val context: Context) { * Initiate rekey for a session (replaces old session with new handshake) */ fun initiateRekey(peerID: String): ByteArray? { - Log.d(TAG, "Initiating rekey for session with $peerID") + Log.d(TAG, "Rekeying session with $peerID") // Remove old session sessionManager.removeSession(peerID) @@ -437,7 +433,6 @@ class NoiseEncryptionService(private val context: Context) { // Calculate fingerprint for logging and callback val fingerprint = calculateFingerprint(remoteStaticKey) - Log.d(TAG, "Session established with $peerID, fingerprint: ${fingerprint.take(16)}...") // Notify about authentication onPeerAuthenticated?.invoke(peerID, fingerprint) diff --git a/app/src/main/java/com/bitchat/android/noise/NoiseSession.kt b/app/src/main/java/com/bitchat/android/noise/NoiseSession.kt index cb2654dd..1314d9b7 100644 --- a/app/src/main/java/com/bitchat/android/noise/NoiseSession.kt +++ b/app/src/main/java/com/bitchat/android/noise/NoiseSession.kt @@ -2,7 +2,6 @@ package com.bitchat.android.noise import android.util.Log import com.bitchat.android.noise.southernstorm.protocol.* -import com.bitchat.android.util.toHexString import java.security.SecureRandom @@ -123,7 +122,6 @@ class NoiseSession( } // Extract ciphertext (remaining bytes) val ciphertext = combinedPayload.copyOfRange(NONCE_SIZE_BYTES, combinedPayload.size) - Log.d(TAG, "Extracted nonce: $extractedNonce, ciphertext size: ${ciphertext.size}") return Pair(extractedNonce, ciphertext) } catch (e: Exception) { @@ -210,9 +208,7 @@ class NoiseSession( init { try { - // Validate static keys validateStaticKeys() - Log.d(TAG, "Created ${if (isInitiator) "initiator" else "responder"} session for $peerID") } catch (e: Exception) { state = NoiseSessionState.Failed(e) Log.e(TAG, "Failed to initialize Noise session: ${e.message}") @@ -237,8 +233,6 @@ class NoiseSession( if (localStaticPublicKey.all { it == 0.toByte() }) { throw IllegalArgumentException("Local static public key cannot be all zeros") } - - Log.d(TAG, "Static keys validated successfully - private: ${localStaticPrivateKey.size} bytes, public: ${localStaticPublicKey.size} bytes") } /** @@ -247,60 +241,24 @@ class NoiseSession( */ private fun initializeNoiseHandshake(role: Int) { try { - Log.d(TAG, "Creating HandshakeState with role: ${if (role == HandshakeState.INITIATOR) "INITIATOR" else "RESPONDER"}") - - // LOGGING: Track Android handshake initialization (matching iOS) - Log.d(TAG, "=== ANDROID NOISE SESSION - BEFORE HANDSHAKE INIT ===") - Log.d(TAG, "Creating NoiseHandshakeState for peer: $peerID") - Log.d(TAG, "Role: ${if (role == HandshakeState.INITIATOR) "INITIATOR" else "RESPONDER"}") - handshakeState = HandshakeState(PROTOCOL_NAME, role) - Log.d(TAG, "HandshakeState created successfully") - - Log.d(TAG, "=== ANDROID NOISE SESSION - AFTER HANDSHAKE INIT ===") - Log.d(TAG, "NoiseHandshakeState created and mixPreMessageKeys() completed") - + if (handshakeState?.needsLocalKeyPair() == true) { - Log.d(TAG, "Local static key pair is required for XX pattern") - val localKeyPair = handshakeState?.getLocalKeyPair() if (localKeyPair != null) { - // FIXED: Use the provided persistent identity keys with our local fork - // Our local fork properly supports setting pre-existing keys - Log.d(TAG, "Setting persistent static identity keys...") - localKeyPair.setPrivateKey(localStaticPrivateKey, 0) - + if (!localKeyPair.hasPrivateKey() || !localKeyPair.hasPublicKey()) { throw IllegalStateException("Failed to set static identity keys - local fork issue") } - - Log.d(TAG, "✓ Successfully set persistent static identity keys") - Log.d(TAG, "Algorithm: ${localKeyPair.dhName}") - Log.d(TAG, "Private key length: ${localKeyPair.privateKeyLength}") - Log.d(TAG, "Public key length: ${localKeyPair.publicKeyLength}") - - // Verify the keys were set correctly - val verifyPrivate = ByteArray(32) - val verifyPublic = ByteArray(32) - localKeyPair.getPrivateKey(verifyPrivate, 0) - localKeyPair.getPublicKey(verifyPublic, 0) - - Log.d(TAG, "Persistent identity public key: ${localStaticPublicKey.joinToString("") { "%02x".format(it) }}") - Log.d(TAG, "Set public key: ${verifyPublic.joinToString("") { "%02x".format(it) }}") - } else { throw IllegalStateException("HandshakeState returned null for local key pair") } - - } else { - Log.d(TAG, "Local static key pair not needed for this handshake pattern/role") } handshakeState?.start() - Log.d(TAG, "Handshake state started successfully with persistent identity keys") } catch (e: Exception) { - Log.e(TAG, "Exception during handshake initialization: ${e.message}", e) + Log.e(TAG, "Handshake init failed for $peerID: ${e.message}", e) throw e } } @@ -315,8 +273,6 @@ class NoiseSession( */ @Synchronized fun startHandshake(): ByteArray { - Log.d(TAG, "Starting noise XX handshake with $peerID as INITIATOR") - if (!isInitiator) { throw IllegalStateException("Only initiator can start handshake") } @@ -343,15 +299,13 @@ class NoiseSession( // Validate message size matches XX pattern expectations if (firstMessage.size != XX_MESSAGE_1_SIZE) { - Log.w(TAG, "Warning: XX message 1 size ${firstMessage.size} != expected $XX_MESSAGE_1_SIZE") + Log.w(TAG, "XX message 1 size ${firstMessage.size} != expected $XX_MESSAGE_1_SIZE") } - - val ePrefix = firstMessage.take(4).toByteArray().toHexString() - Log.d(TAG, "Sending XX handshake message 1 to $peerID (${firstMessage.size} bytes) e_prefix=$ePrefix currentPattern: $currentPattern") + return firstMessage } catch (e: Exception) { state = NoiseSessionState.Failed(e) - Log.e(TAG, "Failed to start handshake: ${e.message}") + Log.e(TAG, "Failed to start handshake with $peerID: ${e.message}") throw e } } @@ -362,9 +316,6 @@ class NoiseSession( */ @Synchronized fun processHandshakeMessage(message: ByteArray): ByteArray? { - val inputPrefix = message.take(4).toByteArray().toHexString() - Log.d(TAG, "Processing handshake message from $peerID (${message.size} bytes) prefix=$inputPrefix") - try { // Initialize as responder if receiving first message if (state == NoiseSessionState.Uninitialized && !isInitiator) { @@ -373,7 +324,6 @@ class NoiseSession( if (handshakeStartMs == null) { handshakeStartMs = System.currentTimeMillis() } - Log.d(TAG, "Initialized as RESPONDER for XX handshake with $peerID") } if (state != NoiseSessionState.Handshaking) { @@ -389,13 +339,10 @@ class NoiseSession( // Read the incoming message - the Noise library will handle validation val payloadLength = handshakeStateLocal.readMessage(message, 0, message.size, payloadBuffer, 0) currentPattern++ - val readPrefix = message.take(4).toByteArray().toHexString() - Log.d(TAG, "Read handshake message, payload length: $payloadLength prefix=$readPrefix currentPattern: $currentPattern") - + // Check what action the handshake state wants us to take next val action = handshakeStateLocal.getAction() - Log.d(TAG, "Handshake action after processing message: $action") - + return when (action) { HandshakeState.WRITE_MESSAGE -> { // Noise library says we need to send a response @@ -403,33 +350,22 @@ class NoiseSession( val responseLength = handshakeStateLocal.writeMessage(responseBuffer, 0, null, 0, 0) currentPattern++ val response = responseBuffer.copyOf(responseLength) - - Log.d(TAG, "Generated handshake response: ${response.size} bytes, action still: ${handshakeStateLocal.getAction()} currentPattern: $currentPattern") + completeHandshake() response } - + HandshakeState.SPLIT -> { // Handshake complete, split into transport keys completeHandshake() - Log.d(TAG, "SPLIT ✅ XX handshake completed with $peerID") null } - + HandshakeState.FAILED -> { throw Exception("Handshake failed - Noise library reported FAILED state") } - - HandshakeState.READ_MESSAGE -> { - // Noise library expects us to read another message - Log.d(TAG, "Handshake waiting for next message from $peerID") - null - } - - else -> { - Log.d(TAG, "Handshake action: $action - no immediate action needed") - null - } + + else -> null } } catch (e: Exception) { @@ -448,8 +384,6 @@ class NoiseSession( return } - Log.d(TAG, "Completing XX handshake with $peerID") - try { val activeHandshake = handshakeState ?: throw NoiseSessionError.HandshakeFailed @@ -466,7 +400,6 @@ class NoiseSession( throw NoiseSessionError.PeerIdentityMismatch(peerID, derivedPeerID) } remoteStaticPublicKey = authenticatedRemoteKey - Log.d(TAG, "Remote static public key is bound to $peerID") // Only a bound remote identity may derive transport ciphers. val cipherPair = activeHandshake.split() @@ -492,8 +425,7 @@ class NoiseSession( replayWindow = ByteArray(REPLAY_WINDOW_BYTES) state = NoiseSessionState.Established - Log.d(TAG, "Handshake completed with $peerID as isInitiator: $isInitiator - transport keys derived") - Log.d(TAG, "✅ XX handshake completed with $peerID") + Log.i(TAG, "Handshake established with $peerID (${if (isInitiator) "initiator" else "responder"})") } catch (e: Exception) { state = NoiseSessionState.Failed(e) Log.e(TAG, "Failed to complete handshake: ${e.message}") @@ -556,20 +488,13 @@ class NoiseSession( // Log high nonce values that might indicate issues if (currentNonce > HIGH_NONCE_WARNING_THRESHOLD) { - Log.w(TAG, "High nonce value detected: $currentNonce - consider rekeying") + Log.w(TAG, "High send nonce $currentNonce for $peerID - rekey recommended") } - - Log.d(TAG, "✅ ANDROID ENCRYPT: ${data.size} → ${combinedPayload.size} bytes (nonce: $currentNonce, ciphertextLength+TAG: ${ciphertextLength}) for $peerID (msg #$messagesSent, role: ${if (isInitiator) "INITIATOR" else "RESPONDER"})") + return combinedPayload - + } catch (e: Exception) { - Log.e(TAG, "Real encryption failed - exception: ${e.message}") - - // ENHANCED: Log cipher state for debugging - if (sendCipher != null) { - Log.e(TAG, "Send cipher state: ${sendCipher!!.javaClass.simpleName}") - } - + Log.e(TAG, "Encryption failed for $peerID: ${e.message}") throw SessionError.EncryptionFailed } } @@ -630,23 +555,13 @@ class NoiseSession( // Log high nonce values that might indicate issues if (extractedNonce > HIGH_NONCE_WARNING_THRESHOLD) { - Log.w(TAG, "High nonce value detected: $extractedNonce - consider rekeying") + Log.w(TAG, "High receive nonce $extractedNonce for $peerID - rekey recommended") } - val result = plaintext.copyOf(plaintextLength) - Log.d(TAG, "✅ ANDROID DECRYPT: ${combinedPayload.size} → ${result.size} bytes from $peerID (nonce: $extractedNonce, highest: $highestReceivedNonce, role: ${if (isInitiator) "INITIATOR" else "RESPONDER"})") - return result - + return plaintext.copyOf(plaintextLength) + } catch (e: Exception) { - Log.e(TAG, "Decryption failed - exception: ${e.message}") - - // ENHANCED: Log cipher state and session details for debugging - if (receiveCipher != null) { - Log.e(TAG, "Receive cipher state: ${receiveCipher!!.javaClass.simpleName}") - } - Log.e(TAG, "Session state: $state, highest received nonce: $highestReceivedNonce") - Log.e(TAG, "Input data size: ${combinedPayload.size} bytes") - + Log.w(TAG, "Decrypt failed for $peerID: ${e.message} (state=$state, highestNonce=$highestReceivedNonce)") throw SessionError.DecryptionFailed } } @@ -747,9 +662,7 @@ class NoiseSession( if (state !is NoiseSessionState.Failed) { state = NoiseSessionState.Failed(Exception("Session destroyed")) } - - Log.d(TAG, "Session destroyed for $peerID") - + } catch (e: Exception) { Log.w(TAG, "Error during session cleanup: ${e.message}") } diff --git a/app/src/main/java/com/bitchat/android/noise/NoiseSessionManager.kt b/app/src/main/java/com/bitchat/android/noise/NoiseSessionManager.kt index fa577ff3..c089255f 100644 --- a/app/src/main/java/com/bitchat/android/noise/NoiseSessionManager.kt +++ b/app/src/main/java/com/bitchat/android/noise/NoiseSessionManager.kt @@ -73,7 +73,6 @@ class NoiseSessionManager( fun addSession(peerID: String, session: NoiseSession) { val previous = sessions.put(peerID, session) if (previous != null && previous !== session) previous.destroy() - Log.d(TAG, "Added new session for $peerID") } /** @@ -91,7 +90,6 @@ class NoiseSessionManager( fun removeSession(peerID: String) { sessions.remove(peerID)?.destroy() responderCandidates.remove(peerID)?.destroy() - Log.d(TAG, "Removed session for $peerID") } /** @@ -99,15 +97,12 @@ class NoiseSessionManager( */ @Synchronized fun initiateHandshake(peerID: String, replaceEstablished: Boolean = false): ByteArray? { - Log.d(TAG, "initiateHandshake($peerID)") - val now = System.currentTimeMillis() val existing = getSession(peerID) if (existing != null) { when { existing.isEstablished() -> { if (!replaceEstablished) { - Log.d(TAG, "Handshake already established with $peerID, skipping initiate") return null } val candidate = createSession(peerID, isInitiator = true) @@ -123,10 +118,9 @@ class NoiseSessionManager( } existing.isHandshaking() -> { if (!isHandshakeStale(existing, now)) { - Log.d(TAG, "Handshake already in progress with $peerID, not restarting") return null } - Log.d(TAG, "Handshake with $peerID is stale; restarting") + Log.d(TAG, "Restarting stale handshake with $peerID") removeSession(peerID) } else -> { @@ -142,13 +136,10 @@ class NoiseSessionManager( localStaticPrivateKey = localStaticPrivateKey, localStaticPublicKey = localStaticPublicKey ) - Log.d(TAG, "Storing new INITIATOR session for $peerID") addSession(peerID, session) try { - val handshakeData = session.startHandshake() - Log.d(TAG, "Started handshake with $peerID as INITIATOR") - return handshakeData + return session.startHandshake() } catch (e: Exception) { if (sessions.remove(peerID, session)) session.destroy() throw e @@ -167,8 +158,6 @@ class NoiseSessionManager( peerID: String, message: ByteArray ): NoiseHandshakeProcessingResult { - Log.d(TAG, "processHandshakeMessage($peerID, ${message.size} bytes)") - var activeSession: NoiseSession? = null var isReplacementCandidate = false var establishedRemoteKey: ByteArray? = null @@ -182,19 +171,12 @@ class NoiseSessionManager( if (existingCandidate.isInitiatorRole()) { val shouldYield = localPeerID > peerID if (!shouldYield) { - Log.d( - TAG, - "Replacement handshake collision with $peerID; keeping initiator role" - ) return NoiseHandshakeProcessingResult( response = null, establishedNow = false ) } - Log.d( - TAG, - "Replacement handshake collision with $peerID; yielding to responder role" - ) + Log.d(TAG, "Replacement collision with $peerID; yielding to responder") } responderCandidates.remove(peerID, existingCandidate) existingCandidate.destroy() @@ -216,25 +198,19 @@ class NoiseSessionManager( ) { val shouldYield = localPeerID > peerID if (shouldYield) { - Log.d(TAG, "Handshake collision with $peerID; yielding to responder role") + Log.d(TAG, "Handshake collision with $peerID; yielding to responder") if (sessions.remove(peerID, session)) session.destroy() session = null } else { - Log.d(TAG, "Handshake collision with $peerID; keeping initiator role") return NoiseHandshakeProcessingResult(response = null, establishedNow = false) } } activeSession = when { session == null -> { - Log.d(TAG, "Creating new RESPONDER session for $peerID") createSession(peerID, isInitiator = false).also { sessions[peerID] = it } } session.isEstablished() -> { - Log.d( - TAG, - "Validating replacement handshake for $peerID while preserving active session" - ) isReplacementCandidate = true createSession(peerID, isInitiator = false).also { responderCandidates[peerID] = it @@ -276,7 +252,6 @@ class NoiseSessionManager( establishedRemoteKey = remoteStaticKey establishedSessionToken = sessionToken - Log.d(TAG, "✅ Session ESTABLISHED with bound identity $peerID") } } catch (e: Exception) { val session = activeSession @@ -368,9 +343,7 @@ class NoiseSessionManager( * Check if session is established with peer */ fun hasEstablishedSession(peerID: String): Boolean { - val hasSession = getSession(peerID)?.isEstablished() ?: false - Log.d(TAG, "hasEstablishedSession($peerID): $hasSession") - return hasSession + return getSession(peerID)?.isEstablished() ?: false } /** diff --git a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/HandshakeState.java b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/HandshakeState.java index 91fec78a..3780da88 100644 --- a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/HandshakeState.java +++ b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/HandshakeState.java @@ -22,8 +22,6 @@ package com.bitchat.android.noise.southernstorm.protocol; -import android.util.Log; - import java.security.NoSuchAlgorithmException; import java.util.Arrays; @@ -35,8 +33,6 @@ import javax.crypto.ShortBufferException; */ public class HandshakeState implements Destroyable { - private static final String TAG = "AndroidHandshake"; - private SymmetricState symmetric; private boolean isInitiator; private DHState localKeyPair; @@ -467,17 +463,6 @@ public class HandshakeState implements Destroyable { // Empty value for when the prologue is not supplied. private static final byte[] emptyPrologue = new byte [0]; - /** - * Converts a byte array to hex string for logging (matching iOS hex format) - */ - private static String bytesToHex(byte[] bytes) { - StringBuilder sb = new StringBuilder(); - for (byte b : bytes) { - sb.append(String.format("%02x", b)); - } - return sb.toString(); - } - /** * Starts the handshake running. * @@ -524,27 +509,18 @@ public class HandshakeState implements Destroyable { throw new IllegalStateException("Pre-shared key required"); } - // Log the symmetric state BEFORE any mixing operations (matching iOS) - Log.d(TAG, "=== ANDROID HANDSHAKE START - INITIAL STATE ==="); - Log.d(TAG, "Protocol: " + symmetric.getProtocolName()); - Log.d(TAG, "Role: " + (isInitiator ? "INITIATOR" : "RESPONDER")); - Log.d(TAG, "Initial symmetric hash: " + bytesToHex(symmetric.getHandshakeHash())); - // Hash the prologue value. - Log.d(TAG, "Mixing empty prologue"); if (prologue != null) symmetric.mixHash(prologue, 0, prologue.length); else symmetric.mixHash(emptyPrologue, 0, 0); - Log.d(TAG, "Hash after empty prologue: " + bytesToHex(symmetric.getHandshakeHash())); - + // Hash the pre-shared key into the chaining key and handshake hash. if (preSharedKey != null) symmetric.mixPreSharedKey(preSharedKey); - + // Mix the pre-supplied public keys into the handshake hash. if (isInitiator) { - Log.d(TAG, "XX pattern - no pre-message keys to mix"); if ((requirements & LOCAL_PREMSG) != 0) symmetric.mixPublicKey(localKeyPair); if ((requirements & FALLBACK_PREMSG) != 0) { @@ -557,7 +533,6 @@ public class HandshakeState implements Destroyable { if ((requirements & REMOTE_PREMSG) != 0) symmetric.mixPublicKey(remotePublicKey); } else { - Log.d(TAG, "XX pattern - no pre-message keys to mix"); if ((requirements & REMOTE_PREMSG) != 0) symmetric.mixPublicKey(remotePublicKey); if ((requirements & FALLBACK_PREMSG) != 0) { @@ -571,11 +546,6 @@ public class HandshakeState implements Destroyable { symmetric.mixPublicKey(localKeyPair); } - // Log final state after all initialization (matching iOS) - Log.d(TAG, "=== ANDROID HANDSHAKE START - FINAL STATE ==="); - Log.d(TAG, "Final symmetric hash after mixPreMessageKeys(): " + bytesToHex(symmetric.getHandshakeHash())); - Log.d(TAG, "==========================================="); - // The handshake has officially started - set the first action. if (isInitiator) action = WRITE_MESSAGE; diff --git a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/SymmetricState.java b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/SymmetricState.java index 0e70f9f3..4568d07d 100644 --- a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/SymmetricState.java +++ b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/SymmetricState.java @@ -22,8 +22,6 @@ package com.bitchat.android.noise.southernstorm.protocol; -import android.util.Log; - import java.io.UnsupportedEncodingException; import java.security.DigestException; import java.security.MessageDigest; @@ -37,9 +35,7 @@ import javax.crypto.ShortBufferException; * Symmetric state for helping manage a Noise handshake. */ class SymmetricState implements Destroyable { - - private static final String TAG = "AndroidSymmetric"; - + private String name; private CipherState cipher; private MessageDigest hash; @@ -47,17 +43,6 @@ class SymmetricState implements Destroyable { private byte[] h; private byte[] prev_h; - /** - * Converts a byte array to hex string for logging (matching iOS hex format) - */ - private static String bytesToHex(byte[] bytes) { - StringBuilder sb = new StringBuilder(); - for (byte b : bytes) { - sb.append(String.format("%02x", b)); - } - return sb.toString(); - } - /** * Constructs a new symmetric state object. * @@ -94,14 +79,6 @@ class SymmetricState implements Destroyable { } System.arraycopy(h, 0, ck, 0, hashLength); - - // LOGGING: Initial symmetric state after protocol name initialization (matching iOS) - Log.d(TAG, "=== ANDROID SYMMETRIC STATE INITIALIZED ==="); - Log.d(TAG, "Protocol: " + protocolName); - Log.d(TAG, "Initial hash (h): " + bytesToHex(h)); - Log.d(TAG, "Initial chaining key (ck): " + bytesToHex(ck)); - Log.d(TAG, "Hash length: " + h.length); - Log.d(TAG, "========================================="); } /** @@ -134,14 +111,6 @@ class SymmetricState implements Destroyable { */ public void mixKey(byte[] data, int offset, int length) { - // LOGGING: Before mixKey operation (matching iOS) - byte[] inputData = new byte[length]; - System.arraycopy(data, offset, inputData, 0, length); - Log.d(TAG, "*** Android mixKey() BEFORE ***"); - Log.d(TAG, "Input data (" + length + " bytes): " + bytesToHex(inputData)); - Log.d(TAG, "Current CK: " + bytesToHex(ck)); - Log.d(TAG, "Current Hash: " + bytesToHex(h)); - int keyLength = cipher.getKeyLength(); byte[] tempKey = new byte [keyLength]; try { @@ -150,12 +119,6 @@ class SymmetricState implements Destroyable { } finally { Noise.destroy(tempKey); } - - // LOGGING: After mixKey operation (matching iOS) - Log.d(TAG, "*** Android mixKey() AFTER ***"); - Log.d(TAG, "New CK: " + bytesToHex(ck)); - Log.d(TAG, "Hash unchanged: " + bytesToHex(h)); - Log.d(TAG, "Cipher now has key: " + (cipher.getMACLength() > 0)); } /** @@ -167,18 +130,7 @@ class SymmetricState implements Destroyable { */ public void mixHash(byte[] data, int offset, int length) { - // LOGGING: Before mixHash operation (matching iOS) - byte[] inputData = new byte[length]; - System.arraycopy(data, offset, inputData, 0, length); - Log.d(TAG, "*** Android mixHash() BEFORE ***"); - Log.d(TAG, "Input data (" + length + " bytes): " + bytesToHex(inputData)); - Log.d(TAG, "Current Hash: " + bytesToHex(h)); - hashTwo(h, 0, h.length, data, offset, length, h, 0, h.length); - - // LOGGING: After mixHash operation (matching iOS) - Log.d(TAG, "*** Android mixHash() AFTER ***"); - Log.d(TAG, "New Hash: " + bytesToHex(h)); } /** diff --git a/app/src/main/java/com/bitchat/android/nostr/LocationNotesManager.kt b/app/src/main/java/com/bitchat/android/nostr/LocationNotesManager.kt index 8873c271..2fa962ef 100644 --- a/app/src/main/java/com/bitchat/android/nostr/LocationNotesManager.kt +++ b/app/src/main/java/com/bitchat/android/nostr/LocationNotesManager.kt @@ -406,7 +406,7 @@ class LocationNotesManager private constructor() { val currentNotes = _notes.value ?: emptyList() _notes.value = (currentNotes + note).sortedByDescending { it.createdAt } - Log.d(TAG, "📥 Added note: ${note.displayName} - ${note.content.take(50)}") + Log.d(TAG, "Added note from ${note.displayName}") // Trim if exceeds max if (noteIDs.size > MAX_NOTES_IN_MEMORY) { diff --git a/app/src/main/java/com/bitchat/android/nostr/NostrProtocol.kt b/app/src/main/java/com/bitchat/android/nostr/NostrProtocol.kt index 501cf60c..4376d1e9 100644 --- a/app/src/main/java/com/bitchat/android/nostr/NostrProtocol.kt +++ b/app/src/main/java/com/bitchat/android/nostr/NostrProtocol.kt @@ -274,8 +274,6 @@ object NostrProtocol { giftWrap: NostrEvent, recipientPrivateKey: String ): NostrEvent? { - Log.d(TAG, "Unwrapping gift wrap; content prefix='${giftWrap.content.take(3)}' length=${giftWrap.content.length}") - return try { val decrypted = NostrCrypto.decryptNIP44( ciphertext = giftWrap.content, diff --git a/app/src/main/java/com/bitchat/android/nostr/NostrRelayManager.kt b/app/src/main/java/com/bitchat/android/nostr/NostrRelayManager.kt index d44e6e0b..f50d65cc 100644 --- a/app/src/main/java/com/bitchat/android/nostr/NostrRelayManager.kt +++ b/app/src/main/java/com/bitchat/android/nostr/NostrRelayManager.kt @@ -139,7 +139,7 @@ class NostrRelayManager private constructor() { return } geohashToRelays[geohash] = selected - Log.i(TAG, "🌐 Geohash $geohash using ${selected.size} relays: ${selected.joinToString()}") + Log.d(TAG, "Geohash $geohash using ${selected.size} relays") ensureConnectionsFor(selected) } catch (e: Exception) { Log.e(TAG, "Failed to ensure relays for $geohash: ${e.message}") @@ -166,7 +166,6 @@ class NostrRelayManager private constructor() { ): String { ensureGeohashRelaysConnected(geohash, nRelays, includeDefaults) val relayUrls = getRelaysForGeohash(geohash) - Log.d(TAG, "📡 Subscribing id=$id for geohash=$geohash on ${relayUrls.size} relays") return subscribe( filter = filter, id = id, @@ -191,7 +190,6 @@ class NostrRelayManager private constructor() { sendEvent(event, Companion.defaultRelays()) return } - Log.v(TAG, "📤 Sending event kind=${event.kind} to ${relayUrls.size} relays for geohash=$geohash") sendEvent(event, relayUrls) } @@ -229,7 +227,6 @@ class NostrRelayManager private constructor() { relaysList.addAll(defaultRelayUrls.map { Relay(it) }) _relays.value = relaysList.toList() updateConnectionStatus() - Log.d(TAG, "✅ NostrRelayManager initialized with ${relaysList.size} default relays") } catch (e: Exception) { Log.e(TAG, "Failed to initialize NostrRelayManager: ${e.message}", e) // Initialize with empty list as fallback @@ -242,7 +239,7 @@ class NostrRelayManager private constructor() { * Connect to all configured relays */ fun connect() { - Log.d(TAG, "🌐 Connecting to ${relaysList.size} Nostr relays") + Log.i(TAG, "Connecting to ${relaysList.size} Nostr relays") scope.launch { relaysList.forEach { relay -> @@ -260,7 +257,7 @@ class NostrRelayManager private constructor() { * Disconnect from all relays */ fun disconnect() { - Log.d(TAG, "Disconnecting from all relays") + Log.i(TAG, "Disconnecting from all Nostr relays") // Stop subscription validation stopSubscriptionValidation() @@ -318,9 +315,7 @@ class NostrRelayManager private constructor() { activeSubscriptions[id] = subscriptionInfo messageHandlers[id] = handler - - Log.d(TAG, "📡 Subscribing to Nostr filter id=$id ${filter.getDebugDescription()}") - + // Send subscription to appropriate relays sendSubscriptionToRelays(subscriptionInfo) @@ -333,10 +328,7 @@ class NostrRelayManager private constructor() { private fun sendSubscriptionToRelays(subscriptionInfo: SubscriptionInfo) { val request = NostrRequest.Subscribe(subscriptionInfo.id, listOf(subscriptionInfo.filter)) val message = gson.toJson(request, NostrRequest::class.java) - - // DEBUG: Log the actual serialized message format - Log.v(TAG, "🔍 DEBUG: Serialized subscription message: $message") - + scope.launch { val targetRelays = subscriptionInfo.targetRelayUrls?.toList() ?: connections.keys.toList() @@ -349,21 +341,17 @@ class NostrRelayManager private constructor() { // Track subscription for this relay val currentSubs = subscriptions[relayUrl] ?: emptySet() subscriptions[relayUrl] = currentSubs + subscriptionInfo.id - - Log.v(TAG, "✅ Subscription '${subscriptionInfo.id}' sent to relay: $relayUrl") } else { - Log.w(TAG, "❌ Failed to send subscription to $relayUrl: WebSocket send failed") + Log.w(TAG, "Failed to send subscription to $relayUrl: WebSocket send failed") } } catch (e: Exception) { - Log.e(TAG, "❌ Failed to send subscription to $relayUrl: ${e.message}") + Log.e(TAG, "Failed to send subscription to $relayUrl: ${e.message}") } - } else { - Log.v(TAG, "⏳ Relay $relayUrl not connected, subscription will be sent on reconnection") } } - + if (connections.isEmpty()) { - Log.w(TAG, "⚠️ No relay connections available for subscription, will retry on reconnection") + Log.w(TAG, "No relay connections available for subscription, will retry on reconnection") } } } @@ -377,12 +365,10 @@ class NostrRelayManager private constructor() { messageHandlers.remove(id) if (subscriptionInfo == null) { - Log.w(TAG, "⚠️ Attempted to unsubscribe from unknown subscription: $id") + Log.w(TAG, "Attempted to unsubscribe from unknown subscription: $id") return } - - Log.d(TAG, "🚫 Unsubscribing from subscription: $id") - + val request = NostrRequest.Close(id) val message = gson.toJson(request, NostrRequest::class.java) @@ -393,7 +379,6 @@ class NostrRelayManager private constructor() { try { webSocket.send(message) subscriptions[relayUrl] = currentSubs - id - Log.v(TAG, "Unsubscribed '$id' from relay: $relayUrl") } catch (e: Exception) { Log.e(TAG, "Failed to unsubscribe from $relayUrl: ${e.message}") } @@ -445,8 +430,6 @@ class NostrRelayManager private constructor() { * Useful for ensuring subscription consistency after network issues */ fun reestablishAllSubscriptions() { - Log.d(TAG, "🔄 Force re-establishing all ${activeSubscriptions.size} active subscriptions") - scope.launch { connections.forEach { (relayUrl, webSocket) -> restoreSubscriptionsForRelay(relayUrl, webSocket) @@ -473,7 +456,7 @@ class NostrRelayManager private constructor() { messageQueue.clear() } - Log.i(TAG, "🧹 Cleared all Nostr subscriptions and routing caches") + Log.i(TAG, "Cleared all Nostr subscriptions and routing caches") } catch (e: Exception) { Log.e(TAG, "Failed to clear subscriptions: ${e.message}") } @@ -498,7 +481,6 @@ class NostrRelayManager private constructor() { */ fun clearDeduplicationCache() { eventDeduplicator.clear() - Log.i(TAG, "🧹 Cleared event deduplication cache") } /** @@ -570,7 +552,7 @@ class NostrRelayManager private constructor() { try { val report = validateSubscriptionConsistency() if (!report.isConsistent && report.connectedRelayCount > 0) { - Log.w(TAG, "⚠️ Subscription inconsistencies detected: ${report.inconsistencies}") + Log.w(TAG, "Subscription inconsistencies detected: ${report.inconsistencies}") // Auto-repair: re-establish subscriptions for relays with missing ones connections.forEach { (relayUrl, webSocket) -> @@ -582,7 +564,7 @@ class NostrRelayManager private constructor() { val missingSubs = expectedSubs - currentSubs if (missingSubs.isNotEmpty()) { - Log.i(TAG, "🔧 Auto-repairing ${missingSubs.size} missing subscriptions for $relayUrl") + Log.i(TAG, "Auto-repairing ${missingSubs.size} missing subscriptions for $relayUrl") restoreSubscriptionsForRelay(relayUrl, webSocket) } } @@ -592,8 +574,6 @@ class NostrRelayManager private constructor() { } } } - - Log.d(TAG, "🔄 Started periodic subscription validation (${SUBSCRIPTION_VALIDATION_INTERVAL / 1000}s interval)") } /** @@ -602,7 +582,6 @@ class NostrRelayManager private constructor() { private fun stopSubscriptionValidation() { subscriptionValidationJob?.cancel() subscriptionValidationJob = null - Log.v(TAG, "⏹️ Stopped subscription validation") } // MARK: - Private Methods @@ -612,9 +591,7 @@ class NostrRelayManager private constructor() { if (connections.containsKey(urlString)) { return } - - Log.v(TAG, "Attempting to connect to Nostr relay: $urlString") - + try { val request = Request.Builder() .url(urlString) @@ -624,7 +601,7 @@ class NostrRelayManager private constructor() { connections[urlString] = webSocket } catch (e: Exception) { - Log.e(TAG, "❌ Failed to create WebSocket connection to $urlString: ${e.message}") + Log.e(TAG, "Failed to create WebSocket connection to $urlString: ${e.message}") handleDisconnection(urlString, e) } } @@ -633,9 +610,7 @@ class NostrRelayManager private constructor() { try { val request = NostrRequest.Event(event) val message = gson.toJson(request, NostrRequest::class.java) - - Log.v(TAG, "📤 Sending Nostr event (kind: ${event.kind}) to relay: $relayUrl") - + val success = webSocket.send(message) if (success) { // Update relay stats @@ -643,10 +618,10 @@ class NostrRelayManager private constructor() { relay?.messagesSent = (relay?.messagesSent ?: 0) + 1 updateRelaysList() } else { - Log.e(TAG, "❌ Failed to send event to $relayUrl: WebSocket send failed") + Log.e(TAG, "Failed to send event to $relayUrl: WebSocket send failed") } } catch (e: Exception) { - Log.e(TAG, "❌ Failed to send event to $relayUrl: ${e.message}") + Log.e(TAG, "Failed to send event to $relayUrl: ${e.message}") } } @@ -671,24 +646,13 @@ class NostrRelayManager private constructor() { activeSubscriptions[response.subscriptionId]?.let { subInfo -> val matches = try { subInfo.filter.matches(response.event) } catch (e: Exception) { true } if (!matches) { - Log.v(TAG, "🚫 Dropping event ${response.event.id.take(16)}... not matching filter for sub=${response.subscriptionId}") // Do NOT call deduplicator here to allow the correct subscription to process it later return } } - + // DEDUPLICATION: Check if we've already processed this event - val wasProcessed = eventDeduplicator.processEvent(response.event) { event -> - // Only log non-gift-wrap events to reduce noise - if (event.kind != NostrKind.GIFT_WRAP) { - val originGeo = activeSubscriptions[response.subscriptionId]?.originGeohash - if (originGeo != null) { - Log.v(TAG, "📥 Processing event (kind=${event.kind}) from relay=$relayUrl geo=$originGeo sub=${response.subscriptionId}") - } else { - Log.v(TAG, "📥 Processing event (kind=${event.kind}) from relay=$relayUrl sub=${response.subscriptionId}") - } - } - + eventDeduplicator.processEvent(response.event) { event -> // Call handler for new events only val handler = messageHandlers[response.subscriptionId] if (handler != null) { @@ -696,35 +660,29 @@ class NostrRelayManager private constructor() { handler(event) } } else { - Log.w(TAG, "⚠️ No handler for subscription ${response.subscriptionId}") + Log.d(TAG, "No handler for subscription ${response.subscriptionId}") } } - - if (!wasProcessed) { - //Log.v(TAG, "🔄 Duplicate event ${response.event.id.take(16)}... from relay: $relayUrl") - } } - + is NostrResponse.EndOfStoredEvents -> { - Log.v(TAG, "End of stored events for subscription: ${response.subscriptionId}") + // No action needed } - + is NostrResponse.Ok -> { val wasGiftWrap = pendingGiftWrapIDs.remove(response.eventId) - if (response.accepted) { - Log.d(TAG, "✅ Event accepted id=${response.eventId.take(16)}... by relay: $relayUrl") - } else { + if (!response.accepted) { val level = if (wasGiftWrap) Log.WARN else Log.ERROR - Log.println(level, TAG, "📮 Event ${response.eventId.take(16)}... rejected by relay: ${response.message ?: "no reason"}") + Log.println(level, TAG, "Event rejected by relay $relayUrl: ${response.message ?: "no reason"}") } } - + is NostrResponse.Notice -> { - Log.i(TAG, "📢 Notice from $relayUrl: ${response.message}") + Log.d(TAG, "Notice from $relayUrl: ${response.message}") } - + is NostrResponse.Unknown -> { - Log.v(TAG, "Unknown message type from $relayUrl: ${response.raw}") + Log.d(TAG, "Unknown message type from $relayUrl") } } } catch (e: Exception) { @@ -820,29 +778,26 @@ class NostrRelayManager private constructor() { } if (subscriptionsToRestore.isEmpty()) { - Log.v(TAG, "🔄 No subscriptions to restore for relay: $relayUrl") return } - - Log.d(TAG, "🔄 Restoring ${subscriptionsToRestore.size} subscriptions for relay: $relayUrl") - + + Log.d(TAG, "Restoring ${subscriptionsToRestore.size} subscriptions for relay: $relayUrl") + subscriptionsToRestore.forEach { subscriptionInfo -> try { val request = NostrRequest.Subscribe(subscriptionInfo.id, listOf(subscriptionInfo.filter)) val message = gson.toJson(request, NostrRequest::class.java) - + val success = webSocket.send(message) if (success) { // Track subscription for this relay val currentSubs = subscriptions[relayUrl] ?: emptySet() subscriptions[relayUrl] = currentSubs + subscriptionInfo.id - - Log.v(TAG, "✅ Restored subscription '${subscriptionInfo.id}' to relay: $relayUrl") } else { - Log.w(TAG, "❌ Failed to restore subscription '${subscriptionInfo.id}' to $relayUrl: WebSocket send failed") + Log.w(TAG, "Failed to restore subscription '${subscriptionInfo.id}' to $relayUrl: WebSocket send failed") } } catch (e: Exception) { - Log.e(TAG, "❌ Failed to restore subscription '${subscriptionInfo.id}' to $relayUrl: ${e.message}") + Log.e(TAG, "Failed to restore subscription '${subscriptionInfo.id}' to $relayUrl: ${e.message}") } } } @@ -853,7 +808,7 @@ class NostrRelayManager private constructor() { private inner class RelayWebSocketListener(private val relayUrl: String) : WebSocketListener() { override fun onOpen(webSocket: WebSocket, response: Response) { - Log.d(TAG, "✅ Connected to Nostr relay: $relayUrl") + Log.i(TAG, "Connected to Nostr relay: $relayUrl") updateRelayStatus(relayUrl, true) // Restore all active subscriptions for this relay @@ -876,17 +831,17 @@ class NostrRelayManager private constructor() { } override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { - Log.d(TAG, "WebSocket closing for $relayUrl: $code $reason") + // Server-initiated close; onClosed will follow } - + override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { - Log.d(TAG, "WebSocket closed for $relayUrl: $code $reason") + Log.i(TAG, "Disconnected from Nostr relay $relayUrl: $code $reason") val error = Exception("WebSocket closed: $code $reason") handleDisconnection(relayUrl, error) } - + override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { - Log.e(TAG, "❌ WebSocket failure for $relayUrl: ${t.message}") + Log.e(TAG, "WebSocket failure for $relayUrl: ${t.message}") handleDisconnection(relayUrl, t) } } diff --git a/app/src/main/java/com/bitchat/android/nostr/NostrTestManager.kt b/app/src/main/java/com/bitchat/android/nostr/NostrTestManager.kt index 66f3b095..76f42249 100644 --- a/app/src/main/java/com/bitchat/android/nostr/NostrTestManager.kt +++ b/app/src/main/java/com/bitchat/android/nostr/NostrTestManager.kt @@ -165,7 +165,7 @@ class NostrTestManager(private val context: Context) { // Subscribe to private messages (won't receive any in test, but tests the subscription mechanism) nostrClient.subscribeToPrivateMessages { content, senderNpub, timestamp -> - Log.d(TAG, "📥 Received test private message from $senderNpub: $content") + Log.d(TAG, "Received test private message (${content.length} chars)") messageReceived = true } diff --git a/app/src/main/java/com/bitchat/android/nostr/NostrTransport.kt b/app/src/main/java/com/bitchat/android/nostr/NostrTransport.kt index 1ce0fc9f..f39324fd 100644 --- a/app/src/main/java/com/bitchat/android/nostr/NostrTransport.kt +++ b/app/src/main/java/com/bitchat/android/nostr/NostrTransport.kt @@ -68,20 +68,18 @@ class NostrTransport( return@launch } - Log.d(TAG, "NostrTransport: preparing PM to ${recipientNostrPubkey.take(16)}... for peerID ${to.take(8)}... id=${messageID.take(8)}...") - val recipientHex = ContactIdentityResolver.nostrPubkeyHex(recipientNostrPubkey) if (recipientHex == null) { Log.e(TAG, "NostrTransport: recipient key is not a valid Nostr pubkey") return@launch } - + val recipientPeerIDForEmbed = try { com.bitchat.android.favorites.FavoritesPersistenceService.shared .findPeerIDForNostrPubkey(recipientNostrPubkey) } catch (_: Exception) { null } if (recipientPeerIDForEmbed.isNullOrBlank()) { - Log.e(TAG, "NostrTransport: no peerID stored for recipient npub; cannot embed PM. npub=${recipientNostrPubkey.take(16)}...") + Log.e(TAG, "NostrTransport: no peerID stored for recipient npub; cannot embed PM") return@launch } val embedded = NostrEmbeddedBitChat.encodePMForNostr( @@ -104,11 +102,10 @@ class NostrTransport( ) giftWraps.forEach { event -> - Log.d(TAG, "NostrTransport: sending PM giftWrap id=${event.id.take(16)}...") NostrRelayManager.registerPendingGiftWrap(event.id) NostrRelayManager.getInstance(context).sendEvent(event) } - + } catch (e: Exception) { Log.e(TAG, "Failed to send private message via Nostr: ${e.message}") } @@ -153,8 +150,6 @@ class NostrTransport( return@launch } - Log.d(TAG, "NostrTransport: preparing READ ack for id=${item.receipt.originalMessageID.take(8)}... to ${recipientNostrPubkey.take(16)}...") - val recipientHex = ContactIdentityResolver.nostrPubkeyHex(recipientNostrPubkey) if (recipientHex == null) { scheduleNextReadAck() @@ -181,11 +176,10 @@ class NostrTransport( ) giftWraps.forEach { event -> - Log.d(TAG, "NostrTransport: sending READ ack giftWrap id=${event.id.take(16)}...") NostrRelayManager.registerPendingGiftWrap(event.id) NostrRelayManager.getInstance(context).sendEvent(event) } - + scheduleNextReadAck() } catch (e: Exception) { @@ -220,9 +214,7 @@ class NostrTransport( } val content = FavoriteControlMessage.encode(isFavorite, senderIdentity.npub) - - Log.d(TAG, "NostrTransport: preparing FAVORITE($isFavorite) to ${recipientNostrPubkey.take(16)}...") - + val recipientHex = ContactIdentityResolver.nostrPubkeyHex(recipientNostrPubkey) if (recipientHex == null) { return@launch @@ -247,11 +239,10 @@ class NostrTransport( ) giftWraps.forEach { event -> - Log.d(TAG, "NostrTransport: sending favorite giftWrap id=${event.id.take(16)}...") NostrRelayManager.registerPendingGiftWrap(event.id) NostrRelayManager.getInstance(context).sendEvent(event) } - + } catch (e: Exception) { Log.e(TAG, "Failed to send favorite notification via Nostr: ${e.message}") } @@ -274,13 +265,11 @@ class NostrTransport( return@launch } - Log.d(TAG, "NostrTransport: preparing DELIVERED ack for id=${messageID.take(8)}... to ${recipientNostrPubkey.take(16)}...") - val recipientHex = ContactIdentityResolver.nostrPubkeyHex(recipientNostrPubkey) if (recipientHex == null) { return@launch } - + val ack = NostrEmbeddedBitChat.encodeAckForNostr( type = NoisePayloadType.DELIVERED, messageID = messageID, @@ -300,11 +289,10 @@ class NostrTransport( ) giftWraps.forEach { event -> - Log.d(TAG, "NostrTransport: sending DELIVERED ack giftWrap id=${event.id.take(16)}...") NostrRelayManager.registerPendingGiftWrap(event.id) NostrRelayManager.getInstance(context).sendEvent(event) } - + } catch (e: Exception) { Log.e(TAG, "Failed to send delivery ack via Nostr: ${e.message}") } @@ -320,8 +308,6 @@ class NostrTransport( ) { transportScope.launch { try { - Log.d(TAG, "GeoDM: send DELIVERED -> recip=${toRecipientHex.take(8)}... mid=${messageID.take(8)}... from=${fromIdentity.publicKeyHex.take(8)}...") - val embedded = NostrEmbeddedBitChat.encodeAckForNostrNoRecipient( type = NoisePayloadType.DELIVERED, messageID = messageID, @@ -355,8 +341,6 @@ class NostrTransport( ) { transportScope.launch { try { - Log.d(TAG, "GeoDM: send READ -> recip=${toRecipientHex.take(8)}... mid=${messageID.take(8)}... from=${fromIdentity.publicKeyHex.take(8)}...") - val embedded = NostrEmbeddedBitChat.encodeAckForNostrNoRecipient( type = NoisePayloadType.READ_RECEIPT, messageID = messageID, @@ -414,11 +398,6 @@ class NostrTransport( try { if (toRecipientHex.isEmpty()) return@launch - Log.d( - TAG, - "GeoDM: send PM -> recip=${toRecipientHex.take(8)}... mid=${messageID.take(8)}... from=${fromIdentity.publicKeyHex.take(8)}... geohash=$geohash" - ) - // Build embedded BitChat packet without recipient peer ID val embedded = NostrEmbeddedBitChat.encodePMForNostrNoRecipient( content = content, @@ -436,7 +415,6 @@ class NostrTransport( ) giftWraps.forEach { event -> - Log.d(TAG, "NostrTransport: sending geohash PM giftWrap id=${event.id.take(16)}...") NostrRelayManager.registerPendingGiftWrap(event.id) NostrRelayManager.getInstance(context).sendEvent(event) } 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 d6c9d7e2..ec8c064a 100644 --- a/app/src/main/java/com/bitchat/android/ui/MediaSendingManager.kt +++ b/app/src/main/java/com/bitchat/android/ui/MediaSendingManager.kt @@ -99,13 +99,12 @@ class MediaSendingManager( val filePacket = withContext(mediaWorkDispatcher) { val file = java.io.File(filePath) if (!file.exists()) { - Log.e(TAG, "❌ File does not exist: $filePath") + Log.e(TAG, "Voice note file does not exist") return@withContext null } - Log.d(TAG, "📁 File exists: size=${file.length()} bytes, name=${file.name}") if (file.length() > MAX_FILE_SIZE) { - Log.e(TAG, "❌ File too large: ${file.length()} bytes (max: $MAX_FILE_SIZE)") + Log.e(TAG, "File too large: ${file.length()} bytes (max: $MAX_FILE_SIZE)") return@withContext null } @@ -143,16 +142,14 @@ class MediaSendingManager( ) { try { val filePacket = withContext(mediaWorkDispatcher) { - Log.d(TAG, "🔄 Starting image send: $filePath") val file = java.io.File(filePath) if (!file.exists()) { - Log.e(TAG, "❌ File does not exist: $filePath") + Log.e(TAG, "Image file does not exist") return@withContext null } - Log.d(TAG, "📁 File exists: size=${file.length()} bytes, name=${file.name}") if (file.length() > MAX_FILE_SIZE) { - Log.e(TAG, "❌ File too large: ${file.length()} bytes (max: $MAX_FILE_SIZE)") + Log.e(TAG, "File too large: ${file.length()} bytes (max: $MAX_FILE_SIZE)") return@withContext null } @@ -170,10 +167,7 @@ class MediaSendingManager( sendPublicFile(channelOrNull, filePacket, filePath, BitchatMessageType.Image) } } catch (e: Exception) { - Log.e(TAG, "❌ CRITICAL: Image send failed completely", e) - Log.e(TAG, "❌ Image path: $filePath") - Log.e(TAG, "❌ Error details: ${e.message}") - Log.e(TAG, "❌ Error type: ${e.javaClass.simpleName}") + Log.e(TAG, "Image send failed: ${e.message}", e) } } @@ -193,16 +187,14 @@ class MediaSendingManager( ) { try { val filePacket = withContext(mediaWorkDispatcher) { - Log.d(TAG, "🔄 Starting file send: $filePath") val file = java.io.File(filePath) if (!file.exists()) { - Log.e(TAG, "❌ File does not exist: $filePath") + Log.e(TAG, "File does not exist") return@withContext null } - Log.d(TAG, "📁 File exists: size=${file.length()} bytes, name=${file.name}") if (file.length() > MAX_FILE_SIZE) { - Log.e(TAG, "❌ File too large: ${file.length()} bytes (max: $MAX_FILE_SIZE)") + Log.e(TAG, "File too large: ${file.length()} bytes (max: $MAX_FILE_SIZE)") return@withContext null } @@ -212,7 +204,6 @@ class MediaSendingManager( } catch (_: Exception) { "application/octet-stream" } - Log.d(TAG, "🏷️ MIME type: $mimeType") // Try to preserve the original file name if our copier prefixed it earlier val originalName = run { @@ -228,7 +219,6 @@ class MediaSendingManager( ?: base stripped + ext } - Log.d(TAG, "📝 Original filename: $originalName") BitchatFilePacket( fileName = originalName, @@ -237,7 +227,6 @@ class MediaSendingManager( content = file.readBytes() ) } ?: return - Log.d(TAG, "📦 Created file packet successfully") val messageType = when { filePacket.mimeType.lowercase().startsWith("image/") -> BitchatMessageType.Image @@ -251,10 +240,7 @@ class MediaSendingManager( sendPublicFile(channelOrNull, filePacket, filePath, messageType) } } catch (e: Exception) { - Log.e(TAG, "❌ CRITICAL: File send failed completely", e) - Log.e(TAG, "❌ File path: $filePath") - Log.e(TAG, "❌ Error details: ${e.message}") - Log.e(TAG, "❌ Error type: ${e.javaClass.simpleName}") + Log.e(TAG, "File send failed: ${e.message}", e) } } @@ -269,17 +255,14 @@ class MediaSendingManager( ) { val payload = withContext(mediaWorkDispatcher) { filePacket.encode() } ?: run { - Log.e(TAG, "❌ Failed to encode file packet for private send") + Log.e(TAG, "Failed to encode file packet for private send") return } - Log.d(TAG, "🔒 Encoded private packet: ${payload.size} bytes") - val (transferId, contentHash) = withContext(mediaWorkDispatcher) { - sha256Hex(payload) to sha256Hex(filePacket.content) + val transferId = withContext(mediaWorkDispatcher) { + sha256Hex(payload) } - Log.d(TAG, "📤 FILE_TRANSFER send (private): name='${filePacket.fileName}', size=${filePacket.fileSize}, mime='${filePacket.mimeType}', sha256=$contentHash, to=${toPeerID.take(8)} transferId=${transferId.take(16)}…") - val pending = PendingAutomaticPrivateMedia( requestId = UUID.randomUUID().toString(), peerID = toPeerID, @@ -465,7 +448,7 @@ class MediaSendingManager( PrivateMediaPreparation.NeedsHandshake -> { ensureAutomaticPendingTimeout(pending) - Log.i(TAG, "Private media needs a Noise handshake; retaining first-send intent") + Log.d(TAG, "Private media needs a Noise handshake; retaining first-send intent") try { meshService.initiateNoiseHandshake(pending.peerID) } catch (e: Exception) { @@ -475,7 +458,7 @@ class MediaSendingManager( PrivateMediaPreparation.AwaitingPeerState -> { ensureAutomaticPendingTimeout(pending) - Log.i(TAG, "Private media is waiting for authenticated peer state; first-send intent retained") + Log.d(TAG, "Private media is waiting for authenticated peer state; first-send intent retained") } is PrivateMediaPreparation.Rejected -> { @@ -614,7 +597,6 @@ class MediaSendingManager( ) return } - Log.d(TAG, "✅ Private media committed using ${preparation.transfer.wireMode}") } /** @@ -628,16 +610,13 @@ class MediaSendingManager( ) { val payload = withContext(mediaWorkDispatcher) { filePacket.encode() } ?: run { - Log.e(TAG, "❌ Failed to encode file packet for broadcast send") + Log.e(TAG, "Failed to encode file packet for broadcast send") return } - Log.d(TAG, "🔓 Encoded broadcast packet: ${payload.size} bytes") - val (transferId, contentHash) = withContext(mediaWorkDispatcher) { - sha256Hex(payload) to sha256Hex(filePacket.content) + val transferId = withContext(mediaWorkDispatcher) { + sha256Hex(payload) } - - Log.d(TAG, "📤 FILE_TRANSFER send (broadcast): name='${filePacket.fileName}', size=${filePacket.fileSize}, mime='${filePacket.mimeType}', sha256=$contentHash, transferId=${transferId.take(16)}…") val message = BitchatMessage( id = java.util.UUID.randomUUID().toString().uppercase(), // Generate unique ID for each message @@ -667,11 +646,9 @@ class MediaSendingManager( com.bitchat.android.model.DeliveryStatus.PartiallyDelivered(0, 100) ) - Log.d(TAG, "📤 Calling meshService.sendFileBroadcast") withContext(mediaWorkDispatcher) { meshService.sendFileBroadcast(filePacket) } - Log.d(TAG, "✅ File broadcast completed successfully") } /** diff --git a/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareMeshService.kt b/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareMeshService.kt index eab371e7..d6cbf981 100644 --- a/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareMeshService.kt +++ b/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareMeshService.kt @@ -199,10 +199,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor registerProvisionalWifiClaim(pid, claim) if (!meshCore.initiateNoiseHandshakeOnLink(pid, relay, linkID)) { provisionalWifiClaims.remove(pid, claim) - Log.w( - TAG, - "Could not send Noise challenge on exact Wi-Fi link for ${pid.take(8)}" - ) + Log.w(TAG, "Could not send Noise challenge on Wi-Fi link for ${pid.take(8)}") } } } @@ -255,16 +252,13 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor * Broadcasts raw bytes to currently connected peer. */ private fun broadcastRaw(bytes: ByteArray) { - var sent = 0 connectionTracker.peerSockets.forEach { (pid, sock) -> try { sock.write(bytes) - sent++ } catch (e: IOException) { Log.e(TAG, "TX: write failed to ${pid.take(8)}: ${e.message}") } } - Log.i(TAG, "TX: broadcast via Wi-Fi Aware to $sent peers (bytes=${bytes.size})") } // TransportLayer implementation @@ -282,22 +276,17 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor * Broadcasts routed packet to currently connected peers. */ private fun broadcastPacket(routed: RoutedPacket) { - Log.d(TAG, "TX: packet type=${routed.packet.type} broadcast (ttl=${routed.packet.ttl})") - val packet = routed.packet if (packet.senderID.toHexString() == myPeerID && !packet.route.isNullOrEmpty()) { val firstHop = packet.route!![0].toHexString() if (sendRoutedPacketToPeer(firstHop, routed)) { - Log.d(TAG, "TX: source-routed packet sent only to first Wi-Fi hop ${firstHop.take(8)}") return } - Log.w(TAG, "TX: first Wi-Fi source-route hop ${firstHop.take(8)} unavailable; falling back to broadcast") } val recipientId = packet.recipientID?.toHexString() if (recipientId != null && !packet.recipientID.contentEquals(SpecialRecipients.BROADCAST)) { if (sendRoutedPacketToPeer(recipientId, routed)) { - Log.d(TAG, "TX: addressed packet sent directly to Wi-Fi peer ${recipientId.take(8)}") return } } @@ -321,7 +310,6 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor private fun sendRoutedPacketToPeer(peerID: String, routed: RoutedPacket): Boolean { if (connectionTracker.getSocketForPeer(peerID) == null) { - Log.w(TAG, "TX: no socket for ${peerID.take(8)}") return false } return fragmentingSender.send(routed, "Wi-Fi Aware peer ${peerID.take(8)}") { single -> @@ -339,12 +327,11 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor val data = packet.toBinaryData() ?: return false val sock = connectionTracker.getSocketForPeer(peerID) if (sock == null) { - Log.w(TAG, "TX: no socket for ${peerID.take(8)}") + Log.d(TAG, "TX: no socket for ${peerID.take(8)}") return false } try { sock.write(data) - Log.d(TAG, "TX: packet type=${packet.type} to ${peerID.take(8)} (bytes=${data.size})") return true } catch (e: IOException) { Log.e(TAG, "TX: write to ${peerID.take(8)} failed: ${e.message}") @@ -409,7 +396,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor return } wifiAwareSession = session - Log.i(TAG, "Wi-Fi Aware attached; starting publish & subscribe (peerID=$myPeerID)") + Log.i(TAG, "Wi-Fi Aware attached; starting publish & subscribe") // PUBLISH (server role) session.publish( @@ -424,7 +411,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor return } publishSession = pub - Log.d(TAG, "PUBLISH: onPublishStarted()") + Log.d(TAG, "Wi-Fi Aware publish started") try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(com.bitchat.android.ui.debug.DebugMessage.SystemMessage("Wi-Fi Aware Publish Started")) } catch (_: Exception) {} } override fun onServiceDiscovered( @@ -438,12 +425,10 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor if (peerId.isNotBlank()) { rememberDiscoveredPeer(peerId) publishHandles[peerId] = peerHandle - Log.i(TAG, "PUBLISH: Discovered subscriber '$peerId' via Aware") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { offerServerPathIfAppropriate(peerId, peerHandle, "publish discovery") } } - Log.d(TAG, "PUBLISH: onServiceDiscovered ssi='${peerId.take(16)}' len=${serviceSpecificInfo.size}") } @RequiresApi(Build.VERSION_CODES.Q) @@ -466,18 +451,16 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor rememberDiscoveredPeer(subscriberId) publishHandles[subscriberId] = peerHandle } - Log.i(TAG, "PUBLISH: Received discovery ping from subscriber '$subscriberId'") handleSubscriberPing(publishSession!!, peerHandle) } override fun onSessionTerminated() { if (!isCurrentSession(generation)) return - Log.e(TAG, "PUBLISH: onSessionTerminated()") publishSession = null val shouldRestart = isActive && com.bitchat.android.wifiaware.WifiAwareController.enabled.value + Log.w(TAG, "Wi-Fi Aware publish session terminated (restart=$shouldRestart)") handleUnexpectedStop(generation) if (shouldRestart) { - Log.i(TAG, "PUBLISH: Scheduling Wi-Fi Aware restart") com.bitchat.android.wifiaware.WifiAwareController.restartIfStillEnabled(2000) } } @@ -498,7 +481,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor return } subscribeSession = sub - Log.d(TAG, "SUBSCRIBE: onSubscribeStarted()") + Log.d(TAG, "Wi-Fi Aware subscribe started") try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(com.bitchat.android.ui.debug.DebugMessage.SystemMessage("Wi-Fi Aware Subscribe Started")) } catch (_: Exception) {} } override fun onServiceDiscovered( @@ -528,12 +511,11 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor override fun onSessionTerminated() { if (!isCurrentSession(generation)) return - Log.e(TAG, "SUBSCRIBE: onSessionTerminated()") subscribeSession = null val shouldRestart = isActive && com.bitchat.android.wifiaware.WifiAwareController.enabled.value + Log.w(TAG, "Wi-Fi Aware subscribe session terminated (restart=$shouldRestart)") handleUnexpectedStop(generation) if (shouldRestart) { - Log.i(TAG, "SUBSCRIBE: Scheduling Wi-Fi Aware restart") com.bitchat.android.wifiaware.WifiAwareController.restartIfStillEnabled(2000) } } @@ -552,7 +534,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor override fun onAwareSessionTerminated() { if (!isCurrentSession(generation)) return - Log.e(TAG, "Aware Session Terminated unexpectedly") + Log.e(TAG, "Wi-Fi Aware session terminated unexpectedly") wifiAwareSession = null val shouldRestart = com.bitchat.android.wifiaware.WifiAwareController.enabled.value handleUnexpectedStop(generation) @@ -679,7 +661,6 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor if (peerId.isBlank() || peerId == myPeerID || !amIServerFor(peerId)) return if (!connectionTracker.isConnectionAttemptAllowed(peerId)) return - Log.d(TAG, "PUBLISH: offering server path to ${peerId.take(8)} after $reason") handleSubscriberPing(pubSession, peerHandle) } @@ -691,7 +672,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor if ((now - lastRefresh) < DISCOVERY_SESSION_REFRESH_MIN_INTERVAL_MS) return false if (!lastDiscoveryRefreshAt.compareAndSet(lastRefresh, now)) return false - Log.i(TAG, "Maintenance: refreshing Wi-Fi Aware discovery sessions ($reason)") + Log.i(TAG, "Refreshing Wi-Fi Aware discovery sessions ($reason)") handleUnexpectedStop(sessionGeneration.get()) return true } @@ -701,7 +682,6 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor */ private fun startPeriodicConnectionMaintenance() { serviceScope.launch { - Log.d(TAG, "Starting periodic connection maintenance loop") while (isActive) { try { delay(15_000) // Check every 15 seconds @@ -719,7 +699,6 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor handleToPeerId.entries.removeIf { it.value in staleIds } staleIds.forEach { subscribeHandles.remove(it) } staleIds.forEach { publishHandles.remove(it) } - Log.d(TAG, "Maintenance: pruned ${staleIds.size} stale discovery entries") } // 1. Identify peers that are discovered (recently seen) but not currently connected @@ -745,7 +724,6 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor } if (!connectionTracker.isConnectionAttemptAllowed(peerId)) continue if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - Log.i(TAG, "Maintenance: offering Wi-Fi Aware server path to ${peerId.take(8)}") offerServerPathIfAppropriate(peerId, handle, "maintenance") attemptedReconnect = true } @@ -763,7 +741,6 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor // Check tracker policy if (!connectionTracker.isConnectionAttemptAllowed(peerId)) continue - Log.i(TAG, "Maintenance: attempting Wi-Fi Aware reconnect to ${peerId.take(8)}") sendSubscribePing(peerId, handle, "maintenance") attemptedReconnect = true } @@ -795,9 +772,8 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor val msgId = (System.nanoTime() and 0x7fffffff).toInt() try { subscribeSession?.sendMessage(peerHandle, msgId, myPeerID.toByteArray()) - Log.d(TAG, "SUBSCRIBE: sent $reason ping to '${peerId.take(16)}' (msgId=$msgId)") } catch (e: Exception) { - Log.w(TAG, "Failed to send $reason ping to ${peerId.take(8)}: ${e.message}") + Log.d(TAG, "Failed to send $reason ping to ${peerId.take(8)}: ${e.message}") } } @@ -809,7 +785,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor val handle = subscribeHandles[peerId] if (handle == null) { - Log.i(TAG, "CLIENT: role reversal queued for ${peerId.take(8)} until subscribe handle is available") + Log.d(TAG, "CLIENT: role reversal queued for ${peerId.take(8)} until subscribe handle is available") return } @@ -817,7 +793,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor val payload = "$ROLE_REVERSAL_PREFIX$myPeerID".toByteArray() try { subscribeSession?.sendMessage(handle, msgId, payload) - Log.i(TAG, "CLIENT: requested Wi-Fi Aware role reversal with ${peerId.take(8)} (msgId=$msgId)") + Log.d(TAG, "CLIENT: requested role reversal with ${peerId.take(8)}") } catch (e: Exception) { Log.w(TAG, "CLIENT: failed to request role reversal with ${peerId.take(8)}: ${e.message}") } @@ -830,9 +806,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor val shouldReverse = failures >= CLIENT_ROLE_REVERSAL_FAILURES if (shouldReverse) { clientSocketFailures.remove(peerId) - Log.i(TAG, "CLIENT: ${peerId.take(8)} failed $failures client socket attempts; requesting role reversal") - } else { - Log.d(TAG, "CLIENT: ${peerId.take(8)} failed client socket attempt $failures/$CLIENT_ROLE_REVERSAL_FAILURES; retrying same role") + Log.d(TAG, "CLIENT: ${peerId.take(8)} failed $failures client socket attempts; requesting role reversal") } return shouldReverse } @@ -843,7 +817,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor discoveredTimestamps[requesterId] = System.currentTimeMillis() forcedClientPeers.add(requesterId) forcedServerPeers.remove(requesterId) - Log.i(TAG, "PUBLISH: role reversal requested by ${requesterId.take(8)}; switching to client role") + Log.i(TAG, "Role reversal requested by ${requesterId.take(8)}; switching to client role") subscribeHandles[requesterId]?.let { handle -> sendSubscribePing(requesterId, handle, "role-reversal") @@ -865,16 +839,12 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor if (!amIServerFor(peerId)) return if (connectionTracker.isConnected(peerId)) { - Log.v(TAG, "↪ already connected to $peerId, skipping serve") return } if (connectionTracker.hasOpenServerSocket(peerId)) { - Log.v(TAG, "↪ already serving $peerId, skipping") return } if (connectionTracker.hasPendingDataPathRequest(peerId)) { - val pending = connectionTracker.pendingDataPathPeerIds(peerId).joinToString(", ") { it.take(8) } - Log.d(TAG, "SERVER: deferring serve for ${peerId.take(8)}; pending Aware data path(s): $pending") return } if (!connectionTracker.addPendingConnection(peerId)) { @@ -895,8 +865,6 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor connectionTracker.addServerSocket(peerId, ss) val port = ss.localPort - Log.d(TAG, "SERVER: listening for ${peerId.take(8)} on ${ss.localSocketAddress}") - val spec = WifiAwareNetworkSpecifier.Builder(pubSession, peerHandle) .setPskPassphrase(PSK) .setPort(port) @@ -914,7 +882,6 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor private val acceptStarted = AtomicBoolean(false) override fun onAvailable(network: Network) { - Log.i(TAG, "SERVER: onAvailable() - Aware network is ready for ${peerId.take(8)}") // Only accept once per network request if (!acceptStarted.compareAndSet(false, true)) return // Offload the blocking accept() off the callback thread so we never stall @@ -923,10 +890,9 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor try { try { ss.soTimeout = ACCEPT_TIMEOUT_MS } catch (_: Exception) {} val client = ss.accept() - Log.i(TAG, "SERVER: Accepted raw TCP connection from ${peerId.take(8)}") try { network.bindSocket(client) } catch (e: Exception) { Log.w(TAG, "Server bindSocket EPERM: ${e.message}") } client.keepAlive = true - Log.i(TAG, "SERVER: Bound and established TCP with ${peerId.take(8)} addr=${client.inetAddress?.hostAddress}") + Log.i(TAG, "Connected to ${peerId.take(8)} (server)") val synced = SyncedSocket(client) activeSocket = synced connectionTracker.onClientConnected(peerId, synced) @@ -943,14 +909,11 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor // Kick off Noise handshake for this logical peer if (myPeerID < peerId) { meshCore.initiateNoiseHandshake(peerId) - Log.i(TAG, "SERVER: Initiating Noise handshake to ${peerId.take(8)}") } // Ensure fast presence even before handshake settles serviceScope.launch { delay(150); sendBroadcastAnnounce() } } catch (ioe: IOException) { - if (ss.isClosed || !isActive) { - Log.d(TAG, "SERVER: accept stopped for ${peerId.take(8)} after socket cleanup") - } else { + if (!ss.isClosed && isActive) { Log.e(TAG, "SERVER: accept failed for ${peerId.take(8)}", ioe) handleNetworkFailure(peerId) } @@ -959,18 +922,17 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor } override fun onUnavailable() { - Log.e(TAG, "SERVER: onUnavailable() - Failed to acquire Aware network for ${peerId.take(8)} (timeout or refused)") + Log.e(TAG, "SERVER: failed to acquire Aware network for ${peerId.take(8)}") handleNetworkFailure(peerId) } override fun onLost(network: Network) { handlePeerDisconnection(peerId, activeSocket) - Log.i(TAG, "SERVER: WiFi Aware network lost for ${peerId.take(8)}") + Log.i(TAG, "Disconnected from ${peerId.take(8)} (server: network lost)") } } connectionTracker.addNetworkCallback(peerId, cb) - Log.i(TAG, "SERVER: [Calling requestNetwork] for ${peerId.take(8)} with port $port") try { // use requestNetwork with a timeout to trigger onUnavailable if it fails cm.requestNetwork(req, cb, NETWORK_REQUEST_TIMEOUT_MS) @@ -983,10 +945,9 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor val readyPayload = buildServerReadyPayload(port) Handler(Looper.getMainLooper()).post { try { - val sent = pubSession.sendMessage(peerHandle, readyId, readyPayload) - Log.d(TAG, "PUBLISH: server-ready sent=$sent (msgId=$readyId, port=$port)") + pubSession.sendMessage(peerHandle, readyId, readyPayload) } catch (e: Exception) { - Log.e(TAG, "PUBLISH: Exception sending server-ready to $peerHandle", e) + Log.e(TAG, "PUBLISH: failed to send server-ready to ${peerId.take(8)}", e) } } } @@ -1054,15 +1015,12 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor sock.tcpNoDelay = true sock.keepAlive = true sock.connect(java.net.InetSocketAddress(scopedAddr, port), CLIENT_CONNECT_TIMEOUT_MS) - if (attempt > 1) { - Log.i(TAG, "CLIENT: socket connect succeeded for ${peerId.take(8)} on attempt $attempt") - } return sock } catch (e: IOException) { lastFailure = e try { sock?.close() } catch (_: Exception) { } if (attempt < CLIENT_SOCKET_ATTEMPTS) { - Log.w(TAG, "CLIENT: socket attempt $attempt/$CLIENT_SOCKET_ATTEMPTS failed for ${peerId.take(8)}: ${e.message}; retrying") + Log.d(TAG, "CLIENT: socket attempt $attempt/$CLIENT_SOCKET_ATTEMPTS failed for ${peerId.take(8)}: ${e.message}") } } } @@ -1096,16 +1054,12 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor val mappedPeerId = handleToPeerId[peerHandle]?.takeIf { it.isNotBlank() } val peerId = advertisedPeerId ?: mappedPeerId if (peerId == null) { - Log.w(TAG, "SUBSCRIBE: dropped server-ready with no peer mapping and no peer ID payload (payload=${payload.size}B)") return null } handleToPeerId[peerHandle] = peerId subscribeHandles[peerId] = peerHandle rememberDiscoveredPeer(peerId) - if (advertisedPeerId != null && mappedPeerId != null && advertisedPeerId != mappedPeerId) { - Log.d(TAG, "SUBSCRIBE: server-ready remapped handle ${mappedPeerId.take(8)} -> ${advertisedPeerId.take(8)}") - } return peerId } @@ -1118,7 +1072,6 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor payload: ByteArray ) { if (payload.size < Int.SIZE_BYTES) { - Log.w(TAG, "handleServerReady called with invalid payload size=${payload.size}, dropping") return } @@ -1126,17 +1079,14 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor if (peerId == myPeerID) return if (amIServerFor(peerId)) return if (connectionTracker.peerSockets.containsKey(peerId)) { - Log.v(TAG, "↪ already client-connected to $peerId, skipping") return } val cancelledServerOffers = connectionTracker.cancelPendingServerDataPaths(peerId) if (cancelledServerOffers.isNotEmpty()) { val cancelled = cancelledServerOffers.joinToString(", ") { it.take(8) } - Log.i(TAG, "CLIENT: preempted pending server offer(s) for $cancelled to connect ${peerId.take(8)}") + Log.d(TAG, "CLIENT: preempted pending server offer(s) for $cancelled to connect ${peerId.take(8)}") } if (connectionTracker.hasPendingDataPathRequest(peerId)) { - val pending = connectionTracker.pendingDataPathPeerIds(peerId).joinToString(", ") { it.take(8) } - Log.d(TAG, "CLIENT: deferring server-ready for ${peerId.take(8)}; pending Aware data path(s): $pending") return } if (!connectionTracker.addPendingConnection(peerId)) { @@ -1144,10 +1094,9 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor } val port = ByteBuffer.wrap(payload, 0, Int.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).int - Log.i(TAG, "CLIENT: Received server-ready from ${peerId.take(8)} on port $port (payload=${payload.size}B). Requesting network...") val subSession = subscribeSession ?: run { - Log.w(TAG, "CLIENT: subscribe session missing for server-ready from ${peerId.take(8)}") + Log.d(TAG, "CLIENT: subscribe session missing for server-ready from ${peerId.take(8)}") connectionTracker.removePendingConnection(peerId) return } @@ -1164,12 +1113,11 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor private val connectStarted = AtomicBoolean(false) override fun onAvailable(network: Network) { - Log.i(TAG, "CLIENT: onAvailable() - Aware network is ready for ${peerId.take(8)}") // Do not bind process for Aware; use per-socket binding instead } - + override fun onUnavailable() { - Log.e(TAG, "CLIENT: onUnavailable() - Failed to acquire Aware network for ${peerId.take(8)}") + Log.e(TAG, "CLIENT: failed to acquire Aware network for ${peerId.take(8)}") if (shouldRequestRoleReversalAfterClientFailure(peerId)) { requestRoleReversal(peerId, allowForcedClientOverride = true) } @@ -1183,7 +1131,6 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor val connectPort = if (info.port > 0) info.port else port // onCapabilitiesChanged can fire multiple times; only connect once if (!connectStarted.compareAndSet(false, true)) return - Log.i(TAG, "CLIENT: onCapabilitiesChanged() - Peer IPv6 discovered: $addr port=$connectPort") val lp = cm.getLinkProperties(network) val iface = lp?.interfaceName @@ -1203,7 +1150,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor } val sock = connectAwareClientSocket(network, scopedAddr, connectPort, peerId) - Log.i(TAG, "CLIENT: TCP connected to ${peerId.take(8)} at $scopedAddr:$connectPort") + Log.i(TAG, "Connected to ${peerId.take(8)} (client)") val synced = SyncedSocket(sock) activeSocket = synced @@ -1217,7 +1164,6 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor // Kick off Noise handshake for this logical peer if (myPeerID < peerId) { meshCore.initiateNoiseHandshake(peerId) - Log.i(TAG, "CLIENT: Initiating Noise handshake to ${peerId.take(8)}") } // Ensure fast presence even before handshake settles serviceScope.launch { delay(150); sendBroadcastAnnounce() } @@ -1232,12 +1178,11 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor } override fun onLost(network: Network) { handlePeerDisconnection(peerId, activeSocket) - Log.i(TAG, "CLIENT: WiFi Aware network lost for ${peerId.take(8)}") + Log.i(TAG, "Disconnected from ${peerId.take(8)} (client: network lost)") } } connectionTracker.addNetworkCallback(peerId, cb) - Log.i(TAG, "CLIENT: [Calling requestNetwork] for ${peerId.take(8)}") try { cm.requestNetwork(req, cb, NETWORK_REQUEST_TIMEOUT_MS) } catch (e: Exception) { @@ -1305,10 +1250,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor ingressLinkID ) ) { - Log.w( - TAG, - "Ignoring unsolicited or cross-link Noise promotion for ${canonicalPeerId.take(8)}" - ) + Log.w(TAG, "Ignoring unsolicited or cross-link Noise promotion for ${canonicalPeerId.take(8)}") return } provisionalWifiClaims.remove(canonicalPeerId, expectedClaim) @@ -1332,18 +1274,12 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor return } if (existingCanonical != provisionalPeerId) { - Log.w( - TAG, - "Refusing authenticated Wi-Fi rebind ${existingCanonical.take(8)} -> ${canonicalPeerId.take(8)} on an existing alias" - ) + Log.w(TAG, "Refusing authenticated Wi-Fi rebind ${existingCanonical.take(8)} -> ${canonicalPeerId.take(8)} on existing alias") return } if (!connectionTracker.rebindPeerIdIfCurrent(provisionalPeerId, canonicalPeerId, link.transport)) { - Log.w( - TAG, - "Ignoring Noise link promotion for ${canonicalPeerId.take(8)}: provisional socket changed before rebind" - ) + Log.w(TAG, "Ignoring Noise link promotion for ${canonicalPeerId.take(8)}: provisional socket changed") return } authenticatedWifiLinks[canonicalPeerId] = @@ -1362,10 +1298,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor try { meshCore.setDirectConnection(canonicalPeerId, true) } catch (_: Exception) { } try { meshCore.gossipSyncManager.scheduleInitialSyncToPeer(canonicalPeerId, 1_000) } catch (_: Exception) { } - Log.i( - TAG, - "Noise-authenticated Wi-Fi peer ${provisionalPeerId.take(8)} -> ${canonicalPeerId.take(8)} on exact ingress link" - ) + Log.i(TAG, "Noise-authenticated Wi-Fi peer ${provisionalPeerId.take(8)} -> ${canonicalPeerId.take(8)}") } /** @@ -1396,16 +1329,12 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor // The socket's discovery identity remains provisional until Noise proves possession // of the claimed static key on this link. A canonical self-signed announcement is // only TOFU and cannot safely rebind/remove transport state on its own. - Log.w( - TAG, - "RX: deferred Wi-Fi peer rebind ${logicalPeerId.take(8)} -> ${senderPeerHex.take(8)} pending Noise proof" - ) + Log.d(TAG, "RX: deferred Wi-Fi peer rebind ${logicalPeerId.take(8)} -> ${senderPeerHex.take(8)} pending Noise proof") } - - // Route the packet: + + // Route the packet: // - peerID = Originator (who signed it) // - relayAddress = Neighbor (who sent it to us over this socket) - Log.d(TAG, "RX: packet type=${pkt.type} from ${senderPeerHex.take(8)} via ${logicalPeerId.take(8)} (bytes=${raw.size})") meshCore.processIncoming(pkt, senderPeerHex, logicalPeerId, ingressLinkID) } @@ -1413,7 +1342,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor clearProvisionalWifiClaimsForLink(logicalPeerId, ingressLinkID) // Breaking out of the loop means the socket is dead or service is stopping. - Log.i(TAG, "Socket loop terminated for ${logicalPeerId.take(8)} removing peer.") + Log.i(TAG, "Disconnected from ${logicalPeerId.take(8)} (socket closed)") handlePeerDisconnection(logicalPeerId, socket) socket.close() } @@ -1427,8 +1356,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor delay(WIFI_AUTHENTICATION_TIMEOUT_MS) if (provisionalWifiClaims.remove(peerID, claim)) { Log.d(TAG, "Expired provisional Wi-Fi authentication claim for ${peerID.take(8)}") - } - } + } } } private fun clearProvisionalWifiClaimsForLink(relayAddress: String, linkID: String) { @@ -1442,7 +1370,6 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor private fun handleNetworkFailure(peerId: String) { serviceScope.launch { - Log.d(TAG, "Network failure cleanup for: $peerId") if (!connectionTracker.isConnected(peerId)) { val canonicalPeerId = connectionTracker.canonicalPeerId(peerId) connectionTracker.disconnect(peerId) @@ -1450,8 +1377,6 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor if (canonicalPeerId != peerId) { meshCore.removePeer(peerId) } - } else { - Log.d(TAG, "Network failure ignored for $peerId - another socket is active") } } } @@ -1462,7 +1387,6 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor val currentSocket = connectionTracker.getSocketForPeer(initialId) val canonicalPeerId = connectionTracker.canonicalPeerId(initialId) if (currentSocket === socket) { - Log.d(TAG, "Cleaning up peer: $canonicalPeerId (active socket)") connectionTracker.disconnect(initialId) meshCore.removePeer(canonicalPeerId) if (canonicalPeerId != initialId) { @@ -1470,16 +1394,13 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor } } else if (socket == null && currentSocket == null) { // Fallback: If we don't have a specific socket context but we are already disconnected, ensure cleanup - Log.d(TAG, "Cleaning up peer: $initialId (no active socket)") connectionTracker.disconnect(initialId) meshCore.removePeer(canonicalPeerId) if (canonicalPeerId != initialId) { meshCore.removePeer(initialId) } - } else { - Log.d(TAG, "Ignored disconnection for $initialId - socket replaced or inactive") - // Do not remove peer/session, as a new socket has likely taken over } + // Else: socket replaced or inactive; do not remove peer/session, as a new socket has likely taken over } } diff --git a/docs/security-review-jul-27.md b/docs/security-review-jul-27.md new file mode 100644 index 00000000..ed7ce672 --- /dev/null +++ b/docs/security-review-jul-27.md @@ -0,0 +1,182 @@ +# Bitchat Android — Security Review + +**Commit:** 92d07b22 (worktree: `~/.opencode/worktrees/bitchat-security-review`) +**Scope:** Exploitable bugs, privacy violations, tracking opportunities, DoS vectors. +**Method:** Read-only static review of `crypto/`, `noise/`, `identity/`, `mesh/`, `protocol/`, `net/`, `service/`, `nostr/`, `geohash/`, `ui/`, `features/`, manifest & build config. All findings verified against source. + +--- + +## CRITICAL + +### C1. Noise session key material (DH shared secrets, chaining keys) written to logcat — ships in release +`noise/southernstorm/protocol/SymmetricState.java:135-159` (also 98-104, 168-182) +`mixKey()` logs the raw X25519 shared secret ("Input data"), current and new chaining keys in hex. `proguard-rules.pro` has **no** `assumenosideeffects` for `android.util.Log`, so this reaches the release APK. Anyone with the logcat transcript of a handshake can recompute `split()` outputs and fully decrypt the transport session. **Release-blocking.** + +### C2. Remote decompression bomb — pre-auth memory exhaustion via ~30-byte BLE write +`protocol/BinaryProtocol.kt:429-454`, `protocol/CompressionUtil.kt:75-118` +For compressed v2 packets the decoder reads a 4-byte attacker-controlled `originalSize` and immediately does `ByteArray(originalSize)` — up to 2 GiB per packet. The ratio guard is skipped when `compressedSize == 0` and is 50,000:1 anyway (real deflate max ≈ 1032:1). Runs inline on the GATT callback path; any nearby unauthenticated BLE peer can OOM/crash the foreground service with tiny writes. + +--- + +## HIGH + +### H1. Replay-window bit shift direction is wrong — Noise transport replay protection broken +`noise/NoiseSession.kt:69-106` +Bit indexing is LSB-first, but the window-shift code right-shifts the byte string. A replayed captured frame (nonce 0) tests a zero bit and is accepted; only the single most-recent nonce is reliably blocked. A passive BLE observer can replay DMs, ACKs, and receipts. + +### H2. Announcement signing key not bound to Noise key — "first announce wins" peer-ID impersonation +`mesh/AnnouncementIdentityValidator.kt:13-36`, `mesh/SecurityManager.kt:282-313`, `mesh/MessageHandler.kt:265-299` +An attacker can replay a victim's Noise public key (→ victim's peerID) inside an announcement signed with the *attacker's* Ed25519 key. On fresh installs or for never-authenticated peers the forged binding wins, and the genuine victim's later announce is rejected as "key replacement". + +### H3. Decrypted private message content written to logcat +`mesh/MessageHandler.kt:88` (also 125; `nostr/NostrProtocol.kt:277`) +`Log.d` logs the first 30 chars of every decrypted E2E private message plus sender peerID. Not stripped in release. Defeats E2E guarantees for any logcat reader. Related: `ui/DataManager.kt:161-196` dumps the full favorite/fingerprint social graph at startup (Low). + +### H4. E2E-encrypted DM content exposed in system notifications without lock-screen redaction +`ui/NotificationManager.kt:224-246` +Decrypted DM bodies (mesh + Nostr) are posted with full text in `BigTextStyle`/`InboxStyle`; no `VISIBILITY_SECRET` / lockscreen visibility set. Content renders on lock screen and to any notification-listener app; no opt-out. + +### H5. No signature verification on incoming public Nostr events — relay can forge any user +`nostr/GeohashMessageHandler.kt:44-105`, `nostr/LocationNotesManager.kt:358-414`, `nostr/NostrRelayManager.kt:663-706` +`NostrEvent.isValidSignature()` (exists, `NostrEvent.kt:171`) is never called for kind 20000/20001/1. A malicious/compromised relay (auto-selected from a daily third-party list, see M9) can inject events with arbitrary pubkeys — impersonating any user in any geohash channel, spoofing participants, caching attacker-chosen nicknames, registering DM aliases. + +### H6. Automatic delivery/read receipts over Nostr = online-presence & location oracle +`nostr/NostrDirectMessageHandler.kt:134-138,165-175`, `nostr/NostrTransport.kt:316-384` +Any decryptable gift-wrapped DM triggers an automatic signed DELIVERED ack. For geohash DMs the ack uses the public deterministic per-geohash identity, so anyone can probe a target's known geohash pubkey and confirm the device is online *now*, and chart activity patterns. No setting gates this. + +### H7. Building-precision geohash published publicly and permanently in location notes +`geohash/LocationChannel.kt:8` (precision 8 ≈ 19×38 m), `nostr/LocationNotesManager.kt:119-165` +Kind-1 (relay-archived, persistent) notes carry the 8-char geohash, exact `created_at`, and an optional plaintext nickname, signed by a stable pubkey — a permanent public record "pubkey X was within ~30 m of this spot at time T". REQ `#g` filters also disclose precise location to relays. + +### H8. Deterministic per-geohash identities are stable forever — long-term passive location tracking +`nostr/NostrIdentity.kt:137-177`, `ui/GeohashViewModel.kt:184-193` +Geohash identity = HMAC(deviceSeed, geohash), never rotated. A passive observer of a channel can recognize returning users months later and assemble their full history in that channel; the plaintext `["n", nickname]` tag (`nostr/NostrProtocol.kt:167-169`) links the same person across channels. + +### H9. Unbounded per-peer actor map and unlimited packet queues — memory/coroutine exhaustion +`mesh/PacketProcessor.kt:43-95`, `mesh/BluetoothPacketBroadcaster.kt:122-124` +A new coroutine actor with `Channel.UNLIMITED` is created per attacker-chosen `senderID` *before* any security validation, with no eviction. Spraying packets with random sender IDs grows live coroutines + unbounded channels forever. + +### H10. Unauthenticated packet types relayed mesh-wide with attacker-controlled TTL — flood/amplification +`mesh/SecurityManager.kt:268-280`, `mesh/PacketRelayManager.kt:59-107,134-163` +Signature verification enforced only for ANNOUNCE/MESSAGE/FILE_TRANSFER/LEAVE. FRAGMENT, REQUEST_SYNC, NOISE_* verify unconditionally and are relayed (unconditionally at TTL ≥ 4) with no rate limit. One BLE radio can make the entire mesh re-broadcast forged traffic, draining bandwidth and battery. + +### H11. Unsigned REQUEST_SYNC forces bulk re-broadcast — amplification +`mesh/BluetoothMeshService.kt:671-676`, `sync/GossipSyncManager.kt:168-199` +A spoofed REQUEST_SYNC with an empty (forgeable) GCS filter makes a victim dump its entire sync cache onto the radio; repeating keeps neighbors transmitting continuously. No rate limit or response budget. + +### H12. ANNOUNCE replay with TTL=7 forces Noise session teardown + fake "direct neighbor" +`mesh/SecurityManager.kt:78-90`, `mesh/BluetoothMeshService.kt:586-621` +Duplicate ANNOUNCEs are re-accepted at TTL ≥ 7; direct-link is inferred from TTL alone (excluded from the signature, attacker-settable). Replaying a victim's recent ANNOUNCE at TTL=7 repeatedly tears down the victim's Noise sessions, breaking in-flight DMs. + +--- + +## MEDIUM + +### M1. No low-order point / all-zero DH rejection — Noise spec violation, key-compromise downgrade +`noise/southernstorm/protocol/HandshakeState.java:750-762,1034-1040,1050-1068` +Only the all-zero ephemeral key is rejected; other low-order Curve25519 points pass, DH outputs never checked, remote static key unvalidated. A malicious identity with low-order keys yields all-zero DH outputs → publicly derivable session keys. `SecureIdentityStateManager.validatePublicKey()` (identity/…:403-417) is debug-only and its blocklist misses real low-order points. + +### M2. Unbounded crypto work + session state per forged handshake — crypto/memory DoS +`noise/NoiseSessionManager.kt:228-252,312-316` +Any frame from any spoofed peerID creates a responder session (~3 X25519 scalar mults) inside a `@Synchronized` method, with no cap on half-open responder sessions (staleness check only on the initiate path). + +### M3. Stable 8-byte peerID in BLE scan response — passive long-term tracking +`mesh/BluetoothGattServerManager.kt:400-412`, `mesh/BluetoothMeshService.kt:54` +The scan response embeds the truncated fingerprint of the *persistent* static Noise identity, surviving restarts and MAC rotation (by design, for dedup). Any passive sniffer can track a user across time/place. Nickname is broadcast in every ANNOUNCE; ANNOUNCE gossip TLVs disclose the user's neighbor graph. + +### M4. Fragment reassembly poisoning — cross-sender fragment-ID collision +`mesh/FragmentManager.kt:32,202-263` +Reassembly keyed by 8-byte fragment ID only (not sender). Fragments are unsigned; an attacker injects one colliding fragment to destroy a victim's in-flight (up to 1 MB) transfer. Bounded by existing caps; DoS/corruption, not forgery. + +### M5. Legacy plaintext Ed25519 private key can persist after "migration" +`crypto/EncryptionService.kt:505-524` +Old plaintext key is deleted only if the encrypted store doesn't already have one; otherwise the plaintext private key remains on disk indefinitely. + +### M6. Attacker-controlled image decode bombs / main-thread decode +`ui/media/ImageMessageItem.kt:74`, `ui/media/FullScreenImageViewer.kt:75`, `ui/MessageComponents.kt:294-304` +Received images decoded with `BitmapFactory.decodeFile` — no bounds check, no `inSampleSize`, on the main thread during composition. A small PNG with huge dimensions → instant OOM when the chat renders. `readBytes()` re-reads whole files on every recomposition. + +### M7. Received files auto-downloaded unencrypted; size limit only on send path +`features/file/FileUtils.kt:194-263`, `nostr/NostrDirectMessageHandler.kt:191-194` +Nostr DM path accepts up to 10 MB per message (`AppConstants.kt:71`); a malicious contact can fill `cacheDir` indefinitely. Filenames are sanitized (path traversal verified not exploitable). + +### M8. Exported MainActivity acts on attacker-supplied intent extras +`AndroidManifest.xml:98-115`, `MainActivity.kt:828-894` +Any app can fire intents with `EXTRA_OPEN_PRIVATE_CHAT`/`EXTRA_PEER_ID` to open arbitrary chat sheets and silently clear the victim's pending notifications, or trigger the verification sheet UI. QR payload itself is cryptographically validated — no verification forgery. + +### M9. Relay directory auto-updates daily from third-party GitHub CSV, unsigned/unpinned +`nostr/RelayDirectory.kt:29,152-191` +Compromise of `permissionlesstech/georelays` steers all users to attacker relays → precise `#g` filters + forged-event injection (H5). + +### M10. Geohash subscription filters & geo-nearest relay selection leak location to relays/observers +`nostr/NostrRelayManager.kt:131-147`, `nostr/RelayDirectory.kt:88-106`, `nostr/NostrFilter.kt:37-71` +Each relay learns subscribed cells; `#p` DM filters reveal owned pubkeys. Mitigated: Tor ON by default, fail-closed proxy config (`net/ArtiTorManager.kt:151,224`). + +### M11. Unbounded outbound Nostr message queue — never drained, re-sent on reconnect +`nostr/NostrRelayManager.kt:107,286-288,862-871` +`messageQueue` entries are never removed after send; every relay reconnect re-sends full history (duplicate gift wraps, memory growth, extra metadata). + +### M12. Unbounded identity-keyed caches — memory DoS by malicious relay (compounds H5) +`nostr/GeohashRepository.kt:22-29,61,104-118` +`geohashParticipants`, `geoNicknames`, etc. grow per unique pubkey with no eviction; forged events from unlimited fresh pubkeys exhaust memory. No WebSocket frame size cap (`NostrRelayManager.kt:874-876`). + +### M13. "NIP-44" DM encryption omits padding — exact plaintext length leakage; not real NIP-44 +`nostr/NostrCrypto.kt:211-260,267-293` +Raw XChaCha20-Poly1305 over unpadded UTF-8; relays/observers see exact DM lengths (receipt vs message vs file) and it's incompatible with real NIP-44 clients. AEAD itself sound. + +### M14. Peer-table flooding with throwaway identities +`mesh/PeerManager.kt:93,228-240,551-564` +No cap on the peers map; cheap self-signed ANNOUNCEs create verified entries and can each trigger Noise handshakes. 3-minute sweep is the only bound. + +### M15. No rate limiting on GATT writes / inbound packet processing +`mesh/BluetoothGattServerManager.kt:232-277`, `mesh/BluetoothGattClientManager.kt:644-656` +Every write is fully decoded/verified with no per-connection or global rate limit; combined with H9 guarantees backlog growth. + +### M16. Two independent persistent Ed25519 signing identities per device +`crypto/EncryptionService.kt:485-503` vs `identity/SecureIdentityStateManager.kt:145-195` +Different keys in different pref files; same device presents two signing identities; panic wipe doesn't rotate both. Also: `EncryptionService.sign()` returns an empty signature and `verify()` ignores its inputs entirely (EncryptionService.kt:226-242) — a latent trap. + +--- + +## LOW + +- **L1. Metadata leakage:** only NOISE_* frames padded; public MESSAGE/ANNOUNCE/FILE_TRANSFER leak exact sizes; every packet carries ms wall-clock timestamps (`mesh/BLEPacketPaddingPolicy.kt:11-17`, `protocol/BinaryProtocol.kt:42-43,76`). +- **L2. Signed MESSAGE/FILE_TRANSFER replayable after 5-min dedup expiry** — no freshness check (`mesh/SecurityManager.kt:55-73,101`). +- **L3. Silent identity regeneration on key-store corruption** — masks tampering, destroys identity on transient Keystore failure (`crypto/EncryptionService.kt:466-483`, `noise/NoiseEncryptionService.kt:85-119`). +- **L4. Channel KDF salt = channel name; PBKDF2 mislabeled as "Argon2id" in comments; passwords retained in memory; dead plaintext channel-key-sharing packet code** (`noise/NoiseChannelEncryption.kt:148-168,203-243`). +- **L5. Reverse geocoding sends precise GPS to OSM Nominatim (or Google Fused) with app-identifying UA** — rides Tor when on (`geohash/OpenStreetMapGeocoderProvider.kt:21-29`). +- **L6. Persistent account Nostr identity links all DM activity; `#p` filters announce pubkey ownership to relays** — mitigated by randomized gift-wrap timestamps (`nostr/NostrIdentity.kt:108-128`). +- **L7. Predictable subscription IDs (`sub--`) aid per-session correlation** (`nostr/NostrRelayManager.kt:809-811`). +- **L8. NIP-17 validation gaps:** rumor kind not checked to be 14; replay window uses attacker-controlled `created_at`. Sender spoofing inside gift wraps *is* correctly prevented (`nostr/NostrProtocol.kt:77-96`). +- **L9. No FLAG_SECURE anywhere; Recents preview leaks chats on API < 33** (`MainActivity.kt:84-86`). +- **L10. Clipboard copies not marked sensitive** (`ui/ChatUserSheet.kt:87`, `ui/SecurityVerificationSheet.kt:411`). +- **L11. WebView with JS + file access + unescaped geohash interpolation into `evaluateJavascript`** — activity not exported, limited impact (`ui/GeohashPickerActivity.kt:104-148`). +- **L12. `packet.recipientID != SpecialRecipients.BROADCAST` is ByteArray reference comparison — always true** (`mesh/BluetoothPacketBroadcaster.kt:340`). +- **L13. GATT server ignores preparedWrite/offset; force-unwrap NPEs on hot paths degrade gracefully** (`mesh/BluetoothGattServerManager.kt:232-239`, `mesh/PacketProcessor.kt:127`). + +--- + +## Verified positives + +- SecureRandom everywhere; no weak RNG. +- Keys in EncryptedSharedPreferences (AES256-GCM Keystore master key); `allowBackup="false"` + backup rules. +- No plaintext message DB; messages in-memory only; message IDs in encrypted prefs. +- PeerID = SHA-256(static key)[:8] with session establishment refusing mismatches; session-generation binding defeats re-handshake downgrade. +- AEAD verify-before-decrypt, constant-time tag check, nonce-wraparound enforcement, rekey limits, zeroization on destroy. +- NIP-17 seal signature verified + `seal.pubkey == rumor.pubkey` enforced; Schnorr nonces use SecureRandom. +- Tor fail-closed (proxy set before bootstrap; clients rebuilt on mode change); no telemetry/analytics SDKs; all traffic wss/https. +- Filename sanitization blocks path traversal; FileProvider per-URI grants; no auto-opening URLs; links require explicit tap and are coerced to https. +- Fragment caps (256/ID, 1 MB/set, 4 MB global, 30 s timeout); dedup caches bounded; connection limits + backoff; presence heartbeats only at city-level precision with jitter. + +## Top remediations (priority order) + +1. Strip all key/content logging (`SymmetricState`, `MessageHandler.kt:88`); add ProGuard `assumenosideeffects` for `android.util.Log`. *(C1, H3)* +2. Hard-cap `originalSize` in `CompressionUtil.decompress` (~1–2 MB); tighten ratio to ≈1032:1. *(C2)* +3. Fix replay-window shift direction + unit tests; reject low-order points / all-zero DH in `mixDH`. *(H1, M1)* +4. Verify `isValidSignature()` on all incoming Nostr events; pin/sign the relay directory. *(H5, M9)* +5. Cross-sign Ed key with Noise key (proof-of-possession) in announcements. *(H2)* +6. Bound PacketProcessor actor map (validate before actor creation), bound queues, authenticate/rate-limit FRAGMENT & REQUEST_SYNC relay, budget sync responses. *(H9–H11)* +7. Key fragment reassembly by (senderID, fragmentID); rotate the advertised peerID. *(M4, M3)* +8. Gate auto delivery/read receipts behind a privacy setting; warn about location-note precision/permanence. *(H6, H7)* +9. Lock-screen-redact DM notifications; image decode bounds checks off main thread. *(H4, M6)* +10. Bound Nostr `messageQueue` (drain after send) and `GeohashRepository` caches. *(M11, M12)*