Merge 7dd620b340a3c3b262c65dcbc2a3adec3cc8eeff into c127eb83ab94c069c32d37530d2faecd381cd2a8

This commit is contained in:
Taksh Kothari 2026-09-14 09:43:25 +05:30 committed by GitHub
commit f767397820
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 77 additions and 1 deletions

View File

@ -146,7 +146,10 @@ object VerificationService {
val service = encryptionServiceRef?.get() ?: return null
val qr = VerificationQR.fromUrlString(urlString) ?: return null
val now = System.currentTimeMillis() / 1000L
if (now - qr.ts > maxAgeSeconds) return null
// Freshness in both directions: a future-dated timestamp must not
// buy a QR a longer validity window than a fresh one gets. iOS uses
// the same abs() check in VerificationService.verifyScannedQR.
if (verificationTimestampSkewSeconds(now, qr.ts) > maxAgeSeconds) return null
val sig = qr.sigHex.dataFromHexString() ?: return null
val signKey = qr.signKeyHex.dataFromHexString() ?: return null
@ -292,3 +295,8 @@ object VerificationService {
var last: CacheEntry? = null
}
}
/** Absolute age of a verification QR timestamp, in seconds. */
internal fun verificationTimestampSkewSeconds(nowSeconds: Long, qrTimestampSeconds: Long): Long {
return kotlin.math.abs(nowSeconds - qrTimestampSeconds)
}

View File

@ -0,0 +1,68 @@
package com.bitchat.android.services
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import com.bitchat.android.crypto.EncryptionService
import com.bitchat.android.util.hexEncodedString
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class VerificationServiceTest {
private lateinit var encryptionService: EncryptionService
@Before
fun setup() {
val context: Context = ApplicationProvider.getApplicationContext()
encryptionService = EncryptionService(context)
VerificationService.configure(encryptionService)
}
@Test
fun `freshness check rejects both stale and future-dated timestamps`() {
assertEquals(0L, verificationTimestampSkewSeconds(1_700_000_000L, 1_700_000_000L))
assertEquals(60L, verificationTimestampSkewSeconds(1_700_000_060L, 1_700_000_000L))
assertEquals(3_600L, verificationTimestampSkewSeconds(1_700_000_000L, 1_700_003_600L))
}
@Test
fun `verifyScannedQR accepts a freshly signed payload`() {
val qr = VerificationService.buildMyQRString(nickname = "alice", npub = null)
assertNotNull(qr)
assertNotNull(VerificationService.verifyScannedQR(qr!!, maxAgeSeconds = 60))
}
@Test
fun `verifyScannedQR rejects a future-dated payload`() {
val qr = signedQr(ts = (System.currentTimeMillis() / 1000L) + 3_600L)
assertNull(VerificationService.verifyScannedQR(qr, maxAgeSeconds = 60))
}
@Test
fun `verifyScannedQR rejects an expired payload`() {
val qr = signedQr(ts = (System.currentTimeMillis() / 1000L) - 3_600L)
assertNull(VerificationService.verifyScannedQR(qr, maxAgeSeconds = 60))
}
private fun signedQr(ts: Long): String {
val noise = encryptionService.getStaticPublicKey()!!.joinToString("") { "%02x".format(it) }
val sign = encryptionService.getSigningPublicKey()!!.joinToString("") { "%02x".format(it) }
val payload = VerificationService.VerificationQR(
v = 1,
noiseKeyHex = noise,
signKeyHex = sign,
npub = null,
nickname = "future-alice",
ts = ts,
nonceB64 = "AAAAAAAAAAAAAAAAAAAAAA",
sigHex = ""
)
val signature = encryptionService.signData(payload.canonicalBytes())!!
return payload.copy(sigHex = signature.hexEncodedString()).toUrlString()
}
}