mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-01 10:47:30 +00:00
Compare commits
2 Commits
92dee2d2d0
...
198fecea11
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
198fecea11 | ||
| ab67c981dd |
52
.github/workflows/build-android-apk.yml
vendored
Normal file
52
.github/workflows/build-android-apk.yml
vendored
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# 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: read
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
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")
|
||||||
Loading…
Reference in New Issue
Block a user