RendezvousTest.kt

4.7 KB · raw

1package com.peachpasswords.sync23import org.junit.Assert.assertEquals4import org.junit.Assert.assertNull5import org.junit.Assert.assertSame6import org.junit.Assert.assertThrows7import org.junit.Test8import java.security.SecureRandom910class RendezvousTest {11    private val handshakeHash = Rendezvous.secretFromHex(12        "0fbab8cd1782bf4b17d69beb69f5b80b0bf24d93da4924959c76ee701c32e655",13    )1415    @Test fun `matches browser rendezvous KATs`() {16        val secret = Rendezvous.deriveSecret(handshakeHash)17        assertEquals(18            "0803691c8ac7330883c0987e33cea4a171992366ce4b06b88691e84ca689773d",19            Rendezvous.bytesToHex(secret),20        )21        assertEquals(22            "9UY3j5AHnwYSApvymtY965su9n60rQOv8QZ2PZzHuKI",23            Rendezvous.deriveId(secret, 500_000),24        )25    }2627    @Test fun `matches browser symmetric mesh capability KAT`() {28        val root = ByteArray(32) { it.toByte() }29        val low = ByteArray(32) { (it + 32).toByte() }30        val high = ByteArray(32) { (it + 64).toByte() }31        val forward = Rendezvous.deriveMeshSecret(root, low, high)32        val reverse = Rendezvous.deriveMeshSecret(root, high, low)3334        assertEquals(35            "ad0359be7dc8fbb7df19ed027354b5029133a23a6023f43c2b3500b43addae7b",36            Rendezvous.bytesToHex(forward),37        )38        assertEquals(Rendezvous.bytesToHex(forward), Rendezvous.bytesToHex(reverse))39    }4041    @Test fun `uses required waiting and dialing window order`() {42        val secret = Rendezvous.deriveSecret(handshakeHash)43        val now = 500_000L * 3600 + 359944        assertEquals(45            listOf(46                Rendezvous.deriveId(secret, 500_000),47                Rendezvous.deriveId(secret, 499_999),48            ),49            Rendezvous.waitingIds(secret, now),50        )51        assertEquals(52            listOf(53                Rendezvous.deriveId(secret, 500_000),54                Rendezvous.deriveId(secret, 499_999),55                Rendezvous.deriveId(secret, 500_001),56            ),57            Rendezvous.dialingIds(secret, now),58        )59    }6061    @Test fun `opaque capabilities resolve current and previous windows only`() {62        val secretHex = "42".repeat(32)63        val peer = PairedDevice(64            deviceId = "browser-1",65            longTermPublicKeyHex = "ab".repeat(32),66            pairedAt = 1L,67            rendezvousSecretHex = secretHex,68        )69        val legacy = PairedDevice(70            deviceId = "legacy",71            longTermPublicKeyHex = "cd".repeat(32),72            pairedAt = 1L,73        )74        val secret = Rendezvous.secretFromHex(secretHex)75        val now = 500_000L * 3600 + 359976        val current = Rendezvous.deriveId(secret, 500_000)77        val previous = Rendezvous.deriveId(secret, 499_999)78        val next = Rendezvous.deriveId(secret, 500_001)7980        assertSame(peer, Rendezvous.resolvePeer(listOf(legacy, peer), current, now))81        assertSame(peer, Rendezvous.resolvePeer(listOf(legacy, peer), previous, now))82        assertNull(Rendezvous.resolvePeer(listOf(legacy, peer), next, now))83        assertNull(Rendezvous.resolvePeer(listOf(legacy, peer), "Z".repeat(43), now))84        assertNull(Rendezvous.resolvePeer(listOf(legacy), current, now))85    }8687    @Test fun `first-pair token is 128-bit base64url`() {88        val deterministic = object : SecureRandom() {89            override fun nextBytes(bytes: ByteArray) {90                bytes.indices.forEach { bytes[it] = it.toByte() }91            }92        }93        assertEquals("AAECAwQFBgcICQoLDA0ODw", Rendezvous.newPairingToken(deterministic))94    }9596    @Test fun `relay URL requires credential-free wss`() {97        assertEquals(98            "wss://relay.example.test/base",99            Rendezvous.normalizeRelayUrl("wss://relay.example.test/base/"),100        )101        assertThrows(IllegalArgumentException::class.java) {102            Rendezvous.normalizeRelayUrl("ws://relay.example.test")103        }104        assertThrows(IllegalArgumentException::class.java) {105            Rendezvous.normalizeRelayUrl("wss://user:[email protected]")106        }107    }108109    @Test fun `completed pairing persists secret and the listener relay`() {110        val device = PairedDevice(111            deviceId = "browser-1",112            deviceName = "Firefox",113            deviceType = "extension",114            longTermPublicKeyHex = "ab".repeat(32),115            pairedAt = 1L,116        )117        val bound = Rendezvous.bindPairState(118            device,119            handshakeHash,120            "wss://relay.example.test/",121        )122        assertEquals(123            "0803691c8ac7330883c0987e33cea4a171992366ce4b06b88691e84ca689773d",124            bound.rendezvousSecretHex,125        )126        assertEquals("wss://relay.example.test", bound.relayUrl)127    }128}