mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-01 17:17:14 +00:00
Compare commits
4 Commits
92dee2d2d0
...
46dfd781d3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46dfd781d3 | ||
| cf3aef48c8 | |||
|
|
198fecea11 | ||
| ab67c981dd |
70
.github/workflows/build-android-apk.yml
vendored
Normal file
70
.github/workflows/build-android-apk.yml
vendored
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||
# Build the SecuBox Android ToolBox client APK (#531).
|
||||
# No Gradle wrapper jar is committed (text-only scaffold) — setup-gradle
|
||||
# provides Gradle ; setup-android provides the SDK. Produces a debug APK
|
||||
# artifact (sideloadable). Release signing is a follow-up (needs a
|
||||
# keystore secret).
|
||||
name: build-android-apk
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths: [ "clients/android-toolbox/**", ".github/workflows/build-android-apk.yml" ]
|
||||
tags: [ "android-v*" ]
|
||||
pull_request:
|
||||
paths: [ "clients/android-toolbox/**" ]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write # needed to attach the APK to a release on tags
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: clients/android-toolbox
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
|
||||
- name: Set up Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
with:
|
||||
gradle-version: "8.9"
|
||||
|
||||
- name: Build debug APK
|
||||
run: gradle :app:assembleDebug --no-daemon --stacktrace
|
||||
|
||||
- name: Upload APK artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: secubox-toolbox-android-debug
|
||||
path: clients/android-toolbox/app/build/outputs/apk/debug/*.apk
|
||||
if-no-files-found: error
|
||||
|
||||
# On android-v* tags, publish the APK as a release asset under the
|
||||
# stable name the toolbox fetch helper + /wg/toolbox.apk expect
|
||||
# (#536). `latest/download/secubox-toolbox-android.apk` resolves to
|
||||
# whichever release is newest.
|
||||
- name: Stage release asset
|
||||
if: startsWith(github.ref, 'refs/tags/android-v')
|
||||
run: |
|
||||
mkdir -p "$GITHUB_WORKSPACE/release"
|
||||
cp app/build/outputs/apk/debug/app-debug.apk \
|
||||
"$GITHUB_WORKSPACE/release/secubox-toolbox-android.apk"
|
||||
|
||||
- name: Publish release
|
||||
if: startsWith(github.ref, 'refs/tags/android-v')
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: release/secubox-toolbox-android.apk
|
||||
fail_on_unmatched_files: true
|
||||
11
clients/android-toolbox/.gitignore
vendored
Normal file
11
clients/android-toolbox/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Android / Gradle build artifacts
|
||||
.gradle/
|
||||
build/
|
||||
app/build/
|
||||
local.properties
|
||||
*.apk
|
||||
*.aab
|
||||
*.keystore
|
||||
*.jks
|
||||
.idea/
|
||||
captures/
|
||||
39
clients/android-toolbox/README.md
Normal file
39
clients/android-toolbox/README.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
|
||||
# SecuBox Android ToolBox client (#531)
|
||||
|
||||
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
|
||||
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).
|
||||
|
||||
## Build
|
||||
No Gradle wrapper jar is committed (text-only scaffold). CI builds it:
|
||||
- **GitHub Actions** `build-android-apk.yml` → debug APK artifact.
|
||||
Locally (with Android SDK + Gradle 8.9 + JDK 17):
|
||||
```bash
|
||||
cd clients/android-toolbox
|
||||
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.
|
||||
- Debug APK is self-signed (sideload). Release signing (published
|
||||
fingerprint, served from the toolbox) is a follow-up needing a keystore
|
||||
secret in CI.
|
||||
|
||||
## Tech
|
||||
Kotlin + Jetpack Compose, minSdk 26 / targetSdk 34. API client is plain
|
||||
`HttpURLConnection` (no Retrofit/OkHttp) to keep deps + CI minimal.
|
||||
|
||||
Package `in.secubox.toolbox`. License `LicenseRef-CMSD-1.0`.
|
||||
49
clients/android-toolbox/app/build.gradle.kts
Normal file
49
clients/android-toolbox/app/build.gradle.kts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "in.secubox.toolbox"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "in.secubox.toolbox"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "0.1.0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
// Signed in CI with a published-fingerprint key (sideload APK,
|
||||
// no Play Store). Debug builds are self-signed by the SDK.
|
||||
signingConfig = signingConfigs.findByName("release")
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions { jvmTarget = "17" }
|
||||
buildFeatures { compose = true }
|
||||
composeOptions { kotlinCompilerExtensionVersion = "1.5.14" }
|
||||
packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
val composeBom = platform("androidx.compose:compose-bom:2024.06.00")
|
||||
implementation(composeBom)
|
||||
implementation("androidx.core:core-ktx:1.13.1")
|
||||
implementation("androidx.activity:activity-compose:1.9.0")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.3")
|
||||
implementation("androidx.compose.ui:ui")
|
||||
implementation("androidx.compose.ui:ui-graphics")
|
||||
implementation("androidx.compose.material3:material3")
|
||||
implementation("androidx.compose.material:material-icons-extended")
|
||||
// No Retrofit/OkHttp — the API client uses HttpURLConnection to keep
|
||||
// the dependency graph (and CI) minimal.
|
||||
}
|
||||
41
clients/android-toolbox/app/src/main/AndroidManifest.xml
Normal file
41
clients/android-toolbox/app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<!-- Query the WireGuard app so we can hand it the generated profile. -->
|
||||
<queries>
|
||||
<package android:name="com.wireguard.android" />
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:theme="@style/Theme.SecuBoxToolBox"
|
||||
android:supportsRtl="true">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- FileProvider to share the downloaded CA + WG .conf with the
|
||||
system cert installer / the WireGuard app. -->
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,227 @@
|
|||
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||
//
|
||||
// SecuBox-Deb :: Android ToolBox client (#531)
|
||||
// One-tap R3 onboarding : discover -> install CA -> import WG profile ->
|
||||
// verify tunnel -> live cartographie sociale. Replaces the manual
|
||||
// multi-step Android tutorial.
|
||||
package `in`.secubox.toolbox
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.security.KeyChain
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.content.FileProvider
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
private val Cosmos = Color(0xFF0A0A0F)
|
||||
private val Gold = Color(0xFFC9A84C)
|
||||
private val Cyan = Color(0xFF00D4FF)
|
||||
private val Matrix = Color(0xFF00FF41)
|
||||
private val Cinnabar = Color(0xFFE63946)
|
||||
private val TextPrimary = Color(0xFFE8E6D9)
|
||||
|
||||
enum class Step { Discover, InstallCa, ImportProfile, Verify, Done }
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent { OnboardApp() }
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun OnboardApp() {
|
||||
val ctx = androidx.compose.ui.platform.LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
var host by remember { mutableStateOf("kbin.gk2.secubox.in") }
|
||||
var step by remember { mutableStateOf(Step.Discover) }
|
||||
var status by remember { mutableStateOf("") }
|
||||
var busy by remember { mutableStateOf(false) }
|
||||
var onTunnel by remember { mutableStateOf(false) }
|
||||
var peerIp by remember { mutableStateOf<String?>(null) }
|
||||
val api = remember(host) { ToolboxApi(host) }
|
||||
|
||||
MaterialTheme(colorScheme = darkColorScheme(
|
||||
primary = Gold, secondary = Cyan, background = Cosmos, surface = Cosmos,
|
||||
onBackground = TextPrimary, onSurface = TextPrimary,
|
||||
)) {
|
||||
Surface(Modifier.fillMaxSize(), color = Cosmos) {
|
||||
Column(
|
||||
Modifier.fillMaxSize().padding(20.dp).verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text("📡 VILLAGE3B", color = Gold, fontSize = 26.sp, fontWeight = FontWeight.Bold)
|
||||
Text("ToolBoX — installation R3", color = TextPrimary, fontSize = 14.sp)
|
||||
Spacer(Modifier.height(20.dp))
|
||||
|
||||
Stepper(step)
|
||||
Spacer(Modifier.height(20.dp))
|
||||
|
||||
when (step) {
|
||||
Step.Discover -> {
|
||||
OutlinedTextField(
|
||||
value = host, onValueChange = { host = it },
|
||||
label = { Text("Borne (kbin…)") }, singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text("Scanne le QR de la cabine ou saisis l'adresse, puis Suivant.",
|
||||
color = TextPrimary, fontSize = 12.sp)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
BigButton("Suivant", busy) {
|
||||
busy = true; status = "Vérification de la borne…"
|
||||
scope.launch {
|
||||
val ok = withContext(Dispatchers.IO) { api.reachable() }
|
||||
busy = false
|
||||
if (ok) { step = Step.InstallCa; status = "" }
|
||||
else status = "Borne injoignable — vérifie l'adresse / le réseau."
|
||||
}
|
||||
}
|
||||
}
|
||||
Step.InstallCa -> {
|
||||
StepBody("1 · Installer le certificat (CA R3)",
|
||||
"Le certificat permet l'analyse TLS de la cabine. " +
|
||||
"Android te demandera de confirmer (Paramètres → Sécurité → " +
|
||||
"Certificat utilisateur).")
|
||||
BigButton("Installer le certificat", busy) {
|
||||
busy = true
|
||||
scope.launch {
|
||||
try {
|
||||
val ca = withContext(Dispatchers.IO) { api.downloadCa(ctx.cacheDir) }
|
||||
val der = ca.readBytes()
|
||||
val intent = KeyChain.createInstallIntent().apply {
|
||||
putExtra(KeyChain.EXTRA_CERTIFICATE, der)
|
||||
putExtra(KeyChain.EXTRA_NAME, "VILLAGE3B ToolBoX CA")
|
||||
}
|
||||
ctx.startActivity(intent)
|
||||
status = "Confirme l'installation dans Android, puis Suivant."
|
||||
} catch (e: Exception) {
|
||||
status = "Échec téléchargement CA : ${e.message}"
|
||||
} finally { busy = false }
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
TextButton(onClick = {
|
||||
ctx.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
|
||||
}) { Text("Ouvrir Paramètres sécurité", color = Cyan) }
|
||||
Spacer(Modifier.height(8.dp))
|
||||
BigButton("Suivant", false) { step = Step.ImportProfile; status = "" }
|
||||
}
|
||||
Step.ImportProfile -> {
|
||||
StepBody("2 · Importer le profil WireGuard",
|
||||
"On génère un profil dédié et on l'ouvre dans l'app WireGuard. " +
|
||||
"Active le tunnel dans WireGuard, puis reviens ici.")
|
||||
BigButton("Importer dans WireGuard", busy) {
|
||||
busy = true
|
||||
scope.launch {
|
||||
try {
|
||||
val conf = withContext(Dispatchers.IO) { api.downloadProfile(ctx.cacheDir) }
|
||||
val uri = FileProvider.getUriForFile(
|
||||
ctx, "${ctx.packageName}.fileprovider", conf)
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(uri, "application/octet-stream")
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
setPackage("com.wireguard.android")
|
||||
}
|
||||
try { ctx.startActivity(intent) }
|
||||
catch (_: Exception) {
|
||||
// WireGuard not installed -> open Play / generic chooser.
|
||||
ctx.startActivity(Intent(Intent.ACTION_VIEW,
|
||||
Uri.parse("https://play.google.com/store/apps/details?id=com.wireguard.android")))
|
||||
status = "Installe l'app WireGuard puis réessaie."
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
status = "Échec profil : ${e.message}"
|
||||
} finally { busy = false }
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
BigButton("Suivant", false) { step = Step.Verify; status = "" }
|
||||
}
|
||||
Step.Verify -> {
|
||||
StepBody("3 · Vérifier le tunnel R3",
|
||||
"Active le tunnel dans WireGuard, puis vérifie.")
|
||||
BigButton("Vérifier", busy) {
|
||||
busy = true; status = "Vérification…"
|
||||
scope.launch {
|
||||
val (t, ip) = withContext(Dispatchers.IO) { api.r3Check() }
|
||||
busy = false; onTunnel = t; peerIp = ip
|
||||
if (t) { step = Step.Done; status = "" }
|
||||
else status = "Pas encore sur le tunnel — active WireGuard puis réessaie."
|
||||
}
|
||||
}
|
||||
}
|
||||
Step.Done -> {
|
||||
Icon(Icons.Filled.CheckCircle, null, tint = Matrix, modifier = Modifier.size(56.dp))
|
||||
Text("Tunnel R3 actif ✓", color = Matrix, fontSize = 20.sp, fontWeight = FontWeight.Bold)
|
||||
peerIp?.let { Text("pair : $it", color = TextPrimary, fontSize = 12.sp) }
|
||||
Spacer(Modifier.height(20.dp))
|
||||
BigButton("🕸️ Voir ma cartographie sociale", false) {
|
||||
ctx.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(api.socialMeUrl)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (status.isNotBlank()) {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text(status, color = if (status.contains("Échec") || status.contains("injoignable")) Cinnabar else Cyan,
|
||||
fontSize = 13.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Stepper(cur: Step) {
|
||||
val steps = listOf(Step.Discover, Step.InstallCa, Step.ImportProfile, Step.Verify, Step.Done)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
steps.forEach { s ->
|
||||
val done = s.ordinal < cur.ordinal
|
||||
val active = s == cur
|
||||
Box(Modifier.size(if (active) 14.dp else 10.dp)) {
|
||||
Surface(shape = MaterialTheme.shapes.small,
|
||||
color = when { done -> Matrix; active -> Gold; else -> Color(0xFF333333) },
|
||||
modifier = Modifier.fillMaxSize()) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StepBody(title: String, body: String) {
|
||||
Text(title, color = Gold, fontSize = 16.sp, fontWeight = FontWeight.Bold)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(body, color = TextPrimary, fontSize = 13.sp)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BigButton(label: String, busy: Boolean, onClick: () -> Unit) {
|
||||
Button(onClick = onClick, enabled = !busy, modifier = Modifier.fillMaxWidth(),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Gold, contentColor = Cosmos)) {
|
||||
if (busy) CircularProgressIndicator(Modifier.size(18.dp), color = Cosmos, strokeWidth = 2.dp)
|
||||
else Text(label, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||
package `in`.secubox.toolbox
|
||||
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
/**
|
||||
* Minimal HTTP client for the SecuBox ToolBox R3 endpoints. Uses
|
||||
* HttpURLConnection (no Retrofit/OkHttp) to keep the dependency graph
|
||||
* and CI minimal. All calls are blocking — invoke off the main thread.
|
||||
*
|
||||
* Endpoints (served on the kbin vhost, e.g. kbin.gk2.secubox.in) :
|
||||
* GET /wg/ca.crt -> CA root cert (Android DER/PEM)
|
||||
* GET /wg/profile/new -> wg-quick .conf (one fresh peer per call)
|
||||
* GET /wg/r3-check -> {"tunnel": bool, "peer_ip": "10.99.1.x"}
|
||||
* GET /social/me -> per-client cartographie sociale (web view)
|
||||
*/
|
||||
class ToolboxApi(rawHost: String) {
|
||||
|
||||
// Accept "kbin.gk2.secubox.in", "https://kbin…", trailing slashes…
|
||||
val base: String = rawHost.trim()
|
||||
.removePrefix("https://").removePrefix("http://")
|
||||
.trim('/')
|
||||
.let { "https://$it" }
|
||||
|
||||
val socialMeUrl: String get() = "$base/social/me"
|
||||
|
||||
private fun open(path: String): HttpURLConnection =
|
||||
(URL("$base$path").openConnection() as HttpURLConnection).apply {
|
||||
connectTimeout = 8000
|
||||
readTimeout = 12000
|
||||
setRequestProperty("User-Agent", "secubox-toolbox-android/0.1")
|
||||
instanceFollowRedirects = true
|
||||
}
|
||||
|
||||
/** Download a file (CA or WG profile) into the app cache, return it. */
|
||||
fun download(path: String, outName: String, cacheDir: File): File {
|
||||
val c = open(path)
|
||||
try {
|
||||
if (c.responseCode !in 200..299)
|
||||
throw RuntimeException("HTTP ${c.responseCode} for $path")
|
||||
val out = File(cacheDir, outName)
|
||||
c.inputStream.use { input -> out.outputStream().use { input.copyTo(it) } }
|
||||
return out
|
||||
} finally { c.disconnect() }
|
||||
}
|
||||
|
||||
fun downloadCa(cacheDir: File): File = download("/wg/ca.crt", "village3b-ca.crt", cacheDir)
|
||||
fun downloadProfile(cacheDir: File): File = download("/wg/profile/new", "village3b-toolbox.conf", cacheDir)
|
||||
|
||||
/** R3 tunnel status. Returns (onTunnel, peerIp?). */
|
||||
fun r3Check(): Pair<Boolean, String?> {
|
||||
val c = open("/wg/r3-check")
|
||||
try {
|
||||
if (c.responseCode !in 200..299) return false to null
|
||||
val body = c.inputStream.bufferedReader().readText()
|
||||
val j = JSONObject(body)
|
||||
return j.optBoolean("tunnel", false) to j.optString("peer_ip", null)
|
||||
} catch (_: Exception) {
|
||||
return false to null
|
||||
} finally { c.disconnect() }
|
||||
}
|
||||
|
||||
/** Cheap reachability probe for the discover step. */
|
||||
fun reachable(): Boolean = try {
|
||||
val c = open("/wg/r3-check"); val ok = c.responseCode in 200..499; c.disconnect(); ok
|
||||
} catch (_: Exception) { false }
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp" android:height="108dp"
|
||||
android:viewportWidth="108" android:viewportHeight="108">
|
||||
<path android:fillColor="#C9A84C"
|
||||
android:pathData="M54,40c-10,0 -19,6 -24,14c5,8 14,14 24,14s19,-6 24,-14c-5,-8 -14,-14 -24,-14zM54,64a10,10 0 1,1 0,-20a10,10 0 0,1 0,20z" />
|
||||
<path android:fillColor="#00D4FF"
|
||||
android:pathData="M54,49a5,5 0 1,0 0,10a5,5 0 0,0 0,-10z" />
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#0A0A0F</color>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
|
||||
<resources>
|
||||
<string name="app_name">VILLAGE3B ToolBoX</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
|
||||
<resources>
|
||||
<style name="Theme.SecuBoxToolBox" parent="android:Theme.Material.NoActionBar">
|
||||
<item name="android:statusBarColor">#0A0A0F</item>
|
||||
<item name="android:navigationBarColor">#0A0A0F</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
|
||||
<paths>
|
||||
<cache-path name="cache" path="." />
|
||||
</paths>
|
||||
5
clients/android-toolbox/build.gradle.kts
Normal file
5
clients/android-toolbox/build.gradle.kts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||
plugins {
|
||||
id("com.android.application") version "8.5.2" apply false
|
||||
id("org.jetbrains.kotlin.android") version "1.9.24" apply false
|
||||
}
|
||||
5
clients/android-toolbox/gradle.properties
Normal file
5
clients/android-toolbox/gradle.properties
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
android.useAndroidX=true
|
||||
kotlin.code.style=official
|
||||
android.nonTransitiveRClass=true
|
||||
18
clients/android-toolbox/settings.gradle.kts
Normal file
18
clients/android-toolbox/settings.gradle.kts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||
// SecuBox-Deb :: Android ToolBox client (#531)
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
rootProject.name = "SecuBoxToolBox"
|
||||
include(":app")
|
||||
|
|
@ -1,3 +1,21 @@
|
|||
secubox-toolbox (2.6.13-1~bookworm1) bookworm; urgency=medium
|
||||
|
||||
* Serve the Android ToolBox APK from the toolbox (#536, follow-up #531).
|
||||
- api.py GET /wg/toolbox.apk : serves the local APK
|
||||
(/var/lib/secubox/toolbox/android/village3b-toolbox.apk) with
|
||||
content-type application/vnd.android.package-archive ; if absent,
|
||||
302 → the latest public GitHub release asset (button never
|
||||
dead-ends, offline-capable once fetched).
|
||||
- /wg/onboard Android panel (both the inline + the _install_panels
|
||||
variants) : new "📱 Installer l'app ToolBoX (1-tap)" button.
|
||||
- sbin/secubox-toolbox-fetch-apk : pulls the latest release asset
|
||||
into the serve path (best-effort, APK-magic sanity check).
|
||||
- postinst : create the android serve dir + best-effort first fetch.
|
||||
- build-android-apk.yml : on android-v* tags, publish the APK as a
|
||||
release asset named secubox-toolbox-android.apk (contents:write).
|
||||
|
||||
-- Gerald KERMA <devel@cybermind.fr> Thu, 12 Jun 2026 14:30:00 +0200
|
||||
|
||||
secubox-toolbox (2.6.12-1~bookworm1) bookworm; urgency=medium
|
||||
|
||||
* fix(threat_intel) #530 — ThreatFox ingested 0 IOCs for weeks because
|
||||
|
|
|
|||
|
|
@ -44,6 +44,14 @@ case "$1" in
|
|||
|
||||
# 4. Storage dir (SQLite + future PDF reports)
|
||||
install -d -m 0750 -o secubox-toolbox -g secubox-toolbox /var/lib/secubox/toolbox
|
||||
# #536 : Android APK serve dir + best-effort fetch of the latest
|
||||
# release asset (so GET /wg/toolbox.apk serves it locally/offline).
|
||||
# Non-blocking : if there's no release yet / no network, the endpoint
|
||||
# falls back to redirecting to the public release.
|
||||
install -d -m 0755 -o secubox-toolbox -g secubox-toolbox /var/lib/secubox/toolbox/android
|
||||
if [ -x /usr/sbin/secubox-toolbox-fetch-apk ]; then
|
||||
/usr/sbin/secubox-toolbox-fetch-apk 2>&1 | head -2 || true
|
||||
fi
|
||||
# /var/log/secubox is a SHARED parent traversed by many service users
|
||||
# (the aggregator runs as `secubox` and reads waf-threats.log under
|
||||
# here). It MUST be 0755 — a 0750 owned by secubox-toolbox silently
|
||||
|
|
|
|||
|
|
@ -102,6 +102,9 @@ execute_after_dh_auto_install:
|
|||
debian/secubox-toolbox/lib/systemd/system/
|
||||
install -m 0644 systemd/secubox-escalate.timer \
|
||||
debian/secubox-toolbox/lib/systemd/system/
|
||||
# #536 : Android APK fetch helper.
|
||||
install -m 0755 sbin/secubox-toolbox-fetch-apk \
|
||||
debian/secubox-toolbox/usr/sbin/
|
||||
install -m 0755 sbin/secubox-toolbox-wg-restore \
|
||||
debian/secubox-toolbox/usr/sbin/
|
||||
install -m 0644 systemd/secubox-toolbox-wg-restore.service \
|
||||
|
|
|
|||
44
packages/secubox-toolbox/sbin/secubox-toolbox-fetch-apk
Normal file
44
packages/secubox-toolbox/sbin/secubox-toolbox-fetch-apk
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||
#
|
||||
# SecuBox-Deb :: secubox-toolbox-fetch-apk (#536)
|
||||
#
|
||||
# Pull the latest Android ToolBox APK (published as a GitHub release
|
||||
# asset by build-android-apk.yml on android-v* tags) into the toolbox
|
||||
# serve path, so GET /wg/toolbox.apk serves it locally (offline-capable
|
||||
# sideload from the cabine). Best-effort : a failure leaves any existing
|
||||
# APK in place ; the endpoint falls back to the public release redirect.
|
||||
set -euo pipefail
|
||||
readonly MODULE="secubox-toolbox-fetch-apk"
|
||||
|
||||
DEST_DIR="/var/lib/secubox/toolbox/android"
|
||||
DEST="${DEST_DIR}/village3b-toolbox.apk"
|
||||
RELEASE_URL="https://github.com/CyberMind-FR/secubox-deb/releases/latest/download/secubox-toolbox-android.apk"
|
||||
|
||||
log() { logger -t "$MODULE" -- "$*" 2>/dev/null || echo "[$MODULE] $*" >&2; }
|
||||
|
||||
install -d -m 0755 -o secubox-toolbox -g secubox-toolbox "$DEST_DIR" 2>/dev/null \
|
||||
|| mkdir -p "$DEST_DIR"
|
||||
|
||||
TMP=$(mktemp --suffix=.apk)
|
||||
trap 'rm -f "$TMP"' EXIT
|
||||
|
||||
if command -v wget >/dev/null 2>&1; then
|
||||
if wget -q --timeout=20 --tries=2 "$RELEASE_URL" -O "$TMP" && [ -s "$TMP" ]; then
|
||||
# Sanity : APK is a ZIP — must start with PK\x03\x04.
|
||||
if head -c 2 "$TMP" | grep -q "PK"; then
|
||||
install -m 0644 "$TMP" "$DEST"
|
||||
chown secubox-toolbox:secubox-toolbox "$DEST" 2>/dev/null || true
|
||||
log "fetched APK -> ${DEST} ($(stat -c%s "$DEST" 2>/dev/null) bytes)"
|
||||
exit 0
|
||||
else
|
||||
log "downloaded file is not an APK (no release asset yet?) — keeping existing"
|
||||
fi
|
||||
else
|
||||
log "fetch failed (no release yet / network) — /wg/toolbox.apk will redirect to the release"
|
||||
fi
|
||||
else
|
||||
log "wget missing — cannot fetch APK"
|
||||
fi
|
||||
exit 0
|
||||
|
|
@ -621,6 +621,11 @@ pre{background:#1a1a25;color:var(--phos-hot);padding:0.6rem 0.8rem;border-radius
|
|||
</div>
|
||||
|
||||
<div class="tab-content" data-content=android>
|
||||
<a href="/wg/toolbox.apk" class="btn btn-go">📱 Installer l'app ToolBoX (1-tap)</a>
|
||||
<div class=warn style="margin-top:0.5rem">
|
||||
✨ <b>Le plus simple</b> : l'app fait tout (CA + tunnel + vérif) en 5 étapes.
|
||||
Active « sources inconnues » à l'installation. Sinon, méthode manuelle ci-dessous :
|
||||
</div>
|
||||
<a href="/wg/ca.crt" class="btn btn-warn">📥 Télécharger CA (.crt format Android)</a>
|
||||
<div class=warn>
|
||||
⚠ Chrome ne peut PAS installer un CA directement (sécurité Android 11+).
|
||||
|
|
@ -1185,6 +1190,9 @@ _ONBOARD_BODY = {
|
|||
<p class=note>Si rien ne se passe : Réglages → Batterie → désactive le mode économie (il coupe parfois les VPN).</p>
|
||||
""",
|
||||
"android": """
|
||||
<p><b>✨ Le plus simple — l'app ToolBoX fait tout :</b></p>
|
||||
<a class=btn href="/wg/toolbox.apk">📱 Installer l'app ToolBoX (.apk, 1-tap)</a>
|
||||
<p class=note>Active « sources inconnues » à l'installation. L'app installe le CA, importe le tunnel et vérifie le R3 en 5 étapes. Sinon, méthode manuelle :</p>
|
||||
<ol>
|
||||
<li>Installe l'app <a class=btn href="https://play.google.com/store/apps/details?id=com.wireguard.android" target=_blank rel=noopener>WireGuard</a> depuis le Play Store.</li>
|
||||
<li>Dans l'app, tap "+" → "Scan from QR code" → scanne ton QR :<br><img src="/wg/qr.png" alt="QR code" style="width:240px;max-width:100%;margin:0.5rem 0;border-radius:6px"></li>
|
||||
|
|
@ -1326,6 +1334,37 @@ async def wg_ca_der() -> Response:
|
|||
)
|
||||
|
||||
|
||||
# Android ToolBox app (#531/#536). CI publishes the APK as a GitHub
|
||||
# release asset on `android-v*` tags ; secubox-toolbox-fetch-apk pulls it
|
||||
# into the serve path below. If absent, we redirect to the public
|
||||
# release so the button always works.
|
||||
_ANDROID_APK = Path("/var/lib/secubox/toolbox/android/village3b-toolbox.apk")
|
||||
_ANDROID_APK_RELEASE = (
|
||||
"https://github.com/CyberMind-FR/secubox-deb/releases/latest/download/"
|
||||
"secubox-toolbox-android.apk"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/wg/toolbox.apk")
|
||||
async def wg_toolbox_apk() -> Response:
|
||||
"""Serve the Android ToolBox installer APK (#536).
|
||||
|
||||
Local file first (sideload from the cabine, works offline) ; if it
|
||||
hasn't been fetched yet, 302 to the latest public GitHub release
|
||||
asset so the onboard button never dead-ends.
|
||||
"""
|
||||
if _ANDROID_APK.exists() and _ANDROID_APK.stat().st_size > 0:
|
||||
return Response(
|
||||
content=_ANDROID_APK.read_bytes(),
|
||||
media_type="application/vnd.android.package-archive",
|
||||
headers={
|
||||
"Content-Disposition": "attachment; filename=village3b-toolbox.apk",
|
||||
"Cache-Control": "public, max-age=300",
|
||||
},
|
||||
)
|
||||
return RedirectResponse(url=_ANDROID_APK_RELEASE, status_code=302)
|
||||
|
||||
|
||||
@router.get("/wg/ca.mobileconfig")
|
||||
async def wg_ca_mobileconfig() -> Response:
|
||||
"""iOS profile that installs the mitm-wg CA in trust store."""
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user