mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-09-19 04:59:59 +00:00
fix: reject a future-dated verification QR
Freshness only subtracted now - ts, so a timestamp in the future made the age negative and kept the QR valid too long. Match iOS abs() skew so a backdated clock cannot extend the 5-minute window.
This commit is contained in:
parent
eab894ce78
commit
407dff34ed
@ -146,7 +146,10 @@ object VerificationService {
|
|||||||
val service = encryptionServiceRef?.get() ?: return null
|
val service = encryptionServiceRef?.get() ?: return null
|
||||||
val qr = VerificationQR.fromUrlString(urlString) ?: return null
|
val qr = VerificationQR.fromUrlString(urlString) ?: return null
|
||||||
val now = System.currentTimeMillis() / 1000L
|
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 sig = qr.sigHex.dataFromHexString() ?: return null
|
||||||
val signKey = qr.signKeyHex.dataFromHexString() ?: return null
|
val signKey = qr.signKeyHex.dataFromHexString() ?: return null
|
||||||
@ -292,3 +295,8 @@ object VerificationService {
|
|||||||
var last: CacheEntry? = null
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user