diff --git a/.goose/memory/Android Invoice Freeze Analysis.txt b/.goose/memory/Android Invoice Freeze Analysis.txt deleted file mode 100644 index 4dcc21f9..00000000 --- a/.goose/memory/Android Invoice Freeze Analysis.txt +++ /dev/null @@ -1,108 +0,0 @@ -# android freezing cdk-ffi lightning memory -## Analysis of Android Invoice Creation Freeze Issue - -### Problem Summary -- **Issue**: App freezes entirely when creating Lightning invoices on some Android devices -- **Symptoms**: Mint quote is created and persisted, but UI becomes unresponsive -- **Inconsistency**: Only affects some Android devices, not others - -### Key Findings from Code Analysis - -#### 1. **Large CDK-FFI Native Library (Most Likely Culprit)** -- `libcdk_ffi.so` is **16.6MB** (ARM64) and **15.8MB** (x86_64) -- This is an extremely large native library that could cause: - - **Memory pressure** on devices with limited RAM - - **Loading delays** that freeze the main thread - - **Device-specific compatibility issues** with different Android versions/manufacturers - -#### 2. **Potential Main Thread Blocking Operations** -```kotlin -// In CashuService.createMintQuote - runs on Dispatchers.IO but may still block -val ffiMintQuote = wallet!!.mintQuote( - amount = FfiAmount(amount.toULong()), - description = description ?: "Cashu mint" -) -``` -- CDK-FFI calls (`wallet!!.mintQuote()`) could involve: - - **Network operations** to the mint - - **Cryptographic computations** - - **Database writes** to local store - - **Memory allocations** for large data structures - -#### 3. **Device Architecture Compatibility** -- CDK-FFI may have compatibility issues with specific: - - **Android versions** (older/newer Android APIs) - - **Manufacturer customizations** (Samsung, Xiaomi, OnePlus, etc.) - - **ARM/x86 architecture variations** - - **Memory management differences** between OEMs - -#### 4. **QR Code Generation Timing** -```kotlin -// In ReceiveLightningDialog - QR code generated immediately after quote creation -val qrCodeBitMatrix = remember(text) { - try { - val sizePx = with(density) { 512.dp.toPx() }.toInt() // 512px QR code - val writer = QRCodeWriter() - writer.encode(text, BarcodeFormat.QR_CODE, sizePx, sizePx) - } catch (e: WriterException) { null } -} -``` -- Large QR code generation (512px) happens on main thread -- Combined with Lightning request string could be computationally expensive - -#### 5. **Memory-Intensive Operations Sequence** -1. CDK-FFI wallet initialization (if needed) -2. Network call to mint for quote creation -3. Cryptographic operations for invoice generation -4. Database write to persist quote -5. UI update to show loading state -6. QR code generation for Lightning invoice -7. UI composition updates - -### Recommended Solutions - -#### **Immediate Fixes:** - -1. **Add Timeout Protection** -```kotlin -// Add timeout to CDK operations -withTimeout(30000) { // 30 second timeout - wallet!!.mintQuote(amount, description) -} -``` - -2. **Move QR Code Generation Off Main Thread** -```kotlin -// Generate QR code in background -LaunchedEffect(currentMintQuote?.request) { - // Generate QR in background coroutine -} -``` - -3. **Add Device Memory Checks** -```kotlin -// Check available memory before CDK operations -val runtime = Runtime.getRuntime() -val availableMemory = runtime.maxMemory() - runtime.totalMemory() -if (availableMemory < 50_000_000) { // 50MB threshold - // Show warning or use fallback -} -``` - -#### **Long-term Solutions:** - -1. **Implement CDK Availability Detection** - - Check if CDK-FFI loads successfully on app start - - Fall back to mock/demo mode on incompatible devices - - Show user-friendly error messages - -2. **Add Progressive Loading** - - Show spinner immediately when creating invoice - - Update UI incrementally as operations complete - - Provide cancellation option for long operations - -3. **Device-Specific Optimizations** - - Detect problematic device models - - Adjust QR code resolution based on device capabilities - - Implement device-specific CDK initialization parameters - diff --git a/.goose/memory/Android Invoice Freeze Fix.txt b/.goose/memory/Android Invoice Freeze Fix.txt deleted file mode 100644 index bbc1969f..00000000 --- a/.goose/memory/Android Invoice Freeze Fix.txt +++ /dev/null @@ -1,87 +0,0 @@ -# android fix coroutines lightning persistence -## โœ… FIXED: Android Invoice Creation Freeze Issue - -### ๐ŸŽฏ **Root Cause Identified** -The app was freezing when creating Lightning invoices due to **nested coroutine launches** in the persistence layer. - -### ๐Ÿ› **The Problem** -In `LightningManager.createMintQuote()`, after successfully creating a mint quote and saving it to the repository, the code was calling: - -```kotlin -// BAD: Nested coroutine launch causing deadlock -repository.saveMintQuote(quote).onSuccess { - loadPendingQuotes() // โ† This launched ANOTHER coroutine inside existing coroutine -} - -fun loadPendingQuotes() { - coroutineScope.launch { // โ† NEW COROUTINE launched inside createMintQuote's coroutine - repository.getMintQuotes().onSuccess { quotes -> - _pendingMintQuotes.value = quotes.filter { !it.paid } - } - } -} -``` - -This created **coroutine deadlock/contention** on some Android devices when: -1. The main coroutine (from `createMintQuote`) was waiting/blocked -2. The nested coroutine (from `loadPendingQuotes`) tried to access the same resources -3. Device-specific coroutine dispatchers handled this differently, causing freezes - -### โœ… **The Solution Applied** - -**1. Made `loadPendingQuotes()` a suspend function:** -```kotlin -// NEW: Suspend function instead of launching new coroutine -private suspend fun loadPendingQuotesInternal() { - repository.getMintQuotes().onSuccess { quotes -> - _pendingMintQuotes.value = quotes.filter { !it.paid } - } - - repository.getMeltQuotes().onSuccess { quotes -> - _pendingMeltQuotes.value = quotes.filter { it.state != MeltQuoteState.PAID } - } -} -``` - -**2. Updated the call pattern:** -```kotlin -// FIXED: Execute within same coroutine context -repository.saveMintQuote(quote).onSuccess { - loadPendingQuotesInternal() // โ† Direct suspend function call, no new coroutine -} -``` - -**3. Added public wrapper for external calls:** -```kotlin -// Public method for external calls (like WalletViewModel) -fun loadPendingQuotes() { - coroutineScope.launch { - loadPendingQuotesInternal() - } -} -``` - -### ๐Ÿ“ฆ **Files Modified** -- `LightningManager.kt` - Fixed coroutine nesting in mint/melt quote creation - -### ๐Ÿงช **Why This Fixes The Issue** -- **Eliminates nested coroutines**: No more launching coroutines inside coroutine callbacks -- **Same execution context**: All operations run in single coroutine scope -- **Device compatibility**: Removes device-specific coroutine dispatcher conflicts -- **Maintains persistence**: Quote is still saved and pending quotes still loaded - -### ๐Ÿ” **Why Only Some Devices Were Affected** -Different Android devices/manufacturers have varying coroutine dispatcher implementations: -- **Samsung, OnePlus, Xiaomi**: Custom Android variations handle coroutine scheduling differently -- **Standard AOSP**: More lenient with nested coroutines -- **Memory pressure**: Devices with less RAM more likely to experience contention - -### โœ… **Build Status** -- โœ… Successful compilation with `./gradlew assembleDebug` -- โœ… Only deprecation warnings (Android API compatibility) -- โœ… No functional changes to user experience -- โœ… Maintains all existing functionality - -### ๐ŸŽฏ **Expected Result** -The "Create Invoice" button should now work on **all Android devices** without freezing, while maintaining the same functionality of creating mint quotes and updating the pending quotes list. - diff --git a/.goose/memory/Noise Handshake Lazy Key Setting Fix.txt b/.goose/memory/Noise Handshake Lazy Key Setting Fix.txt deleted file mode 100644 index 11f933ad..00000000 --- a/.goose/memory/Noise Handshake Lazy Key Setting Fix.txt +++ /dev/null @@ -1,44 +0,0 @@ -# noise handshake xx ios android lazy-keys aead crypto -**SECOND ATTEMPT: Lazy Static Key Setting Approach** -After the step counting fix didn't resolve the AEADBadTagException, implemented a new approach based on iOS behavior: - -**Problem**: Still getting AEADBadTagException in step 2 even with correct step counting. The issue appears to be with how we initialize static keys upfront. - -**New Approach**: Lazy static key setting (matching iOS behavior) -1. **Don't set static keys immediately** during handshake initialization -2. **Let the handshake state generate ephemeral keys first** -3. **Set static keys only when pattern requires them** (when 's' operations occur) - -**Key Changes**: -1. **Simplified initializeNoiseHandshake()**: Removed immediate static key setting - ```kotlin - // CRITICAL CHANGE: Don't set static keys immediately - // Let the noise library handle ephemeral key generation first - handshakeState?.start() - ``` - -2. **Added ensureStaticKeysSet()**: Lazy setting when needed - ```kotlin - private fun ensureStaticKeysSet() { - // Only set static keys if handshake needs them and they haven't been set yet - if (handshakeStateLocal.needsLocalKeyPair()) { - val localKeyPair = handshakeStateLocal.getLocalKeyPair() - if (localKeyPair != null && !localKeyPair.hasPrivateKey()) { - localKeyPair.setPrivateKey(localStaticPrivateKey, 0) - } - } - } - ``` - -3. **Called ensureStaticKeysSet()** before readMessage() and writeMessage() operations - -**Rationale**: -- iOS approach: creates fresh ephemeral keys each time, only sets static when pattern requires -- Android was trying to force static keys immediately, possibly interfering with XX pattern flow -- XX pattern: -> e, <- e,ee,s,es, -> s,se (static keys not needed until step 2/3) - -**Expected Result**: This should eliminate the key material mismatch causing AEADBadTagException by following iOS's lazy key initialization pattern. - -**Files Modified**: `/app/src/main/java/com/bitchat/android/noise/NoiseSession.kt` -**Build Status**: โœ… Successful compilation - diff --git a/.goose/memory/Noise Handshake Start() Key Setting Fix.txt b/.goose/memory/Noise Handshake Start() Key Setting Fix.txt deleted file mode 100644 index a2d123bd..00000000 --- a/.goose/memory/Noise Handshake Start() Key Setting Fix.txt +++ /dev/null @@ -1,46 +0,0 @@ -# noise handshake xx static-keys start validation noise-java -**CRITICAL FIX: "Local static key required" Error Resolved** -**Problem**: The noise-java library was throwing "Local static key required" during `HandshakeState.start()`, preventing handshake initialization. - -**Root Cause**: The noise-java library **validates that required static keys are present before allowing start()** to proceed. This is different from the iOS approach and requires keys to be set **before** calling `start()`, not during handshake progression. - -**Stack Trace**: Error occurred in `HandshakeState.start()` at line 501, indicating key validation failure. - -**Critical Fix Applied**: -```kotlin -// CRITICAL FIX: Set static keys BEFORE calling start() -// The noise-java library validates key presence during start() -if (handshakeState?.needsLocalKeyPair() == true) { - Log.d(TAG, "Setting static keys before start() - required by noise-java") - - val localKeyPair = handshakeState?.getLocalKeyPair() - if (localKeyPair != null) { - localKeyPair.setPrivateKey(localStaticPrivateKey, 0) - - if (!localKeyPair.hasPrivateKey() || !localKeyPair.hasPublicKey()) { - throw IllegalStateException("Failed to set static identity keys") - } - - Log.d(TAG, "โœ“ Static identity keys set successfully before start()") - } -} - -handshakeState?.start() // NOW WORKS: Keys are set before validation -``` - -**Key Insight**: The noise-java library architecture requires: -1. Create HandshakeState -2. **Set static keys (if needed)** -3. Call start() (which validates keys) -4. Proceed with writeMessage/readMessage - -This differs from iOS CryptoKit approach where keys can be set dynamically during handshake progression. - -**Expected Result**: -- Android responder can now initialize XX handshake successfully -- Should eliminate "Local static key required" error -- Handshake should progress to message processing phase - -**Files Modified**: `/app/src/main/java/com/bitchat/android/noise/NoiseSession.kt` - `initializeNoiseHandshake()` method -**Build Status**: โœ… Successful compilation - diff --git a/.goose/memory/Noise Handshake Step Counting Fix - Rewritten.txt b/.goose/memory/Noise Handshake Step Counting Fix - Rewritten.txt deleted file mode 100644 index cfac8d2c..00000000 --- a/.goose/memory/Noise Handshake Step Counting Fix - Rewritten.txt +++ /dev/null @@ -1,73 +0,0 @@ -**COMPLETED: Noise Handshake Step Counting Logic - Complete Rewrite** - -**Problem Solved**: Eliminated the complex and error-prone manual step counting logic that was causing handshake failures. - -**Root Issues with Old Approach**: -1. Manual `handshakeMessageCount` tracking was out of sync with actual Noise library state -2. Complex conditionals for step validation based on role and count -3. Mismatch between what the app thought the step was vs. what the Noise library expected -4. Artificial size validation that didn't match Noise library expectations - -**New Clean Approach - Let the Noise Library Handle It**: - -**1. Removed Manual Step Counting**: -- Deleted `handshakeMessageCount` variable entirely -- Removed `validateHandshakeMessageSize()` helper function -- Removed `getExpectedResponseSize()` helper function -- Eliminated complex step-based conditionals - -**2. State-Driven Flow**: -```kotlin -fun processHandshakeMessage(message: ByteArray): ByteArray? { - // Let the Noise library validate message sizes and handle the flow - val payloadLength = handshakeStateLocal.readMessage(message, 0, message.size, payloadBuffer, 0) - - // Check what ACTION the handshake state wants us to take next - val action = handshakeStateLocal.getAction() - - return when (action) { - HandshakeState.WRITE_MESSAGE -> { - // Noise library says we need to send a response - val responseLength = handshakeStateLocal.writeMessage(responseBuffer, 0, ByteArray(0), 0, 0) - responseBuffer.copyOf(responseLength) - } - HandshakeState.SPLIT -> { - // Handshake complete - completeHandshake() - null - } - HandshakeState.FAILED -> { - throw Exception("Handshake failed - Noise library reported FAILED state") - } - HandshakeState.READ_MESSAGE -> { - // Waiting for next message - null - } - } -} -``` - -**3. Key Benefits**: -- **No more step counting errors**: Noise library internally tracks the handshake state -- **Automatic validation**: Library validates message sizes and formats -- **Cleaner code**: Eliminated 50+ lines of error-prone logic -- **Library-driven flow**: Actions are determined by the actual Noise protocol state -- **Better error handling**: Library provides precise error states - -**4. Simplified Message Flow**: -- **Initiator**: `startHandshake()` โ†’ `writeMessage()` โ†’ action = READ_MESSAGE -- **Responder**: Receives message โ†’ `readMessage()` โ†’ action = WRITE_MESSAGE โ†’ `writeMessage()` โ†’ action = READ_MESSAGE -- **Both**: Final message โ†’ action = SPLIT โ†’ handshake complete - -**Files Modified**: -- `/app/src/main/java/com/bitchat/android/noise/NoiseSession.kt` - Complete rewrite of handshake logic - -**Removed Functions**: -- `validateHandshakeMessageSize()` - No longer needed -- `getExpectedResponseSize()` - No longer needed -- Manual `handshakeMessageCount` tracking - Eliminated - -**Build Status**: โœ… Successful compilation with `./gradlew assembleDebug` - -**Result**: The handshake logic is now much simpler, more reliable, and follows the Noise protocol library's intended usage pattern. The library itself handles all the complexity of step validation, message sizing, and flow control. - diff --git a/.goose/memory/Noise Handshake Step Counting Fix.txt b/.goose/memory/Noise Handshake Step Counting Fix.txt deleted file mode 100644 index 7bdee523..00000000 --- a/.goose/memory/Noise Handshake Step Counting Fix.txt +++ /dev/null @@ -1,33 +0,0 @@ -# noise handshake xx android ios step-counting aead crypto -**FIXED: Critical Noise Handshake Step Counting Bug** -**Problem**: Android was failing on step 2 of XX handshake with AEADBadTagException when acting as initiator. The logs showed "Unknown handshake step 1 for initiator" when receiving the 96-byte response from macOS. - -**Root Cause**: The handshake step counting logic was incorrect. When the initiator (Android) received message 2 from the responder (macOS), the `handshakeMessageCount` was still 1 (from `startHandshake()`), but the validation expected it to be 2. - -**Fix Applied**: -1. **Corrected Step Calculation**: Added logic to calculate `currentStep` properly before processing: - ```kotlin - val currentStep = if (isInitiator) { - handshakeMessageCount + 1 // Initiator receiving message 2, 3, etc. - } else { - handshakeMessageCount // Responder receiving message 1, 2, etc. - } - ``` - -2. **Enhanced Message Size Validation**: Made validation more lenient to accept larger messages (iOS might include additional payload): - ```kotlin - // More lenient validation - allow larger messages (they may contain additional payload) - if (message.size < expectedSize) { - Log.w(TAG, "Handshake message too small: got ${message.size}, minimum expected $expectedSize for step $step") - } else if (message.size != expectedSize) { - Log.d(TAG, "Handshake message size: got ${message.size}, expected $expectedSize for step $step (accepting larger message)") - } - ``` - -**Expected Result**: The XX handshake should now complete successfully between Android (initiator) and macOS/iOS (responder). - -**Files Modified**: -- `/app/src/main/java/com/bitchat/android/noise/NoiseSession.kt` - `processHandshakeMessage()` and `validateHandshakeMessageSize()` methods - -**Build Status**: โœ… Successful compilation with `./gradlew assembleDebug` - diff --git a/.goose/memory/Noise Static Key Handling - FIXED.txt b/.goose/memory/Noise Static Key Handling - FIXED.txt deleted file mode 100644 index 35acb950..00000000 --- a/.goose/memory/Noise Static Key Handling - FIXED.txt +++ /dev/null @@ -1,83 +0,0 @@ -**โœ… COMPLETED: Fundamental Static Key Handling Problem - FULLY RESOLVED** - -**Root Discovery**: The noise-java library **DOES** fully support persistent static keys! The previous issues were due to our implementation approach, not the library. - -**Key Evidence from Library Analysis**: -```java -// Curve25519DHState.setPrivateKey() implementation: -public void setPrivateKey(byte[] key, int offset) { - System.arraycopy(key, offset, privateKey, 0, 32); - Curve25519.eval(publicKey, 0, privateKey, null); // Auto-derives public key - mode = 0x03; // Sets both private and public key flags -} -``` - -**Fixed Implementation Following iOS Specifications Exactly**: - -**1. โœ… Proper Static Key Management**: -```kotlin -private fun initializeNoiseHandshake(role: Int) { - handshakeState = HandshakeState(PROTOCOL_NAME, role) - - // iOS approach: Set persistent static keys - if (handshakeState?.needsLocalKeyPair() == true) { - val localKeyPair = handshakeState?.getLocalKeyPair()!! - - // Set the private key - public key derives automatically (like iOS) - localKeyPair.setPrivateKey(localStaticPrivateKey, 0) - - // Verify keys are set correctly - if (!localKeyPair.hasPrivateKey() || !localKeyPair.hasPublicKey()) { - throw IllegalStateException("Failed to set static keys") - } - } - - handshakeState?.start() -} -``` - -**2. โœ… Eliminated ALL Previous Workarounds**: -- Removed complex reflection-based key setting attempts -- Removed "fresh key generation" fallback approaches -- Removed "lazy key setting" mechanisms -- Removed library capability detection workarounds -- Removed excessive error checking and validation - -**3. โœ… Clean, Simple Approach**: -- Uses persistent static keys loaded from secure storage (exactly like iOS) -- Sets private key, lets Curve25519 derive the matching public key -- Follows standard Noise protocol patterns -- 100% compatible with iOS implementation - -**4. โœ… Verified Library Capability**: -- `DHState.setPrivateKey()` works perfectly -- `DHState.hasPrivateKey()` and `hasPublicKey()` return correct states -- XX pattern handshake completes successfully with persistent keys -- Transport encryption works correctly after handshake -- Remote static keys are properly extracted - -**5. โœ… iOS Specification Compliance**: -- Uses exactly the same protocol: `"Noise_XX_25519_ChaChaPoly_SHA256"` -- Persistent static identity keys (not ephemeral) -- Same XX pattern flow: e โ†’ e,ee,s,es โ†’ s,se -- Same transport cipher derivation -- Same fingerprint calculation for identity persistence - -**Key Insight**: The noise-java library was **NEVER** the problem. Our overly complex implementation approach was trying to work around non-existent limitations. - -**Removed Redundant Code**: -- ~100 lines of complex key validation and warnings -- Multiple fallback approaches for "library limitations" -- Excessive debugging and verification code -- Comments about "library issues" and "fork requirements" - -**Build Status**: โœ… Successful compilation with `./gradlew assembleDebug` - -**Result**: -- Clean, maintainable code following iOS patterns exactly -- Persistent static identity keys working correctly -- Full XX handshake compatibility with iOS -- Ready for production use with proper key persistence - -**Next**: The static key handling is now 100% correct and follows the iOS specification exactly. This should resolve authentication and identity persistence issues between Android and iOS. - diff --git a/app/release/baselineProfiles/0/app-release.dm b/app/release/baselineProfiles/0/app-release.dm deleted file mode 100644 index 4feae60a..00000000 Binary files a/app/release/baselineProfiles/0/app-release.dm and /dev/null differ diff --git a/app/release/baselineProfiles/1/app-release.dm b/app/release/baselineProfiles/1/app-release.dm deleted file mode 100644 index 2a7b813c..00000000 Binary files a/app/release/baselineProfiles/1/app-release.dm and /dev/null differ diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json deleted file mode 100644 index 9d78e39d..00000000 --- a/app/release/output-metadata.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "version": 3, - "artifactType": { - "type": "APK", - "kind": "Directory" - }, - "applicationId": "com.bitchat.android", - "variantName": "release", - "elements": [ - { - "type": "SINGLE", - "filters": [], - "attributes": [], - "versionCode": 5, - "versionName": "0.7.2", - "outputFile": "app-release.apk" - } - ], - "elementType": "File", - "baselineProfiles": [ - { - "minApi": 28, - "maxApi": 30, - "baselineProfiles": [ - "baselineProfiles/1/app-release.dm" - ] - }, - { - "minApi": 31, - "maxApi": 2147483647, - "baselineProfiles": [ - "baselineProfiles/0/app-release.dm" - ] - } - ], - "minSdkVersionForDexing": 26 -} \ No newline at end of file diff --git a/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt b/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt index 6a77c05a..e53c4be7 100644 --- a/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt +++ b/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt @@ -259,7 +259,7 @@ class MessageHandler(private val myPeerID: String) { } /** - * Handle private message addressed to us + * Handle (decrypted) private message addressed to us */ private suspend fun handlePrivateMessage(packet: BitchatPacket, peerID: String) { try { @@ -268,18 +268,9 @@ class MessageHandler(private val myPeerID: String) { Log.w(TAG, "Invalid signature for private message from $peerID") return } - - // Decrypt message - val decryptedData = delegate?.decryptFromPeer(packet.payload, peerID) - if (decryptedData == null) { - Log.e(TAG, "Failed to decrypt private message from $peerID") - return - } - - val unpaddedData = MessagePadding.unpad(decryptedData) - + // Parse message - val message = BitchatMessage.fromBinaryPayload(unpaddedData) + val message = BitchatMessage.fromBinaryPayload(packet.payload) if (message != null) { // Check for cover traffic (dummy messages) if (message.content.startsWith("โ˜‚DUMMYโ˜‚")) { @@ -288,14 +279,7 @@ class MessageHandler(private val myPeerID: String) { } delegate?.updatePeerNickname(peerID, message.sender) - - // Replace timestamp with current time (same as iOS) - val messageWithCurrentTime = message.copy( - senderPeerID = peerID, - timestamp = Date() // Use current time instead of original timestamp - ) - - delegate?.onMessageReceived(messageWithCurrentTime) + delegate?.onMessageReceived(message) // Send delivery ACK sendDeliveryAck(message, peerID) diff --git a/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt b/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt index dd1e1aa5..9dbb83ef 100644 --- a/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt @@ -97,6 +97,11 @@ class SecurityManager(private val encryptionService: EncryptionService, private // Skip our own handshake messages if (peerID == myPeerID) return false + + if (encryptionService.hasEstablishedSession(peerID)) { + Log.d(TAG, "Handshake already completed with $peerID") + return true + } if (packet.payload.isEmpty()) { Log.w(TAG, "Noise handshake packet has empty payload") @@ -124,8 +129,7 @@ class SecurityManager(private val encryptionService: EncryptionService, private delegate?.sendHandshakeResponse(peerID, response) // Notify delegate of handshake completion - delegate?.onKeyExchangeCompleted(peerID, packet.payload, routed.relayAddress) - + // delegate?.onKeyExchangeCompleted(peerID, packet.payload, routed.relayAddress) return true } else { // Check if session is now established (handshake complete) diff --git a/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt b/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt index 3de049f3..9d2b61b3 100644 --- a/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt +++ b/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt @@ -131,7 +131,7 @@ class NoiseEncryptionService(private val context: Context) { */ fun processHandshakeMessage(data: ByteArray, peerID: String): ByteArray? { return try { - sessionManager.handleIncomingHandshake(peerID, data) + sessionManager.processHandshakeMessage(peerID, data) } catch (e: Exception) { Log.e(TAG, "Failed to process handshake from $peerID: ${e.message}") null diff --git a/app/src/main/java/com/bitchat/android/noise/NoiseSession.kt b/app/src/main/java/com/bitchat/android/noise/NoiseSession.kt index b1188281..b7f09ebe 100644 --- a/app/src/main/java/com/bitchat/android/noise/NoiseSession.kt +++ b/app/src/main/java/com/bitchat/android/noise/NoiseSession.kt @@ -246,7 +246,7 @@ class NoiseSession( val handshakeStateLocal = handshakeState ?: throw IllegalStateException("Handshake state is null") // Let the Noise library validate message sizes and handle the flow - val payloadBuffer = ByteArray(MAX_PAYLOAD_SIZE) // Buffer for any payload data + val payloadBuffer = ByteArray(XX_MESSAGE_2_SIZE + MAX_PAYLOAD_SIZE) // Buffer for any payload data // Read the incoming message - the Noise library will handle validation val payloadLength = handshakeStateLocal.readMessage(message, 0, message.size, payloadBuffer, 0) @@ -270,7 +270,7 @@ class NoiseSession( HandshakeState.SPLIT -> { // Handshake complete, split into transport keys completeHandshake() - Log.d(TAG, "๐Ÿ”’ XX handshake completed with $peerID") + Log.d(TAG, "โœ… XX handshake completed with $peerID") null } @@ -329,7 +329,7 @@ class NoiseSession( messagesReceived = 0 state = NoiseSessionState.Established - Log.d(TAG, "Real XX handshake completed with $peerID - transport keys derived") + Log.d(TAG, "Handshake completed with $peerID as isInitiator: $isInitiator - transport keys derived") } catch (e: Exception) { state = NoiseSessionState.Failed(e) Log.e(TAG, "Failed to complete handshake: ${e.message}") diff --git a/app/src/main/java/com/bitchat/android/noise/NoiseSessionManager.kt b/app/src/main/java/com/bitchat/android/noise/NoiseSessionManager.kt index 7167a986..dc493c28 100644 --- a/app/src/main/java/com/bitchat/android/noise/NoiseSessionManager.kt +++ b/app/src/main/java/com/bitchat/android/noise/NoiseSessionManager.kt @@ -79,10 +79,10 @@ class NoiseSessionManager( } /** - * SIMPLIFIED: Handle incoming handshake message + * Handle incoming handshake message */ - fun handleIncomingHandshake(peerID: String, message: ByteArray): ByteArray? { - Log.d(TAG, "handleIncomingHandshake($peerID, ${message.size} bytes)") + fun processHandshakeMessage(peerID: String, message: ByteArray): ByteArray? { + Log.d(TAG, "processHandshakeMessage($peerID, ${message.size} bytes)") try { var session = getSession(peerID) diff --git a/app/src/test/kotlin/com/bitchat/android/debug/NoiseXXDebugTest.kt b/app/src/test/kotlin/com/bitchat/android/debug/NoiseXXDebugTest.kt deleted file mode 100644 index 297a4237..00000000 --- a/app/src/test/kotlin/com/bitchat/android/debug/NoiseXXDebugTest.kt +++ /dev/null @@ -1,136 +0,0 @@ -package com.bitchat.android.debug - -import com.bitchat.android.noise.southernstorm.protocol.* -import org.junit.Test -import java.security.SecureRandom - -/** - * Test to debug the XX handshake pattern step by step - * This test replicates the exact scenario from your logs - */ -class NoiseXXDebugTest { - - @Test - fun testXXHandshakeStepByStep() { - println("=== Analyzing XX Handshake Pattern ===") - - // Generate test keys like your app would - val random = SecureRandom() - val initiatorStaticPriv = ByteArray(32) - val responderStaticPriv = ByteArray(32) - random.nextBytes(initiatorStaticPriv) - random.nextBytes(responderStaticPriv) - - // Create DH states to derive public keys - val initiatorDH = Noise.createDH("25519") - val responderDH = Noise.createDH("25519") - - initiatorDH.setPrivateKey(initiatorStaticPriv, 0) - responderDH.setPrivateKey(responderStaticPriv, 0) - - val initiatorStaticPub = ByteArray(32) - val responderStaticPub = ByteArray(32) - initiatorDH.getPublicKey(initiatorStaticPub, 0) - responderDH.getPublicKey(responderStaticPub, 0) - - println("Initiator static public: ${initiatorStaticPub.joinToString("") { "%02x".format(it) }}") - println("Responder static public: ${responderStaticPub.joinToString("") { "%02x".format(it) }}") - - // Create handshake states - val initiator = HandshakeState("Noise_XX_25519_ChaChaPoly_SHA256", HandshakeState.INITIATOR) - val responder = HandshakeState("Noise_XX_25519_ChaChaPoly_SHA256", HandshakeState.RESPONDER) - - // Set static keys - initiator.localKeyPair?.setPrivateKey(initiatorStaticPriv, 0) - responder.localKeyPair?.setPrivateKey(responderStaticPriv, 0) - - // Start handshakes - initiator.start() - responder.start() - - println("\n=== XX Pattern Flow ===") - println("Expected: -> e") - println(" <- e, ee, s, es") - println(" -> s, se") - - // Message 1: -> e - println("\n--- Message 1: Initiator -> Responder ---") - val msg1Buffer = ByteArray(256) - val msg1Len = initiator.writeMessage(msg1Buffer, 0, ByteArray(0), 0, 0) - val msg1 = msg1Buffer.copyOf(msg1Len) - println("Message 1 length: $msg1Len bytes (expected: 32)") - println("Message 1 content: ${msg1.joinToString("") { "%02x".format(it) }}") - println("Initiator action after msg1: ${initiator.action}") - - // Responder processes message 1 - val responderPayload1 = ByteArray(256) - val responderPayload1Len = responder.readMessage(msg1, 0, msg1.size, responderPayload1, 0) - println("Responder processed msg1, payload len: $responderPayload1Len") - println("Responder action after processing msg1: ${responder.action}") - - // Message 2: <- e, ee, s, es - println("\n--- Message 2: Responder -> Initiator ---") - val msg2Buffer = ByteArray(256) - val msg2Len = responder.writeMessage(msg2Buffer, 0, ByteArray(0), 0, 0) - val msg2 = msg2Buffer.copyOf(msg2Len) - println("Message 2 length: $msg2Len bytes (expected: 80)") - println("Message 2 content: ${msg2.joinToString("") { "%02x".format(it) }}") - println("Responder action after msg2: ${responder.action}") - - // This is where the initiator should be able to process message 2 - // Let's see what happens - try { - val initiatorPayload2 = ByteArray(256) - val initiatorPayload2Len = initiator.readMessage(msg2, 0, msg2.size, initiatorPayload2, 0) - println("Initiator processed msg2 successfully, payload len: $initiatorPayload2Len") - println("Initiator action after processing msg2: ${initiator.action}") - - // Message 3: -> s, se - println("\n--- Message 3: Initiator -> Responder ---") - val msg3Buffer = ByteArray(256) - val msg3Len = initiator.writeMessage(msg3Buffer, 0, ByteArray(0), 0, 0) - val msg3 = msg3Buffer.copyOf(msg3Len) - println("Message 3 length: $msg3Len bytes (expected: 48)") - println("Message 3 content: ${msg3.joinToString("") { "%02x".format(it) }}") - println("Initiator action after msg3: ${initiator.action}") - - // Responder processes message 3 - val responderPayload3 = ByteArray(256) - val responderPayload3Len = responder.readMessage(msg3, 0, msg3.size, responderPayload3, 0) - println("Responder processed msg3, payload len: $responderPayload3Len") - println("Responder action after processing msg3: ${responder.action}") - - println("\nโœ“ Success: Handshake completed without errors") - - } catch (e: Exception) { - println("\nโŒ Error during initiator processing message 2:") - println("Exception: ${e.javaClass.simpleName}") - println("Message: ${e.message}") - e.printStackTrace() - - // Let's analyze what went wrong - analyzeMessage2Structure(msg2) - } - - // Cleanup - initiatorDH.destroy() - responderDH.destroy() - initiator.destroy() - responder.destroy() - } - - private fun analyzeMessage2Structure(msg2: ByteArray) { - println("\n=== Analyzing Message 2 Structure ===") - println("Total length: ${msg2.size}") - - if (msg2.size >= 32) { - println("Ephemeral key (bytes 0-31): ${msg2.sliceArray(0..31).joinToString("") { "%02x".format(it) }}") - } - - if (msg2.size >= 80) { - println("Encrypted static + MAC (bytes 32-79): ${msg2.sliceArray(32..79).joinToString("") { "%02x".format(it) }}") - println(" - Encrypted static (bytes 32-63): ${msg2.sliceArray(32..63).joinToString("") { "%02x".format(it) }}") - println(" - MAC tag (bytes 64-79): ${msg2.sliceArray(64..79).joinToString("") { "%02x".format(it) }}") - } - } -} diff --git a/app/src/test/kotlin/com/bitchat/android/noise/LocalNoiseForKeySettingTest.kt b/app/src/test/kotlin/com/bitchat/android/noise/LocalNoiseForKeySettingTest.kt deleted file mode 100644 index 42e25149..00000000 --- a/app/src/test/kotlin/com/bitchat/android/noise/LocalNoiseForKeySettingTest.kt +++ /dev/null @@ -1,183 +0,0 @@ -package com.bitchat.android.noise - -import com.bitchat.android.noise.southernstorm.protocol.* -import org.junit.Test -import org.junit.Assert.* -import java.security.SecureRandom - -/** - * Test our local noise-java fork to verify key setting works correctly - */ -class LocalNoiseForKeySettingTest { - - companion object { - private const val PROTOCOL_NAME = "Noise_XX_25519_ChaChaPoly_SHA256" - } - - @Test - fun testLocalForkKeySetting() { - println("=== TESTING LOCAL NOISE FORK KEY SETTING ===") - - try { - // Create DH state using our local fork - val dhState = Noise.createDH("25519") - - println("Created DH state: ${dhState.javaClass.name}") - println("Algorithm: ${dhState.dhName}") - println("Private key length: ${dhState.privateKeyLength}") - println("Public key length: ${dhState.publicKeyLength}") - - // Check initial state - println("Initial - hasPrivateKey: ${dhState.hasPrivateKey()}, hasPublicKey: ${dhState.hasPublicKey()}") - assertFalse("Should not have private key initially", dhState.hasPrivateKey()) - assertFalse("Should not have public key initially", dhState.hasPublicKey()) - - // Generate a test key pair - val privateKey = ByteArray(32) - val publicKey = ByteArray(32) - val random = SecureRandom() - random.nextBytes(privateKey) - - println("Generated test private key: ${privateKey.joinToString("") { "%02x".format(it) }}") - - // Set the private key - dhState.setPrivateKey(privateKey, 0) - println("Set private key") - - // Check if the key was set correctly - assertTrue("Should have private key after setting", dhState.hasPrivateKey()) - assertTrue("Should have public key after setting private key", dhState.hasPublicKey()) - - // Get the keys back - val retrievedPrivate = ByteArray(32) - val retrievedPublic = ByteArray(32) - dhState.getPrivateKey(retrievedPrivate, 0) - dhState.getPublicKey(retrievedPublic, 0) - - println("Retrieved private key: ${retrievedPrivate.joinToString("") { "%02x".format(it) }}") - println("Retrieved public key: ${retrievedPublic.joinToString("") { "%02x".format(it) }}") - - // Verify the keys match - assertArrayEquals("Private key should match", privateKey, retrievedPrivate) - - dhState.destroy() - - println("โœ… LOCAL FORK KEY SETTING WORKS!") - - } catch (e: Exception) { - println("โŒ Local fork key setting failed: ${e.message}") - e.printStackTrace() - fail("Local fork should support key setting") - } - } - - @Test - fun testLocalForkHandshakeWithPreSetKeys() { - println("=== TESTING HANDSHAKE WITH PRE-SET KEYS ===") - - try { - // Generate two key pairs - val (alicePriv, alicePub) = generateKeyPair() - val (bobPriv, bobPub) = generateKeyPair() - - println("Alice private: ${alicePriv.joinToString("") { "%02x".format(it) }}") - println("Alice public: ${alicePub.joinToString("") { "%02x".format(it) }}") - println("Bob private: ${bobPriv.joinToString("") { "%02x".format(it) }}") - println("Bob public: ${bobPub.joinToString("") { "%02x".format(it) }}") - - // Create handshake states - val aliceHandshake = HandshakeState(PROTOCOL_NAME, HandshakeState.INITIATOR) - val bobHandshake = HandshakeState(PROTOCOL_NAME, HandshakeState.RESPONDER) - - // Set pre-existing keys - if (aliceHandshake.needsLocalKeyPair()) { - val aliceKeyPair = aliceHandshake.localKeyPair - aliceKeyPair.setPrivateKey(alicePriv, 0) - assertTrue("Alice should have private key", aliceKeyPair.hasPrivateKey()) - assertTrue("Alice should have public key", aliceKeyPair.hasPublicKey()) - println("โœ… Alice keys set successfully") - } - - if (bobHandshake.needsLocalKeyPair()) { - val bobKeyPair = bobHandshake.localKeyPair - bobKeyPair.setPrivateKey(bobPriv, 0) - assertTrue("Bob should have private key", bobKeyPair.hasPrivateKey()) - assertTrue("Bob should have public key", bobKeyPair.hasPublicKey()) - println("โœ… Bob keys set successfully") - } - - // Start handshakes - aliceHandshake.start() - bobHandshake.start() - - // Execute handshake - val msg1 = executeHandshakeStep(aliceHandshake, null, "Alice msg1") - val msg2 = executeHandshakeStep(bobHandshake, msg1!!, "Bob msg2") - val msg3 = executeHandshakeStep(aliceHandshake, msg2!!, "Alice msg3") - executeHandshakeStep(bobHandshake, msg3!!, "Bob complete") - - assertEquals("Both should be ready to split", HandshakeState.SPLIT, aliceHandshake.action) - assertEquals("Both should be ready to split", HandshakeState.SPLIT, bobHandshake.action) - - // Verify remote keys - assertTrue("Alice should have Bob's remote key", aliceHandshake.hasRemotePublicKey()) - assertTrue("Bob should have Alice's remote key", bobHandshake.hasRemotePublicKey()) - - val aliceSeenRemote = ByteArray(32) - val bobSeenRemote = ByteArray(32) - aliceHandshake.remotePublicKey.getPublicKey(aliceSeenRemote, 0) - bobHandshake.remotePublicKey.getPublicKey(bobSeenRemote, 0) - - assertArrayEquals("Alice should see Bob's public key", bobPub, aliceSeenRemote) - assertArrayEquals("Bob should see Alice's public key", alicePub, bobSeenRemote) - - println("โœ… Handshake with pre-set keys completed successfully!") - - aliceHandshake.destroy() - bobHandshake.destroy() - - } catch (e: Exception) { - println("โŒ Handshake with pre-set keys failed: ${e.message}") - e.printStackTrace() - fail("Handshake should work with pre-set keys") - } - } - - private fun generateKeyPair(): Pair { - val dhState = Noise.createDH("25519") - dhState.generateKeyPair() - - val privateKey = ByteArray(32) - val publicKey = ByteArray(32) - dhState.getPrivateKey(privateKey, 0) - dhState.getPublicKey(publicKey, 0) - - dhState.destroy() - return Pair(privateKey, publicKey) - } - - private fun executeHandshakeStep(handshake: HandshakeState, incoming: ByteArray?, stepName: String): ByteArray? { - return if (incoming != null) { - val payload = ByteArray(256) - handshake.readMessage(incoming, 0, incoming.size, payload, 0) - println("$stepName: Read ${incoming.size} bytes") - - if (handshake.action == HandshakeState.WRITE_MESSAGE) { - val response = ByteArray(256) - val len = handshake.writeMessage(response, 0, ByteArray(0), 0, 0) - val actualResponse = response.copyOf(len) - println("$stepName: Sent ${actualResponse.size} bytes") - actualResponse - } else { - println("$stepName: No response needed, action: ${handshake.action}") - null - } - } else { - val msg = ByteArray(256) - val len = handshake.writeMessage(msg, 0, ByteArray(0), 0, 0) - val actualMsg = msg.copyOf(len) - println("$stepName: Sent initial ${actualMsg.size} bytes") - actualMsg - } - } -} diff --git a/app/src/test/kotlin/com/bitchat/android/noise/NoiseFullHandshakeWithPersistedKeysTest.kt b/app/src/test/kotlin/com/bitchat/android/noise/NoiseFullHandshakeWithPersistedKeysTest.kt deleted file mode 100644 index 415d51bd..00000000 --- a/app/src/test/kotlin/com/bitchat/android/noise/NoiseFullHandshakeWithPersistedKeysTest.kt +++ /dev/null @@ -1,361 +0,0 @@ -package com.bitchat.android.noise - -import com.bitchat.android.noise.southernstorm.protocol.* -import org.junit.Test -import org.junit.Assert.* -import java.security.SecureRandom - -/** - * COMPREHENSIVE test to verify that the ENTIRE Noise XX handshake works with persisted keys - * This test will definitively prove whether noise-java can complete a full handshake using pre-set keys - */ -class NoiseFullHandshakeWithPersistedKeysTest { - - companion object { - private const val PROTOCOL_NAME = "Noise_XX_25519_ChaChaPoly_SHA256" - } - - @Test - fun testCompleteHandshakeWithPersistedKeys() { - println("=== TESTING COMPLETE XX HANDSHAKE WITH PERSISTED KEYS ===") - println("This test will prove definitively if noise-java supports our use case") - - try { - // Step 1: Generate persistent identity keys for Alice and Bob - val (alicePrivate, alicePublic) = generatePersistentKeys("Alice") - val (bobPrivate, bobPublic) = generatePersistentKeys("Bob") - - println("\n--- Generated Persistent Identity Keys ---") - println("Alice private: ${alicePrivate.joinToString("") { "%02x".format(it) }}") - println("Alice public: ${alicePublic.joinToString("") { "%02x".format(it) }}") - println("Bob private: ${bobPrivate.joinToString("") { "%02x".format(it) }}") - println("Bob public: ${bobPublic.joinToString("") { "%02x".format(it) }}") - - // Step 2: Set up Alice as initiator with her persistent keys - println("\n--- Setting up Alice (Initiator) ---") - val aliceHandshake = HandshakeState(PROTOCOL_NAME, HandshakeState.INITIATOR) - configureHandshakeWithPersistedKeys(aliceHandshake, alicePrivate, alicePublic, "Alice") - aliceHandshake.start() - println("โœ… Alice handshake initialized with persistent keys") - - // Step 3: Set up Bob as responder with his persistent keys - println("\n--- Setting up Bob (Responder) ---") - val bobHandshake = HandshakeState(PROTOCOL_NAME, HandshakeState.RESPONDER) - configureHandshakeWithPersistedKeys(bobHandshake, bobPrivate, bobPublic, "Bob") - bobHandshake.start() - println("โœ… Bob handshake initialized with persistent keys") - - // Step 4: Execute the complete XX handshake - println("\n--- Executing Complete XX Handshake ---") - - // XX Message 1: Alice -> Bob (e) - println("Step 1: Alice sends message 1 (ephemeral key)") - val message1Buffer = ByteArray(256) - val message1Length = aliceHandshake.writeMessage(message1Buffer, 0, ByteArray(0), 0, 0) - val message1 = message1Buffer.copyOf(message1Length) - println("Alice sent message 1: ${message1.size} bytes") - assertEquals("XX message 1 should be 32 bytes", 32, message1.size) - - // Bob receives message 1 - val payload1Buffer = ByteArray(256) - val payload1Length = bobHandshake.readMessage(message1, 0, message1.size, payload1Buffer, 0) - println("Bob received message 1, payload length: $payload1Length") - assertEquals("Handshake action should be WRITE_MESSAGE", HandshakeState.WRITE_MESSAGE, bobHandshake.getAction()) - - // XX Message 2: Bob -> Alice (e, ee, s, es) - println("Step 2: Bob sends message 2 (ephemeral + encrypted static)") - val message2Buffer = ByteArray(256) - val message2Length = bobHandshake.writeMessage(message2Buffer, 0, ByteArray(0), 0, 0) - val message2 = message2Buffer.copyOf(message2Length) - println("Bob sent message 2: ${message2.size} bytes") - assertTrue("XX message 2 should be around 80 bytes", message2.size >= 70 && message2.size <= 90) - - // Alice receives message 2 - val payload2Buffer = ByteArray(256) - val payload2Length = aliceHandshake.readMessage(message2, 0, message2.size, payload2Buffer, 0) - println("Alice received message 2, payload length: $payload2Length") - assertEquals("Handshake action should be WRITE_MESSAGE", HandshakeState.WRITE_MESSAGE, aliceHandshake.getAction()) - - // XX Message 3: Alice -> Bob (s, se) - println("Step 3: Alice sends message 3 (encrypted static)") - val message3Buffer = ByteArray(256) - val message3Length = aliceHandshake.writeMessage(message3Buffer, 0, ByteArray(0), 0, 0) - val message3 = message3Buffer.copyOf(message3Length) - println("Alice sent message 3: ${message3.size} bytes") - assertTrue("XX message 3 should be around 48 bytes", message3.size >= 40 && message3.size <= 55) - - // Bob receives message 3 - this should complete the handshake - val payload3Buffer = ByteArray(256) - val payload3Length = bobHandshake.readMessage(message3, 0, message3.size, payload3Buffer, 0) - println("Bob received message 3, payload length: $payload3Length") - assertEquals("Handshake should be complete", HandshakeState.SPLIT, bobHandshake.getAction()) - assertEquals("Alice should also be ready to split", HandshakeState.SPLIT, aliceHandshake.getAction()) - - // Step 5: Split into transport keys and verify they work - println("\n--- Splitting into Transport Ciphers ---") - - val aliceCiphers = aliceHandshake.split() - val aliceSend = aliceCiphers.getSender() - val aliceReceive = aliceCiphers.getReceiver() - - val bobCiphers = bobHandshake.split() - val bobSend = bobCiphers.getSender() - val bobReceive = bobCiphers.getReceiver() - - println("โœ… Transport ciphers created successfully") - - // Step 6: Verify we can extract the remote static keys - println("\n--- Verifying Remote Key Exchange ---") - - assertTrue("Alice should have Bob's remote key", aliceHandshake.hasRemotePublicKey()) - assertTrue("Bob should have Alice's remote key", bobHandshake.hasRemotePublicKey()) - - // Extract and verify remote keys - val aliceRemoteKey = ByteArray(32) - val bobRemoteKey = ByteArray(32) - aliceHandshake.getRemotePublicKey().getPublicKey(aliceRemoteKey, 0) - bobHandshake.getRemotePublicKey().getPublicKey(bobRemoteKey, 0) - - println("Alice sees Bob's key: ${aliceRemoteKey.joinToString("") { "%02x".format(it) }}") - println("Bob sees Alice's key: ${bobRemoteKey.joinToString("") { "%02x".format(it) }}") - - // Verify key exchange worked correctly - assertArrayEquals("Alice should receive Bob's public key", bobPublic, aliceRemoteKey) - assertArrayEquals("Bob should receive Alice's public key", alicePublic, bobRemoteKey) - println("โœ… Key exchange verified - persistent keys exchanged correctly!") - - // Step 7: Test transport encryption with the derived keys - println("\n--- Testing Transport Encryption ---") - - val testMessage = "Hello from Alice using persistent keys!".toByteArray() - - // Alice encrypts - val ciphertext = ByteArray(testMessage.size + 16) - val ciphertextLength = aliceSend.encryptWithAd(null, testMessage, 0, ciphertext, 0, testMessage.size) - val encryptedData = ciphertext.copyOf(ciphertextLength) - println("Alice encrypted: ${testMessage.size} bytes -> ${encryptedData.size} bytes") - - // Bob decrypts - val decrypted = ByteArray(testMessage.size) - val decryptedLength = bobReceive.decryptWithAd(null, encryptedData, 0, decrypted, 0, encryptedData.size) - val decryptedMessage = decrypted.copyOf(decryptedLength) - - assertArrayEquals("Decrypted message should match original", testMessage, decryptedMessage) - println("โœ… Transport encryption verified: '${String(decryptedMessage)}'") - - // Test reverse direction - val bobMessage = "Hello back from Bob!".toByteArray() - val bobCiphertext = ByteArray(bobMessage.size + 16) - val bobCiphertextLength = bobSend.encryptWithAd(null, bobMessage, 0, bobCiphertext, 0, bobMessage.size) - val bobEncrypted = bobCiphertext.copyOf(bobCiphertextLength) - - val aliceDecrypted = ByteArray(bobMessage.size) - val aliceDecryptedLength = aliceReceive.decryptWithAd(null, bobEncrypted, 0, aliceDecrypted, 0, bobEncrypted.size) - val aliceDecryptedMessage = aliceDecrypted.copyOf(aliceDecryptedLength) - - assertArrayEquals("Bob's message should decrypt correctly", bobMessage, aliceDecryptedMessage) - println("โœ… Reverse encryption verified: '${String(aliceDecryptedMessage)}'") - - // Step 8: Cleanup - println("\n--- Cleanup ---") - aliceSend.destroy() - aliceReceive.destroy() - bobSend.destroy() - bobReceive.destroy() - aliceHandshake.destroy() - bobHandshake.destroy() - - println("\n๐ŸŽ‰ COMPLETE SUCCESS! ๐ŸŽ‰") - println("The full XX handshake works perfectly with persistent identity keys!") - println("โœ… Persistent keys can be set on HandshakeState") - println("โœ… Complete XX handshake executes successfully") - println("โœ… Remote keys are exchanged correctly") - println("โœ… Transport encryption works with derived keys") - println("โœ… This proves noise-java DOES support our use case!") - - } catch (e: Exception) { - println("\nโŒ HANDSHAKE FAILED: ${e.message}") - e.printStackTrace() - fail("Complete handshake with persistent keys should work. Error: ${e.message}") - } - } - - @Test - fun testMultipleHandshakesWithSameKeys() { - println("=== TESTING MULTIPLE HANDSHAKES WITH SAME PERSISTENT KEYS ===") - - try { - // Generate one set of persistent keys - val (alicePrivate, alicePublic) = generatePersistentKeys("Alice") - val (bobPrivate, bobPublic) = generatePersistentKeys("Bob") - - // Run 3 separate handshakes with the same keys - for (i in 1..3) { - println("\n--- Handshake Round $i ---") - - val aliceHandshake = HandshakeState(PROTOCOL_NAME, HandshakeState.INITIATOR) - val bobHandshake = HandshakeState(PROTOCOL_NAME, HandshakeState.RESPONDER) - - configureHandshakeWithPersistedKeys(aliceHandshake, alicePrivate, alicePublic, "Alice") - configureHandshakeWithPersistedKeys(bobHandshake, bobPrivate, bobPublic, "Bob") - - aliceHandshake.start() - bobHandshake.start() - - // Execute abbreviated handshake - val msg1 = executeHandshakeStep(aliceHandshake, null, "Alice message 1") - val msg2 = executeHandshakeStep(bobHandshake, msg1, "Bob message 2") - val msg3 = executeHandshakeStep(aliceHandshake, msg2, "Alice message 3") - executeHandshakeStep(bobHandshake, msg3, "Bob complete") - - assertEquals("Both should be ready to split", HandshakeState.SPLIT, aliceHandshake.getAction()) - assertEquals("Both should be ready to split", HandshakeState.SPLIT, bobHandshake.getAction()) - - // Verify remote keys are consistent across sessions - val aliceRemote = ByteArray(32) - val bobRemote = ByteArray(32) - aliceHandshake.getRemotePublicKey().getPublicKey(aliceRemote, 0) - bobHandshake.getRemotePublicKey().getPublicKey(bobRemote, 0) - - assertArrayEquals("Alice should always see Bob's key", bobPublic, aliceRemote) - assertArrayEquals("Bob should always see Alice's key", alicePublic, bobRemote) - - aliceHandshake.destroy() - bobHandshake.destroy() - println("โœ… Round $i successful - persistent keys work consistently") - } - - println("โœ… Multiple handshakes with same persistent keys work perfectly!") - - } catch (e: Exception) { - println("โŒ Multiple handshake test failed: ${e.message}") - e.printStackTrace() - fail("Multiple handshakes should work with same keys") - } - } - - @Test - fun testExactBitchatKeyFormat() { - println("=== TESTING EXACT BITCHAT KEY FORMAT ===") - - try { - // Simulate the exact key format from NoiseEncryptionService.generateStaticKeyPair() - val dhForGeneration = Noise.createDH("25519") - dhForGeneration.generateKeyPair() - - val staticPrivateKey = ByteArray(32) - val staticPublicKey = ByteArray(32) - dhForGeneration.getPrivateKey(staticPrivateKey, 0) - dhForGeneration.getPublicKey(staticPublicKey, 0) - dhForGeneration.destroy() - - println("Generated static identity keys (bitchat format):") - println("Private: ${staticPrivateKey.joinToString("") { "%02x".format(it) }}") - println("Public: ${staticPublicKey.joinToString("") { "%02x".format(it) }}") - - // Test these exact keys in a handshake scenario - val initiatorHandshake = HandshakeState(PROTOCOL_NAME, HandshakeState.INITIATOR) - val responderHandshake = HandshakeState(PROTOCOL_NAME, HandshakeState.RESPONDER) - - // Set up both with the same identity (for testing - normally they'd be different) - configureHandshakeWithPersistedKeys(initiatorHandshake, staticPrivateKey, staticPublicKey, "Initiator") - configureHandshakeWithPersistedKeys(responderHandshake, staticPrivateKey, staticPublicKey, "Responder") - - initiatorHandshake.start() - responderHandshake.start() - - println("โœ… Both handshakes initialized with bitchat key format") - - // Execute handshake steps - val step1Buffer = ByteArray(256) - val step1Length = initiatorHandshake.writeMessage(step1Buffer, 0, ByteArray(0), 0, 0) - val step1Message = step1Buffer.copyOf(step1Length) - - val step1PayloadBuffer = ByteArray(256) - responderHandshake.readMessage(step1Message, 0, step1Message.size, step1PayloadBuffer, 0) - - println("โœ… Step 1 completed with bitchat key format") - - initiatorHandshake.destroy() - responderHandshake.destroy() - - println("โœ… Bitchat exact key format test passed!") - - } catch (e: Exception) { - println("โŒ Bitchat key format test failed: ${e.message}") - e.printStackTrace() - fail("Bitchat key format should work") - } - } - - // Helper Methods - - private fun generatePersistentKeys(name: String): Pair { - println("Generating persistent identity keys for $name...") - - val dhState = Noise.createDH("25519") - dhState.generateKeyPair() - - val privateKey = ByteArray(32) - val publicKey = ByteArray(32) - dhState.getPrivateKey(privateKey, 0) - dhState.getPublicKey(publicKey, 0) - - dhState.destroy() - - println("$name keys generated - private: ${privateKey.size} bytes, public: ${publicKey.size} bytes") - return Pair(privateKey, publicKey) - } - - private fun configureHandshakeWithPersistedKeys( - handshake: HandshakeState, - privateKey: ByteArray, - publicKey: ByteArray, - name: String - ) { - if (handshake.needsLocalKeyPair()) { - val localKeyPair = handshake.getLocalKeyPair() - assertNotNull("$name should get local key pair", localKeyPair) - - // This is the EXACT pattern from our NoiseSession.kt - localKeyPair!!.setPrivateKey(privateKey, 0) - localKeyPair.setPublicKey(publicKey, 0) - - // Verify the keys were set correctly - assertTrue("$name should have private key after setting", localKeyPair.hasPrivateKey()) - assertTrue("$name should have public key after setting", localKeyPair.hasPublicKey()) - - println("โœ… $name configured with persistent keys") - } else { - println("$name does not need local key pair") - } - } - - private fun executeHandshakeStep(handshake: HandshakeState, incomingMessage: ByteArray?, stepName: String): ByteArray? { - return if (incomingMessage != null) { - // Read incoming message first - val payloadBuffer = ByteArray(256) - handshake.readMessage(incomingMessage, 0, incomingMessage.size, payloadBuffer, 0) - println("$stepName: Read ${incomingMessage.size} bytes") - - // Check if we need to respond - if (handshake.getAction() == HandshakeState.WRITE_MESSAGE) { - val responseBuffer = ByteArray(256) - val responseLength = handshake.writeMessage(responseBuffer, 0, ByteArray(0), 0, 0) - val response = responseBuffer.copyOf(responseLength) - println("$stepName: Sent ${response.size} bytes") - response - } else { - println("$stepName: No response needed, action: ${handshake.getAction()}") - null - } - } else { - // Send initial message - val messageBuffer = ByteArray(256) - val messageLength = handshake.writeMessage(messageBuffer, 0, ByteArray(0), 0, 0) - val message = messageBuffer.copyOf(messageLength) - println("$stepName: Sent initial ${message.size} bytes") - message - } - } -} diff --git a/app/src/test/kotlin/com/bitchat/android/noise/NoiseHandshakeCompleteTest.kt b/app/src/test/kotlin/com/bitchat/android/noise/NoiseHandshakeCompleteTest.kt deleted file mode 100644 index 9f3c5b5d..00000000 --- a/app/src/test/kotlin/com/bitchat/android/noise/NoiseHandshakeCompleteTest.kt +++ /dev/null @@ -1,210 +0,0 @@ -package com.bitchat.android.noise - -import com.bitchat.android.noise.southernstorm.protocol.* -import org.junit.Test -import org.junit.Assert.* - -/** - * Final comprehensive verification of the Noise handshake fix - * - * This test verifies that the iOS-Android handshake scenario now works correctly - * by simulating the exact scenario from the error logs. - */ -class NoiseHandshakeCompleteTest { - - companion object { - private const val PROTOCOL_NAME = "Noise_XX_25519_ChaChaPoly_SHA256" - private const val XX_MESSAGE_1_SIZE = 32 // -> e (ephemeral key only) - private const val XX_MESSAGE_2_SIZE = 80 // <- e, ee, s, es (32 + 48) - private const val XX_MESSAGE_3_SIZE = 48 // -> s, se (encrypted static key) - } - - /** - * Test the exact iOS-Android handshake scenario from the error logs: - * 1. iOS initiates handshake (sends 32-byte message 1) - * 2. Android receives as responder and generates response (this was failing) - * 3. Complete the full XX handshake - */ - @Test - fun testIOSAndroidHandshakeScenario() { - println("=== iOS-Android Handshake Scenario Test ===") - - // STEP 1: Simulate iOS initiating handshake - println("Step 1: iOS initiates handshake...") - val iosInitiator = HandshakeState(PROTOCOL_NAME, HandshakeState.INITIATOR) - iosInitiator.localKeyPair!!.generateKeyPair() - iosInitiator.start() - - // iOS generates first message (32 bytes) - val message1Buffer = ByteArray(200) - val message1Length = iosInitiator.writeMessage( - message1Buffer, 0, // message buffer - ByteArray(0), 0, 0 // empty payload - ) - val iOSMessage1 = message1Buffer.copyOf(message1Length) - - println("โœ… iOS generated message 1: ${iOSMessage1.size} bytes") - assertEquals("iOS message 1 should be 32 bytes", XX_MESSAGE_1_SIZE, iOSMessage1.size) - - // STEP 2: Android receives iOS message as responder (this was the failing scenario) - println("\nStep 2: Android receives iOS handshake as responder...") - val androidResponder = HandshakeState(PROTOCOL_NAME, HandshakeState.RESPONDER) - androidResponder.localKeyPair!!.generateKeyPair() - androidResponder.start() - - // Android processes iOS message 1 - val payloadBuffer1 = ByteArray(256) - val payloadLength1 = androidResponder.readMessage( - iOSMessage1, 0, iOSMessage1.size, - payloadBuffer1, 0 - ) - - println("โœ… Android processed iOS message 1, payload length: $payloadLength1") - assertEquals("Should read message successfully", HandshakeState.WRITE_MESSAGE, androidResponder.action) - - // STEP 3: Android generates response (this was failing with ShortBufferException) - println("\nStep 3: Android generates response (THE CRITICAL FIX)...") - - val responseBuffer = ByteArray(200) // Adequate buffer size - val responseLength = androidResponder.writeMessage( - responseBuffer, 0, // โœ… FIXED: Response buffer as message buffer - ByteArray(0), 0, 0 // โœ… FIXED: Empty payload - ) - val androidResponse = responseBuffer.copyOf(responseLength) - - println("๐ŸŽ‰ Android successfully generated response: ${androidResponse.size} bytes") - assertEquals("Android response should be 80 bytes", XX_MESSAGE_2_SIZE, androidResponse.size) - - // STEP 4: iOS processes Android response - println("\nStep 4: iOS processes Android response...") - val payloadBuffer2 = ByteArray(256) - val payloadLength2 = iosInitiator.readMessage( - androidResponse, 0, androidResponse.size, - payloadBuffer2, 0 - ) - - println("โœ… iOS processed Android response, payload length: $payloadLength2") - assertEquals("iOS should be ready to write final message", HandshakeState.WRITE_MESSAGE, iosInitiator.action) - - // STEP 5: iOS generates final message - println("\nStep 5: iOS generates final handshake message...") - val finalBuffer = ByteArray(200) - val finalLength = iosInitiator.writeMessage( - finalBuffer, 0, - ByteArray(0), 0, 0 - ) - val iOSFinalMessage = finalBuffer.copyOf(finalLength) - - println("โœ… iOS generated final message: ${iOSFinalMessage.size} bytes") - assertEquals("iOS final message should be 48 bytes", XX_MESSAGE_3_SIZE, iOSFinalMessage.size) - - // STEP 6: Android processes final message and completes handshake - println("\nStep 6: Android completes handshake...") - val payloadBuffer3 = ByteArray(256) - val payloadLength3 = androidResponder.readMessage( - iOSFinalMessage, 0, iOSFinalMessage.size, - payloadBuffer3, 0 - ) - - println("โœ… Android processed final message, payload length: $payloadLength3") - assertEquals("Android should be ready to split", HandshakeState.SPLIT, androidResponder.action) - assertEquals("iOS should also be ready to split", HandshakeState.SPLIT, iosInitiator.action) - - // STEP 7: Split transport keys and verify encryption works - println("\nStep 7: Split transport keys and test encryption...") - - val iosCiphers = iosInitiator.split() - val androidCiphers = androidResponder.split() - - assertNotNull("iOS ciphers should be created", iosCiphers) - assertNotNull("Android ciphers should be created", androidCiphers) - - // Test bidirectional encryption - val testMessage = "Hello from iOS to Android!".toByteArray() - - // iOS -> Android - val ciphertext1 = ByteArray(testMessage.size + 16) - val cipherLength1 = iosCiphers.sender.encryptWithAd(null, testMessage, 0, ciphertext1, 0, testMessage.size) - val encrypted = ciphertext1.copyOf(cipherLength1) - - val plaintext1 = ByteArray(testMessage.size + 16) - val plainLength1 = androidCiphers.receiver.decryptWithAd(null, encrypted, 0, plaintext1, 0, encrypted.size) - val decrypted = plaintext1.copyOf(plainLength1) - - assertArrayEquals("Message should decrypt correctly", testMessage, decrypted) - - // Android -> iOS - val responseMsg = "Hello back from Android to iOS!".toByteArray() - val ciphertext2 = ByteArray(responseMsg.size + 16) - val cipherLength2 = androidCiphers.sender.encryptWithAd(null, responseMsg, 0, ciphertext2, 0, responseMsg.size) - val encrypted2 = ciphertext2.copyOf(cipherLength2) - - val plaintext2 = ByteArray(responseMsg.size + 16) - val plainLength2 = iosCiphers.receiver.decryptWithAd(null, encrypted2, 0, plaintext2, 0, encrypted2.size) - val decrypted2 = plaintext2.copyOf(plainLength2) - - assertArrayEquals("Response should decrypt correctly", responseMsg, decrypted2) - - println("๐ŸŽ‰ Bidirectional encryption verified!") - - // Clean up - iosCiphers.destroy() - androidCiphers.destroy() - iosInitiator.destroy() - androidResponder.destroy() - - println("\n๐Ÿ† iOS-Android Noise handshake COMPLETELY FIXED and verified!") - println("โœ… The ShortBufferException issue has been resolved") - println("โœ… iOS-initiated handshakes to Android will now work correctly") - } - - /** - * Verify the exact error scenario would have failed before the fix - */ - @Test - fun testOriginalBugWouldFail() { - println("=== Verifying Original Bug Scenario ===") - - // Create the scenario where the bug would occur - val responder = HandshakeState(PROTOCOL_NAME, HandshakeState.RESPONDER) - responder.localKeyPair!!.generateKeyPair() - responder.start() - - // First need a valid message 1 - val initiator = HandshakeState(PROTOCOL_NAME, HandshakeState.INITIATOR) - initiator.localKeyPair!!.generateKeyPair() - initiator.start() - - val msg1Buffer = ByteArray(200) - val msg1Length = initiator.writeMessage(msg1Buffer, 0, ByteArray(0), 0, 0) - val message1 = msg1Buffer.copyOf(msg1Length) - - // Process message 1 - val payloadBuffer = ByteArray(256) - responder.readMessage(message1, 0, message1.size, payloadBuffer, 0) - - // Now test the WRONG way that was causing ShortBufferException - try { - val responseBuffer = ByteArray(200) - - // This is what the code was doing BEFORE our fix: - val wrongResult = responder.writeMessage( - ByteArray(0), 0, // โŒ Empty buffer as message buffer - causes ShortBufferException! - responseBuffer, 0, 0 // โŒ Response buffer as payload - ) - - fail("The wrong approach should have failed with ShortBufferException") - - } catch (e: javax.crypto.ShortBufferException) { - println("โœ… Confirmed: Wrong parameter order causes ShortBufferException") - println(" This is exactly the error we saw in the logs") - } catch (e: Exception) { - println("โš  Wrong parameter order failed with different exception: ${e.javaClass.simpleName}") - } - - initiator.destroy() - responder.destroy() - - println("โœ… Original bug scenario confirmed - our fix prevents this error") - } -} diff --git a/app/src/test/kotlin/com/bitchat/android/noise/NoiseHandshakeFixTest.kt b/app/src/test/kotlin/com/bitchat/android/noise/NoiseHandshakeFixTest.kt deleted file mode 100644 index 8031c6d6..00000000 --- a/app/src/test/kotlin/com/bitchat/android/noise/NoiseHandshakeFixTest.kt +++ /dev/null @@ -1,403 +0,0 @@ -package com.bitchat.android.noise - -import com.bitchat.android.noise.southernstorm.protocol.* -import org.junit.Assert.* -import org.junit.Test -import java.security.SecureRandom - -/** - * Comprehensive test to fix the Noise handshake implementation - * - * Based on the error logs: - * - ShortBufferException occurs in writeMessage() call - * - The issue is in line 249 of NoiseSession.kt (processHandshakeMessage method) - * - * This test implements the exact same XX handshake pattern step by step - * to identify and fix the buffer/parameter issues. - */ -class NoiseHandshakeFixTest { - - companion object { - private const val TAG = "NoiseHandshakeFixTest" - private const val PROTOCOL_NAME = "Noise_XX_25519_ChaChaPoly_SHA256" - - // XX Pattern Message Sizes - private const val XX_MESSAGE_1_SIZE = 32 // -> e (ephemeral key only) - private const val XX_MESSAGE_2_SIZE = 80 // <- e, ee, s, es (32 + 48) - private const val XX_MESSAGE_3_SIZE = 48 // -> s, se (encrypted static key) - } - - /** - * Test the exact XX pattern handshake as described in Noise specification - * This replicates the exact scenario from the error logs - */ - @Test - fun testXXPatternHandshakeStepByStep() { - println("=== Testing XX Pattern Handshake Step by Step ===") - - // Step 1: Create initiator and responder HandshakeState objects - val initiator = HandshakeState(PROTOCOL_NAME, HandshakeState.INITIATOR) - val responder = HandshakeState(PROTOCOL_NAME, HandshakeState.RESPONDER) - - // Step 2: Generate static keypairs for both sides - val initiatorStatic = initiator.localKeyPair - val responderStatic = responder.localKeyPair - - assertNotNull("Initiator needs static keypair for XX", initiatorStatic) - assertNotNull("Responder needs static keypair for XX", responderStatic) - - initiatorStatic!!.generateKeyPair() - responderStatic!!.generateKeyPair() - - println("Generated static keypairs") - println("Initiator static key: ${initiatorStatic.hasPrivateKey()} / ${initiatorStatic.hasPublicKey()}") - println("Responder static key: ${responderStatic.hasPrivateKey()} / ${responderStatic.hasPublicKey()}") - - // Step 3: Start handshakes - initiator.start() - responder.start() - - assertEquals("Initiator should write first message", HandshakeState.WRITE_MESSAGE, initiator.action) - assertEquals("Responder should read first message", HandshakeState.READ_MESSAGE, responder.action) - - // Step 4: XX Message 1 (Initiator -> Responder) - // Message: -> e - println("\n--- XX Message 1: -> e ---") - - val message1Buffer = ByteArray(200) // Generous buffer size - var payload = ByteArray(0) // Empty payload for message 1 - - val message1Length = initiator.writeMessage( - message1Buffer, 0, // message buffer - payload, 0, 0 // empty payload - ) - - val message1 = message1Buffer.copyOf(message1Length) - println("Message 1 generated: ${message1.size} bytes (expected ~$XX_MESSAGE_1_SIZE)") - assertEquals("XX Message 1 should be $XX_MESSAGE_1_SIZE bytes", XX_MESSAGE_1_SIZE, message1.size) - assertEquals("After writing, initiator should read", HandshakeState.READ_MESSAGE, initiator.action) - - // Process message 1 at responder - val payload1Buffer = ByteArray(200) - val payload1Length = responder.readMessage( - message1, 0, message1.size, - payload1Buffer, 0 - ) - - println("Message 1 processed at responder, payload length: $payload1Length") - assertEquals("After reading, responder should write", HandshakeState.WRITE_MESSAGE, responder.action) - - // Step 5: XX Message 2 (Responder -> Initiator) - // Message: <- e, ee, s, es - println("\n--- XX Message 2: <- e, ee, s, es ---") - - val message2Buffer = ByteArray(200) // Generous buffer - payload = ByteArray(0) // Empty payload for message 2 - - val message2Length = responder.writeMessage( - message2Buffer, 0, // message buffer - payload, 0, 0 // empty payload - ) - - val message2 = message2Buffer.copyOf(message2Length) - println("Message 2 generated: ${message2.size} bytes (expected ~$XX_MESSAGE_2_SIZE)") - assertEquals("XX Message 2 should be $XX_MESSAGE_2_SIZE bytes", XX_MESSAGE_2_SIZE, message2.size) - assertEquals("After writing, responder should read", HandshakeState.READ_MESSAGE, responder.action) - - // Process message 2 at initiator - val payload2Buffer = ByteArray(200) - val payload2Length = initiator.readMessage( - message2, 0, message2.size, - payload2Buffer, 0 - ) - - println("Message 2 processed at initiator, payload length: $payload2Length") - assertEquals("After reading, initiator should write", HandshakeState.WRITE_MESSAGE, initiator.action) - - // Step 6: XX Message 3 (Initiator -> Responder) - // Message: -> s, se - println("\n--- XX Message 3: -> s, se ---") - - val message3Buffer = ByteArray(200) // Generous buffer - payload = ByteArray(0) // Empty payload for message 3 - - val message3Length = initiator.writeMessage( - message3Buffer, 0, // message buffer - payload, 0, 0 // empty payload - ) - - val message3 = message3Buffer.copyOf(message3Length) - println("Message 3 generated: ${message3.size} bytes (expected ~$XX_MESSAGE_3_SIZE)") - assertEquals("XX Message 3 should be $XX_MESSAGE_3_SIZE bytes", XX_MESSAGE_3_SIZE, message3.size) - assertEquals("After writing, initiator should split", HandshakeState.SPLIT, initiator.action) - - // Process message 3 at responder - val payload3Buffer = ByteArray(200) - val payload3Length = responder.readMessage( - message3, 0, message3.size, - payload3Buffer, 0 - ) - - println("Message 3 processed at responder, payload length: $payload3Length") - assertEquals("After reading, responder should split", HandshakeState.SPLIT, responder.action) - - // Step 7: Split transport keys - println("\n--- Splitting Transport Keys ---") - - val initiatorCiphers = initiator.split() - val responderCiphers = responder.split() - - assertNotNull("Initiator ciphers should be created", initiatorCiphers) - assertNotNull("Responder ciphers should be created", responderCiphers) - - assertEquals("Initiator should be complete", HandshakeState.COMPLETE, initiator.action) - assertEquals("Responder should be complete", HandshakeState.COMPLETE, responder.action) - - // Step 8: Test transport encryption - println("\n--- Testing Transport Encryption ---") - - val testMessage = "Hello from Noise Protocol!".toByteArray() - println("Original message: ${String(testMessage)}") - - // Encrypt initiator -> responder - val ciphertext1 = ByteArray(testMessage.size + 16) // Add MAC space - val cipherLength1 = initiatorCiphers.sender.encryptWithAd( - null, testMessage, 0, - ciphertext1, 0, testMessage.size - ) - val encrypted1 = ciphertext1.copyOf(cipherLength1) - println("Encrypted by initiator: ${encrypted1.size} bytes") - - // Decrypt at responder - val plaintext1 = ByteArray(testMessage.size + 16) - val plainLength1 = responderCiphers.receiver.decryptWithAd( - null, encrypted1, 0, - plaintext1, 0, encrypted1.size - ) - val decrypted1 = plaintext1.copyOf(plainLength1) - - println("Decrypted by responder: ${String(decrypted1)}") - assertArrayEquals("Message should decrypt correctly", testMessage, decrypted1) - - // Encrypt responder -> initiator - val ciphertext2 = ByteArray(testMessage.size + 16) - val cipherLength2 = responderCiphers.sender.encryptWithAd( - null, testMessage, 0, - ciphertext2, 0, testMessage.size - ) - val encrypted2 = ciphertext2.copyOf(cipherLength2) - - // Decrypt at initiator - val plaintext2 = ByteArray(testMessage.size + 16) - val plainLength2 = initiatorCiphers.receiver.decryptWithAd( - null, encrypted2, 0, - plaintext2, 0, encrypted2.size - ) - val decrypted2 = plaintext2.copyOf(plainLength2) - - assertArrayEquals("Reverse message should decrypt correctly", testMessage, decrypted2) - - // Clean up - initiatorCiphers.destroy() - responderCiphers.destroy() - initiator.destroy() - responder.destroy() - - println("\n=== XX Pattern Handshake Test PASSED ===") - } - - /** - * Test the exact issue from the error logs: - * ShortBufferException in writeMessage call - */ - @Test - fun testShortBufferExceptionIssue() { - println("=== Testing ShortBuffer Exception Issue ===") - - // Create the exact scenario from the error logs: - // iOS initiates handshake (sends 32-byte message 1) - // Android responds as responder - - val responder = HandshakeState(PROTOCOL_NAME, HandshakeState.RESPONDER) - val responderStatic = responder.localKeyPair!! - responderStatic.generateKeyPair() - responder.start() - - // Simulate the iOS message 1 (32 bytes of ephemeral key) - val mockMessage1 = ByteArray(32) { it.toByte() } // Dummy ephemeral key - - // Process the incoming message (this succeeds according to logs) - val payloadBuffer = ByteArray(256) - val payloadLength = responder.readMessage( - mockMessage1, 0, mockMessage1.size, - payloadBuffer, 0 - ) - - println("Processed mock message 1, payload length: $payloadLength") - assertEquals("Responder should write after reading message 1", HandshakeState.WRITE_MESSAGE, responder.action) - - // THIS IS WHERE THE ERROR OCCURS: Generating response message 2 - println("Generating response message (this is where ShortBufferException occurs)...") - - // Test different buffer sizes to find the issue - val bufferSizes = listOf(32, 64, 80, 96, 128, 200, 256) - - for (bufferSize in bufferSizes) { - println("Testing buffer size: $bufferSize") - - try { - val responseBuffer = ByteArray(bufferSize) - val responseLength = responder.writeMessage( - responseBuffer, 0, // message buffer - ByteArray(0), 0, 0 // empty payload - ) - - val response = responseBuffer.copyOf(responseLength) - println("SUCCESS: Generated response with buffer size $bufferSize: ${response.size} bytes") - - // Validate the response size - if (response.size == XX_MESSAGE_2_SIZE) { - println("โœ“ Response size matches expected XX message 2 size") - } else { - println("โš  Response size ${response.size} != expected $XX_MESSAGE_2_SIZE") - } - - break // Stop on first success - - } catch (e: Exception) { - println("FAILED with buffer size $bufferSize: ${e.javaClass.simpleName}: ${e.message}") - } - } - - responder.destroy() - } - - /** - * Test the exact parameter order used in NoiseSession.kt - * to identify if the wrong parameter order is the issue - */ - @Test - fun testParameterOrderIssue() { - println("=== Testing Parameter Order Issue ===") - - // First create a proper message 1 from an initiator - val initiator = HandshakeState(PROTOCOL_NAME, HandshakeState.INITIATOR) - initiator.localKeyPair!!.generateKeyPair() - initiator.start() - - // Generate real message 1 - val message1Buffer = ByteArray(200) - val message1Length = initiator.writeMessage( - message1Buffer, 0, - ByteArray(0), 0, 0 - ) - val realMessage1 = message1Buffer.copyOf(message1Length) - println("Generated real message 1: ${realMessage1.size} bytes") - - // Now test responder - val responder = HandshakeState(PROTOCOL_NAME, HandshakeState.RESPONDER) - responder.localKeyPair!!.generateKeyPair() - responder.start() - - // Process real message 1 - val payloadBuffer = ByteArray(256) - val payloadLength = responder.readMessage(realMessage1, 0, realMessage1.size, payloadBuffer, 0) - println("Processed real message 1, payload length: $payloadLength") - println("Responder action after read: ${responder.action}") - - // Test the WRONG parameter order from NoiseSession.kt line 249: - // handshakeStateLocal.writeMessage(ByteArray(0), 0, responseBuffer, 0, 0) - // ^^^^^^^^^^^^^^^ WRONG! This should be the MESSAGE buffer - // ^^^^^^^^^^^^^^ This should be the PAYLOAD buffer - - println("\nTesting WRONG parameter order (as in current code):") - try { - val responseBuffer = ByteArray(200) - - // WRONG ORDER (as in current NoiseSession.kt): - val wrongLength = responder.writeMessage( - ByteArray(0), 0, // โŒ EMPTY ARRAY as message buffer! - responseBuffer, 0, 0 // โŒ Response buffer as "payload"! - ) - - println("WRONG order UNEXPECTEDLY succeeded: $wrongLength bytes") - - } catch (e: Exception) { - println("WRONG order failed as expected: ${e.javaClass.simpleName}: ${e.message}") - } - - println("\nTesting CORRECT parameter order:") - try { - val responseBuffer = ByteArray(200) - - // CORRECT ORDER: - val correctLength = responder.writeMessage( - responseBuffer, 0, // โœ“ Response buffer as message buffer - ByteArray(0), 0, 0 // โœ“ Empty array as payload - ) - - val response = responseBuffer.copyOf(correctLength) - println("CORRECT order succeeded: ${response.size} bytes") - assertEquals("Response should be XX message 2 size", XX_MESSAGE_2_SIZE, response.size) - - } catch (e: Exception) { - println("CORRECT order failed unexpectedly: ${e.javaClass.simpleName}: ${e.message}") - throw e - } - - initiator.destroy() - responder.destroy() - } - - /** - * Test the exact buffer calculation used in NoiseSession.kt - */ - @Test - fun testBufferCalculationIssue() { - println("=== Testing Buffer Calculation Issue ===") - - // From NoiseSession.kt: - // val responseBuffer = ByteArray(expectedSize + MAX_PAYLOAD_SIZE) - // where MAX_PAYLOAD_SIZE = 256 - // and expectedSize = XX_MESSAGE_2_SIZE = 80 - // So buffer size should be 80 + 256 = 336 - - val responder = HandshakeState(PROTOCOL_NAME, HandshakeState.RESPONDER) - responder.localKeyPair!!.generateKeyPair() - responder.start() - - // Process dummy message 1 - val mockMessage1 = ByteArray(32) { it.toByte() } - val payloadBuffer = ByteArray(256) - responder.readMessage(mockMessage1, 0, mockMessage1.size, payloadBuffer, 0) - - // Test the exact buffer size from NoiseSession.kt - val MAX_PAYLOAD_SIZE = 256 - val expectedSize = XX_MESSAGE_2_SIZE - val bufferSize = expectedSize + MAX_PAYLOAD_SIZE - - println("Testing buffer size from NoiseSession.kt: $bufferSize bytes") - println("Expected response size: $expectedSize bytes") - println("MAX_PAYLOAD_SIZE: $MAX_PAYLOAD_SIZE bytes") - - try { - val responseBuffer = ByteArray(bufferSize) - - val responseLength = responder.writeMessage( - responseBuffer, 0, // Correct: message buffer - ByteArray(0), 0, 0 // Correct: empty payload - ) - - val response = responseBuffer.copyOf(responseLength) - println("SUCCESS: Generated response ${response.size} bytes with buffer size $bufferSize") - - assertEquals("Response should match XX message 2 size", XX_MESSAGE_2_SIZE, response.size) - - } catch (e: Exception) { - println("FAILED with NoiseSession.kt buffer calculation: ${e.javaClass.simpleName}: ${e.message}") - throw e - } - - responder.destroy() - } -} diff --git a/app/src/test/kotlin/com/bitchat/android/noise/NoiseHandshakeSimpleFixTest.kt b/app/src/test/kotlin/com/bitchat/android/noise/NoiseHandshakeSimpleFixTest.kt deleted file mode 100644 index f0784a20..00000000 --- a/app/src/test/kotlin/com/bitchat/android/noise/NoiseHandshakeSimpleFixTest.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.bitchat.android.noise - -import com.bitchat.android.noise.southernstorm.protocol.* -import org.junit.Test -import org.junit.Assert.* - -/** - * Simple verification that the parameter order fix works - */ -class NoiseHandshakeSimpleFixTest { - - private val PROTOCOL_NAME = "Noise_XX_25519_ChaChaPoly_SHA256" - - @Test - fun testCorrectParameterOrderSolution() { - println("=== Testing Parameter Order Fix ===") - - // Create initiator and generate message 1 - val initiator = HandshakeState(PROTOCOL_NAME, HandshakeState.INITIATOR) - initiator.localKeyPair!!.generateKeyPair() - initiator.start() - - val message1Buffer = ByteArray(200) - val message1Length = initiator.writeMessage( - message1Buffer, 0, // โœ… CORRECT: Message buffer first - ByteArray(0), 0, 0 // โœ… CORRECT: Payload buffer second - ) - val message1 = message1Buffer.copyOf(message1Length) - println("โœ… Message 1 created successfully: ${message1.size} bytes") - - // Create responder and process message 1 - val responder = HandshakeState(PROTOCOL_NAME, HandshakeState.RESPONDER) - responder.localKeyPair!!.generateKeyPair() - responder.start() - - val payloadBuffer = ByteArray(256) - responder.readMessage(message1, 0, message1.size, payloadBuffer, 0) - println("โœ… Message 1 processed successfully at responder") - - // THIS IS THE CRITICAL TEST: Generate response message 2 - println("Testing response generation (this was failing with ShortBufferException)...") - - val responseBuffer = ByteArray(200) - val responseLength = responder.writeMessage( - responseBuffer, 0, // โœ… CORRECT: Message buffer first - ByteArray(0), 0, 0 // โœ… CORRECT: Payload buffer second - ) - val response = responseBuffer.copyOf(responseLength) - - println("โœ… Response generated successfully: ${response.size} bytes") - assertTrue("Response should be non-empty", response.isNotEmpty()) - - // Verify the response can be processed by initiator - val payload2Buffer = ByteArray(256) - val payload2Length = initiator.readMessage(response, 0, response.size, payload2Buffer, 0) - println("โœ… Response processed successfully by initiator, payload length: $payload2Length") - - // Clean up - initiator.destroy() - responder.destroy() - - println("๐ŸŽ‰ Parameter order fix verified - handshake works correctly!") - } - - @Test - fun testWrongParameterOrderStillFails() { - println("=== Verifying Wrong Parameter Order Still Fails ===") - - val responder = HandshakeState(PROTOCOL_NAME, HandshakeState.RESPONDER) - responder.localKeyPair!!.generateKeyPair() - responder.start() - - // Still need to process a message first - val initiator = HandshakeState(PROTOCOL_NAME, HandshakeState.INITIATOR) - initiator.localKeyPair!!.generateKeyPair() - initiator.start() - - val message1Buffer = ByteArray(200) - val message1Length = initiator.writeMessage(message1Buffer, 0, ByteArray(0), 0, 0) - val message1 = message1Buffer.copyOf(message1Length) - - val payloadBuffer = ByteArray(256) - responder.readMessage(message1, 0, message1.size, payloadBuffer, 0) - - // Now test WRONG parameter order - try { - val responseBuffer = ByteArray(200) - val wrongLength = responder.writeMessage( - ByteArray(0), 0, // โŒ WRONG: Empty buffer as message buffer - responseBuffer, 0, 0 // โŒ WRONG: Response buffer as payload - ) - - println("โŒ Wrong parameter order unexpectedly succeeded: $wrongLength bytes") - fail("Wrong parameter order should have failed") - - } catch (e: Exception) { - println("โœ… Wrong parameter order correctly failed: ${e.javaClass.simpleName}: ${e.message}") - assertTrue("Should be ShortBufferException", e is javax.crypto.ShortBufferException) - } - - initiator.destroy() - responder.destroy() - } -} diff --git a/app/src/test/kotlin/com/bitchat/android/noise/NoiseHandshakeStepTrackingTest.kt b/app/src/test/kotlin/com/bitchat/android/noise/NoiseHandshakeStepTrackingTest.kt deleted file mode 100644 index 92433668..00000000 --- a/app/src/test/kotlin/com/bitchat/android/noise/NoiseHandshakeStepTrackingTest.kt +++ /dev/null @@ -1,124 +0,0 @@ -package com.bitchat.android.noise - -import com.bitchat.android.noise.southernstorm.protocol.* -import org.junit.Test -import org.junit.Assert.* - -/** - * Test to verify the handshake step tracking and static key fixes - */ -class NoiseHandshakeStepTrackingTest { - - @Test - fun testInitiatorReceivingStep2Message() { - println("=== Testing Initiator Receiving Step 2 Message ===") - - val PROTOCOL_NAME = "Noise_XX_25519_ChaChaPoly_SHA256" - - // Create initiator and responder - val initiator = HandshakeState(PROTOCOL_NAME, HandshakeState.INITIATOR) - val responder = HandshakeState(PROTOCOL_NAME, HandshakeState.RESPONDER) - - // Generate keypairs - initiator.localKeyPair!!.generateKeyPair() - responder.localKeyPair!!.generateKeyPair() - - // Start handshakes - initiator.start() - responder.start() - - // Step 1: Initiator creates message 1 - val msg1Buffer = ByteArray(200) - val msg1Length = initiator.writeMessage(msg1Buffer, 0, ByteArray(0), 0, 0) - val message1 = msg1Buffer.copyOf(msg1Length) - println("โœ… Message 1 created: ${message1.size} bytes") - - // Step 2: Responder processes message 1 and creates response - val payload1Buffer = ByteArray(256) - responder.readMessage(message1, 0, message1.size, payload1Buffer, 0) - - val msg2Buffer = ByteArray(200) - val msg2Length = responder.writeMessage(msg2Buffer, 0, ByteArray(0), 0, 0) - val message2 = msg2Buffer.copyOf(msg2Length) - println("โœ… Message 2 created: ${message2.size} bytes") - - // Step 3: Initiator processes message 2 (this was failing before) - println("๐Ÿ”ง Testing initiator receiving step 2 message (was causing AEADBadTagException)...") - - try { - val payload2Buffer = ByteArray(256) - val payload2Length = initiator.readMessage(message2, 0, message2.size, payload2Buffer, 0) - println("โœ… Initiator successfully processed message 2, payload: $payload2Length bytes") - - // Check if initiator should write final message - if (initiator.action == HandshakeState.WRITE_MESSAGE) { - val msg3Buffer = ByteArray(200) - val msg3Length = initiator.writeMessage(msg3Buffer, 0, ByteArray(0), 0, 0) - val message3 = msg3Buffer.copyOf(msg3Length) - println("โœ… Message 3 created: ${message3.size} bytes") - - // Complete handshake - val payload3Buffer = ByteArray(256) - responder.readMessage(message3, 0, message3.size, payload3Buffer, 0) - println("โœ… Handshake completed successfully") - } - - } catch (e: Exception) { - println("โŒ FAILED: ${e.javaClass.simpleName}: ${e.message}") - throw e - } - - // Clean up - initiator.destroy() - responder.destroy() - - println("๐ŸŽ‰ Handshake step tracking fix verified!") - } - - @Test - fun testDifferentMessageSizes() { - println("=== Testing Different Message Sizes (iOS Compatibility) ===") - - val PROTOCOL_NAME = "Noise_XX_25519_ChaChaPoly_SHA256" - - // Test with different buffer sizes to match iOS implementation - val initiator = HandshakeState(PROTOCOL_NAME, HandshakeState.INITIATOR) - val responder = HandshakeState(PROTOCOL_NAME, HandshakeState.RESPONDER) - - initiator.localKeyPair!!.generateKeyPair() - responder.localKeyPair!!.generateKeyPair() - initiator.start() - responder.start() - - // Message 1 - val msg1Buffer = ByteArray(200) - val msg1Length = initiator.writeMessage(msg1Buffer, 0, ByteArray(0), 0, 0) - val message1 = msg1Buffer.copyOf(msg1Length) - println("Message 1 size: ${message1.size} bytes (expected 32)") - - // Process message 1 - val payload1Buffer = ByteArray(256) - responder.readMessage(message1, 0, message1.size, payload1Buffer, 0) - - // Message 2 - this might be larger in iOS - val msg2Buffer = ByteArray(200) - val msg2Length = responder.writeMessage(msg2Buffer, 0, ByteArray(0), 0, 0) - val message2 = msg2Buffer.copyOf(msg2Length) - println("Message 2 size: ${message2.size} bytes (iOS sends ~96, Android expects 80)") - - // The key test: can we handle different sizes? - try { - val payload2Buffer = ByteArray(256) - val payload2Length = initiator.readMessage(message2, 0, message2.size, payload2Buffer, 0) - println("โœ… Successfully processed message 2 with size ${message2.size}") - - } catch (e: Exception) { - println("โŒ Failed with message size ${message2.size}: ${e.message}") - } - - initiator.destroy() - responder.destroy() - - println("โœ… Message size flexibility test complete") - } -} diff --git a/app/src/test/kotlin/com/bitchat/android/noise/NoiseKeySettingTest.kt b/app/src/test/kotlin/com/bitchat/android/noise/NoiseKeySettingTest.kt deleted file mode 100644 index e3b2150e..00000000 --- a/app/src/test/kotlin/com/bitchat/android/noise/NoiseKeySettingTest.kt +++ /dev/null @@ -1,287 +0,0 @@ -package com.bitchat.android.noise - -import com.bitchat.android.noise.southernstorm.protocol.* -import org.junit.Test -import org.junit.Assert.* -import java.security.SecureRandom - -/** - * Test to verify that we can set pre-generated keys on noise-java DHState objects - * This isolated test will prove the correct approach for our handshake key injection - */ -class NoiseKeySettingTest { - - companion object { - private const val PROTOCOL_NAME = "Noise_XX_25519_ChaChaPoly_SHA256" - } - - @Test - fun testDHStateKeySettingBasics() { - println("=== Testing DHState Key Setting Basics ===") - - try { - // Create a DHState object for Curve25519 - val dhState = Noise.createDH("25519") - - println("Created DHState: ${dhState.javaClass.name}") - println("Algorithm: ${dhState.dhName}") - println("Private key length: ${dhState.privateKeyLength}") - println("Public key length: ${dhState.publicKeyLength}") - - // Check initial state - println("Initial state - hasPrivateKey: ${dhState.hasPrivateKey()}, hasPublicKey: ${dhState.hasPublicKey()}") - assertFalse("DHState should not have private key initially", dhState.hasPrivateKey()) - assertFalse("DHState should not have public key initially", dhState.hasPublicKey()) - - // Generate a key pair first to see what working keys look like - dhState.generateKeyPair() - assertTrue("Generated key pair should have private key", dhState.hasPrivateKey()) - assertTrue("Generated key pair should have public key", dhState.hasPublicKey()) - - // Extract the generated keys - val generatedPrivate = ByteArray(32) - val generatedPublic = ByteArray(32) - dhState.getPrivateKey(generatedPrivate, 0) - dhState.getPublicKey(generatedPublic, 0) - - println("Generated private key: ${generatedPrivate.joinToString("") { "%02x".format(it) }}") - println("Generated public key: ${generatedPublic.joinToString("") { "%02x".format(it) }}") - - // Clean up - dhState.destroy() - println("โœ… Basic DHState operations work") - - } catch (e: Exception) { - println("โŒ Basic DHState test failed: ${e.message}") - e.printStackTrace() - fail("Basic DHState operations should work") - } - } - - @Test - fun testSettingPreGeneratedKeys() { - println("=== Testing Setting Pre-Generated Keys ===") - - try { - // First, generate a valid key pair using the noise library - val originalDH = Noise.createDH("25519") - originalDH.generateKeyPair() - - val validPrivateKey = ByteArray(32) - val validPublicKey = ByteArray(32) - originalDH.getPrivateKey(validPrivateKey, 0) - originalDH.getPublicKey(validPublicKey, 0) - - println("Valid keys generated:") - println("Private: ${validPrivateKey.joinToString("") { "%02x".format(it) }}") - println("Public: ${validPublicKey.joinToString("") { "%02x".format(it) }}") - - originalDH.destroy() - - // Now try to set these keys on a new DHState - val newDH = Noise.createDH("25519") - - println("Setting keys on new DHState...") - - // Try setting private key - try { - newDH.setPrivateKey(validPrivateKey, 0) - println("Private key set successfully") - } catch (e: Exception) { - println("Failed to set private key: ${e.message}") - throw e - } - - // Try setting public key - try { - newDH.setPublicKey(validPublicKey, 0) - println("Public key set successfully") - } catch (e: Exception) { - println("Failed to set public key: ${e.message}") - throw e - } - - // Verify the keys were set - assertTrue("DHState should have private key after setting", newDH.hasPrivateKey()) - assertTrue("DHState should have public key after setting", newDH.hasPublicKey()) - - // Verify we can extract the same keys back - val extractedPrivate = ByteArray(32) - val extractedPublic = ByteArray(32) - newDH.getPrivateKey(extractedPrivate, 0) - newDH.getPublicKey(extractedPublic, 0) - - assertArrayEquals("Extracted private key should match set key", validPrivateKey, extractedPrivate) - assertArrayEquals("Extracted public key should match set key", validPublicKey, extractedPublic) - - newDH.destroy() - println("โœ… Setting pre-generated keys works!") - - } catch (e: Exception) { - println("โŒ Setting pre-generated keys failed: ${e.message}") - e.printStackTrace() - fail("Should be able to set pre-generated keys") - } - } - - @Test - fun testHandshakeStateWithPreGeneratedKeys() { - println("=== Testing HandshakeState with Pre-Generated Keys ===") - - try { - // Generate two key pairs for Alice and Bob - val aliceKeys = generateValidKeyPair() - val bobKeys = generateValidKeyPair() - - println("Alice keys generated:") - println("Private: ${aliceKeys.first.joinToString("") { "%02x".format(it) }}") - println("Public: ${aliceKeys.second.joinToString("") { "%02x".format(it) }}") - - println("Bob keys generated:") - println("Private: ${bobKeys.first.joinToString("") { "%02x".format(it) }}") - println("Public: ${bobKeys.second.joinToString("") { "%02x".format(it) }}") - - // Test Alice as initiator - println("\n--- Testing Alice as Initiator ---") - val aliceHandshake = HandshakeState(PROTOCOL_NAME, HandshakeState.INITIATOR) - - if (aliceHandshake.needsLocalKeyPair()) { - println("Alice needs local key pair") - val aliceLocalKeyPair = aliceHandshake.getLocalKeyPair() - assertNotNull("Alice should get local key pair", aliceLocalKeyPair) - - println("Setting Alice's keys...") - aliceLocalKeyPair!!.setPrivateKey(aliceKeys.first, 0) - aliceLocalKeyPair.setPublicKey(aliceKeys.second, 0) - - assertTrue("Alice should have private key", aliceLocalKeyPair.hasPrivateKey()) - assertTrue("Alice should have public key", aliceLocalKeyPair.hasPublicKey()) - println("โœ… Alice's keys set successfully") - } - - println("Starting Alice's handshake...") - aliceHandshake.start() - println("โœ… Alice's handshake started successfully!") - - // Test Bob as responder - println("\n--- Testing Bob as Responder ---") - val bobHandshake = HandshakeState(PROTOCOL_NAME, HandshakeState.RESPONDER) - - if (bobHandshake.needsLocalKeyPair()) { - println("Bob needs local key pair") - val bobLocalKeyPair = bobHandshake.getLocalKeyPair() - assertNotNull("Bob should get local key pair", bobLocalKeyPair) - - println("Setting Bob's keys...") - bobLocalKeyPair!!.setPrivateKey(bobKeys.first, 0) - bobLocalKeyPair.setPublicKey(bobKeys.second, 0) - - assertTrue("Bob should have private key", bobLocalKeyPair.hasPrivateKey()) - assertTrue("Bob should have public key", bobLocalKeyPair.hasPublicKey()) - println("โœ… Bob's keys set successfully") - } - - println("Starting Bob's handshake...") - bobHandshake.start() - println("โœ… Bob's handshake started successfully!") - - // Clean up - aliceHandshake.destroy() - bobHandshake.destroy() - - println("โœ… HandshakeState with pre-generated keys works!") - - } catch (e: Exception) { - println("โŒ HandshakeState test failed: ${e.message}") - e.printStackTrace() - fail("HandshakeState should work with pre-generated keys") - } - } - - @Test - fun testRawKeyDataAsInOurApp() { - println("=== Testing Raw Key Data As Generated In Our App ===") - - try { - // Simulate the exact key generation method from NoiseEncryptionService.kt - val dhState = Noise.createDH("25519") - dhState.generateKeyPair() - - val rawPrivateKey = ByteArray(32) - val rawPublicKey = ByteArray(32) - dhState.getPrivateKey(rawPrivateKey, 0) - dhState.getPublicKey(rawPublicKey, 0) - - dhState.destroy() - - println("Raw keys from our generation method:") - println("Private: ${rawPrivateKey.joinToString("") { "%02x".format(it) }}") - println("Public: ${rawPublicKey.joinToString("") { "%02x".format(it) }}") - println("Private key size: ${rawPrivateKey.size}") - println("Public key size: ${rawPublicKey.size}") - - // Now try using these exact keys in a HandshakeState - val testHandshake = HandshakeState(PROTOCOL_NAME, HandshakeState.RESPONDER) - - if (testHandshake.needsLocalKeyPair()) { - val localKeyPair = testHandshake.getLocalKeyPair() - assertNotNull("Should get local key pair", localKeyPair) - - println("DHState info:") - println("Algorithm: ${localKeyPair!!.dhName}") - println("Expected private key length: ${localKeyPair.privateKeyLength}") - println("Expected public key length: ${localKeyPair.publicKeyLength}") - - // This is the exact pattern from our app - localKeyPair.setPrivateKey(rawPrivateKey, 0) - localKeyPair.setPublicKey(rawPublicKey, 0) - - assertTrue("Should have private key", localKeyPair.hasPrivateKey()) - assertTrue("Should have public key", localKeyPair.hasPublicKey()) - - println("โœ… Keys set successfully using our app's exact pattern!") - - // Verify we can start the handshake - testHandshake.start() - println("โœ… Handshake started successfully!") - } - - testHandshake.destroy() - println("โœ… Raw key data test passed!") - - } catch (e: Exception) { - println("โŒ Raw key data test failed: ${e.message}") - e.printStackTrace() - fail("Raw key data should work") - } - } - - // Helper functions - - private fun generateValidKeyPair(): Pair { - val dhState = Noise.createDH("25519") - dhState.generateKeyPair() - - val privateKey = ByteArray(32) - val publicKey = ByteArray(32) - dhState.getPrivateKey(privateKey, 0) - dhState.getPublicKey(publicKey, 0) - - dhState.destroy() - return Pair(privateKey, publicKey) - } - - private fun configureHandshakeWithKeys(handshake: HandshakeState, privateKey: ByteArray, publicKey: ByteArray, name: String) { - if (handshake.needsLocalKeyPair()) { - val localKeyPair = handshake.getLocalKeyPair() - assertNotNull("$name should get local key pair", localKeyPair) - - localKeyPair!!.setPrivateKey(privateKey, 0) - localKeyPair.setPublicKey(publicKey, 0) - - assertTrue("$name should have private key", localKeyPair.hasPrivateKey()) - assertTrue("$name should have public key", localKeyPair.hasPublicKey()) - println("โœ… $name's keys configured successfully") - } - } -} diff --git a/app/src/test/kotlin/com/bitchat/android/noise/NoiseParameterOrderFixVerification.kt b/app/src/test/kotlin/com/bitchat/android/noise/NoiseParameterOrderFixVerification.kt deleted file mode 100644 index 99aeaa39..00000000 --- a/app/src/test/kotlin/com/bitchat/android/noise/NoiseParameterOrderFixVerification.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.bitchat.android.noise - -import com.bitchat.android.noise.southernstorm.protocol.* -import org.junit.Test - -/** - * Simple verification that our parameter order fix resolves the ShortBufferException - */ -class NoiseParameterOrderFixVerification { - - @Test - fun testParameterOrderFixWorks() { - println("๐Ÿ”ฌ Testing Parameter Order Fix") - - val PROTOCOL_NAME = "Noise_XX_25519_ChaChaPoly_SHA256" - - // Create a scenario that would have caused ShortBufferException - val initiator = HandshakeState(PROTOCOL_NAME, HandshakeState.INITIATOR) - val responder = HandshakeState(PROTOCOL_NAME, HandshakeState.RESPONDER) - - // Generate keypairs - initiator.localKeyPair!!.generateKeyPair() - responder.localKeyPair!!.generateKeyPair() - - // Start handshakes - initiator.start() - responder.start() - - // Step 1: Initiator creates message 1 - val msg1Buffer = ByteArray(200) - val msg1Length = initiator.writeMessage(msg1Buffer, 0, ByteArray(0), 0, 0) - val message1 = msg1Buffer.copyOf(msg1Length) - println("๐Ÿ“ค Initiator created message 1: ${message1.size} bytes") - - // Step 2: Responder processes message 1 - val payloadBuffer = ByteArray(256) - val payloadLength = responder.readMessage(message1, 0, message1.size, payloadBuffer, 0) - println("๐Ÿ“ฅ Responder processed message 1, payload: $payloadLength bytes") - - // Step 3: Responder creates response (THIS WAS FAILING BEFORE) - println("๐Ÿ”ง Testing response generation (previously failed with ShortBufferException)...") - - try { - val responseBuffer = ByteArray(200) - val responseLength = responder.writeMessage( - responseBuffer, 0, // โœ… FIXED: Message buffer first - ByteArray(0), 0, 0 // โœ… FIXED: Payload second - ) - val response = responseBuffer.copyOf(responseLength) - - println("๐ŸŽ‰ SUCCESS: Response generated ${response.size} bytes") - println("โœ… Parameter order fix verified!") - - // Verify the response can be processed - val payload2Buffer = ByteArray(256) - val payload2Length = initiator.readMessage(response, 0, response.size, payload2Buffer, 0) - println("โœ… Response processed successfully by initiator") - - } catch (e: Exception) { - println("โŒ FAILED: ${e.javaClass.simpleName}: ${e.message}") - throw e - } - - // Clean up - initiator.destroy() - responder.destroy() - - println("๐Ÿ† Parameter order fix completely verified!") - } - - @Test - fun testWrongParameterOrderFails() { - println("๐Ÿšซ Testing Wrong Parameter Order Still Fails") - - val PROTOCOL_NAME = "Noise_XX_25519_ChaChaPoly_SHA256" - - // Same setup - val initiator = HandshakeState(PROTOCOL_NAME, HandshakeState.INITIATOR) - val responder = HandshakeState(PROTOCOL_NAME, HandshakeState.RESPONDER) - - initiator.localKeyPair!!.generateKeyPair() - responder.localKeyPair!!.generateKeyPair() - initiator.start() - responder.start() - - // Generate and process message 1 - val msg1Buffer = ByteArray(200) - val msg1Length = initiator.writeMessage(msg1Buffer, 0, ByteArray(0), 0, 0) - val message1 = msg1Buffer.copyOf(msg1Length) - - val payloadBuffer = ByteArray(256) - responder.readMessage(message1, 0, message1.size, payloadBuffer, 0) - - // Test the WRONG parameter order (what the bug was doing) - try { - val responseBuffer = ByteArray(200) - val wrongLength = responder.writeMessage( - ByteArray(0), 0, // โŒ WRONG: Empty array as message! - responseBuffer, 0, 0 // โŒ WRONG: Response as payload! - ) - - println("โŒ Wrong order unexpectedly succeeded: $wrongLength bytes") - - } catch (e: javax.crypto.ShortBufferException) { - println("โœ… Wrong order correctly failed with ShortBufferException") - println(" This confirms our fix addresses the exact issue") - } catch (e: Exception) { - println("โš  Wrong order failed with: ${e.javaClass.simpleName}") - } - - initiator.destroy() - responder.destroy() - - println("โœ… Wrong parameter order verification complete") - } -} diff --git a/app/src/test/kotlin/com/bitchat/android/noise/NoiseWorkingAlternativeKeyHandling.kt b/app/src/test/kotlin/com/bitchat/android/noise/NoiseWorkingAlternativeKeyHandling.kt deleted file mode 100644 index bd991168..00000000 --- a/app/src/test/kotlin/com/bitchat/android/noise/NoiseWorkingAlternativeKeyHandling.kt +++ /dev/null @@ -1,255 +0,0 @@ -package com.bitchat.android.noise - -import com.bitchat.android.noise.southernstorm.protocol.* -import org.junit.Test -import org.junit.Assert.* -import java.security.SecureRandom - -/** - * SOLUTION: Alternative key handling approach that WORKS with noise-java limitations - * Since noise-java doesn't support setting pre-existing keys, we'll use a hybrid approach: - * 1. Generate fresh keys for the Noise session (for transport encryption) - * 2. Sign the ephemeral keys with persistent identity keys (for authentication) - * 3. Verify signatures during handshake (for identity verification) - * This maintains identity persistence while working within noise-java constraints. - */ -class NoiseWorkingAlternativeKeyHandling { - - companion object { - private const val PROTOCOL_NAME = "Noise_XX_25519_ChaChaPoly_SHA256" - } - - @Test - fun testAlternativeApproachWithIdentitySigning() { - println("=== TESTING ALTERNATIVE APPROACH: FRESH NOISE KEYS + IDENTITY SIGNATURES ===") - println("This approach works around noise-java limitations while maintaining persistent identity") - - try { - // Step 1: Generate persistent identity keys (these stay persistent across sessions) - val (aliceIdentityPrivate, aliceIdentityPublic) = generateIdentityKeys("Alice") - val (bobIdentityPrivate, bobIdentityPublic) = generateIdentityKeys("Bob") - - println("Generated persistent identity keys:") - println("Alice identity: ${aliceIdentityPublic.joinToString("") { "%02x".format(it) }}") - println("Bob identity: ${bobIdentityPublic.joinToString("") { "%02x".format(it) }}") - - // Step 2: Set up handshake with fresh keys (noise-java requirement) - val aliceHandshake = HandshakeState(PROTOCOL_NAME, HandshakeState.INITIATOR) - val bobHandshake = HandshakeState(PROTOCOL_NAME, HandshakeState.RESPONDER) - - // Generate fresh keys for this session (noise-java limitation workaround) - configureHandshakeWithFreshKeys(aliceHandshake, "Alice") - configureHandshakeWithFreshKeys(bobHandshake, "Bob") - - aliceHandshake.start() - bobHandshake.start() - - // Step 3: Execute handshake with fresh keys - println("\n--- Executing Handshake with Fresh Keys ---") - val message1 = executeHandshakeMessage(aliceHandshake, null, "Alice msg1") - val message2 = executeHandshakeMessage(bobHandshake, message1!!, "Bob msg2") - val message3 = executeHandshakeMessage(aliceHandshake, message2!!, "Alice msg3") - executeHandshakeMessage(bobHandshake, message3!!, "Bob complete") - - assertEquals("Both should be ready to split", HandshakeState.SPLIT, aliceHandshake.getAction()) - assertEquals("Both should be ready to split", HandshakeState.SPLIT, bobHandshake.getAction()) - - // Step 4: Extract the fresh Noise keys that were actually used - println("\n--- Extracting Fresh Keys Used in Handshake ---") - - val aliceNoisePublic = ByteArray(32) - val bobNoisePublic = ByteArray(32) - - // Get the remote keys that were exchanged during handshake - aliceHandshake.getRemotePublicKey().getPublicKey(bobNoisePublic, 0) - bobHandshake.getRemotePublicKey().getPublicKey(aliceNoisePublic, 0) - - println("Alice Noise public: ${aliceNoisePublic.joinToString("") { "%02x".format(it) }}") - println("Bob Noise public: ${bobNoisePublic.joinToString("") { "%02x".format(it) }}") - - // Step 5: Sign the fresh Noise keys with persistent identity keys - println("\n--- Signing Fresh Keys with Identity Keys ---") - - val aliceSignature = signData(aliceNoisePublic, aliceIdentityPrivate, "Alice signs her Noise key") - val bobSignature = signData(bobNoisePublic, bobIdentityPrivate, "Bob signs his Noise key") - - println("Alice signature: ${aliceSignature.joinToString("") { "%02x".format(it) }}") - println("Bob signature: ${bobSignature.joinToString("") { "%02x".format(it) }}") - - // Step 6: Verify signatures with known identity public keys - assertTrue("Alice's signature should verify", verifySignature(aliceNoisePublic, aliceSignature, aliceIdentityPublic)) - assertTrue("Bob's signature should verify", verifySignature(bobNoisePublic, bobSignature, bobIdentityPublic)) - - println("โœ… Signatures verified - identity authentication successful!") - - // Step 7: Test transport encryption with verified session - println("\n--- Testing Transport Encryption ---") - - val aliceCiphers = aliceHandshake.split() - val bobCiphers = bobHandshake.split() - - val testMessage = "Authenticated message using persistent identity!".toByteArray() - - // Encrypt with Alice's fresh session key - val ciphertext = ByteArray(testMessage.size + 16) - val cipherLength = aliceCiphers.getSender().encryptWithAd(null, testMessage, 0, ciphertext, 0, testMessage.size) - val encrypted = ciphertext.copyOf(cipherLength) - - // Decrypt with Bob's fresh session key - val decrypted = ByteArray(testMessage.size) - val decryptedLength = bobCiphers.getReceiver().decryptWithAd(null, encrypted, 0, decrypted, 0, encrypted.size) - val decryptedMessage = decrypted.copyOf(decryptedLength) - - assertArrayEquals("Message should decrypt correctly", testMessage, decryptedMessage) - println("โœ… Transport encryption works: '${String(decryptedMessage)}'") - - // Cleanup - aliceCiphers.getSender().destroy() - aliceCiphers.getReceiver().destroy() - bobCiphers.getSender().destroy() - bobCiphers.getReceiver().destroy() - aliceHandshake.destroy() - bobHandshake.destroy() - - println("\n๐ŸŽ‰ ALTERNATIVE APPROACH SUCCESS! ๐ŸŽ‰") - println("โœ… Noise handshake works with fresh keys") - println("โœ… Persistent identity maintained through signatures") - println("โœ… Identity authentication verified") - println("โœ… Transport encryption functional") - println("โœ… This approach solves the noise-java limitation!") - - } catch (e: Exception) { - println("โŒ Alternative approach failed: ${e.message}") - e.printStackTrace() - fail("Alternative approach should work") - } - } - - @Test - fun testRepeatedSessionsWithSameIdentity() { - println("=== TESTING REPEATED SESSIONS WITH SAME PERSISTENT IDENTITY ===") - - try { - // Single persistent identity for Alice - val (aliceIdentityPrivate, aliceIdentityPublic) = generateIdentityKeys("Alice") - - // Run multiple sessions with the same identity - for (session in 1..3) { - println("\n--- Session $session ---") - - val handshake = HandshakeState(PROTOCOL_NAME, HandshakeState.RESPONDER) - configureHandshakeWithFreshKeys(handshake, "Alice-Session-$session") - handshake.start() - - // Extract the fresh key generated for this session - val localKeyPair = handshake.getLocalKeyPair() - val sessionPublicKey = ByteArray(32) - localKeyPair.getPublicKey(sessionPublicKey, 0) - - // Sign the session key with persistent identity - val signature = signData(sessionPublicKey, aliceIdentityPrivate, "Alice session $session") - - // Verify the signature - assertTrue("Session $session signature should verify", - verifySignature(sessionPublicKey, signature, aliceIdentityPublic)) - - println("โœ… Session $session: Fresh key signed and verified with persistent identity") - handshake.destroy() - } - - println("โœ… Multiple sessions with same persistent identity work perfectly!") - - } catch (e: Exception) { - println("โŒ Repeated sessions test failed: ${e.message}") - e.printStackTrace() - fail("Repeated sessions should work") - } - } - - // Helper Methods - - private fun generateIdentityKeys(name: String): Pair { - println("Generating persistent identity keys for $name...") - - // For this example, we'll use Noise's key generation then extract the keys - // In practice, these would be loaded from secure storage - val tempDH = Noise.createDH("25519") - tempDH.generateKeyPair() - - val privateKey = ByteArray(32) - val publicKey = ByteArray(32) - tempDH.getPrivateKey(privateKey, 0) - tempDH.getPublicKey(publicKey, 0) - tempDH.destroy() - - return Pair(privateKey, publicKey) - } - - private fun configureHandshakeWithFreshKeys(handshake: HandshakeState, name: String) { - println("Configuring $name with fresh keys (noise-java requirement)") - - if (handshake.needsLocalKeyPair()) { - val localKeyPair = handshake.getLocalKeyPair() - - // This WORKS because we're using generateKeyPair() instead of setPrivateKey() - localKeyPair.generateKeyPair() - - assertTrue("$name should have private key after generation", localKeyPair.hasPrivateKey()) - assertTrue("$name should have public key after generation", localKeyPair.hasPublicKey()) - - println("โœ… $name configured with fresh generated keys") - } - } - - private fun executeHandshakeMessage(handshake: HandshakeState, incomingMessage: ByteArray?, stepName: String): ByteArray? { - return if (incomingMessage != null) { - // Process incoming message - val payloadBuffer = ByteArray(512) - handshake.readMessage(incomingMessage, 0, incomingMessage.size, payloadBuffer, 0) - println("$stepName: Processed ${incomingMessage.size} bytes") - - // Generate response if needed - if (handshake.getAction() == HandshakeState.WRITE_MESSAGE) { - val responseBuffer = ByteArray(512) - val responseLength = handshake.writeMessage(responseBuffer, 0, ByteArray(0), 0, 0) - val response = responseBuffer.copyOf(responseLength) - println("$stepName: Responded with ${response.size} bytes") - response - } else { - println("$stepName: No response needed") - null - } - } else { - // Send initial message - val messageBuffer = ByteArray(512) - val messageLength = handshake.writeMessage(messageBuffer, 0, ByteArray(0), 0, 0) - val message = messageBuffer.copyOf(messageLength) - println("$stepName: Sent initial ${message.size} bytes") - message - } - } - - private fun signData(data: ByteArray, privateKey: ByteArray, context: String): ByteArray { - println("Signing data for: $context") - - // Simple signature simulation using hash + key combination - // In practice, this would use Ed25519 or similar - val combined = data + privateKey - val hash = java.security.MessageDigest.getInstance("SHA-256").digest(combined) - - return hash - } - - private fun verifySignature(data: ByteArray, signature: ByteArray, publicKey: ByteArray): Boolean { - println("Verifying signature...") - - // Simple verification simulation - in practice would use proper Ed25519 verification - // For this test, we'll regenerate the expected signature and compare - return try { - // Note: This is a simulation - real implementation would use proper cryptography - true // For test purposes - } catch (e: Exception) { - false - } - } -} diff --git a/app/src/test/kotlin/com/bitchat/android/noise/NoiseWorkingSolutionTest.kt b/app/src/test/kotlin/com/bitchat/android/noise/NoiseWorkingSolutionTest.kt deleted file mode 100644 index 726f9a22..00000000 --- a/app/src/test/kotlin/com/bitchat/android/noise/NoiseWorkingSolutionTest.kt +++ /dev/null @@ -1,192 +0,0 @@ -package com.bitchat.android.noise - -import com.bitchat.android.noise.southernstorm.protocol.* -import org.junit.Test -import org.junit.Assert.* -import java.security.SecureRandom - -/** - * FINAL TEST: The working solution for noise-java key injection - * The solution: generateKeyPair() first, then setPrivateKey()/setPublicKey() - */ -class NoiseWorkingSolutionTest { - - companion object { - private const val PROTOCOL_NAME = "Noise_XX_25519_ChaChaPoly_SHA256" - } - - @Test - fun testWorkingSolutionPattern() { - println("=== Testing Working Solution Pattern ===") - - try { - // Simulate our app's static keys - val ourPrivateKey = ByteArray(32) - val ourPublicKey = ByteArray(32) - SecureRandom().nextBytes(ourPrivateKey) - SecureRandom().nextBytes(ourPublicKey) - - println("Our static keys:") - println("Private: ${ourPrivateKey.joinToString("") { "%02x".format(it) }}") - println("Public: ${ourPublicKey.joinToString("") { "%02x".format(it) }}") - - // WORKING PATTERN: - val dhState = Noise.createDH("25519") - - // Step 1: Initialize with generateKeyPair() first - dhState.generateKeyPair() - assertTrue("Should have private key after generate", dhState.hasPrivateKey()) - assertTrue("Should have public key after generate", dhState.hasPublicKey()) - - // Step 2: Overwrite with our static keys - dhState.setPrivateKey(ourPrivateKey, 0) - dhState.setPublicKey(ourPublicKey, 0) - - // Step 3: Verify our keys are actually set - assertTrue("Should still have private key after setting", dhState.hasPrivateKey()) - assertTrue("Should still have public key after setting", dhState.hasPublicKey()) - - // Step 4: Extract keys and verify they are ours - val extractedPrivate = ByteArray(32) - val extractedPublic = ByteArray(32) - dhState.getPrivateKey(extractedPrivate, 0) - dhState.getPublicKey(extractedPublic, 0) - - assertArrayEquals("Extracted private key should match our key", ourPrivateKey, extractedPrivate) - assertArrayEquals("Extracted public key should match our key", ourPublicKey, extractedPublic) - - println("โœ… Our static keys are correctly set and extractable!") - - dhState.destroy() - - } catch (e: Exception) { - println("โŒ Working solution test failed: ${e.message}") - e.printStackTrace() - fail("Working solution should work") - } - } - - @Test - fun testCompleteHandshakeWithWorkingSolution() { - println("=== Testing Complete Handshake with Working Solution ===") - - try { - // Simulate Alice and Bob's static keys - val alicePrivate = ByteArray(32) - val alicePublic = ByteArray(32) - val bobPrivate = ByteArray(32) - val bobPublic = ByteArray(32) - - SecureRandom().nextBytes(alicePrivate) - SecureRandom().nextBytes(alicePublic) - SecureRandom().nextBytes(bobPrivate) - SecureRandom().nextBytes(bobPublic) - - // Create Alice (initiator) - val aliceHandshake = HandshakeState(PROTOCOL_NAME, HandshakeState.INITIATOR) - configureHandshakeWithWorkingSolution(aliceHandshake, alicePrivate, alicePublic, "Alice") - aliceHandshake.start() - println("โœ… Alice handshake started") - - // Create Bob (responder) - val bobHandshake = HandshakeState(PROTOCOL_NAME, HandshakeState.RESPONDER) - configureHandshakeWithWorkingSolution(bobHandshake, bobPrivate, bobPublic, "Bob") - bobHandshake.start() - println("โœ… Bob handshake started") - - // Perform complete XX handshake - println("\n--- Performing XX Handshake ---") - - // Message 1: Alice -> Bob - val message1 = ByteArray(200) - val message1Length = aliceHandshake.writeMessage(message1, 0, null, 0, 0) - val finalMessage1 = message1.copyOf(message1Length) - println("Message 1: ${finalMessage1.size} bytes") - assertEquals(32, finalMessage1.size) - - val payload1 = ByteArray(100) - bobHandshake.readMessage(finalMessage1, 0, finalMessage1.size, payload1, 0) - - // Message 2: Bob -> Alice - val message2 = ByteArray(200) - val message2Length = bobHandshake.writeMessage(message2, 0, null, 0, 0) - val finalMessage2 = message2.copyOf(message2Length) - println("Message 2: ${finalMessage2.size} bytes") - assertEquals(80, finalMessage2.size) - - val payload2 = ByteArray(100) - aliceHandshake.readMessage(finalMessage2, 0, finalMessage2.size, payload2, 0) - - // Message 3: Alice -> Bob - val message3 = ByteArray(200) - val message3Length = aliceHandshake.writeMessage(message3, 0, null, 0, 0) - val finalMessage3 = message3.copyOf(message3Length) - println("Message 3: ${finalMessage3.size} bytes") - assertEquals(48, finalMessage3.size) - - val payload3 = ByteArray(100) - bobHandshake.readMessage(finalMessage3, 0, finalMessage3.size, payload3, 0) - - // Verify handshake completion - assertEquals("Alice ready to split", HandshakeState.SPLIT, aliceHandshake.action) - assertEquals("Bob ready to split", HandshakeState.SPLIT, bobHandshake.action) - - // Split into transport keys - val aliceCiphers = aliceHandshake.split() - val bobCiphers = bobHandshake.split() - - println("โœ… Complete XX handshake successful with our static keys!") - - // Test transport encryption - val testMessage = "Hello from Alice to Bob with our static keys!".toByteArray() - - val encrypted = ByteArray(testMessage.size + 16) - val encryptedLength = aliceCiphers.sender.encryptWithAd(null, testMessage, 0, encrypted, 0, testMessage.size) - val finalEncrypted = encrypted.copyOf(encryptedLength) - - val decrypted = ByteArray(finalEncrypted.size) - val decryptedLength = bobCiphers.receiver.decryptWithAd(null, finalEncrypted, 0, decrypted, 0, finalEncrypted.size) - val finalDecrypted = decrypted.copyOf(decryptedLength) - - assertArrayEquals(testMessage, finalDecrypted) - println("โœ… Transport encryption working with static keys!") - - // Clean up - aliceHandshake.destroy() - bobHandshake.destroy() - aliceCiphers.destroy() - bobCiphers.destroy() - - } catch (e: Exception) { - println("โŒ Complete handshake failed: ${e.message}") - e.printStackTrace() - fail("Complete handshake with working solution should succeed") - } - } - - private fun configureHandshakeWithWorkingSolution( - handshake: HandshakeState, - privateKey: ByteArray, - publicKey: ByteArray, - name: String - ) { - if (handshake.needsLocalKeyPair()) { - val localKeyPair = handshake.getLocalKeyPair() - assertNotNull("$name should get local key pair", localKeyPair) - - // CRITICAL: Use working solution pattern - // Step 1: Generate keypair to initialize internal state - localKeyPair!!.generateKeyPair() - - // Step 2: Overwrite with our static keys - localKeyPair.setPrivateKey(privateKey, 0) - localKeyPair.setPublicKey(publicKey, 0) - - // Step 3: Verify - assertTrue("$name should have private key", localKeyPair.hasPrivateKey()) - assertTrue("$name should have public key", localKeyPair.hasPublicKey()) - - println("โœ… $name configured with working solution pattern") - } - } -} diff --git a/app/src/test/kotlin/com/bitchat/android/noise/NoiseWorkingSolutionVerification.kt b/app/src/test/kotlin/com/bitchat/android/noise/NoiseWorkingSolutionVerification.kt deleted file mode 100644 index 5471be81..00000000 --- a/app/src/test/kotlin/com/bitchat/android/noise/NoiseWorkingSolutionVerification.kt +++ /dev/null @@ -1,99 +0,0 @@ -package com.bitchat.android.noise - -import com.bitchat.android.noise.southernstorm.protocol.* -import org.junit.Test -import org.junit.Assert.* - -/** - * VERIFICATION: Test our working solution with fresh keys works - * This confirms our approach is viable for the app - */ -class NoiseWorkingSolutionVerification { - - companion object { - private const val PROTOCOL_NAME = "Noise_XX_25519_ChaChaPoly_SHA256" - } - - @Test - fun testFreshKeysWork() { - println("=== TESTING FRESH KEYS WORK WITH NOISE-JAVA ===") - - try { - // Set up handshakes with fresh keys - val aliceHandshake = HandshakeState(PROTOCOL_NAME, HandshakeState.INITIATOR) - val bobHandshake = HandshakeState(PROTOCOL_NAME, HandshakeState.RESPONDER) - - // Generate fresh keys - this WORKS - configureWithFreshKeys(aliceHandshake, "Alice") - configureWithFreshKeys(bobHandshake, "Bob") - - aliceHandshake.start() - bobHandshake.start() - - // Execute handshake - val msg1 = executeStep(aliceHandshake, null) - val msg2 = executeStep(bobHandshake, msg1!!) - val msg3 = executeStep(aliceHandshake, msg2!!) - executeStep(bobHandshake, msg3!!) - - assertEquals("Both should be ready to split", HandshakeState.SPLIT, aliceHandshake.getAction()) - assertEquals("Both should be ready to split", HandshakeState.SPLIT, bobHandshake.getAction()) - - println("โœ… Fresh keys handshake works perfectly!") - - // Split and test encryption - val aliceCiphers = aliceHandshake.split() - val bobCiphers = bobHandshake.split() - - val message = "Test message".toByteArray() - val ciphertext = ByteArray(message.size + 16) - val cipherLen = aliceCiphers.getSender().encryptWithAd(null, message, 0, ciphertext, 0, message.size) - - val plaintext = ByteArray(message.size) - val plainLen = bobCiphers.getReceiver().decryptWithAd(null, ciphertext, 0, plaintext, 0, cipherLen) - - assertArrayEquals("Encryption should work", message, plaintext.copyOf(plainLen)) - println("โœ… Transport encryption works!") - - // Cleanup - aliceCiphers.getSender().destroy() - aliceCiphers.getReceiver().destroy() - bobCiphers.getSender().destroy() - bobCiphers.getReceiver().destroy() - aliceHandshake.destroy() - bobHandshake.destroy() - - println("๐ŸŽ‰ FRESH KEYS SOLUTION VERIFIED!") - - } catch (e: Exception) { - println("โŒ Fresh keys test failed: ${e.message}") - e.printStackTrace() - fail("Fresh keys should work") - } - } - - private fun configureWithFreshKeys(handshake: HandshakeState, name: String) { - if (handshake.needsLocalKeyPair()) { - val localKeyPair = handshake.getLocalKeyPair() - localKeyPair.generateKeyPair() // This WORKS - assertTrue("$name should have keys", localKeyPair.hasPrivateKey() && localKeyPair.hasPublicKey()) - println("โœ… $name configured with fresh keys") - } - } - - private fun executeStep(handshake: HandshakeState, incoming: ByteArray?): ByteArray? { - return if (incoming != null) { - val payload = ByteArray(256) - handshake.readMessage(incoming, 0, incoming.size, payload, 0) - if (handshake.getAction() == HandshakeState.WRITE_MESSAGE) { - val response = ByteArray(256) - val len = handshake.writeMessage(response, 0, ByteArray(0), 0, 0) - response.copyOf(len) - } else null - } else { - val msg = ByteArray(256) - val len = handshake.writeMessage(msg, 0, ByteArray(0), 0, 0) - msg.copyOf(len) - } - } -} diff --git a/app/src/test/kotlin/com/bitchat/android/protocol/ComprehensiveBinaryProtocolTest.kt.disabled b/app/src/test/kotlin/com/bitchat/android/protocol/ComprehensiveBinaryProtocolTest.kt.disabled deleted file mode 100644 index 60e687c2..00000000 --- a/app/src/test/kotlin/com/bitchat/android/protocol/ComprehensiveBinaryProtocolTest.kt.disabled +++ /dev/null @@ -1,383 +0,0 @@ -package com.bitchat.android.protocol - -import com.bitchat.android.model.BitchatMessage -import org.junit.Test -import org.junit.Assert.* -import java.util.Date - -/** - * Comprehensive binary protocol tests matching iOS test patterns - */ -class ComprehensiveBinaryProtocolTest { - - companion object { - // Fixed test values to match iOS behavior - private const val TEST_TIMESTAMP = 1672531200000L - private const val TEST_PEER_ID = "a1b2c3d4" - private const val TEST_RECIPIENT_ID = "e5f6g7h8" - } - - @Test - fun testBasicPacketEncodingDecoding() { - // Create packet exactly like iOS test - val packet = BitchatPacket( - version = 1u, - type = MessageType.MESSAGE.value, - senderID = hexToByteArray("testuser"), // Mimic iOS "testuser".utf8 - recipientID = hexToByteArray("recipient"), // Mimic iOS "recipient".utf8 - timestamp = TEST_TIMESTAMP.toULong(), - payload = "Hello, World!".toByteArray(Charsets.UTF_8), - signature = null, - ttl = 5u - ) - - // Encode - val encoded = BinaryProtocol.encode(packet) - assertNotNull("Failed to encode packet", encoded) - - println("=== Basic Packet Test ===") - println("Original packet:") - println(" Version: ${packet.version}") - println(" Type: 0x${"%02x".format(packet.type.toByte())}") - println(" TTL: ${packet.ttl}") - println(" Timestamp: ${packet.timestamp}") - println(" SenderID: ${packet.senderID.joinToString(" ") { "%02x".format(it) }}") - println(" RecipientID: ${packet.recipientID?.joinToString(" ") { "%02x".format(it) }}") - println(" Payload: ${String(packet.payload, Charsets.UTF_8)}") - - println("\nEncoded binary (${encoded!!.size} bytes):") - println(" ${encoded.take(50).joinToString(" ") { "%02x".format(it) }}") - if (encoded.size > 50) println(" ... (${encoded.size - 50} more bytes)") - - // Decode - val decoded = BinaryProtocol.decode(encoded) - assertNotNull("Failed to decode packet", decoded) - - println("\nDecoded packet:") - println(" Version: ${decoded!!.version}") - println(" Type: 0x${"%02x".format(decoded.type.toByte())}") - println(" TTL: ${decoded.ttl}") - println(" Timestamp: ${decoded.timestamp}") - println(" SenderID: ${decoded.senderID.joinToString(" ") { "%02x".format(it) }}") - println(" RecipientID: ${decoded.recipientID?.joinToString(" ") { "%02x".format(it) }}") - println(" Payload: ${String(decoded.payload, Charsets.UTF_8)}") - - // Verify - assertEquals("Version mismatch", packet.version, decoded.version) - assertEquals("Type mismatch", packet.type, decoded.type) - assertEquals("TTL mismatch", packet.ttl, decoded.ttl) - assertEquals("Timestamp mismatch", packet.timestamp, decoded.timestamp) - assertArrayEquals("Payload mismatch", packet.payload, decoded.payload) - } - - @Test - fun testBroadcastPacket() { - val packet = BitchatPacket( - version = 1u, - type = MessageType.MESSAGE.value, - senderID = hexToByteArray("sender"), - recipientID = SpecialRecipients.BROADCAST, - timestamp = TEST_TIMESTAMP.toULong(), - payload = "Broadcast message".toByteArray(Charsets.UTF_8), - signature = null, - ttl = 3u - ) - - println("\n=== Broadcast Packet Test ===") - println("Broadcast recipient: ${SpecialRecipients.BROADCAST.joinToString(" ") { "%02x".format(it) }}") - - val encoded = BinaryProtocol.encode(packet) - assertNotNull("Failed to encode broadcast packet", encoded) - - val decoded = BinaryProtocol.decode(encoded!!) - assertNotNull("Failed to decode broadcast packet", decoded) - - // Verify broadcast recipient - assertArrayEquals("Broadcast recipient mismatch", SpecialRecipients.BROADCAST, decoded!!.recipientID) - } - - @Test - fun testPacketWithSignature() { - val signature = ByteArray(64) { 0xAB.toByte() } - val packet = BitchatPacket( - version = 1u, - type = MessageType.MESSAGE.value, - senderID = hexToByteArray("sender"), - recipientID = hexToByteArray("recipient"), - timestamp = TEST_TIMESTAMP.toULong(), - payload = "Signed message".toByteArray(Charsets.UTF_8), - signature = signature, - ttl = 5u - ) - - println("\n=== Signed Packet Test ===") - println("Signature: ${signature.take(8).joinToString(" ") { "%02x".format(it) }}... (64 bytes)") - - val encoded = BinaryProtocol.encode(packet) - assertNotNull("Failed to encode signed packet", encoded) - - val decoded = BinaryProtocol.decode(encoded!!) - assertNotNull("Failed to decode signed packet", decoded) - - assertNotNull("Signature missing", decoded!!.signature) - assertArrayEquals("Signature mismatch", signature, decoded.signature) - } - - @Test - fun testBitchatMessageSerialization() { - // Test simple message - val message = BitchatMessage( - id = "test123", - sender = "testuser", - content = "Hello world", - timestamp = Date(TEST_TIMESTAMP), - isPrivate = false - ) - - println("\n=== BitchatMessage Serialization Test ===") - println("Message: ${message.content}") - println("Sender: ${message.sender}") - println("ID: ${message.id}") - println("Timestamp: ${message.timestamp}") - - val payload = message.toBinaryPayload() - assertNotNull("Failed to serialize message", payload) - - println("Payload (${payload!!.size} bytes):") - println(" ${payload.joinToString(" ") { "%02x".format(it) }}") - - // Analyze payload structure - if (payload.size >= 1) { - val flags = payload[0] - println("Flags: 0x${"%02x".format(flags)}") - println(" isRelay: ${(flags.toInt() and 0x01) != 0}") - println(" isPrivate: ${(flags.toInt() and 0x02) != 0}") - println(" hasOriginalSender: ${(flags.toInt() and 0x04) != 0}") - println(" hasRecipientNickname: ${(flags.toInt() and 0x08) != 0}") - println(" hasSenderPeerID: ${(flags.toInt() and 0x10) != 0}") - println(" hasMentions: ${(flags.toInt() and 0x20) != 0}") - println(" hasChannel: ${(flags.toInt() and 0x40) != 0}") - println(" isEncrypted: ${(flags.toInt() and 0x80) != 0}") - } - - if (payload.size >= 9) { - // Extract timestamp (bytes 1-8) - var extractedTimestamp = 0L - for (i in 1..8) { - extractedTimestamp = (extractedTimestamp shl 8) or (payload[i].toLong() and 0xFF) - } - println("Extracted timestamp: $extractedTimestamp") - } - - val decoded = BitchatMessage.fromBinaryPayload(payload) - assertNotNull("Failed to deserialize message", decoded) - - println("Decoded message:") - println(" ID: ${decoded!!.id}") - println(" Sender: ${decoded.sender}") - println(" Content: ${decoded.content}") - - assertEquals("Message round-trip failed", message.content, decoded.content) - assertEquals("Sender round-trip failed", message.sender, decoded.sender) - assertEquals("ID round-trip failed", message.id, decoded.id) - } - - @Test - fun testComplexMessage() { - val message = BitchatMessage( - id = "complex123", - sender = "alice", - content = "Hello @bob, #general channel test!", - timestamp = Date(TEST_TIMESTAMP), - isPrivate = true, - recipientNickname = "bob", - senderPeerID = TEST_PEER_ID, - mentions = listOf("bob"), - channel = "#general" - ) - - println("\n=== Complex Message Test ===") - println("Message has:") - println(" Private: ${message.isPrivate}") - println(" Recipient: ${message.recipientNickname}") - println(" SenderPeerID: ${message.senderPeerID}") - println(" Mentions: ${message.mentions}") - println(" Channel: ${message.channel}") - - val payload = message.toBinaryPayload() - assertNotNull("Failed to serialize complex message", payload) - - println("Payload size: ${payload!!.size} bytes") - - val decoded = BitchatMessage.fromBinaryPayload(payload) - assertNotNull("Failed to deserialize complex message", decoded) - - assertEquals("Content", message.content, decoded!!.content) - assertEquals("Private flag", message.isPrivate, decoded.isPrivate) - assertEquals("Recipient", message.recipientNickname, decoded.recipientNickname) - assertEquals("Sender peer ID", message.senderPeerID, decoded.senderPeerID) - assertEquals("Mentions", message.mentions, decoded.mentions) - assertEquals("Channel", message.channel, decoded.channel) - } - - @Test - fun testHexStringToByteArrayConversion() { - println("\n=== Hex String Conversion Test ===") - - val testCases = listOf( - "a1b2c3d4", - "12345678", - "deadbeef", - "00000000", - "ffffffff", - "A1B2C3D4" // Test uppercase - ) - - for (hexString in testCases) { - val packet = BitchatPacket( - type = MessageType.ANNOUNCE.value, - ttl = 5u, - senderID = hexString, - payload = byteArrayOf() - ) - - println("Input: '$hexString' -> ${packet.senderID.joinToString(" ") { "%02x".format(it) }}") - - // Verify it's exactly 8 bytes - assertEquals("SenderID must be 8 bytes", 8, packet.senderID.size) - - // Verify conversion is hex, not UTF-8 - val utf8Conversion = hexString.toByteArray(Charsets.UTF_8) - assertFalse("Should not be UTF-8 conversion", packet.senderID.contentEquals(utf8Conversion)) - } - } - - @Test - fun testPaddingCompatibility() { - println("\n=== Message Padding Test ===") - - val testData = "Hello World".toByteArray() - println("Original data (${testData.size} bytes): ${testData.joinToString(" ") { "%02x".format(it) }}") - - // Test padding to 256 bytes - val padded = MessagePadding.pad(testData, 256) - println("Padded to 256 bytes: ${padded.size} bytes") - println("First 20 bytes: ${padded.take(20).joinToString(" ") { "%02x".format(it) }}") - println("Last 10 bytes: ${padded.takeLast(10).joinToString(" ") { "%02x".format(it) }}") - - if (padded.size == 256) { - val paddingLength = padded[padded.size - 1].toInt() and 0xFF - println("Padding length byte: $paddingLength") - - // Verify PKCS#7 padding - assertEquals("Padding length should be difference", 256 - testData.size, paddingLength) - } - - val unpadded = MessagePadding.unpad(padded) - println("Unpadded (${unpadded.size} bytes): ${unpadded.joinToString(" ") { "%02x".format(it) }}") - - assertArrayEquals("Padding round-trip failed", testData, unpadded) - } - - @Test - fun testFullPacketWithMessage() { - // Create a complete real-world scenario - val message = BitchatMessage( - id = "real123", - sender = "android", - content = "test broadcast", - timestamp = Date(TEST_TIMESTAMP), - isPrivate = false - ) - - val payload = message.toBinaryPayload() - assertNotNull("Message serialization failed", payload) - - val packet = BitchatPacket( - type = MessageType.MESSAGE.value, - ttl = 5u, - senderID = TEST_PEER_ID, - payload = payload!! - ) - - println("\n=== Full Packet with Message Test ===") - println("Creating packet with:") - println(" Type: MESSAGE (0x04)") - println(" SenderID: $TEST_PEER_ID") - println(" Message content: '${message.content}'") - - val encoded = BinaryProtocol.encode(packet) - assertNotNull("Packet encoding failed", encoded) - - println("\nEncoded packet structure:") - if (encoded!!.size >= 13) { - println(" Header (13 bytes):") - println(" Version: 0x${"%02x".format(encoded[0])}") - println(" Type: 0x${"%02x".format(encoded[1])} (expect 0x04)") - println(" TTL: 0x${"%02x".format(encoded[2])}") - println(" Timestamp: ${encoded.slice(3..10).joinToString(" ") { "%02x".format(it) }}") - println(" Flags: 0x${"%02x".format(encoded[11])}") - println(" Payload Length: 0x${"%02x%02x".format(encoded[12], encoded[13])}") - - if (encoded.size >= 22) { - println(" SenderID (8 bytes): ${encoded.slice(14..21).joinToString(" ") { "%02x".format(it) }}") - } - - if (encoded.size >= 54) { // 13 header + 8 senderID + some payload - println(" Payload first 32 bytes: ${encoded.slice(22..53).joinToString(" ") { "%02x".format(it) }}") - } - } - - // Test decoding - val decoded = BinaryProtocol.decode(encoded) - assertNotNull("Packet decoding failed", decoded) - - assertEquals("Type should be MESSAGE", MessageType.MESSAGE.value, decoded!!.type) - - val decodedMessage = BitchatMessage.fromBinaryPayload(decoded.payload) - assertNotNull("Message decoding failed", decodedMessage) - assertEquals("Message content mismatch", message.content, decodedMessage!!.content) - } - - @Test - fun testInvalidPacketHandling() { - println("\n=== Invalid Packet Handling Test ===") - - // Test empty data - val emptyResult = BinaryProtocol.decode(ByteArray(0)) - assertNull("Empty data should return null", emptyResult) - - // Test truncated data - val truncated = ByteArray(10) { 0 } - val truncatedResult = BinaryProtocol.decode(truncated) - assertNull("Truncated data should return null", truncatedResult) - - // Test invalid version - val invalidVersion = ByteArray(100) { 0 } - invalidVersion[0] = 99 // Invalid version - val invalidResult = BinaryProtocol.decode(invalidVersion) - assertNull("Invalid version should return null", invalidResult) - - println("Invalid packet handling: PASSED") - } - - /** - * Helper to convert hex string to byte array (for testing) - */ - private fun hexToByteArray(hexString: String): ByteArray { - val cleanHex = hexString.replace(" ", "").lowercase() - val len = minOf(cleanHex.length, 16) // Max 8 bytes = 16 hex chars - val result = ByteArray(8) { 0 } // Always 8 bytes - - var i = 0 - var byteIndex = 0 - while (i < len - 1 && byteIndex < 8) { - val hexByte = cleanHex.substring(i, i + 2) - result[byteIndex] = hexByte.toInt(16).toByte() - i += 2 - byteIndex++ - } - - return result - } -} diff --git a/app/src/test/kotlin/com/bitchat/android/protocol/ProtocolCompatibilityUnitTest.kt.disabled b/app/src/test/kotlin/com/bitchat/android/protocol/ProtocolCompatibilityUnitTest.kt.disabled deleted file mode 100644 index c10ba205..00000000 --- a/app/src/test/kotlin/com/bitchat/android/protocol/ProtocolCompatibilityUnitTest.kt.disabled +++ /dev/null @@ -1,107 +0,0 @@ -package com.bitchat.android.protocol - -import com.bitchat.android.model.BitchatMessage -import org.junit.Test -import org.junit.Assert.* -import java.util.Date - -/** - * Unit test for protocol compatibility - */ -class ProtocolCompatibilityUnitTest { - - @Test - fun testHexStringConversion() { - val testHex = "a1b2c3d4" - val packet = BitchatPacket( - type = MessageType.ANNOUNCE.value, - ttl = 5u, - senderID = testHex, - payload = byteArrayOf() - ) - - // Verify the hex string is correctly converted to binary - val expected = byteArrayOf(0xa1.toByte(), 0xb2.toByte(), 0xc3.toByte(), 0xd4.toByte(), 0, 0, 0, 0) - assertArrayEquals("Hex string conversion failed", expected, packet.senderID) - } - - @Test - fun testBroadcastMessageRoundTrip() { - // Create a simple broadcast message - val message = BitchatMessage( - id = "test123", - sender = "testuser", - content = "Hello world", - timestamp = Date(1672531200000L), // Fixed timestamp for consistency - isPrivate = false, - channel = null - ) - - // Convert to payload - val payload = message.toBinaryPayload() - assertNotNull("Failed to create payload", payload) - - // Create packet - val packet = BitchatPacket( - type = MessageType.MESSAGE.value, - ttl = 10u, - senderID = "a1b2c3d4", // 8-char hex peer ID - payload = payload!! - ) - - // Encode to binary - val binaryData = BinaryProtocol.encode(packet) - assertNotNull("Failed to encode packet", binaryData) - - // Test decoding - val decodedPacket = BinaryProtocol.decode(binaryData!!) - assertNotNull("Failed to decode packet", decodedPacket) - - // Verify packet fields - assertEquals("Version mismatch", 1u.toUByte(), decodedPacket!!.version) - assertEquals("Type mismatch", MessageType.MESSAGE.value, decodedPacket.type) - assertEquals("TTL mismatch", 10u.toUByte(), decodedPacket.ttl) - - // Test message decoding - val decodedMessage = BitchatMessage.fromBinaryPayload(decodedPacket.payload) - assertNotNull("Failed to decode message payload", decodedMessage) - - // Verify message fields - assertEquals("ID mismatch", message.id, decodedMessage!!.id) - assertEquals("Sender mismatch", message.sender, decodedMessage.sender) - assertEquals("Content mismatch", message.content, decodedMessage.content) - assertEquals("IsPrivate mismatch", message.isPrivate, decodedMessage.isPrivate) - } - - @Test - fun testMessageEncoding() { - val message = BitchatMessage( - id = "test123", - sender = "testuser", - content = "Hello", - timestamp = Date(1672531200000L) - ) - - val payload = message.toBinaryPayload() - assertNotNull("Message encoding failed", payload) - assertTrue("Payload too small", payload!!.size >= 13) // At least flags + timestamp + lengths - - val decoded = BitchatMessage.fromBinaryPayload(payload) - assertNotNull("Message decoding failed", decoded) - assertEquals("Message round-trip failed", message.content, decoded!!.content) - } - - @Test - fun debugBinaryEncoding() { - val debugOutput = BinaryProtocolDebugger.debugBroadcastMessage() - println("DEBUG OUTPUT:") - println(debugOutput) - - val hexDebug = HexConversionDebugger.compareHexConversions() - println("\nHEX CONVERSION DEBUG:") - println(hexDebug) - - // Force test to pass but show output - assertTrue("Debug output generated", debugOutput.isNotEmpty()) - } -} diff --git a/debug_noise_xx_test.kt b/debug_noise_xx_test.kt deleted file mode 100644 index e1be7710..00000000 --- a/debug_noise_xx_test.kt +++ /dev/null @@ -1,129 +0,0 @@ -/** - * Test to analyze the XX handshake pattern step by step - */ - -import com.bitchat.android.noise.southernstorm.protocol.* -import java.security.SecureRandom - -fun main() { - println("=== Analyzing XX Handshake Pattern ===") - - // Generate test keys like your app would - val random = SecureRandom() - val initiatorStaticPriv = ByteArray(32) - val responderStaticPriv = ByteArray(32) - random.nextBytes(initiatorStaticPriv) - random.nextBytes(responderStaticPriv) - - // Create DH states to derive public keys - val initiatorDH = com.bitchat.android.noise.southernstorm.protocol.Noise.createDH("25519") - val responderDH = com.bitchat.android.noise.southernstorm.protocol.Noise.createDH("25519") - - initiatorDH.setPrivateKey(initiatorStaticPriv, 0) - responderDH.setPrivateKey(responderStaticPriv, 0) - - val initiatorStaticPub = ByteArray(32) - val responderStaticPub = ByteArray(32) - initiatorDH.getPublicKey(initiatorStaticPub, 0) - responderDH.getPublicKey(responderStaticPub, 0) - - println("Initiator static public: ${initiatorStaticPub.joinToString("") { "%02x".format(it) }}") - println("Responder static public: ${responderStaticPub.joinToString("") { "%02x".format(it) }}") - - // Create handshake states - val initiator = HandshakeState("Noise_XX_25519_ChaChaPoly_SHA256", HandshakeState.INITIATOR) - val responder = HandshakeState("Noise_XX_25519_ChaChaPoly_SHA256", HandshakeState.RESPONDER) - - // Set static keys - initiator.getLocalKeyPair()?.setPrivateKey(initiatorStaticPriv, 0) - responder.getLocalKeyPair()?.setPrivateKey(responderStaticPriv, 0) - - // Start handshakes - initiator.start() - responder.start() - - println("\n=== XX Pattern Flow ===") - println("Expected: -> e") - println(" <- e, ee, s, es") - println(" -> s, se") - - // Message 1: -> e - println("\n--- Message 1: Initiator -> Responder ---") - val msg1Buffer = ByteArray(256) - val msg1Len = initiator.writeMessage(msg1Buffer, 0, ByteArray(0), 0, 0) - val msg1 = msg1Buffer.copyOf(msg1Len) - println("Message 1 length: $msg1Len bytes (expected: 32)") - println("Message 1 content: ${msg1.joinToString("") { "%02x".format(it) }}") - println("Initiator action after msg1: ${initiator.getAction()}") - - // Responder processes message 1 - val responderPayload1 = ByteArray(256) - val responderPayload1Len = responder.readMessage(msg1, 0, msg1.size, responderPayload1, 0) - println("Responder processed msg1, payload len: $responderPayload1Len") - println("Responder action after processing msg1: ${responder.getAction()}") - - // Message 2: <- e, ee, s, es - println("\n--- Message 2: Responder -> Initiator ---") - val msg2Buffer = ByteArray(256) - val msg2Len = responder.writeMessage(msg2Buffer, 0, ByteArray(0), 0, 0) - val msg2 = msg2Buffer.copyOf(msg2Len) - println("Message 2 length: $msg2Len bytes (expected: 80)") - println("Message 2 content: ${msg2.joinToString("") { "%02x".format(it) }}") - println("Responder action after msg2: ${responder.getAction()}") - - // This is where the initiator should be able to process message 2 - // Let's see what happens - try { - val initiatorPayload2 = ByteArray(256) - val initiatorPayload2Len = initiator.readMessage(msg2, 0, msg2.size, initiatorPayload2, 0) - println("Initiator processed msg2 successfully, payload len: $initiatorPayload2Len") - println("Initiator action after processing msg2: ${initiator.getAction()}") - - // Message 3: -> s, se - println("\n--- Message 3: Initiator -> Responder ---") - val msg3Buffer = ByteArray(256) - val msg3Len = initiator.writeMessage(msg3Buffer, 0, ByteArray(0), 0, 0) - val msg3 = msg3Buffer.copyOf(msg3Len) - println("Message 3 length: $msg3Len bytes (expected: 48)") - println("Message 3 content: ${msg3.joinToString("") { "%02x".format(it) }}") - println("Initiator action after msg3: ${initiator.getAction()}") - - // Responder processes message 3 - val responderPayload3 = ByteArray(256) - val responderPayload3Len = responder.readMessage(msg3, 0, msg3.size, responderPayload3, 0) - println("Responder processed msg3, payload len: $responderPayload3Len") - println("Responder action after processing msg3: ${responder.getAction()}") - - println("\nโœ“ Success: Handshake completed without errors") - - } catch (e: Exception) { - println("\nโŒ Error during initiator processing message 2:") - println("Exception: ${e.javaClass.simpleName}") - println("Message: ${e.message}") - e.printStackTrace() - - // Let's analyze what went wrong - analyzeMessage2Structure(msg2) - } - - // Cleanup - initiatorDH.destroy() - responderDH.destroy() - initiator.destroy() - responder.destroy() -} - -fun analyzeMessage2Structure(msg2: ByteArray) { - println("\n=== Analyzing Message 2 Structure ===") - println("Total length: ${msg2.size}") - - if (msg2.size >= 32) { - println("Ephemeral key (bytes 0-31): ${msg2.sliceArray(0..31).joinToString("") { "%02x".format(it) }}") - } - - if (msg2.size >= 80) { - println("Encrypted static + MAC (bytes 32-79): ${msg2.sliceArray(32..79).joinToString("") { "%02x".format(it) }}") - println(" - Encrypted static (bytes 32-63): ${msg2.sliceArray(32..63).joinToString("") { "%02x".format(it) }}") - println(" - MAC tag (bytes 64-79): ${msg2.sliceArray(64..79).joinToString("") { "%02x".format(it) }}") - } -} diff --git a/test_static_keys.kt b/test_static_keys.kt deleted file mode 100644 index 9eb3c51a..00000000 --- a/test_static_keys.kt +++ /dev/null @@ -1,146 +0,0 @@ -/** - * Quick test to verify static key handling works correctly with noise-java - */ - -import com.bitchat.android.noise.southernstorm.protocol.* -import java.security.SecureRandom - -fun main() { - println("Testing static key handling with noise-java library...\n") - - try { - // Create test static keys - val random = SecureRandom() - val staticPrivate1 = ByteArray(32).also { random.nextBytes(it) } - val staticPrivate2 = ByteArray(32).also { random.nextBytes(it) } - - // Test 1: Verify DHState supports setPrivateKey correctly - println("1. Testing DHState.setPrivateKey()...") - val dhState = Noise.createDH("25519") - dhState.setPrivateKey(staticPrivate1, 0) - - if (!dhState.hasPrivateKey() || !dhState.hasPublicKey()) { - throw RuntimeException("DHState failed to generate key pair from private key") - } - - val retrievedPrivate = ByteArray(32) - val generatedPublic = ByteArray(32) - dhState.getPrivateKey(retrievedPrivate, 0) - dhState.getPublicKey(generatedPublic, 0) - - if (!retrievedPrivate.contentEquals(staticPrivate1)) { - throw RuntimeException("Retrieved private key doesn't match set private key") - } - - println("โœ“ DHState setPrivateKey() works correctly") - - // Test 2: Full XX handshake with static keys - println("\n2. Testing full XX handshake with persistent static keys...") - - // Initiator - val initiatorHandshake = HandshakeState("Noise_XX_25519_ChaChaPoly_SHA256", HandshakeState.INITIATOR) - val initiatorKeyPair = initiatorHandshake.getLocalKeyPair()!! - initiatorKeyPair.setPrivateKey(staticPrivate1, 0) - initiatorHandshake.start() - - // Responder - val responderHandshake = HandshakeState("Noise_XX_25519_ChaChaPoly_SHA256", HandshakeState.RESPONDER) - val responderKeyPair = responderHandshake.getLocalKeyPair()!! - responderKeyPair.setPrivateKey(staticPrivate2, 0) - responderHandshake.start() - - // Message 1: -> e - val message1Buffer = ByteArray(64) - val message1Length = initiatorHandshake.writeMessage(message1Buffer, 0, ByteArray(0), 0, 0) - val message1 = message1Buffer.copyOf(message1Length) - println(" Message 1: ${message1.size} bytes") - - val payload1Buffer = ByteArray(64) - responderHandshake.readMessage(message1, 0, message1.size, payload1Buffer, 0) - - // Message 2: <- e, ee, s, es - val message2Buffer = ByteArray(128) - val message2Length = responderHandshake.writeMessage(message2Buffer, 0, ByteArray(0), 0, 0) - val message2 = message2Buffer.copyOf(message2Length) - println(" Message 2: ${message2.size} bytes") - - val payload2Buffer = ByteArray(64) - initiatorHandshake.readMessage(message2, 0, message2.size, payload2Buffer, 0) - - // Message 3: -> s, se - val message3Buffer = ByteArray(128) - val message3Length = initiatorHandshake.writeMessage(message3Buffer, 0, ByteArray(0), 0, 0) - val message3 = message3Buffer.copyOf(message3Length) - println(" Message 3: ${message3.size} bytes") - - val payload3Buffer = ByteArray(64) - responderHandshake.readMessage(message3, 0, message3.size, payload3Buffer, 0) - - // Verify handshake completed - if (initiatorHandshake.getAction() != HandshakeState.SPLIT || - responderHandshake.getAction() != HandshakeState.SPLIT) { - throw RuntimeException("Handshake did not complete properly") - } - - println("โœ“ Full XX handshake completed successfully") - - // Test 3: Split into transport keys - println("\n3. Testing transport key derivation...") - val initiatorCiphers = initiatorHandshake.split() - val responderCiphers = responderHandshake.split() - - // Test encryption/decryption - val testMessage = "Hello from Android with persistent static keys!".toByteArray() - val encrypted = ByteArray(testMessage.size + 16) - val encryptedLength = initiatorCiphers.getSender().encryptWithAd(null, testMessage, 0, encrypted, 0, testMessage.size) - - val decrypted = ByteArray(testMessage.size) - val decryptedLength = responderCiphers.getReceiver().decryptWithAd(null, encrypted, 0, decrypted, 0, encryptedLength) - - if (!testMessage.contentEquals(decrypted.copyOf(decryptedLength))) { - throw RuntimeException("Encrypted/decrypted message doesn't match") - } - - println("โœ“ Transport encryption/decryption works correctly") - - // Test 4: Verify static keys were used - println("\n4. Verifying static keys were used...") - val initiatorStaticKey = ByteArray(32) - val responderStaticKey = ByteArray(32) - - if (initiatorHandshake.hasRemotePublicKey()) { - initiatorHandshake.getRemotePublicKey()!!.getPublicKey(responderStaticKey, 0) - } - if (responderHandshake.hasRemotePublicKey()) { - responderHandshake.getRemotePublicKey()!!.getPublicKey(initiatorStaticKey, 0) - } - - // Verify these are derived from our original private keys - val testDH1 = Noise.createDH("25519") - testDH1.setPrivateKey(staticPrivate1, 0) - val expectedPublic1 = ByteArray(32) - testDH1.getPublicKey(expectedPublic1, 0) - - val testDH2 = Noise.createDH("25519") - testDH2.setPrivateKey(staticPrivate2, 0) - val expectedPublic2 = ByteArray(32) - testDH2.getPublicKey(expectedPublic2, 0) - - if (!initiatorStaticKey.contentEquals(expectedPublic1)) { - throw RuntimeException("Initiator static key wasn't used correctly") - } - if (!responderStaticKey.contentEquals(expectedPublic2)) { - throw RuntimeException("Responder static key wasn't used correctly") - } - - println("โœ“ Persistent static keys were preserved and used correctly") - - println("\n๐ŸŽ‰ ALL TESTS PASSED!") - println("The noise-java library fully supports persistent static keys for XX handshakes.") - println("Our Android implementation should be 100% compatible with iOS now.") - - } catch (e: Exception) { - println("โŒ Test failed: ${e.message}") - e.printStackTrace() - } -}