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}