mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 18:05:20 +00:00
decrypt correctly
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) }}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ByteArray, ByteArray> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
-361
@@ -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<ByteArray, ByteArray> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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<ByteArray, ByteArray> {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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<ByteArray, ByteArray> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
-383
@@ -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
|
||||
}
|
||||
}
|
||||
-107
@@ -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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user