package com.peachpasswords.sync import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Test /** * Bounds tests for [verifyDeviceManifest]. The arithmetic fix lives in the * verify path itself; these tests pin the inputs that used to slip past it. */ class DeviceManifestSupportTest { private val master = ByteArray(32) { it.toByte() } private val signing = deriveSyncSigningKeypair(master, "issuer") private fun validEntry(deviceId: String = "peer", pairedAt: Long = 1L) = ManifestDeviceEntry( deviceId = deviceId, deviceName = "Peer", deviceType = "android", longTermPublicKeyHex = "aa".repeat(32), signingPublicKeyHex = "bb".repeat(32), pairedAt = pairedAt, ) private fun manifestWith(issuedAt: Long, devices: List = listOf(validEntry())) = buildSignedDeviceManifest( issuerDeviceId = "issuer", devices = devices, signingPrivateKey = signing.privateKey, nowMs = issuedAt, ) @Test fun rejects_issued_at_long_min_value() { // abs(Long.MIN_VALUE) is still negative, so the legacy abs()-based gate // accepted this. The new two-sided bound must reject it. val manifest = manifestWith(issuedAt = Long.MIN_VALUE) assertNull(verifyDeviceManifest(manifest, signing.publicKeyRaw, nowMs = 1_700_000_000_000L)) } @Test fun rejects_issued_at_too_old() { val manifest = manifestWith(issuedAt = 1L) assertNull(verifyDeviceManifest(manifest, signing.publicKeyRaw, nowMs = 1_700_000_000_000L)) } @Test fun rejects_issued_at_too_far_in_future() { val now = 1_700_000_000_000L // 5 min + 1 ms past the default freshness window. val manifest = manifestWith(issuedAt = now + 5 * 60_000L + 1L) assertNull(verifyDeviceManifest(manifest, signing.publicKeyRaw, nowMs = now)) } @Test fun accepts_issued_at_within_window() { val now = 1_700_000_000_000L val manifest = manifestWith(issuedAt = now) assertNotNull(verifyDeviceManifest(manifest, signing.publicKeyRaw, nowMs = now)) } @Test fun rejects_oversized_device_id_string() { val manifest = manifestWith( issuedAt = 1_000L, devices = listOf(validEntry(deviceId = "x".repeat(10_000))), ) assertNull(verifyDeviceManifest(manifest, signing.publicKeyRaw, nowMs = 1_000L)) } @Test fun rejects_negative_paired_at() { val manifest = manifestWith( issuedAt = 1_000L, devices = listOf(validEntry(pairedAt = -1L)), ) assertNull(verifyDeviceManifest(manifest, signing.publicKeyRaw, nowMs = 1_000L)) } @Test fun rejects_too_many_devices() { val manifest = buildSignedDeviceManifest( issuerDeviceId = "issuer", devices = List(300) { validEntry(deviceId = "peer-$it") }, signingPrivateKey = signing.privateKey, nowMs = 1_000L, ) assertNull(verifyDeviceManifest(manifest, signing.publicKeyRaw, nowMs = 1_000L)) } @Test fun rejects_oversized_issuer_device_id() { val manifest = manifestWith(issuedAt = 1_000L) val oversized = manifest.copy(body = manifest.body.copy(issuerDeviceId = "y".repeat(2_000))) assertNull(verifyDeviceManifest(oversized, signing.publicKeyRaw, nowMs = 1_000L)) } @Test fun rejects_negative_revoked_at() { val manifest = buildSignedDeviceManifest( issuerDeviceId = "issuer", devices = emptyList(), signingPrivateKey = signing.privateKey, nowMs = 1_000L, revoked = listOf( DeviceRevocation( deviceId = "peer", longTermPublicKeyHex = "aa".repeat(32), revokedAt = -1L, revokedByDeviceId = "issuer", ), ), ) assertNull(verifyDeviceManifest(manifest, signing.publicKeyRaw, nowMs = 1_000L)) } }