PairingInvitation.kt

1.9 KB · raw

1package com.peachpasswords.sync23import java.util.Base644import kotlinx.serialization.Serializable5import kotlinx.serialization.encodeToString6import kotlinx.serialization.json.Json78private const val PAIRING_INVITATION_PREFIX = "peach-pair-v1:"9private val PairingTokenPattern = Regex("^[A-Za-z0-9_-]{22}$")1011data class PairingInvitation(12    val relayUrl: String,13    val token: String,14)1516@Serializable17private data class PairingInvitationWire(18    val relay: String,19    val token: String,20)2122/**23 * QR/manual first-pair payload. Only reachability is present before Noise:24 * a credential-free wss relay and a fresh 128-bit token. Never add identity or25 * vault fields here.26 */27fun createPairingInvitation(28    relayUrl: String,29    token: String = Rendezvous.newPairingToken(),30): String {31    require(PairingTokenPattern.matches(token)) {32        "pairing invitation token must be 128-bit base64url"33    }34    val wire = PairingInvitationWire(Rendezvous.normalizeRelayUrl(relayUrl), token)35    val encoded = Base64.getUrlEncoder().withoutPadding()36        .encodeToString(Json.encodeToString(wire).toByteArray(Charsets.UTF_8))37    return PAIRING_INVITATION_PREFIX + encoded38}3940fun parsePairingInvitation(input: String): PairingInvitation? {41    val value = input.trim()42    if (!value.startsWith(PAIRING_INVITATION_PREFIX)) return null43    return runCatching {44        val decoded = Base64.getUrlDecoder().decode(45            value.removePrefix(PAIRING_INVITATION_PREFIX),46        )47        val wire = Json.decodeFromString<PairingInvitationWire>(48            String(decoded, Charsets.UTF_8),49        )50        require(PairingTokenPattern.matches(wire.token))51        PairingInvitation(52            relayUrl = Rendezvous.normalizeRelayUrl(wire.relay),53            token = wire.token,54        )55    }.getOrNull()56}5758fun PairingInvitation.roomUrl(): String = Rendezvous.buildUrl(relayUrl, token)