mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-06-30 14:10:44 +00:00
Compare commits
2 Commits
92b203cbbc
...
94f40c9162
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94f40c9162 | ||
| c1fa245a6a |
|
|
@ -12,13 +12,20 @@ import the WireGuard profile, verify the tunnel, then open the live
|
||||||
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, #551)
|
## Root path — real zero-tap, fully automated (#538, #551, #558)
|
||||||
When the device is **rooted**, the app runs the whole onboarding with **zero
|
On a **rooted** device the app onboards with **zero taps**, two ways:
|
||||||
taps**: on launch it auto-detects root and, if this cabine host hasn't been
|
|
||||||
onboarded yet, starts the silent sequence automatically (`RootAuto` step,
|
- **On launch** — auto-detects root and runs the silent sequence immediately
|
||||||
streaming log). The **⚡ Installation automatique (root)** button stays for
|
every launch (no gate), retrying reachability while WiFi/tunnel settle.
|
||||||
re-runs. The "already onboarded" flag is persisted per host (SharedPreferences)
|
- **On boot** — a `BOOT_COMPLETED` receiver starts a short foreground service
|
||||||
so reopening the app doesn't redo it. Steps:
|
(`OnboardService`) that runs the same silent sequence **without opening the
|
||||||
|
app**, then stops. After one reboot the device self-onboards.
|
||||||
|
|
||||||
|
The **⚡ Installation automatique (root)** button remains as a manual
|
||||||
|
re-trigger. Two interactions are **mandated by Android and unavoidable** for
|
||||||
|
any app: the sideload install confirm ("install unknown apps") and the
|
||||||
|
first-time superuser (Magisk/su) grant prompt. Everything after those is
|
||||||
|
zero-tap. Steps:
|
||||||
|
|
||||||
1. **System CA install** — downloads `/wg/ca.pem`, computes the OpenSSL
|
1. **System CA install** — downloads `/wg/ca.pem`, computes the OpenSSL
|
||||||
`subject_hash_old` in pure Kotlin, and bind-mounts a populated copy of
|
`subject_hash_old` in pure Kotlin, and bind-mounts a populated copy of
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ android {
|
||||||
applicationId = "in.secubox.toolbox"
|
applicationId = "in.secubox.toolbox"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 2
|
versionCode = 3
|
||||||
versionName = "0.2.0"
|
versionName = "0.3.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,11 @@
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<!-- #558 full-auto: run the silent onboarding on device boot, no app open. -->
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<!-- Query the WireGuard app so we can hand it the generated profile. -->
|
<!-- Query the WireGuard app so we can hand it the generated profile. -->
|
||||||
<queries>
|
<queries>
|
||||||
<package android:name="com.wireguard.android" />
|
<package android:name="com.wireguard.android" />
|
||||||
|
|
@ -26,6 +31,26 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<!-- #558 : boot-completed → start the onboarding foreground service
|
||||||
|
so a rooted device self-onboards with zero taps after a reboot. -->
|
||||||
|
<receiver
|
||||||
|
android:name=".BootReceiver"
|
||||||
|
android:exported="true"
|
||||||
|
android:enabled="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
<service
|
||||||
|
android:name=".OnboardService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="specialUse">
|
||||||
|
<property
|
||||||
|
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||||
|
android:value="Silent R3 onboarding on a rooted, operator-owned cabine device" />
|
||||||
|
</service>
|
||||||
|
|
||||||
<!-- FileProvider to share the downloaded CA + WG .conf with the
|
<!-- FileProvider to share the downloaded CA + WG .conf with the
|
||||||
system cert installer / the WireGuard app. -->
|
system cert installer / the WireGuard app. -->
|
||||||
<provider
|
<provider
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
//
|
||||||
|
// #558 — boot-completed → kick the onboarding foreground service so a
|
||||||
|
// rooted, operator-owned cabine device self-onboards with zero taps after
|
||||||
|
// a reboot (no need to open the app).
|
||||||
|
package `in`.secubox.toolbox
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
|
||||||
|
class BootReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
|
val a = intent?.action ?: return
|
||||||
|
if (a == Intent.ACTION_BOOT_COMPLETED || a == Intent.ACTION_LOCKED_BOOT_COMPLETED) {
|
||||||
|
ContextCompat.startForegroundService(
|
||||||
|
context, Intent(context, OnboardService::class.java),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -70,12 +70,20 @@ fun OnboardApp() {
|
||||||
var autoTried by remember { mutableStateOf(false) }
|
var autoTried by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// The whole root-mode silent run, reused by the ⚡ button AND the
|
// The whole root-mode silent run, reused by the ⚡ button AND the
|
||||||
// zero-tap auto-launch (#551). Persists an onboarded flag per host on
|
// zero-tap auto-launch (#551/#558). NO onboarded gate — it auto-runs
|
||||||
// success so reopening the app doesn't redo it.
|
// every launch (idempotent: re-asserts CA + WG). Reachability is
|
||||||
|
// RETRIED so a WiFi/tunnel race at launch doesn't kill the auto-run.
|
||||||
val runRootAuto: () -> Unit = {
|
val runRootAuto: () -> Unit = {
|
||||||
busy = true; status = ""; rootLog.clear()
|
busy = true; status = ""; rootLog.clear()
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val ok = withContext(Dispatchers.IO) { api.reachable() }
|
// poll reachability up to ~9 s (network may still be settling)
|
||||||
|
var ok = false
|
||||||
|
for (attempt in 1..6) {
|
||||||
|
ok = withContext(Dispatchers.IO) { api.reachable() }
|
||||||
|
if (ok) break
|
||||||
|
status = "Recherche de la borne… ($attempt)"
|
||||||
|
kotlinx.coroutines.delay(1500)
|
||||||
|
}
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
busy = false; status = "Borne injoignable — vérifie le réseau."
|
busy = false; status = "Borne injoignable — vérifie le réseau."
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -86,7 +94,6 @@ fun OnboardApp() {
|
||||||
}
|
}
|
||||||
busy = false
|
busy = false
|
||||||
onTunnel = out.verified
|
onTunnel = out.verified
|
||||||
if (out.verified) prefs.edit().putBoolean("onboarded:$host", true).apply()
|
|
||||||
when {
|
when {
|
||||||
out.verified -> step = Step.Done
|
out.verified -> step = Step.Done
|
||||||
out.wgViaApp -> { step = Step.ImportProfile
|
out.wgViaApp -> { step = Step.ImportProfile
|
||||||
|
|
@ -100,12 +107,13 @@ fun OnboardApp() {
|
||||||
|
|
||||||
// Detect root once, off the main thread.
|
// Detect root once, off the main thread.
|
||||||
LaunchedEffect(Unit) { rootAvail = withContext(Dispatchers.IO) { RootShell.available() } }
|
LaunchedEffect(Unit) { rootAvail = withContext(Dispatchers.IO) { RootShell.available() } }
|
||||||
// Zero-tap (#551): on a rooted device, auto-run the silent onboarding
|
// Zero-tap (#558): on a rooted device, auto-run the silent onboarding
|
||||||
// once on launch — unless this host was already onboarded.
|
// on every launch — no gate. (Boot-time auto-run is handled by
|
||||||
|
// BootReceiver + OnboardService so it runs without opening the app.)
|
||||||
LaunchedEffect(rootAvail) {
|
LaunchedEffect(rootAvail) {
|
||||||
if (rootAvail && !autoTried && step == Step.Discover) {
|
if (rootAvail && !autoTried && step == Step.Discover) {
|
||||||
autoTried = true
|
autoTried = true
|
||||||
if (!prefs.getBoolean("onboarded:$host", false)) runRootAuto()
|
runRootAuto()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
//
|
||||||
|
// #558 — full-auto onboarding service. Started on boot (BootReceiver). On a
|
||||||
|
// rooted device it runs the silent R3 onboarding (system CA + native WG +
|
||||||
|
// verify) with zero taps, retrying reachability while the network settles,
|
||||||
|
// then stops itself. Non-root / unreachable → it just stops (the launcher
|
||||||
|
// activity remains the manual path).
|
||||||
|
package `in`.secubox.toolbox
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.IBinder
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class OnboardService : Service() {
|
||||||
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
private val CHAN = "sbx-onboard"
|
||||||
|
private val NID = 4201
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?): IBinder? = null
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
startForeground(NID, buildNotification())
|
||||||
|
scope.launch {
|
||||||
|
try { runOnce() } finally { stopSelf() }
|
||||||
|
}
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun runOnce() {
|
||||||
|
// root is the precondition for the silent path; bail quietly otherwise.
|
||||||
|
if (!RootShell.available()) return
|
||||||
|
val host = getSharedPreferences("secubox-toolbox", Context.MODE_PRIVATE)
|
||||||
|
.getString("host", null) ?: "kbin.gk2.secubox.in"
|
||||||
|
val api = ToolboxApi(host)
|
||||||
|
// network may still be coming up after boot — retry ~30 s.
|
||||||
|
var ok = false
|
||||||
|
for (i in 1..15) {
|
||||||
|
ok = api.reachable()
|
||||||
|
if (ok) break
|
||||||
|
kotlinx.coroutines.delay(2000)
|
||||||
|
}
|
||||||
|
if (!ok) return
|
||||||
|
RootOnboard(api, cacheDir).runSilent { /* headless: no UI log */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildNotification(): Notification {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val nm = getSystemService(NotificationManager::class.java)
|
||||||
|
nm?.createNotificationChannel(
|
||||||
|
NotificationChannel(CHAN, "SecuBox onboarding",
|
||||||
|
NotificationManager.IMPORTANCE_LOW),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val b = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||||
|
Notification.Builder(this, CHAN) else @Suppress("DEPRECATION") Notification.Builder(this)
|
||||||
|
return b.setContentTitle("VILLAGE3B")
|
||||||
|
.setContentText("Activation R3 automatique…")
|
||||||
|
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||||
|
.setOngoing(true)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
scope.coroutineContext[kotlinx.coroutines.Job]?.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user