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.
This commit is contained in:
callebtc 2026-07-27 10:33:29 +02:00 committed by GitHub
parent 4a34408db9
commit bc49c71ea0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 440 additions and 977 deletions

View File

@ -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}")
}

View File

@ -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<GeohashChannel>()
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()

View File

@ -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

View File

@ -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<ScanResult>) {
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}")

View File

@ -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")
}
}

View File

@ -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}")
}
}
}

View File

@ -122,13 +122,8 @@ class BluetoothPacketBroadcaster(
private val broadcasterActor = broadcasterScope.actor<BroadcastRequest>(
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")
}
}

View File

@ -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..<endOffset)
}
Log.d(TAG, "Creating ${fragmentChunks.size} fragments for ${fullData.size} byte packet (iOS compatible)")
// iOS: for (index, fragment) in fragments.enumerated()
for (index in fragmentChunks.indices) {
val fragmentData = fragmentChunks[index]
@ -167,11 +156,9 @@ class FragmentManager {
fragments.add(fragmentPacket)
}
Log.d(TAG, "✅ Created ${fragments.size} fragments successfully")
return fragments
} catch (e: Exception) {
Log.e(TAG, "❌ Fragment creation failed: ${e.message}", e)
Log.e(TAG, "❌ Packet type: ${packet.type}, payload: ${packet.payload.size} bytes")
Log.e(TAG, "Fragment creation failed (type=${packet.type}, payload=${packet.payload.size} bytes): ${e.message}", e)
return emptyList()
}
}
@ -183,7 +170,7 @@ class FragmentManager {
fun handleFragment(packet: BitchatPacket): BitchatPacket? {
// iOS: guard packet.payload.count > 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..<total { if let fragment = fragments[i] { reassembled.append(fragment) } }
val reassembledData = mutableListOf<Byte>()
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)")
}
}
}

View File

@ -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(),

View File

@ -46,15 +46,8 @@ class PacketProcessor(private val myPeerID: String) {
private fun getOrCreateActorForPeer(peerID: String) = processorScope.actor<RoutedPacket>(
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")
}
}

View File

@ -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")
}
}
/**

View File

@ -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 {

View File

@ -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)

View File

@ -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}")
}

View File

@ -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
}
/**

View File

@ -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;

View File

@ -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));
}
/**

View File

@ -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) {

View File

@ -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,

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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")
}
/**

View File

@ -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
}
}

View File

@ -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-<millis>-<rand>`) 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` (~12 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. *(H9H11)*
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)*