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
↓ peach-android-a4e36e3e.tar.gzatlas at this save
files changed2 files
app/src/main/java/com/peachpasswords/sync/PairingCode.kt+22 −2app/src/test/java/com/peachpasswords/sync/PairingCodeTest.kt+9 −0app/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 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))