package com.peachpasswords.sync import org.junit.Assert.assertEquals import org.junit.Assert.assertThrows import org.junit.Test import java.security.SecureRandom class RendezvousTest { private val handshakeHash = Rendezvous.secretFromHex( "0fbab8cd1782bf4b17d69beb69f5b80b0bf24d93da4924959c76ee701c32e655", ) @Test fun `matches browser rendezvous KATs`() { val secret = Rendezvous.deriveSecret(handshakeHash) assertEquals( "0803691c8ac7330883c0987e33cea4a171992366ce4b06b88691e84ca689773d", Rendezvous.bytesToHex(secret), ) assertEquals( "9UY3j5AHnwYSApvymtY965su9n60rQOv8QZ2PZzHuKI", Rendezvous.deriveId(secret, 500_000), ) } @Test fun `matches browser symmetric mesh capability KAT`() { val alicePrivate = ByteArray(32) { 1 } val bobPrivate = ByteArray(32) { 2 } val alicePublic = org.bouncycastle.crypto.params.X25519PrivateKeyParameters(alicePrivate, 0).generatePublicKey().encoded val bobPublic = org.bouncycastle.crypto.params.X25519PrivateKeyParameters(bobPrivate, 0).generatePublicKey().encoded val forward = Rendezvous.deriveMeshSecret(alicePrivate, alicePublic, bobPublic) val reverse = Rendezvous.deriveMeshSecret(bobPrivate, bobPublic, alicePublic) assertEquals(Rendezvous.bytesToHex(forward), Rendezvous.bytesToHex(reverse)) } @Test fun `uses required waiting and dialing window order`() { val secret = Rendezvous.deriveSecret(handshakeHash) val now = 500_000L * 3600 + 3599 assertEquals( listOf( Rendezvous.deriveId(secret, 500_000), Rendezvous.deriveId(secret, 499_999), ), Rendezvous.waitingIds(secret, now), ) assertEquals( listOf( Rendezvous.deriveId(secret, 500_000), Rendezvous.deriveId(secret, 499_999), Rendezvous.deriveId(secret, 500_001), ), Rendezvous.dialingIds(secret, now), ) } @Test fun `first-pair token is 128-bit base64url`() { val deterministic = object : SecureRandom() { override fun nextBytes(bytes: ByteArray) { bytes.indices.forEach { bytes[it] = it.toByte() } } } assertEquals("AAECAwQFBgcICQoLDA0ODw", Rendezvous.newPairingToken(deterministic)) } @Test fun `relay URL requires credential-free wss`() { assertEquals( "wss://relay.example.test/base", Rendezvous.normalizeRelayUrl("wss://relay.example.test/base/"), ) assertThrows(IllegalArgumentException::class.java) { Rendezvous.normalizeRelayUrl("ws://relay.example.test") } assertThrows(IllegalArgumentException::class.java) { Rendezvous.normalizeRelayUrl("wss://user:pass@relay.example.test") } } @Test fun `completed pairing persists secret and the listener relay`() { val device = PairedDevice( deviceId = "browser-1", deviceName = "Firefox", deviceType = "extension", longTermPublicKeyHex = "ab".repeat(32), pairedAt = 1L, ) val bound = Rendezvous.bindPairState( device, handshakeHash, "wss://relay.example.test/", ) assertEquals( "0803691c8ac7330883c0987e33cea4a171992366ce4b06b88691e84ca689773d", bound.rendezvousSecretHex, ) assertEquals("wss://relay.example.test", bound.relayUrl) } }