- Firebase et le pare-feu local (trou noir, liste blanche par application, coupure cyclique locale) sont retirés - Client WireGuard officiel (GoBackend) : tout le trafic passe par le serveur familyd, qui filtre - Appairage par code ou lien familyd:// ; clé privée WireGuard et jeton chiffrés par l'Android Keystore - Remontées par le tunnel : état, temps d'écran, applications installées, évènements (file persistante) - Mode propriétaire (Device Owner) : VPN toujours actif avec blocage hors VPN, réglages verrouillés, garde-fou si le serveur ne répond plus pendant 15 min - Plan de la refonte : docs/PLAN-wireguard.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R9CBCVP5oDu2sfoYPisNCQ
114 lines
5.1 KiB
Kotlin
114 lines
5.1 KiB
Kotlin
package com.jttools.datacontrol.remote
|
|
|
|
import org.json.JSONArray
|
|
import org.json.JSONObject
|
|
import java.net.URI
|
|
import java.net.URLDecoder
|
|
|
|
/**
|
|
* Liaison avec le serveur familyd, reçue à l'appairage (sans les secrets : jeton de l'appareil et clé
|
|
* privée WireGuard sont stockés à part, chiffrés par l'Android Keystore, voir SecretBox).
|
|
*/
|
|
data class FamilyLink(
|
|
val serverUrl: String,
|
|
val deviceId: String,
|
|
val name: String,
|
|
val address: String,
|
|
val dns: String,
|
|
val mtu: Int,
|
|
val serverPublicKey: String,
|
|
val endpoint: String,
|
|
val allowedIps: List<String>,
|
|
val keepalive: Int,
|
|
val apiBase: String
|
|
) {
|
|
/** Adresse du téléphone dans le tunnel, sans le /32. */
|
|
val tunnelIp: String get() = address.substringBefore('/')
|
|
|
|
fun toJson(): String = JSONObject()
|
|
.put("serverUrl", serverUrl).put("deviceId", deviceId).put("name", name)
|
|
.put("address", address).put("dns", dns).put("mtu", mtu)
|
|
.put("serverPublicKey", serverPublicKey).put("endpoint", endpoint)
|
|
.put("allowedIps", JSONArray(allowedIps)).put("keepalive", keepalive).put("apiBase", apiBase)
|
|
.toString()
|
|
|
|
/** Configuration WireGuard au format wg-quick (lue par la bibliothèque officielle). */
|
|
fun wgQuickConfig(privateKey: String): String = buildString {
|
|
appendLine("[Interface]")
|
|
appendLine("PrivateKey = $privateKey")
|
|
appendLine("Address = $address")
|
|
appendLine("DNS = $dns")
|
|
appendLine("MTU = $mtu")
|
|
appendLine()
|
|
appendLine("[Peer]")
|
|
appendLine("PublicKey = $serverPublicKey")
|
|
appendLine("Endpoint = $endpoint")
|
|
appendLine("AllowedIPs = ${allowedIps.joinToString(", ")}")
|
|
appendLine("PersistentKeepalive = $keepalive")
|
|
}
|
|
|
|
companion object {
|
|
fun fromJson(json: String?): FamilyLink? {
|
|
if (json.isNullOrBlank()) return null
|
|
return runCatching {
|
|
val o = JSONObject(json)
|
|
val ips = o.getJSONArray("allowedIps")
|
|
FamilyLink(
|
|
serverUrl = o.getString("serverUrl"), deviceId = o.getString("deviceId"),
|
|
name = o.optString("name"), address = o.getString("address"), dns = o.getString("dns"),
|
|
mtu = o.optInt("mtu", 1280), serverPublicKey = o.getString("serverPublicKey"),
|
|
endpoint = o.getString("endpoint"),
|
|
allowedIps = (0 until ips.length()).map { ips.getString(it) },
|
|
keepalive = o.optInt("keepalive", 25), apiBase = o.getString("apiBase")
|
|
)
|
|
}.getOrNull()
|
|
}
|
|
|
|
/** Réponse de POST /api/pair → liaison + jeton de l'appareil. */
|
|
fun fromPairResponse(serverUrl: String, json: String): Pair<FamilyLink, String> {
|
|
val o = JSONObject(json)
|
|
val wg = o.getJSONObject("wireguard")
|
|
val ips = wg.getJSONArray("allowed_ips")
|
|
val link = FamilyLink(
|
|
serverUrl = serverUrl, deviceId = o.getString("device_id"), name = o.optString("name"),
|
|
address = wg.getString("address"), dns = wg.getString("dns"), mtu = wg.optInt("mtu", 1280),
|
|
serverPublicKey = wg.getString("server_public_key"), endpoint = wg.getString("endpoint"),
|
|
allowedIps = (0 until ips.length()).map { ips.getString(it) },
|
|
keepalive = wg.optInt("persistent_keepalive", 25), apiBase = o.getString("api").trimEnd('/')
|
|
)
|
|
require(link.serverPublicKey.isNotBlank()) { "Le serveur n'a pas fourni sa clé WireGuard." }
|
|
return link to o.getString("token")
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ce que l'utilisateur saisit pour s'associer : le code seul (« ABCD 2345 »), ou le lien
|
|
* « familyd://pair?url=https://…&code=… » affiché par l'interface des parents.
|
|
*/
|
|
data class PairingInput(val serverUrl: String, val code: String) {
|
|
companion object {
|
|
const val DEFAULT_SERVER = "https://family.tuxtech.fr"
|
|
private val CODE = Regex("^[A-HJ-NP-Z2-9]{8}$") // même alphabet que le serveur (sans I, O, 0, 1)
|
|
|
|
fun parse(input: String, defaultServer: String = DEFAULT_SERVER): PairingInput? {
|
|
val text = input.trim()
|
|
if (text.startsWith("familyd://", ignoreCase = true)) {
|
|
val query = runCatching { URI(text).rawQuery }.getOrNull() ?: return null
|
|
val params = query.split('&').mapNotNull {
|
|
val (k, v) = it.split('=', limit = 2).takeIf { p -> p.size == 2 } ?: return@mapNotNull null
|
|
k to URLDecoder.decode(v, "UTF-8")
|
|
}.toMap()
|
|
val url = params["url"]?.trimEnd('/') ?: return null
|
|
val code = normalize(params["code"] ?: return null) ?: return null
|
|
if (!url.startsWith("https://")) return null
|
|
return PairingInput(url, code)
|
|
}
|
|
return normalize(text)?.let { PairingInput(defaultServer, it) }
|
|
}
|
|
|
|
private fun normalize(code: String): String? =
|
|
code.uppercase().replace(" ", "").replace("-", "").takeIf { CODE.matches(it) }
|
|
}
|
|
}
|