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}