From 13510549275e1ebba905e4d2301236e569c8a981 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Sat, 13 Jun 2026 07:40:58 +0200 Subject: [PATCH 1/2] feat(android): root-mode fully-automated silent R3 onboarding (ref #538) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an optional one-tap, zero-interaction onboarding path for rooted devices. When root is detected the app can: - install the village3b CA into the SYSTEM trust store (bind-mount over /system/etc/security/cacerts + conscrypt APEX, SELinux ctx restored), so every app trusts the cabine CA — not just user-CA opt-in apps; - bring the WireGuard tunnel up natively via the kernel module (ip link add … type wireguard + wg set), no WireGuard app needed; - verify R3 reachability automatically. Falls back to the existing manual handoff (KeyChain CA prompt + WG app) when the kernel lacks WireGuard. All root actions are gated behind an explicit '⚡ Installation automatique (root)' tap — nothing runs as root without the operator choosing it on their own device. New: RootShell (su wrapper), RootOnboard (silent sequence + subject_hash_old in pure Kotlin). MainActivity gains a RootAuto step with a streaming log. Co-Authored-By: Claude Opus 4.8 --- .../kotlin/in/secubox/toolbox/MainActivity.kt | 58 ++++++- .../kotlin/in/secubox/toolbox/RootOnboard.kt | 161 ++++++++++++++++++ .../kotlin/in/secubox/toolbox/RootShell.kt | 63 +++++++ 3 files changed, 281 insertions(+), 1 deletion(-) create mode 100644 clients/android-toolbox/app/src/main/kotlin/in/secubox/toolbox/RootOnboard.kt create mode 100644 clients/android-toolbox/app/src/main/kotlin/in/secubox/toolbox/RootShell.kt diff --git a/clients/android-toolbox/app/src/main/kotlin/in/secubox/toolbox/MainActivity.kt b/clients/android-toolbox/app/src/main/kotlin/in/secubox/toolbox/MainActivity.kt index 6d7f0d86..e154fbe7 100644 --- a/clients/android-toolbox/app/src/main/kotlin/in/secubox/toolbox/MainActivity.kt +++ b/clients/android-toolbox/app/src/main/kotlin/in/secubox/toolbox/MainActivity.kt @@ -14,6 +14,7 @@ import android.provider.Settings import android.security.KeyChain import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -40,7 +41,7 @@ private val Matrix = Color(0xFF00FF41) private val Cinnabar = Color(0xFFE63946) private val TextPrimary = Color(0xFFE8E6D9) -enum class Step { Discover, InstallCa, ImportProfile, Verify, Done } +enum class Step { Discover, RootAuto, InstallCa, ImportProfile, Verify, Done } class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -61,6 +62,10 @@ fun OnboardApp() { var onTunnel by remember { mutableStateOf(false) } var peerIp by remember { mutableStateOf(null) } val api = remember(host) { ToolboxApi(host) } + var rootAvail by remember { mutableStateOf(false) } + val rootLog = remember { mutableStateListOf() } + // Detect root once, off the main thread. + LaunchedEffect(Unit) { rootAvail = withContext(Dispatchers.IO) { RootShell.available() } } MaterialTheme(colorScheme = darkColorScheme( primary = Gold, secondary = Cyan, background = Cosmos, surface = Cosmos, @@ -98,6 +103,57 @@ fun OnboardApp() { else status = "Borne injoignable — vérifie l'adresse / le réseau." } } + if (rootAvail) { + Spacer(Modifier.height(10.dp)) + Text("🔓 Root détecté — installation 100% automatique possible.", + color = Matrix, fontSize = 12.sp) + Spacer(Modifier.height(6.dp)) + OutlinedButton(onClick = { + busy = true; status = ""; rootLog.clear() + scope.launch { + val ok = withContext(Dispatchers.IO) { api.reachable() } + if (!ok) { busy = false; status = "Borne injoignable."; return@launch } + step = Step.RootAuto + val onb = RootOnboard(api, ctx.cacheDir) + val out = withContext(Dispatchers.IO) { + onb.runSilent { line -> + scope.launch(Dispatchers.Main) { rootLog.add(line) } + } + } + busy = false + onTunnel = out.verified + when { + out.verified -> step = Step.Done + out.wgViaApp -> { step = Step.ImportProfile + status = "CA installé en root ✓ — termine le tunnel via l'app WireGuard." } + else -> { step = Step.Verify + status = "Active le tunnel puis vérifie." } + } + } + }, modifier = Modifier.fillMaxWidth(), + border = BorderStroke(1.dp, Matrix), + colors = ButtonDefaults.outlinedButtonColors(contentColor = Matrix)) { + Text("⚡ Installation automatique (root)", fontWeight = FontWeight.Bold) + } + } + } + Step.RootAuto -> { + StepBody("Installation automatique (root)", + "CA système + tunnel WireGuard, sans intervention.") + Surface(color = Color(0xFF0E0E15), shape = MaterialTheme.shapes.small, + modifier = Modifier.fillMaxWidth()) { + Column(Modifier.padding(12.dp)) { + rootLog.forEach { line -> + Text(line, color = if (line.startsWith("✗")) Cinnabar + else if (line.startsWith("✓")) Matrix else TextPrimary, + fontSize = 12.sp, fontFamily = FontFamily.Monospace) + } + if (busy) { + Spacer(Modifier.height(6.dp)) + CircularProgressIndicator(Modifier.size(18.dp), color = Gold, strokeWidth = 2.dp) + } + } + } } Step.InstallCa -> { StepBody("1 · Installer le certificat (CA R3)", diff --git a/clients/android-toolbox/app/src/main/kotlin/in/secubox/toolbox/RootOnboard.kt b/clients/android-toolbox/app/src/main/kotlin/in/secubox/toolbox/RootOnboard.kt new file mode 100644 index 00000000..8b2daa4b --- /dev/null +++ b/clients/android-toolbox/app/src/main/kotlin/in/secubox/toolbox/RootOnboard.kt @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: LicenseRef-CMSD-1.0 +// Copyright (c) 2026 CyberMind — Gérald Kerma +// +// Root-mode fully-automated silent R3 onboarding (#538). +// All actions are gated behind an explicit "root auto" tap in the UI. +package `in`.secubox.toolbox + +import java.io.File +import java.security.MessageDigest +import java.security.cert.CertificateFactory + +class RootOnboard(private val api: ToolboxApi, private val cacheDir: File) { + + /** A line appended to the on-screen log during the silent run. */ + fun interface Logger { fun log(line: String) } + + data class Outcome(val caInstalled: Boolean, val wgUp: Boolean, val verified: Boolean, + val wgViaApp: Boolean) + + // ── system CA install (silent, root) ── + + /** + * OpenSSL `subject_hash_old` (pre-1.0 hash) computed WITHOUT openssl : + * MD5 of the DER-encoded subject name, first 4 bytes as a uint32 + * little-endian, formatted "%08x". This is the filename the Android + * system cacerts store uses (.0). + */ + fun subjectHashOld(pem: ByteArray): String { + val cf = CertificateFactory.getInstance("X.509") + val cert = cf.generateCertificate(pem.inputStream()) as java.security.cert.X509Certificate + val subjectDer = cert.subjectX500Principal.encoded + val md5 = MessageDigest.getInstance("MD5").digest(subjectDer) + val h = (md5[0].toLong() and 0xff) or + ((md5[1].toLong() and 0xff) shl 8) or + ((md5[2].toLong() and 0xff) shl 16) or + ((md5[3].toLong() and 0xff) shl 24) + return String.format("%08x", h) + } + + /** + * Install the CA into the SYSTEM trust store so EVERY app (not just + * those opting into user CAs) trusts it. Uses the bind-mount-over- + * cacerts technique that works on Android 10–14 (incl. the conscrypt + * APEX). Non-persistent across reboot — fine for a temporary cabine + * diagnostic; the app can also unmount to revert. + */ + fun installCaSystem(log: Logger): Boolean { + log.log("• Téléchargement du CA…") + val pem = api.download("/wg/ca.pem", "village3b-ca.pem", cacheDir).readBytes() + val hash = subjectHashOld(pem) + log.log("• CA hash : $hash.0") + val local = File(cacheDir, "$hash.0").apply { writeBytes(pem) } + + // Push the cert to a root-readable scratch path, then bind-mount a + // populated copy of the system store over the live cacerts dir. + val pushed = "/data/local/tmp/sbx-$hash.0" + val push = RootShell.install(local, pushed, "644") + if (!push.ok) { log.log("✗ push échoué : ${push.err.trim()}"); return false } + + val r = RootShell.runScript( + "set -e", + "CERT_DIR=/system/etc/security/cacerts", + "TMP=/data/local/tmp/sbx-cacerts", + "rm -rf \$TMP; mkdir -p \$TMP", + // seed with the existing system + APEX certs so nothing is lost + "cp -f \$CERT_DIR/* \$TMP/ 2>/dev/null || true", + "cp -f /apex/com.android.conscrypt/cacerts/* \$TMP/ 2>/dev/null || true", + "cp -f $pushed \$TMP/$hash.0", + "chmod 644 \$TMP/* 2>/dev/null || true", + "chown 0:0 \$TMP/* 2>/dev/null || true", + "chcon u:object_r:system_security_cacerts_file:s0 \$TMP/* 2>/dev/null || true", + // bind-mount over the live store (and the APEX path on 14) + "mount -o bind \$TMP \$CERT_DIR", + "[ -d /apex/com.android.conscrypt/cacerts ] && mount -o bind \$TMP /apex/com.android.conscrypt/cacerts 2>/dev/null || true", + "echo OK", + ) + if (r.ok && r.out.contains("OK")) { + log.log("✓ CA installé dans le magasin système (toutes les apps le font confiance)") + return true + } + log.log("✗ install CA système : ${r.err.trim().ifBlank { r.out.trim() }}") + return false + } + + fun removeCaSystem(log: Logger): Boolean { + val r = RootShell.runScript( + "umount /system/etc/security/cacerts 2>/dev/null || true", + "umount /apex/com.android.conscrypt/cacerts 2>/dev/null || true", + "echo OK", + ) + log.log(if (r.ok) "✓ CA système retiré (démonté)" else "✗ démontage : ${r.err.trim()}") + return r.ok + } + + // ── WireGuard bring-up ── + + /** Parse the wg-quick .conf into the fields we need. */ + private data class WgConf(val privKey: String, val address: String, + val pubKey: String, val endpoint: String, val allowed: String) + + private fun parse(conf: String): WgConf? { + var pk = ""; var addr = ""; var pub = ""; var ep = ""; var aip = "" + conf.lineSequence().forEach { raw -> + val l = raw.trim() + when { + l.startsWith("PrivateKey", true) -> pk = l.substringAfter("=").trim() + l.startsWith("Address", true) -> addr = l.substringAfter("=").trim() + l.startsWith("PublicKey", true) -> pub = l.substringAfter("=").trim() + l.startsWith("Endpoint", true) -> ep = l.substringAfter("=").trim() + l.startsWith("AllowedIPs", true) -> aip = l.substringAfter("=").trim() + } + } + return if (pk.isNotBlank() && pub.isNotBlank() && ep.isNotBlank()) WgConf(pk, addr, pub, ep, aip) else null + } + + /** + * Bring the tunnel up natively with root IF the kernel has WireGuard + * + `wg`/`ip`. Returns true on success ; false means the caller + * should fall back to the WireGuard-app handoff. + */ + fun setupWireguardRoot(log: Logger): Boolean { + if (!RootShell.hasKernelWireguard()) { + log.log("• Noyau sans module WireGuard — bascule sur l'app WireGuard") + return false + } + log.log("• Génération du profil WireGuard…") + val conf = api.downloadProfile(cacheDir).readText() + val wg = parse(conf) ?: run { log.log("✗ profil illisible"); return false } + val iface = "wg-village3b" + val r = RootShell.runScript( + "set -e", + "ip link del $iface 2>/dev/null || true", + "ip link add $iface type wireguard", + "echo '${wg.privKey}' > /data/local/tmp/sbx-wg.key && chmod 600 /data/local/tmp/sbx-wg.key", + "wg set $iface private-key /data/local/tmp/sbx-wg.key peer ${wg.pubKey} endpoint ${wg.endpoint} allowed-ips ${wg.allowed.ifBlank { "0.0.0.0/0" }} persistent-keepalive 25", + "rm -f /data/local/tmp/sbx-wg.key", + if (wg.address.isNotBlank()) "ip addr add ${wg.address} dev $iface 2>/dev/null || true" else ":", + "ip link set $iface up", + "for n in ${wg.allowed.replace(",", " ")}; do ip route replace \$n dev $iface 2>/dev/null || true; done", + "echo OK", + ) + if (r.ok && r.out.contains("OK")) { log.log("✓ Tunnel $iface actif (root, natif)"); return true } + log.log("✗ WG natif : ${r.err.trim().ifBlank { r.out.trim() }} — bascule sur l'app") + return false + } + + /** Run the whole silent sequence. Blocking — call off-main. */ + fun runSilent(log: Logger): Outcome { + val ca = installCaSystem(log) + val wgRoot = setupWireguardRoot(log) + var verified = false + if (wgRoot) { + log.log("• Vérification R3…") + Thread.sleep(1500) + val (t, ip) = api.r3Check() + verified = t + log.log(if (t) "✓ Tunnel R3 confirmé (${ip ?: "?"})" else "• Pas encore confirmé — réessaie la vérification") + } + return Outcome(caInstalled = ca, wgUp = wgRoot, verified = verified, wgViaApp = !wgRoot) + } +} diff --git a/clients/android-toolbox/app/src/main/kotlin/in/secubox/toolbox/RootShell.kt b/clients/android-toolbox/app/src/main/kotlin/in/secubox/toolbox/RootShell.kt new file mode 100644 index 00000000..d8a65d05 --- /dev/null +++ b/clients/android-toolbox/app/src/main/kotlin/in/secubox/toolbox/RootShell.kt @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: LicenseRef-CMSD-1.0 +// Copyright (c) 2026 CyberMind — Gérald Kerma +package `in`.secubox.toolbox + +import java.io.File + +/** + * Thin wrapper around `su` for the root-mode silent onboarding (#538). + * Every action is gated behind an explicit user tap in the UI — nothing + * runs as root without the operator choosing root mode on their own + * device. + */ +object RootShell { + + data class Result(val code: Int, val out: String, val err: String) { + val ok get() = code == 0 + } + + /** True if a `su` binary is on PATH and grants a root shell. */ + fun available(): Boolean = try { + run("id -u").out.trim() == "0" + } catch (_: Exception) { false } + + /** Run a single command in a root shell. Blocking — call off-main. */ + fun run(cmd: String): Result { + val p = ProcessBuilder("su", "-c", cmd) + .redirectErrorStream(false) + .start() + val out = p.inputStream.bufferedReader().readText() + val err = p.errorStream.bufferedReader().readText() + val code = p.waitFor() + return Result(code, out, err) + } + + /** Run several commands in ONE root shell (atomic-ish, keeps remount). */ + fun runScript(vararg lines: String): Result { + val p = ProcessBuilder("su").redirectErrorStream(false).start() + p.outputStream.bufferedWriter().use { w -> + lines.forEach { w.write(it); w.write("\n") } + w.write("exit $?\n") + } + val out = p.inputStream.bufferedReader().readText() + val err = p.errorStream.bufferedReader().readText() + val code = p.waitFor() + return Result(code, out, err) + } + + /** Push a local file to a root-owned path via cat (avoids cp quirks). */ + fun install(src: File, destPath: String, mode: String = "644"): Result { + val b64 = android.util.Base64.encodeToString(src.readBytes(), android.util.Base64.NO_WRAP) + return runScript( + "echo '$b64' | base64 -d > '$destPath'", + "chmod $mode '$destPath'", + ) + } + + /** Kernel has WireGuard + the `wg` tool available to root? */ + fun hasKernelWireguard(): Boolean = try { + val w = run("command -v wg || ls /system/*bin/wg 2>/dev/null") + val ip = run("command -v ip") + w.out.isNotBlank() && ip.out.isNotBlank() + } catch (_: Exception) { false } +} From ac14e65353ed8bc78678727794893d054a2fdbc8 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Sat, 13 Jun 2026 07:46:50 +0200 Subject: [PATCH 2/2] docs: wiki/README/WIP/TODO/HISTORY for Android ToolBox app + root-mode (ref #538) - new wiki page Android-ToolBox.md (install via /wg/toolbox.apk, manual + root onboarding flows, CI build, endpoints) + sidebar link - README: document the root-mode silent path + revised constraints - WIP/TODO/HISTORY: Android client section (#531/#536/#538) Co-Authored-By: Claude Opus 4.8 --- .claude/HISTORY.md | 18 ++++++ .claude/TODO.md | 14 ++++- .claude/WIP.md | 29 +++++++++- clients/android-toolbox/README.md | 39 ++++++++++--- wiki/Android-ToolBox.md | 91 +++++++++++++++++++++++++++++++ wiki/_Sidebar.md | 1 + 6 files changed, 183 insertions(+), 9 deletions(-) create mode 100644 wiki/Android-ToolBox.md diff --git a/.claude/HISTORY.md b/.claude/HISTORY.md index abe82a6f..e7609988 100644 --- a/.claude/HISTORY.md +++ b/.claude/HISTORY.md @@ -3,6 +3,24 @@ --- +## 2026-06-13 — Android ToolBox app : serve + root-mode silent onboarding (ref #531/#536/#538) + +App compagnon Android one-tap R3 (`clients/android-toolbox/`, Kotlin + Compose). + +- **#531** — scaffold Gradle/Compose + CI `build-android-apk.yml` (debug APK + artifact, release asset sur tag `android-v*`). CI green. +- **#536** — `GET /wg/toolbox.apk` (build local sinon 302 → release GitHub) + + bouton onboard kbin + helper `secubox-toolbox-fetch-apk`. +- **#538** (PR #539) — root-mode silent onboarding : install CA système + (bind-mount cacerts + APEX conscrypt, SELinux ctx, `subject_hash_old` + pur Kotlin) + WireGuard natif noyau + vérif R3 auto, gated derrière le tap + `⚡ Installation automatique (root)`. Fallback handoff app WireGuard. + Fichiers `RootShell.kt`, `RootOnboard.kt`, step `RootAuto`. CI APK build + green (code compile). +- Suivi : release signing (keystore CI) pour empreinte publiée stable. + +--- + ## 2026-06-11 — Phase 12.C + Phase 13 protection enforcement plane COMPLETE (ref #518-#528) `secubox-toolbox 2.6.6 → 2.6.11`, tags v2.13.16 → v2.13.19. diff --git a/.claude/TODO.md b/.claude/TODO.md index d7113595..ad5e943f 100644 --- a/.claude/TODO.md +++ b/.claude/TODO.md @@ -1,10 +1,22 @@ # TODO — SecuBox-DEB Backlog -*Mis à jour : 2026-06-11* +*Mis à jour : 2026-06-13* --- ## 🔥 P0 — Immediate (in flight) +### Android ToolBox client (`clients/android-toolbox/`) + +- [x] **#531 scaffold + CI** — Gradle/Compose one-tap onboarding, debug APK + via `build-android-apk.yml`. CI green. +- [x] **#536 serve from toolbox** — `GET /wg/toolbox.apk` + onboard button + + `secubox-toolbox-fetch-apk` helper. +- [x] **#538 root-mode silent** (PR #539) — system CA install + native kernel + WireGuard + auto R3 verify, gated behind explicit root tap. +- [ ] **release signing** — keystore secret in CI for a stable published + fingerprint (currently debug-signed sideload). +- [ ] **#532 browser XPI** — Firefox/Chrome extension equivalent (later). + ### Phase 13 — Protection enforcement plane (#519) — ✅ COMPLETE - [x] **13.A spine** (#521, `2.6.8`, v2.13.17) — nft blacklist set + forward-drop diff --git a/.claude/WIP.md b/.claude/WIP.md index 7701702a..3772e0f0 100644 --- a/.claude/WIP.md +++ b/.claude/WIP.md @@ -1,5 +1,32 @@ # WIP — Work In Progress -*Mis à jour : 2026-06-11* +*Mis à jour : 2026-06-13* + +--- + +## 🔄 2026-06-13 : Android ToolBox app — serve + root-mode silent onboarding (#531/#536/#538) + +App compagnon Android **one-tap R3** pour la cabine VILLAGE3B +(`clients/android-toolbox/`, `in.secubox.toolbox`, Kotlin + Compose). + +- **#531 — scaffold + CI** : projet Gradle/Compose (5-step stepper + Discover→InstallCa→ImportProfile→Verify→Done), client `HttpURLConnection`, + workflow `build-android-apk.yml` (debug APK artifact, release asset sur + tag `android-v*`). CI **GREEN**. +- **#536 — serve depuis la toolbox** : endpoint `GET /wg/toolbox.apk` + (sert le build local `/var/lib/secubox/toolbox/android/`, sinon 302 → + release GitHub) + bouton *📱 Installer l'app ToolBoX (1-tap)* dans les + panneaux onboard kbin + helper `secubox-toolbox-fetch-apk`. Vérifié : + 200 `application/vnd.android.package-archive`, 14.8 MB. +- **#538 — root-mode silent onboarding** (PR #539, branche poussée) : + bouton *⚡ Installation automatique (root)* sur devices rootés → + install CA dans le magasin **système** (bind-mount cacerts + APEX + conscrypt, SELinux ctx, `subject_hash_old` en Kotlin pur) + tunnel + WireGuard **natif noyau** (`ip link add … type wireguard` + `wg set`) + + vérif R3 auto. Fallback handoff app WireGuard si noyau sans WG. Toutes + les actions root gated derrière le tap explicite. Nouveaux fichiers + `RootShell.kt`, `RootOnboard.kt`, step `RootAuto` (log streamé). +- **Reste à faire** : release signing (keystore secret CI) pour une + empreinte publiée stable — actuellement debug-signé (sideload). --- diff --git a/clients/android-toolbox/README.md b/clients/android-toolbox/README.md index e4ef3698..73488e1a 100644 --- a/clients/android-toolbox/README.md +++ b/clients/android-toolbox/README.md @@ -5,13 +5,36 @@ One-tap **R3 onboarding** for the VILLAGE3B cabine : install the CA, import the WireGuard profile, verify the tunnel, then open the live *cartographie sociale*. Replaces the manual Android tutorial. -## Flow +## Flow (manual path) 1. **Discover** — scan the kbin QR or type the booth host (`kbin.gk2.secubox.in`). 2. **Install CA** — downloads `/wg/ca.crt`, launches the Android cert-install intent (`KeyChain.createInstallIntent`). 3. **Import profile** — downloads `/wg/profile/new`, hands the `.conf` to the WireGuard app via `FileProvider` + `ACTION_VIEW`. 4. **Verify** — polls `/wg/r3-check` → "Tunnel R3 actif ✓". 5. **Live metrics** — opens `/social/me` (cartographie sociale). +## Root path — fully-automated silent onboarding (#538) +When the device is **rooted**, the Discover step shows an extra +**⚡ Installation automatique (root)** button. Tapping it runs the whole +onboarding with zero further interaction (`RootAuto` step, streaming log): + +1. **System CA install** — downloads `/wg/ca.pem`, computes the OpenSSL + `subject_hash_old` in pure Kotlin, and bind-mounts a populated copy of + the trust store over `/system/etc/security/cacerts` (+ the conscrypt + APEX path on Android 14), restoring the SELinux context + (`u:object_r:system_security_cacerts_file:s0`). **Every** app trusts the + cabine CA — not just user-CA opt-in apps. Reversible via `umount`. +2. **Native WireGuard** — if the kernel has the WireGuard module + `wg`/`ip`, + brings the tunnel up natively (`ip link add … type wireguard` + `wg set`), + no WireGuard app required. +3. **Auto R3 verify** — polls `/wg/r3-check`. + +**Fallback** — if the kernel lacks WireGuard, the root path installs the +system CA then hands off to the manual WireGuard-app flow (steps 3–5 above). + +All root actions are **gated behind the explicit tap** — nothing runs as +root without the operator choosing root mode on their own device. +See `RootShell.kt` (su wrapper) and `RootOnboard.kt` (silent sequence). + ## Build No Gradle wrapper jar is committed (text-only scaffold). CI builds it: - **GitHub Actions** `build-android-apk.yml` → debug APK artifact. @@ -22,12 +45,14 @@ gradle :app:assembleDebug # app/build/outputs/apk/debug/app-debug.apk ``` ## Constraints (MVP) -- Android 11+ restricts **user CA trust** ; the app launches the install - intent + guides the manual confirm step. Browsers on the device need - the CA trusted for the mitm R3 break — this is the known Android - limitation (documented, not yet automated). -- WireGuard profile import uses the **official WireGuard app** (no embedded - tunnel in the MVP) — most reliable, no extra native deps. +- Android 11+ restricts **user CA trust** ; the *manual* path launches the + install intent + guides the confirm step. Browsers on the device need the + CA trusted for the mitm R3 break — this is the known Android limitation on + non-rooted devices. **Rooted devices bypass it entirely** via the system + CA install (see Root path above). +- The *manual* path imports the WireGuard profile via the **official + WireGuard app** (no embedded tunnel) — most reliable, no extra native + deps. The *root* path brings the tunnel up natively with the kernel module. - Debug APK is self-signed (sideload). Release signing (published fingerprint, served from the toolbox) is a follow-up needing a keystore secret in CI. diff --git a/wiki/Android-ToolBox.md b/wiki/Android-ToolBox.md new file mode 100644 index 00000000..42962ba5 --- /dev/null +++ b/wiki/Android-ToolBox.md @@ -0,0 +1,91 @@ + +# 📱 Android ToolBox — one-tap R3 onboarding + +The **SecuBox Android ToolBox** is a tiny companion app that onboards a +phone onto the VILLAGE3B *cabine* in one tap: it installs the cabine CA, +brings up the WireGuard R3 tunnel, verifies reachability, then opens the +live *cartographie sociale*. It replaces the manual Android tutorial. + +- Source : [`clients/android-toolbox/`](https://github.com/CyberMind-FR/secubox-deb/tree/master/clients/android-toolbox) +- Package : `in.secubox.toolbox` · Kotlin + Jetpack Compose · minSdk 26 / targetSdk 34 +- License : `LicenseRef-CMSD-1.0` + +## Install + +Grab the APK directly from the cabine — the toolbox serves it: + +``` +https://kbin..secubox.in/wg/toolbox.apk +``` + +The onboard panels in the kbin WebUI expose a **📱 Installer l'app ToolBoX +(1-tap)** button pointing at that endpoint. When the cabine has a locally +fetched build it serves it (`application/vnd.android.package-archive`); +otherwise it 302-redirects to the latest GitHub release asset +`secubox-toolbox-android.apk`. + +> The MVP APK is **debug-signed** (sideload — enable *Install unknown +> apps*). A release-signed build with a published fingerprint is a +> follow-up (needs a keystore secret in CI). + +## Onboarding flows + +### Manual path (non-rooted) +1. **Discover** — scan the kbin QR or type the booth host (`kbin..secubox.in`). +2. **Install CA** — downloads `/wg/ca.crt`, launches the Android cert-install + intent (`KeyChain.createInstallIntent`). +3. **Import profile** — downloads `/wg/profile/new`, hands the `.conf` to the + official WireGuard app (`FileProvider` + `ACTION_VIEW`). +4. **Verify** — polls `/wg/r3-check` → *Tunnel R3 actif ✓*. +5. **Live metrics** — opens `/social/me` (cartographie sociale). + +Android 11+ restricts **user CA trust**, so on a non-rooted device the +browser CA confirm is a guided manual step. + +### Root path — fully-automated, silent (#538) +When the device is **rooted**, the Discover step shows an extra +**⚡ Installation automatique (root)** button. One tap runs everything with +no further interaction (a `RootAuto` step streams the progress log): + +1. **System CA install** — downloads `/wg/ca.pem`, computes the OpenSSL + `subject_hash_old` in pure Kotlin, then bind-mounts a populated copy of + the trust store over `/system/etc/security/cacerts` (+ the conscrypt + APEX on Android 14), restoring the SELinux context + `u:object_r:system_security_cacerts_file:s0`. **Every** app trusts the + cabine CA — not just user-CA opt-in apps. Reversible via `umount`. +2. **Native WireGuard** — if the kernel has the WireGuard module + `wg`/`ip`, + the tunnel comes up natively (`ip link add … type wireguard` + `wg set`) — + no WireGuard app required. +3. **Auto R3 verify** — polls `/wg/r3-check`. + +If the kernel lacks WireGuard, the root path installs the system CA then +falls back to the manual WireGuard-app handoff. + +**Safety** — every root action is gated behind the explicit tap; nothing +runs as root unless the operator chooses root mode on their own device. +Code: `RootShell.kt` (su wrapper) + `RootOnboard.kt` (silent sequence). + +## Build (CI) + +No Gradle wrapper jar is committed (text-only scaffold). CI builds it: + +- GitHub Actions `build-android-apk.yml` → debug APK artifact on push to + `master` / PRs touching `clients/android-toolbox/**`. +- Tagging `android-v*` publishes the APK as a release asset. + +Locally (Android SDK + Gradle 8.9 + JDK 17): + +```bash +cd clients/android-toolbox +gradle :app:assembleDebug # app/build/outputs/apk/debug/app-debug.apk +``` + +## Cabine endpoints consumed + +| Endpoint | Purpose | +|----------|---------| +| `/wg/ca.crt` / `/wg/ca.pem` | cabine CA (user / system store) | +| `/wg/profile/new` | fresh WireGuard `.conf` | +| `/wg/r3-check` | tunnel reachability probe | +| `/social/me` | live cartographie sociale | +| `/wg/toolbox.apk` | the APK itself | diff --git a/wiki/_Sidebar.md b/wiki/_Sidebar.md index a5c37188..05abe930 100644 --- a/wiki/_Sidebar.md +++ b/wiki/_Sidebar.md @@ -29,6 +29,7 @@ * [[ARM-Installation]] | [FR](ARM-Installation-FR) | [DE](ARM-Installation-DE) | [中文](ARM-Installation-ZH) * [[ESPRESSObin]] | [FR](ESPRESSObin-FR) | [DE](ESPRESSObin-DE) | [中文](ESPRESSObin-ZH) * [[Eye-Remote]] 📡 +* [[Android-ToolBox]] 📱 one-tap R3 * [[QEMU-ARM64]] 🖥️ ### 🟢 ROOT — Configuration