mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-01 06:24:59 +00:00
Compare commits
3 Commits
46dfd781d3
...
110133bee9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
110133bee9 | ||
| ac14e65353 | |||
| 1351054927 |
|
|
@ -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)
|
## 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.
|
`secubox-toolbox 2.6.6 → 2.6.11`, tags v2.13.16 → v2.13.19.
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,22 @@
|
||||||
# TODO — SecuBox-DEB Backlog
|
# TODO — SecuBox-DEB Backlog
|
||||||
*Mis à jour : 2026-06-11*
|
*Mis à jour : 2026-06-13*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔥 P0 — Immediate (in flight)
|
## 🔥 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
|
### Phase 13 — Protection enforcement plane (#519) — ✅ COMPLETE
|
||||||
|
|
||||||
- [x] **13.A spine** (#521, `2.6.8`, v2.13.17) — nft blacklist set + forward-drop
|
- [x] **13.A spine** (#521, `2.6.8`, v2.13.17) — nft blacklist set + forward-drop
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,32 @@
|
||||||
# WIP — Work In Progress
|
# 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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
import the WireGuard profile, verify the tunnel, then open the live
|
||||||
*cartographie sociale*. Replaces the manual Android tutorial.
|
*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`).
|
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`).
|
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`.
|
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 ✓".
|
4. **Verify** — polls `/wg/r3-check` → "Tunnel R3 actif ✓".
|
||||||
5. **Live metrics** — opens `/social/me` (cartographie sociale).
|
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
|
## Build
|
||||||
No Gradle wrapper jar is committed (text-only scaffold). CI builds it:
|
No Gradle wrapper jar is committed (text-only scaffold). CI builds it:
|
||||||
- **GitHub Actions** `build-android-apk.yml` → debug APK artifact.
|
- **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)
|
## Constraints (MVP)
|
||||||
- Android 11+ restricts **user CA trust** ; the app launches the install
|
- Android 11+ restricts **user CA trust** ; the *manual* path launches the
|
||||||
intent + guides the manual confirm step. Browsers on the device need
|
install intent + guides the confirm step. Browsers on the device need the
|
||||||
the CA trusted for the mitm R3 break — this is the known Android
|
CA trusted for the mitm R3 break — this is the known Android limitation on
|
||||||
limitation (documented, not yet automated).
|
non-rooted devices. **Rooted devices bypass it entirely** via the system
|
||||||
- WireGuard profile import uses the **official WireGuard app** (no embedded
|
CA install (see Root path above).
|
||||||
tunnel in the MVP) — most reliable, no extra native deps.
|
- 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
|
- Debug APK is self-signed (sideload). Release signing (published
|
||||||
fingerprint, served from the toolbox) is a follow-up needing a keystore
|
fingerprint, served from the toolbox) is a follow-up needing a keystore
|
||||||
secret in CI.
|
secret in CI.
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import android.provider.Settings
|
||||||
import android.security.KeyChain
|
import android.security.KeyChain
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
|
@ -40,7 +41,7 @@ private val Matrix = Color(0xFF00FF41)
|
||||||
private val Cinnabar = Color(0xFFE63946)
|
private val Cinnabar = Color(0xFFE63946)
|
||||||
private val TextPrimary = Color(0xFFE8E6D9)
|
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() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|
@ -61,6 +62,10 @@ fun OnboardApp() {
|
||||||
var onTunnel by remember { mutableStateOf(false) }
|
var onTunnel by remember { mutableStateOf(false) }
|
||||||
var peerIp by remember { mutableStateOf<String?>(null) }
|
var peerIp by remember { mutableStateOf<String?>(null) }
|
||||||
val api = remember(host) { ToolboxApi(host) }
|
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(
|
MaterialTheme(colorScheme = darkColorScheme(
|
||||||
primary = Gold, secondary = Cyan, background = Cosmos, surface = Cosmos,
|
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."
|
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 -> {
|
Step.InstallCa -> {
|
||||||
StepBody("1 · Installer le certificat (CA R3)",
|
StepBody("1 · Installer le certificat (CA R3)",
|
||||||
|
|
|
||||||
|
|
@ -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 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
91
wiki/Android-ToolBox.md
Normal 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 |
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
* [[ARM-Installation]] | [FR](ARM-Installation-FR) | [DE](ARM-Installation-DE) | [中文](ARM-Installation-ZH)
|
* [[ARM-Installation]] | [FR](ARM-Installation-FR) | [DE](ARM-Installation-DE) | [中文](ARM-Installation-ZH)
|
||||||
* [[ESPRESSObin]] | [FR](ESPRESSObin-FR) | [DE](ESPRESSObin-DE) | [中文](ESPRESSObin-ZH)
|
* [[ESPRESSObin]] | [FR](ESPRESSObin-FR) | [DE](ESPRESSObin-DE) | [中文](ESPRESSObin-ZH)
|
||||||
* [[Eye-Remote]] 📡
|
* [[Eye-Remote]] 📡
|
||||||
|
* [[Android-ToolBox]] 📱 one-tap R3
|
||||||
* [[QEMU-ARM64]] 🖥️
|
* [[QEMU-ARM64]] 🖥️
|
||||||
|
|
||||||
### 🟢 ROOT — Configuration
|
### 🟢 ROOT — Configuration
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user