package com.peachpasswords.sync import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Test /** * The phone is the only side that generates pairing codes and the browser is * the only side that reads them, so a drift here is invisible until a real * pairing fails with nothing to look at. The golden vector below was produced * by running the browser's own `shared/pairing-code.ts` — not derived from this * implementation — so it fails if either side moves. */ class PairingCodeTest { // encodePairingCode("192.168.7.115", 49142, deadbeef) in peach-browser, // re-generated after both sides moved to a 4-byte token. private val vectorCode = "R2M0-EWXZ-YVFA-VFQF" private val vectorToken = byteArrayOf(0xde.toByte(), 0xad.toByte(), 0xbe.toByte(), 0xef.toByte()) /** The 8-byte code an older build would have shown. */ private val legacyCode = "R2M0-EWXZ-YVFA-VFQF-04HM-ASR" @Test fun `encodes the browser's golden vector`() { assertEquals(vectorCode, PairingCode.encode("192.168.7.115", 49142, vectorToken)) } @Test fun `parses the browser's golden vector`() { val parsed = PairingCode.parse(vectorCode)!! assertEquals("192.168.7.115", parsed.ip) assertEquals(49142, parsed.port) assertEquals("deadbeef", parsed.tokenHex) } @Test fun `still decodes a code from an older eight-byte build`() { // The browser kept decoding lenient on purpose so a peer that hasn't // updated still pairs rather than hard-failing; match that. val parsed = PairingCode.parse(legacyCode)!! assertEquals("192.168.7.115", parsed.ip) assertEquals(49142, parsed.port) assertEquals("deadbeef01234567", parsed.tokenHex) } @Test fun `round-trips a freshly generated token`() { val token = PairingCode.newToken() val parsed = PairingCode.parse(PairingCode.encode("10.0.0.7", 49142, token))!! assertEquals("10.0.0.7", parsed.ip) assertEquals(49142, parsed.port) assertEquals(token.joinToString("") { "%02x".format(it) }, parsed.tokenHex) } @Test fun `folds the ambiguous glyphs a user might read off a screen`() { // I/L -> 1 and O -> 0, so the doc's own example resolves rather than // failing on characters Crockford's alphabet excludes. val typed = PairingCode.parse("r2mO-ewxz-yvfa-vfqf") assertEquals(PairingCode.parse(vectorCode), typed) } @Test fun `rejects input that is not a pairing code`() { assertNull(PairingCode.parse("")) assertNull(PairingCode.parse("418207")) // the 6-digit SAS assertNull(PairingCode.parse("not-a-code!!")) // U is not in the alphabet } @Test(expected = IllegalArgumentException::class) fun `refuses to encode a zero port`() { PairingCode.encode("10.0.0.1", 0, PairingCode.newToken()) } @Test fun `a generated code is four groups of four`() { // The length is what the user actually experiences, so pin it. Base32 // is 5 bits per character, so this is fixed by TOKEN_BYTES — if that // grows, the code silently gains a group. val code = PairingCode.encode("192.168.1.20", 49142, PairingCode.newToken()) assertEquals(listOf(4, 4, 4, 4), code.split("-").map { it.length }) assertEquals(19, code.length) } @Test(expected = IllegalArgumentException::class) fun `refuses a token shorter than the canonical size`() { PairingCode.encode("10.0.0.1", 49142, byteArrayOf(1, 2, 3)) } @Test(expected = IllegalArgumentException::class) fun `refuses a token longer than the canonical size`() { // The browser throws here too. A longer token encodes fine and quietly // produces a six-group code, which is the regression to prevent. PairingCode.encode("10.0.0.1", 49142, ByteArray(8)) } }