mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 15:45:22 +00:00
Enforce mandatory packet signature verification (#546)
* security: enforce mandatory packet signature verification * tests: add comprehensive unit tests for packet signature verification
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
package com.bitchat.android.mesh
|
||||
|
||||
import android.os.Build
|
||||
import com.bitchat.android.crypto.EncryptionService
|
||||
import com.bitchat.android.model.IdentityAnnouncement
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.kotlin.*
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [Build.VERSION_CODES.P], manifest = Config.NONE)
|
||||
class SecurityManagerTest {
|
||||
|
||||
private lateinit var securityManager: SecurityManager
|
||||
private lateinit var fakeEncryptionService: FakeEncryptionService
|
||||
private lateinit var mockDelegate: SecurityManagerDelegate
|
||||
|
||||
private val myPeerID = "1111222233334444"
|
||||
private val otherPeerID = "aaaabbbbccccdddd"
|
||||
private val unknownPeerID = "9999888877776666"
|
||||
|
||||
private val dummyPayload = "Hello World".toByteArray()
|
||||
private val validSignature = ByteArray(64) { 1 }
|
||||
private val invalidSignature = ByteArray(64) { 0 }
|
||||
|
||||
// Key pairs (using dummy bytes for mock verification)
|
||||
private val otherSigningKey = ByteArray(32) { 0xA }
|
||||
private val otherNoiseKey = ByteArray(32) { 0xB }
|
||||
|
||||
// Fake implementation to bypass initialization issues in tests
|
||||
open class FakeEncryptionService : EncryptionService(RuntimeEnvironment.getApplication()) {
|
||||
var shouldVerify: Boolean = true
|
||||
var lastVerifySignature: ByteArray? = null
|
||||
var lastVerifyKey: ByteArray? = null
|
||||
|
||||
override fun initialize() {
|
||||
// Do nothing to avoid KeyStore access in tests
|
||||
}
|
||||
|
||||
override fun verifyEd25519Signature(signature: ByteArray, data: ByteArray, publicKeyBytes: ByteArray): Boolean {
|
||||
lastVerifySignature = signature
|
||||
lastVerifyKey = publicKeyBytes
|
||||
|
||||
// Simple logic: if configured to verify, check if signature matches validSignature
|
||||
// We use the signature bytes passed in setup()
|
||||
if (shouldVerify) {
|
||||
return signature.contentEquals(byteArrayOf(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1))
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
fakeEncryptionService = FakeEncryptionService()
|
||||
mockDelegate = mock()
|
||||
|
||||
securityManager = SecurityManager(fakeEncryptionService, myPeerID)
|
||||
securityManager.delegate = mockDelegate
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
if (::securityManager.isInitialized) {
|
||||
securityManager.shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validatePacket - rejects packet with missing signature`() {
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.MESSAGE.value,
|
||||
ttl = 10u,
|
||||
senderID = otherPeerID,
|
||||
payload = dummyPayload
|
||||
)
|
||||
packet.signature = null
|
||||
|
||||
val result = securityManager.validatePacket(packet, otherPeerID)
|
||||
|
||||
assertFalse("Packet without signature should be rejected", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validatePacket - rejects packet with invalid signature`() {
|
||||
setupKnownPeer(otherPeerID, otherSigningKey)
|
||||
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.MESSAGE.value,
|
||||
ttl = 10u,
|
||||
senderID = otherPeerID,
|
||||
payload = dummyPayload
|
||||
)
|
||||
packet.signature = invalidSignature
|
||||
|
||||
val result = securityManager.validatePacket(packet, otherPeerID)
|
||||
|
||||
assertFalse("Packet with invalid signature should be rejected", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validatePacket - rejects packet from unknown peer (no key)`() {
|
||||
whenever(mockDelegate.getPeerInfo(unknownPeerID)).thenReturn(null)
|
||||
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.MESSAGE.value,
|
||||
ttl = 10u,
|
||||
senderID = unknownPeerID,
|
||||
payload = dummyPayload
|
||||
)
|
||||
packet.signature = validSignature
|
||||
|
||||
val result = securityManager.validatePacket(packet, unknownPeerID)
|
||||
|
||||
assertFalse("Packet from unknown peer should be rejected (cannot verify signature)", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validatePacket - accepts packet with valid signature from known peer`() {
|
||||
setupKnownPeer(otherPeerID, otherSigningKey)
|
||||
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.MESSAGE.value,
|
||||
ttl = 10u,
|
||||
senderID = otherPeerID,
|
||||
payload = dummyPayload
|
||||
)
|
||||
packet.signature = validSignature
|
||||
|
||||
val result = securityManager.validatePacket(packet, otherPeerID)
|
||||
|
||||
assertTrue("Valid signed packet from known peer should be accepted", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validatePacket - accepts ANNOUNCE packet from unknown peer (extracts key)`() {
|
||||
val announcement = IdentityAnnouncement(
|
||||
nickname = "New User",
|
||||
noisePublicKey = otherNoiseKey,
|
||||
signingPublicKey = otherSigningKey
|
||||
)
|
||||
val payload = announcement.encode()!!
|
||||
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.ANNOUNCE.value,
|
||||
ttl = 10u,
|
||||
senderID = unknownPeerID,
|
||||
payload = payload
|
||||
)
|
||||
packet.signature = validSignature
|
||||
|
||||
whenever(mockDelegate.getPeerInfo(unknownPeerID)).thenReturn(null)
|
||||
|
||||
val result = securityManager.validatePacket(packet, unknownPeerID)
|
||||
|
||||
assertTrue("ANNOUNCE from unknown peer should be accepted (key extracted from payload)", result)
|
||||
// Verify we used the correct key
|
||||
assertTrue("Should have used extracted key for verification",
|
||||
fakeEncryptionService.lastVerifyKey.contentEquals(otherSigningKey))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validatePacket - rejects ANNOUNCE packet with invalid signature`() {
|
||||
val announcement = IdentityAnnouncement(
|
||||
nickname = "New User",
|
||||
noisePublicKey = otherNoiseKey,
|
||||
signingPublicKey = otherSigningKey
|
||||
)
|
||||
val payload = announcement.encode()!!
|
||||
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.ANNOUNCE.value,
|
||||
ttl = 10u,
|
||||
senderID = unknownPeerID,
|
||||
payload = payload
|
||||
)
|
||||
packet.signature = invalidSignature
|
||||
|
||||
val result = securityManager.validatePacket(packet, unknownPeerID)
|
||||
|
||||
assertFalse("ANNOUNCE with invalid signature should be rejected", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validatePacket - rejects ANNOUNCE packet with malformed payload`() {
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.ANNOUNCE.value,
|
||||
ttl = 10u,
|
||||
senderID = unknownPeerID,
|
||||
payload = byteArrayOf(0x00, 0x01, 0x02)
|
||||
)
|
||||
packet.signature = validSignature
|
||||
|
||||
val result = securityManager.validatePacket(packet, unknownPeerID)
|
||||
|
||||
assertFalse("ANNOUNCE with malformed payload should be rejected (cannot extract key)", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validatePacket - ignores own packets`() {
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.MESSAGE.value,
|
||||
ttl = 10u,
|
||||
senderID = myPeerID,
|
||||
payload = dummyPayload
|
||||
)
|
||||
packet.signature = null
|
||||
|
||||
val result = securityManager.validatePacket(packet, myPeerID)
|
||||
|
||||
assertFalse("Own packets should return false (skipped)", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validatePacket - detects duplicates`() {
|
||||
setupKnownPeer(otherPeerID, otherSigningKey)
|
||||
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.MESSAGE.value,
|
||||
ttl = 10u,
|
||||
senderID = otherPeerID,
|
||||
payload = dummyPayload
|
||||
)
|
||||
packet.signature = validSignature
|
||||
|
||||
val result1 = securityManager.validatePacket(packet, otherPeerID)
|
||||
assertTrue("First packet should be accepted", result1)
|
||||
|
||||
val result2 = securityManager.validatePacket(packet, otherPeerID)
|
||||
assertFalse("Duplicate packet should be rejected", result2)
|
||||
}
|
||||
|
||||
private fun setupKnownPeer(peerID: String, signingKey: ByteArray) {
|
||||
val info = PeerInfo(
|
||||
id = peerID,
|
||||
nickname = "Test User",
|
||||
isConnected = true,
|
||||
isDirectConnection = true,
|
||||
noisePublicKey = ByteArray(32),
|
||||
signingPublicKey = signingKey,
|
||||
isVerifiedNickname = false,
|
||||
lastSeen = System.currentTimeMillis()
|
||||
)
|
||||
whenever(mockDelegate.getPeerInfo(peerID)).thenReturn(info)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user