a4e36e3eaef3

shorten pairing code to four groups with a 4-byte token

time
2026-07-24 23:30 UTC · 3 months ago
machine
koh2
agent
claude-opus-5
session
pairing code length
parent
894f871df762
2 files app/src/main/java/com/peachpasswords/sync/PairingCode.kt+22 −2app/src/test/java/com/peachpasswords/sync/PairingCodeTest.kt+9 −0
app/src/main/java/com/peachpasswords/sync/PairingCode.kt+22 −2
@@ -28,8 +28,28 @@2828     private const val PORT_BYTES = 22929     private const val MIN_TOKEN_BYTES = 43030 31-    /** 8 bytes — comfortably inside the browser's 4–10 range, 23 characters. */32-    const val TOKEN_BYTES = 831+    /**32+     * 4 bytes, which is what a four-group code actually costs.33+     *34+     * Base32 packs 5 bits per character, so the code length is fixed by the35+     * payload: IP(4) + port(2) + token(n) bytes → ceil((6+n)*8/5) characters.36+     * A 4-byte token is 10 bytes → exactly 16 characters → four clean groups,37+     * and is the only size that lands on a group boundary. 8 bytes was 2338+     * characters and six groups, which is the code June was looking at.39+     *40+     * The brief asks for both "8–10 byte token" and "~4 groups"; those are not41+     * the same code. Its own worked example — A74H-48J3-KI32-OEP4, 1642+     * characters — is a 4-byte token, so the example is what this follows.43+     *44+     * 32 bits is sound here because the token is not the security boundary:45+     * it is single-use, lives for one short pairing window, is only reachable46+     * from the same LAN, and a peer that guesses it still cannot pair without47+     * the user confirming a matching SAS. Its whole job is to stop an48+     * unrelated host from consuming the window. The browser independently sets49+     * its floor at 4 bytes ([MIN_TOKEN_BYTES] there), so this is the shortest50+     * code both sides accept rather than a number picked to be short.51+     */52+    const val TOKEN_BYTES = 43353 3454     private val rng = SecureRandom()3555 

view file

app/src/test/java/com/peachpasswords/sync/PairingCodeTest.kt+9 −0
@@ -56,6 +56,15 @@5656         PairingCode.encode("10.0.0.1", 0, PairingCode.newToken())5757     }5858 59+    @Test fun `a generated code is four groups of four`() {60+        // The length is what the user actually experiences, so pin it. Base3261+        // is 5 bits per character, so this is fixed by TOKEN_BYTES — if that62+        // grows, the code silently gains a group.63+        val code = PairingCode.encode("192.168.1.20", 49142, PairingCode.newToken())64+        assertEquals(listOf(4, 4, 4, 4), code.split("-").map { it.length })65+        assertEquals(19, code.length)66+    }67+5968     @Test(expected = IllegalArgumentException::class)6069     fun `refuses a token below the browser's four-byte floor`() {6170         PairingCode.encode("10.0.0.1", 8765, byteArrayOf(1, 2, 3))

view file