mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-01 06:24:59 +00:00
Compare commits
No commits in common. "110133bee9676a9eae5c4b1ca6038bfea2516ea1" and "46dfd781d3588fa12fd5aa56e5a17164b9a8b83d" have entirely different histories.
110133bee9
...
46dfd781d3
|
|
@ -3,24 +3,6 @@
|
|||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
|
|
|||
|
|
@ -1,22 +1,10 @@
|
|||
# TODO — SecuBox-DEB Backlog
|
||||
*Mis à jour : 2026-06-13*
|
||||
*Mis à jour : 2026-06-11*
|
||||
|
||||
---
|
||||
|
||||
## 🔥 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
|
||||
|
|
|
|||
|
|
@ -1,32 +1,5 @@
|
|||
# WIP — Work In Progress
|
||||
*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).
|
||||
*Mis à jour : 2026-06-11*
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -5,36 +5,13 @@ 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 (manual path)
|
||||
## Flow
|
||||
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.
|
||||
|
|
@ -45,14 +22,12 @@ gradle :app:assembleDebug # app/build/outputs/apk/debug/app-debug.apk
|
|||
```
|
||||
|
||||
## Constraints (MVP)
|
||||
- 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.
|
||||
- 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.
|
||||
- Debug APK is self-signed (sideload). Release signing (published
|
||||
fingerprint, served from the toolbox) is a follow-up needing a keystore
|
||||
secret in CI.
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ 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
|
||||
|
|
@ -41,7 +40,7 @@ private val Matrix = Color(0xFF00FF41)
|
|||
private val Cinnabar = Color(0xFFE63946)
|
||||
private val TextPrimary = Color(0xFFE8E6D9)
|
||||
|
||||
enum class Step { Discover, RootAuto, InstallCa, ImportProfile, Verify, Done }
|
||||
enum class Step { Discover, InstallCa, ImportProfile, Verify, Done }
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
|
@ -62,10 +61,6 @@ 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,
|
||||
|
|
@ -103,57 +98,6 @@ 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)",
|
||||
|
|
|
|||
|
|
@ -1,161 +0,0 @@
|
|||
// 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 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
// 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 }
|
||||
}
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
<!-- 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 |
|
||||
|
|
@ -29,7 +29,6 @@
|
|||
* [[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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user