mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-01 07:26:08 +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
|
secubox-toolbox (2.6.12-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
* fix(threat_intel) #530 — ThreatFox ingested 0 IOCs for weeks because
|
* 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)
|
# 4. Storage dir (SQLite + future PDF reports)
|
||||||
install -d -m 0750 -o secubox-toolbox -g secubox-toolbox /var/lib/secubox/toolbox
|
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
|
# /var/log/secubox is a SHARED parent traversed by many service users
|
||||||
# (the aggregator runs as `secubox` and reads waf-threats.log under
|
# (the aggregator runs as `secubox` and reads waf-threats.log under
|
||||||
# here). It MUST be 0755 — a 0750 owned by secubox-toolbox silently
|
# 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/
|
debian/secubox-toolbox/lib/systemd/system/
|
||||||
install -m 0644 systemd/secubox-escalate.timer \
|
install -m 0644 systemd/secubox-escalate.timer \
|
||||||
debian/secubox-toolbox/lib/systemd/system/
|
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 \
|
install -m 0755 sbin/secubox-toolbox-wg-restore \
|
||||||
debian/secubox-toolbox/usr/sbin/
|
debian/secubox-toolbox/usr/sbin/
|
||||||
install -m 0644 systemd/secubox-toolbox-wg-restore.service \
|
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>
|
||||||
|
|
||||||
<div class="tab-content" data-content=android>
|
<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>
|
<a href="/wg/ca.crt" class="btn btn-warn">📥 Télécharger CA (.crt format Android)</a>
|
||||||
<div class=warn>
|
<div class=warn>
|
||||||
⚠ Chrome ne peut PAS installer un CA directement (sécurité Android 11+).
|
⚠ 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>
|
<p class=note>Si rien ne se passe : Réglages → Batterie → désactive le mode économie (il coupe parfois les VPN).</p>
|
||||||
""",
|
""",
|
||||||
"android": """
|
"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>
|
<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>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>
|
<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")
|
@router.get("/wg/ca.mobileconfig")
|
||||||
async def wg_ca_mobileconfig() -> Response:
|
async def wg_ca_mobileconfig() -> Response:
|
||||||
"""iOS profile that installs the mitm-wg CA in trust store."""
|
"""iOS profile that installs the mitm-wg CA in trust store."""
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user