1package com.peachpasswords.sync23import java.security.SecureRandom45678910111213141516171819202122232425object PairingCode {26 private const val ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"27 private const val IP_BYTES = 428 private const val PORT_BYTES = 229 private const val MIN_TOKEN_BYTES = 43031 32 const val TOKEN_BYTES = 83334 private val rng = SecureRandom()3536 data class Parsed(val ip: String, val port: Int, val tokenHex: String)3738 fun newToken(): ByteArray = ByteArray(TOKEN_BYTES).also(rng::nextBytes)3940 fun encode(ip: String, port: Int, token: ByteArray): String {41 val octets = ip.split(".")42 require(octets.size == IP_BYTES) { "ip must be dotted-quad IPv4" }43 require(port in 1..65535) { "invalid port: $port" }44 require(token.size >= MIN_TOKEN_BYTES) { "token must be at least $MIN_TOKEN_BYTES bytes" }4546 val bytes = ByteArray(IP_BYTES + PORT_BYTES + token.size)47 for (i in 0 until IP_BYTES) {48 val n = octets[i].toIntOrNull()49 require(n != null && n in 0..255) { "invalid IPv4 octet: ${octets[i]}" }50 bytes[i] = n.toByte()51 }52 bytes[4] = ((port shr 8) and 0xff).toByte()53 bytes[5] = (port and 0xff).toByte()54 token.copyInto(bytes, IP_BYTES + PORT_BYTES)55 return group(encodeCrockford(bytes))56 }5758 59 fun parse(input: String): Parsed? {60 val bytes = decodeCrockford(input) ?: return null61 if (bytes.size < IP_BYTES + PORT_BYTES + MIN_TOKEN_BYTES) return null62 val port = ((bytes[4].toInt() and 0xff) shl 8) or (bytes[5].toInt() and 0xff)63 if (port == 0) return null64 return Parsed(65 ip = (0 until IP_BYTES).joinToString(".") { (bytes[it].toInt() and 0xff).toString() },66 port = port,67 tokenHex = bytes.copyOfRange(IP_BYTES + PORT_BYTES, bytes.size).toHex(),68 )69 }7071 fun ByteArray.toHex(): String = joinToString("") { "%02x".format(it) }7273 private fun group(chars: String): String =74 chars.chunked(4).joinToString("-")7576 private fun encodeCrockford(bytes: ByteArray): String {77 val s = StringBuilder()78 var acc = 079 var bits = 080 for (b in bytes) {81 acc = (acc shl 8) or (b.toInt() and 0xff)82 bits += 883 while (bits >= 5) {84 bits -= 585 s.append(ALPHABET[(acc shr bits) and 0x1f])86 }87 }88 if (bits > 0) s.append(ALPHABET[(acc shl (5 - bits)) and 0x1f])89 return s.toString()90 }9192 private fun decodeCrockford(input: String): ByteArray? {93 94 95 val clean = buildString {96 for (c in input.uppercase()) when (c) {97 ' ', '-', '\t', '\n' -> Unit98 'I', 'L' -> append('1')99 'O' -> append('0')100 else -> append(c)101 }102 }103 if (clean.isEmpty()) return null104105 106 val out = ByteArray(clean.length * 5 / 8)107 var acc = 0108 var bits = 0109 var o = 0110 for (ch in clean) {111 val v = ALPHABET.indexOf(ch)112 if (v < 0) return null113 acc = (acc shl 5) or v114 bits += 5115 if (bits >= 8) {116 bits -= 8117 out[o++] = ((acc shr bits) and 0xff).toByte()118 }119 }120 return out121 }122}