RendezvousTest.kt

3.6 KB · raw

1package com.peachpasswords.sync23import org.junit.Assert.assertEquals4import org.junit.Assert.assertThrows5import org.junit.Test6import java.security.SecureRandom78class RendezvousTest {9    private val handshakeHash = Rendezvous.secretFromHex(10        "0fbab8cd1782bf4b17d69beb69f5b80b0bf24d93da4924959c76ee701c32e655",11    )1213    @Test fun `matches browser rendezvous KATs`() {14        val secret = Rendezvous.deriveSecret(handshakeHash)15        assertEquals(16            "0803691c8ac7330883c0987e33cea4a171992366ce4b06b88691e84ca689773d",17            Rendezvous.bytesToHex(secret),18        )19        assertEquals(20            "9UY3j5AHnwYSApvymtY965su9n60rQOv8QZ2PZzHuKI",21            Rendezvous.deriveId(secret, 500_000),22        )23    }2425    @Test fun `matches browser symmetric mesh capability KAT`() {26        val alicePrivate = ByteArray(32) { 1 }27        val bobPrivate = ByteArray(32) { 2 }28        val alicePublic = org.bouncycastle.crypto.params.X25519PrivateKeyParameters(alicePrivate, 0).generatePublicKey().encoded29        val bobPublic = org.bouncycastle.crypto.params.X25519PrivateKeyParameters(bobPrivate, 0).generatePublicKey().encoded30        val forward = Rendezvous.deriveMeshSecret(alicePrivate, alicePublic, bobPublic)31        val reverse = Rendezvous.deriveMeshSecret(bobPrivate, bobPublic, alicePublic)3233        assertEquals(Rendezvous.bytesToHex(forward), Rendezvous.bytesToHex(reverse))34    }3536    @Test fun `uses required waiting and dialing window order`() {37        val secret = Rendezvous.deriveSecret(handshakeHash)38        val now = 500_000L * 3600 + 359939        assertEquals(40            listOf(41                Rendezvous.deriveId(secret, 500_000),42                Rendezvous.deriveId(secret, 499_999),43            ),44            Rendezvous.waitingIds(secret, now),45        )46        assertEquals(47            listOf(48                Rendezvous.deriveId(secret, 500_000),49                Rendezvous.deriveId(secret, 499_999),50                Rendezvous.deriveId(secret, 500_001),51            ),52            Rendezvous.dialingIds(secret, now),53        )54    }5556    @Test fun `first-pair token is 128-bit base64url`() {57        val deterministic = object : SecureRandom() {58            override fun nextBytes(bytes: ByteArray) {59                bytes.indices.forEach { bytes[it] = it.toByte() }60            }61        }62        assertEquals("AAECAwQFBgcICQoLDA0ODw", Rendezvous.newPairingToken(deterministic))63    }6465    @Test fun `relay URL requires credential-free wss`() {66        assertEquals(67            "wss://relay.example.test/base",68            Rendezvous.normalizeRelayUrl("wss://relay.example.test/base/"),69        )70        assertThrows(IllegalArgumentException::class.java) {71            Rendezvous.normalizeRelayUrl("ws://relay.example.test")72        }73        assertThrows(IllegalArgumentException::class.java) {74            Rendezvous.normalizeRelayUrl("wss://user:[email protected]")75        }76    }7778    @Test fun `completed pairing persists secret and the listener relay`() {79        val device = PairedDevice(80            deviceId = "browser-1",81            deviceName = "Firefox",82            deviceType = "extension",83            longTermPublicKeyHex = "ab".repeat(32),84            pairedAt = 1L,85        )86        val bound = Rendezvous.bindPairState(87            device,88            handshakeHash,89            "wss://relay.example.test/",90        )91        assertEquals(92            "0803691c8ac7330883c0987e33cea4a171992366ce4b06b88691e84ca689773d",93            bound.rendezvousSecretHex,94        )95        assertEquals("wss://relay.example.test", bound.relayUrl)96    }97}