Compare commits

..

3 Commits

Author SHA1 Message Date
CyberMind
110133bee9
Merge pull request #539 from CyberMind-FR/feature/538-android-app-root-mode-fully-automated-si
Some checks are pending
License Headers / check (push) Waiting to run
Android root-mode fully-automated silent R3 onboarding (#538)
2026-06-13 07:47:00 +02:00
ac14e65353 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 <noreply@anthropic.com>
2026-06-13 07:46:50 +02:00
1351054927 feat(android): root-mode fully-automated silent R3 onboarding (ref #538)
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 <noreply@anthropic.com>
2026-06-13 07:40:58 +02:00
9 changed files with 464 additions and 10 deletions

View File

@ -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.

View File

@ -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

View File

@ -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).
---

View File

@ -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 35 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.

View File

@ -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<String?>(null) }
val api = remember(host) { ToolboxApi(host) }
var rootAvail by remember { mutableStateOf(false) }
val rootLog = remember { mutableStateListOf<String>() }
// 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)",

View File

@ -0,0 +1,161 @@
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
//
// 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 (<hash>.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 1014 (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)
}
}

View File

@ -0,0 +1,63 @@
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
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 }
}

91
wiki/Android-ToolBox.md Normal file
View File

@ -0,0 +1,91 @@
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
# 📱 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.<board>.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.<board>.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 |

View File

@ -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