mirror of
https://github.com/QM4RS/FridaBox.git
synced 2026-10-12 19:02:14 +02:00
Merge pull request #12 from QM4RS/agent/gadget-manager-liquid-glass
Agent/gadget manager liquid glass
This commit is contained in:
+169
@@ -0,0 +1,169 @@
|
||||
package top.niunaijun.blackbox.instrumentation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import top.niunaijun.blackbox.BlackBoxCore;
|
||||
|
||||
/** Resolves a selected, downloaded Gadget and prepares per-guest runtime copies. */
|
||||
final class DownloadedGadgetRuntime {
|
||||
private static final String DOWNLOAD_ROOT = "fridabox-gadgets";
|
||||
private static final String RUNTIME_ROOT = "fridabox-gadget-runtimes";
|
||||
private static final String RUNTIME_NAME = "libpayload.so";
|
||||
private static final String CONFIG_NAME = "libpayload.config.so";
|
||||
private static final String SOURCE_NAME = "payload.source";
|
||||
|
||||
private DownloadedGadgetRuntime() {
|
||||
}
|
||||
|
||||
static File prepareListener(String packageName) throws IOException {
|
||||
Context context = BlackBoxCore.getContext();
|
||||
File source = requireSelected(context);
|
||||
File directory = new File(context.getFilesDir(),
|
||||
RUNTIME_ROOT + File.separator + safeName(packageName));
|
||||
if (!directory.isDirectory() && !directory.mkdirs()) {
|
||||
throw new IOException("Unable to create the Gadget runtime directory");
|
||||
}
|
||||
File runtime = new File(directory, RUNTIME_NAME);
|
||||
syncSelected(source, runtime);
|
||||
writeUtf8Atomically(new File(directory, CONFIG_NAME), listenerConfig());
|
||||
return runtime;
|
||||
}
|
||||
|
||||
static File requireSelected(Context context) throws IOException {
|
||||
String selectedPath = InstrumentationSettings.getSelectedGadgetPath();
|
||||
String selectedAbi = InstrumentationSettings.getSelectedGadgetAbi();
|
||||
if (selectedPath == null || selectedPath.trim().isEmpty()) {
|
||||
throw new IOException("No downloaded Frida Gadget is selected");
|
||||
}
|
||||
File root = new File(context.getFilesDir(), DOWNLOAD_ROOT).getCanonicalFile();
|
||||
File selected = new File(selectedPath).getCanonicalFile();
|
||||
if (!isInside(root, selected) || !selected.isFile()) {
|
||||
throw new IOException("The selected Frida Gadget is missing or outside private storage");
|
||||
}
|
||||
if (!supportsAbi(selectedAbi)) {
|
||||
throw new IOException("The selected Frida Gadget ABI is not supported by this device");
|
||||
}
|
||||
validateElf(selected, selectedAbi);
|
||||
return selected;
|
||||
}
|
||||
|
||||
static void syncSelected(File source, File destination) throws IOException {
|
||||
String identity = source.getCanonicalPath() + "\n"
|
||||
+ String.valueOf(InstrumentationSettings.getSelectedGadgetSha256()) + "\n";
|
||||
File marker = new File(destination.getParentFile(), SOURCE_NAME);
|
||||
if (!destination.isFile() || destination.length() != source.length()
|
||||
|| !identity.equals(readUtf8(marker))) {
|
||||
copyAtomically(source, destination);
|
||||
writeUtf8Atomically(marker, identity);
|
||||
}
|
||||
if (!destination.setReadable(true, false)
|
||||
|| !destination.setExecutable(true, false)
|
||||
|| !destination.setWritable(false, false)) {
|
||||
throw new IOException("Unable to secure the private Frida Gadget runtime");
|
||||
}
|
||||
}
|
||||
|
||||
private static String listenerConfig() {
|
||||
return "{\n" +
|
||||
" \"interaction\": {\n" +
|
||||
" \"type\": \"listen\",\n" +
|
||||
" \"address\": \"127.0.0.1\",\n" +
|
||||
" \"port\": " + InstrumentationSettings.getBasePort() + ",\n" +
|
||||
" \"on_port_conflict\": \"pick-next\",\n" +
|
||||
" \"on_load\": \"wait\"\n" +
|
||||
" },\n" +
|
||||
" \"runtime\": \"qjs\",\n" +
|
||||
" \"teardown\": \"minimal\"\n" +
|
||||
"}\n";
|
||||
}
|
||||
|
||||
private static void validateElf(File file, String abi) throws IOException {
|
||||
byte[] header = new byte[20];
|
||||
try (FileInputStream input = new FileInputStream(file)) {
|
||||
if (input.read(header) != header.length) throw new IOException("Downloaded Gadget is truncated");
|
||||
}
|
||||
if ((header[0] & 0xff) != 0x7f || header[1] != 'E' || header[2] != 'L' || header[3] != 'F') {
|
||||
throw new IOException("Downloaded Gadget is not an ELF library");
|
||||
}
|
||||
int machine = (header[18] & 0xff) | ((header[19] & 0xff) << 8);
|
||||
int expected = expectedMachine(abi);
|
||||
if (expected < 0 || machine != expected) {
|
||||
throw new IOException("Downloaded Gadget ELF architecture does not match " + abi);
|
||||
}
|
||||
}
|
||||
|
||||
private static int expectedMachine(String abi) {
|
||||
if ("arm64-v8a".equals(abi)) return 183;
|
||||
if ("armeabi-v7a".equals(abi)) return 40;
|
||||
if ("x86".equals(abi)) return 3;
|
||||
if ("x86_64".equals(abi)) return 62;
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static boolean supportsAbi(String abi) {
|
||||
if (abi == null) return false;
|
||||
for (String supported : Build.SUPPORTED_ABIS) {
|
||||
if (abi.equals(supported)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isInside(File root, File child) {
|
||||
return child.getAbsolutePath().startsWith(root.getAbsolutePath() + File.separator);
|
||||
}
|
||||
|
||||
private static String safeName(String value) {
|
||||
return value == null ? "unknown" : value.replaceAll("[^A-Za-z0-9._-]", "_");
|
||||
}
|
||||
|
||||
private static void copyAtomically(File source, File destination) throws IOException {
|
||||
File temporary = new File(destination.getParentFile(), destination.getName() + ".partial");
|
||||
if (temporary.exists() && !temporary.delete()) throw new IOException("Unable to replace temporary Gadget");
|
||||
try (FileInputStream input = new FileInputStream(source);
|
||||
FileOutputStream output = new FileOutputStream(temporary)) {
|
||||
byte[] buffer = new byte[128 * 1024];
|
||||
int count;
|
||||
while ((count = input.read(buffer)) >= 0) output.write(buffer, 0, count);
|
||||
output.getFD().sync();
|
||||
}
|
||||
replace(temporary, destination);
|
||||
}
|
||||
|
||||
private static String readUtf8(File file) {
|
||||
if (!file.isFile() || file.length() > 4096) return null;
|
||||
try (FileInputStream input = new FileInputStream(file);
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream((int) file.length())) {
|
||||
byte[] buffer = new byte[512];
|
||||
int count;
|
||||
while ((count = input.read(buffer)) >= 0) output.write(buffer, 0, count);
|
||||
return output.toString(StandardCharsets.UTF_8.name());
|
||||
} catch (IOException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static void writeUtf8Atomically(File destination, String value) throws IOException {
|
||||
byte[] expected = value.getBytes(StandardCharsets.UTF_8);
|
||||
File temporary = new File(destination.getParentFile(), destination.getName() + ".partial");
|
||||
try (FileOutputStream output = new FileOutputStream(temporary)) {
|
||||
output.write(expected);
|
||||
output.getFD().sync();
|
||||
}
|
||||
replace(temporary, destination);
|
||||
}
|
||||
|
||||
private static void replace(File temporary, File destination) throws IOException {
|
||||
if (destination.exists() && !destination.delete()) {
|
||||
throw new IOException("Unable to replace " + destination.getName());
|
||||
}
|
||||
if (!temporary.renameTo(destination)) throw new IOException("Unable to install " + destination.getName());
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,8 @@ public final class FridaGadgetLoader {
|
||||
System.load(runtime.getAbsolutePath());
|
||||
} else {
|
||||
Log.i(TAG, "Loading Frida Gadget listener for " + GuestRuntimeRegistry.getGuestProcessName());
|
||||
System.loadLibrary("frida-gadget");
|
||||
File runtime = DownloadedGadgetRuntime.prepareListener(packageName);
|
||||
System.load(runtime.getAbsolutePath());
|
||||
}
|
||||
loaded = true;
|
||||
InstrumentationStatusStore.recordLoaded();
|
||||
|
||||
+53
@@ -12,6 +12,11 @@ public final class InstrumentationSettings {
|
||||
public static final String KEY_BASE_PORT = "frida_base_port";
|
||||
public static final String KEY_SCAN_COUNT = "frida_port_scan_count";
|
||||
public static final String KEY_ADVANCED_LOGS = "show_advanced_logs";
|
||||
public static final String KEY_GADGET_PATH = "selected_gadget_path";
|
||||
public static final String KEY_GADGET_SOURCE = "selected_gadget_source";
|
||||
public static final String KEY_GADGET_VERSION = "selected_gadget_version";
|
||||
public static final String KEY_GADGET_ABI = "selected_gadget_abi";
|
||||
public static final String KEY_GADGET_SHA256 = "selected_gadget_sha256";
|
||||
private static final String PACKAGE_PREFIX = "package_enabled_";
|
||||
private static final String PACKAGE_MODE_PREFIX = "package_mode_";
|
||||
private static final String PACKAGE_SCRIPT_PREFIX = "package_script_";
|
||||
@@ -97,6 +102,54 @@ public final class InstrumentationSettings {
|
||||
return preferences().getBoolean(KEY_ADVANCED_LOGS, false);
|
||||
}
|
||||
|
||||
public static String getSelectedGadgetPath() {
|
||||
return preferences().getString(KEY_GADGET_PATH, null);
|
||||
}
|
||||
|
||||
public static String getSelectedGadgetSource() {
|
||||
return preferences().getString(KEY_GADGET_SOURCE, null);
|
||||
}
|
||||
|
||||
public static String getSelectedGadgetVersion() {
|
||||
return preferences().getString(KEY_GADGET_VERSION, null);
|
||||
}
|
||||
|
||||
public static String getSelectedGadgetAbi() {
|
||||
return preferences().getString(KEY_GADGET_ABI, null);
|
||||
}
|
||||
|
||||
public static String getSelectedGadgetSha256() {
|
||||
return preferences().getString(KEY_GADGET_SHA256, null);
|
||||
}
|
||||
|
||||
public static boolean setSelectedGadget(
|
||||
String path, String source, String version, String abi, String sha256) {
|
||||
if (path == null || path.trim().isEmpty()
|
||||
|| source == null || source.trim().isEmpty()
|
||||
|| version == null || version.trim().isEmpty()
|
||||
|| abi == null || abi.trim().isEmpty()
|
||||
|| sha256 == null || sha256.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return preferences().edit()
|
||||
.putString(KEY_GADGET_PATH, path)
|
||||
.putString(KEY_GADGET_SOURCE, source)
|
||||
.putString(KEY_GADGET_VERSION, version)
|
||||
.putString(KEY_GADGET_ABI, abi)
|
||||
.putString(KEY_GADGET_SHA256, sha256)
|
||||
.commit();
|
||||
}
|
||||
|
||||
public static boolean clearSelectedGadget() {
|
||||
return preferences().edit()
|
||||
.remove(KEY_GADGET_PATH)
|
||||
.remove(KEY_GADGET_SOURCE)
|
||||
.remove(KEY_GADGET_VERSION)
|
||||
.remove(KEY_GADGET_ABI)
|
||||
.remove(KEY_GADGET_SHA256)
|
||||
.commit();
|
||||
}
|
||||
|
||||
static int clamp(int value, int minimum, int maximum, int fallback) {
|
||||
return value < minimum || value > maximum ? fallback : value;
|
||||
}
|
||||
|
||||
+2
-10
@@ -46,18 +46,10 @@ final class LocalScriptGadgetRuntime {
|
||||
throw new IOException("Unable to secure the instrumented JavaScript agent");
|
||||
}
|
||||
|
||||
File source = new File(context.getApplicationInfo().nativeLibraryDir, "libfrida-gadget.so");
|
||||
if (!source.isFile()) throw new IOException("Packaged Frida Gadget is missing");
|
||||
File source = DownloadedGadgetRuntime.requireSelected(context);
|
||||
|
||||
File runtime = new File(directory, RUNTIME_NAME);
|
||||
if (!runtime.isFile() || runtime.length() != source.length()) {
|
||||
copyAtomically(source, runtime);
|
||||
}
|
||||
if (!runtime.setReadable(true, false)
|
||||
|| !runtime.setExecutable(true, false)
|
||||
|| !runtime.setWritable(false, false)) {
|
||||
throw new IOException("Unable to secure private Frida Gadget permissions");
|
||||
}
|
||||
DownloadedGadgetRuntime.syncSelected(source, runtime);
|
||||
|
||||
File config = new File(directory, CONFIG_NAME);
|
||||
writeUtf8Atomically(config, buildConfig(packageName, instrumentedAgent.getAbsolutePath()));
|
||||
|
||||
@@ -182,12 +182,12 @@ in On-device, Computer, and Clean modes. Evidence and exact commands are in
|
||||
- JDK 21;
|
||||
- Android SDK 35 and NDK 29.0.14206865;
|
||||
- Python 3.10+;
|
||||
- Node.js 20+ and npm when rebuilding the bundled Frida 17 agents.
|
||||
- Node.js 20+ and npm for the pinned Frida agent toolchain and official Tabler
|
||||
Icons source package.
|
||||
|
||||
### Build
|
||||
|
||||
```text
|
||||
python tools/fetch_frida_gadget.py
|
||||
npm ci
|
||||
python tools/build_frida_agents.py
|
||||
./gradlew :app:assembleDebug :Bcore:testDebugUnitTest :app:testDebugUnitTest :app:check
|
||||
@@ -200,10 +200,12 @@ documented in [docs/BUILDING.md](docs/BUILDING.md). The output is written under
|
||||
### Use
|
||||
|
||||
1. Install and open FridaBox.
|
||||
2. Tap **Import APK** and select an ARM64 base APK.
|
||||
3. Choose **On-device**, **Computer**, or **Clean** on that guest's card.
|
||||
4. Select an agent for On-device mode, or attach normally in Computer mode.
|
||||
5. Inspect live state in the **Runtime** tab.
|
||||
2. Open **Gadgets** and download a compatible Official Frida version.
|
||||
3. Select one downloaded Gadget as active.
|
||||
4. Tap the import icon and choose an installed app or an ARM64 base APK file.
|
||||
5. Tap the guest icon and choose **On-device**, **Computer**, or **Clean** in the launch panel.
|
||||
6. Select an agent for On-device mode, or attach normally in Computer mode.
|
||||
7. Inspect live state in the **Runtime** tab.
|
||||
|
||||
See [docs/USAGE.md](docs/USAGE.md) and
|
||||
[docs/FRIDA_CONNECTION.md](docs/FRIDA_CONNECTION.md) for the complete workflow.
|
||||
@@ -246,8 +248,9 @@ docs/ Build, usage, limitations and device evidence
|
||||
|
||||
## Scope and limitations
|
||||
|
||||
FridaBox currently targets single-file ARM64 APKs. Split APK sets, privileged or
|
||||
system applications, full Google Play Services, Play Integrity, hardware-backed
|
||||
FridaBox targets ARM64 APKs and can clone the active base/split set of a
|
||||
launchable installed app. Standalone split archives, privileged system
|
||||
applications, full Google Play Services, Play Integrity, hardware-backed
|
||||
attestation, and perfect anti-instrumentation resistance are outside the current
|
||||
scope. Vendor behavior and hidden Android APIs can still affect compatibility.
|
||||
|
||||
|
||||
@@ -42,6 +42,33 @@ FridaBox gratefully acknowledges the Frida maintainers and contributors for the
|
||||
instrumentation engine, GumJS runtime, Java bridge, Gadget deployment model, and
|
||||
controller APIs that make this research possible.
|
||||
|
||||
## Tabler Icons
|
||||
|
||||
FridaBox UI control icons are generated from the official `@tabler/icons`
|
||||
package version 3.45.0, sourced from `https://github.com/tabler/tabler-icons`.
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2026 Paweł Kuna
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
## Frida Java bridge and agent compiler
|
||||
|
||||
The compiled JavaScript agents under `scripts/dist/` include
|
||||
|
||||
+21
-34
@@ -77,7 +77,6 @@ android {
|
||||
jniLibs {
|
||||
excludes.add("**/libandroidx.graphics.path.so")
|
||||
useLegacyPackaging true
|
||||
keepDebugSymbols.add("**/libfrida-gadget.config.so")
|
||||
}
|
||||
resources {
|
||||
excludes.add("**/libandroidx.graphics.path.so")
|
||||
@@ -104,40 +103,15 @@ tasks.matching { it.name == "mergeDebugAssets" }.configureEach { dependsOn("copy
|
||||
tasks.matching { it.name.toLowerCase().contains("debug") && it.name.toLowerCase().contains("lint") }
|
||||
.configureEach { dependsOn("copyDemoGuestDebug") }
|
||||
|
||||
def fridaGadget = file("src/main/jniLibs/arm64-v8a/libfrida-gadget.so")
|
||||
def fridaGadgetConfig = file("src/main/jniLibs/arm64-v8a/libfrida-gadget.config.so")
|
||||
tasks.register("verifyFridaGadget") {
|
||||
inputs.files(fridaGadget, fridaGadgetConfig)
|
||||
doLast {
|
||||
if (!fridaGadget.isFile()) {
|
||||
throw new GradleException("Frida Gadget 17.16.0 is missing. Run: python tools/fetch_frida_gadget.py")
|
||||
}
|
||||
if (!fridaGadgetConfig.isFile()) {
|
||||
throw new GradleException("Frida Gadget configuration is missing: ${fridaGadgetConfig}")
|
||||
}
|
||||
def digest = java.security.MessageDigest.getInstance("SHA-256")
|
||||
.digest(fridaGadget.bytes).collect { String.format("%02x", it & 0xff) }.join()
|
||||
if (digest != "6bf149e5d1c5ec701e7b822cab57bb243f1c2a03318fa974fe373ee711a9ed9e") {
|
||||
throw new GradleException("Frida Gadget 17.16.0 SHA-256 mismatch: ${digest}")
|
||||
}
|
||||
def config = new groovy.json.JsonSlurper().parse(fridaGadgetConfig)
|
||||
if (config.interaction?.address != "127.0.0.1" || config.interaction?.on_load != "wait") {
|
||||
throw new GradleException("Unsafe or invalid Frida Gadget configuration")
|
||||
}
|
||||
}
|
||||
}
|
||||
tasks.matching { it.name == "preBuild" }.configureEach { dependsOn("verifyFridaGadget") }
|
||||
|
||||
tasks.register("verifyDebugApkFridaPackaging") {
|
||||
tasks.register("verifyDebugApkHasNoGadget") {
|
||||
dependsOn("assembleDebug")
|
||||
doLast {
|
||||
def apk = fileTree("$buildDir/outputs/apk/debug").matching { include("*.apk") }.singleFile
|
||||
def entries = zipTree(apk).matching {
|
||||
include("lib/arm64-v8a/libfrida-gadget.so")
|
||||
include("lib/arm64-v8a/libfrida-gadget.config.so")
|
||||
}.files.collect { it.name }.toSet()
|
||||
if (!entries.contains("libfrida-gadget.so") || !entries.contains("libfrida-gadget.config.so")) {
|
||||
throw new GradleException("Debug APK does not contain both Frida Gadget files")
|
||||
def entries = zipTree(apk).files.findAll {
|
||||
it.name.toLowerCase().contains("gadget") && it.name.toLowerCase().endsWith(".so")
|
||||
}
|
||||
if (!entries.isEmpty()) {
|
||||
throw new GradleException("APK must not bundle Frida Gadget: ${entries*.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,7 +119,7 @@ tasks.register("verifyDebugApkFridaPackaging") {
|
||||
tasks.register("verifyInstrumentationOrdering") {
|
||||
doLast {
|
||||
def source = project(":Bcore").file("src/main/java/top/niunaijun/blackbox/app/BActivityThread.java").text
|
||||
def loader = source.indexOf("FridaGadgetLoader.loadIfEnabled()")
|
||||
def loader = source.indexOf("FridaGadgetLoader.loadAtProcessBind()")
|
||||
def makeApplication = source.indexOf("makeApplication(", loader)
|
||||
if (loader < 0 || makeApplication < 0 || loader >= makeApplication) {
|
||||
throw new GradleException("Frida Gadget loader must execute before guest makeApplication")
|
||||
@@ -153,6 +127,17 @@ tasks.register("verifyInstrumentationOrdering") {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("verifyTablerAndroidIcons", Exec) {
|
||||
workingDir(rootProject.projectDir)
|
||||
def nodeExecutable = System.getProperty("os.name").toLowerCase().contains("windows") ? "node.exe" : "node"
|
||||
commandLine(nodeExecutable, "tools/generate_tabler_android_icons.mjs", "--check")
|
||||
doFirst {
|
||||
if (!rootProject.file("node_modules/@tabler/icons/package.json").isFile()) {
|
||||
throw new GradleException("Missing @tabler/icons. Run npm ci before building FridaBox.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("verifyDemoGuestUnmodified") {
|
||||
dependsOn("copyDemoGuestDebug")
|
||||
doLast {
|
||||
@@ -166,7 +151,8 @@ tasks.register("verifyDemoGuestUnmodified") {
|
||||
}
|
||||
|
||||
tasks.named("check").configure {
|
||||
dependsOn("verifyDebugApkFridaPackaging", "verifyInstrumentationOrdering", "verifyDemoGuestUnmodified")
|
||||
dependsOn("verifyDebugApkHasNoGadget", "verifyInstrumentationOrdering",
|
||||
"verifyDemoGuestUnmodified", "verifyTablerAndroidIcons")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -176,6 +162,7 @@ dependencies {
|
||||
implementation libs.material
|
||||
implementation libs.constraintlayout
|
||||
implementation libs.core.ktx
|
||||
implementation "org.tukaani:xz:1.10"
|
||||
testImplementation libs.junit
|
||||
androidTestImplementation libs.ext.junit
|
||||
androidTestImplementation libs.espresso.core
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.qm4rs.fridabox;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
@@ -13,23 +14,36 @@ public final class ApkInspector {
|
||||
}
|
||||
|
||||
public static Result inspect(File apk) throws IOException {
|
||||
return inspect(Collections.singletonList(apk));
|
||||
}
|
||||
|
||||
public static Result inspect(File... apks) throws IOException {
|
||||
return inspect(Arrays.asList(apks));
|
||||
}
|
||||
|
||||
public static Result inspect(Iterable<File> apks) throws IOException {
|
||||
Set<String> abis = new LinkedHashSet<>();
|
||||
boolean hasNativeLibraries = false;
|
||||
try (ZipFile archive = new ZipFile(apk)) {
|
||||
if (archive.getEntry("AndroidManifest.xml") == null) {
|
||||
throw new IOException("The selected file has no AndroidManifest.xml");
|
||||
}
|
||||
for (ZipEntry entry : Collections.list(archive.entries())) {
|
||||
String name = entry.getName();
|
||||
if (!entry.isDirectory() && name.startsWith("lib/") && name.endsWith(".so")) {
|
||||
String[] parts = name.split("/", 3);
|
||||
if (parts.length == 3) {
|
||||
hasNativeLibraries = true;
|
||||
abis.add(parts[1]);
|
||||
boolean inspectedAny = false;
|
||||
for (File apk : apks) {
|
||||
inspectedAny = true;
|
||||
try (ZipFile archive = new ZipFile(apk)) {
|
||||
if (archive.getEntry("AndroidManifest.xml") == null) {
|
||||
throw new IOException("The selected file has no AndroidManifest.xml");
|
||||
}
|
||||
for (ZipEntry entry : Collections.list(archive.entries())) {
|
||||
String name = entry.getName();
|
||||
if (!entry.isDirectory() && name.startsWith("lib/") && name.endsWith(".so")) {
|
||||
String[] parts = name.split("/", 3);
|
||||
if (parts.length == 3) {
|
||||
hasNativeLibraries = true;
|
||||
abis.add(parts[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!inspectedAny) throw new IOException("No APK files were provided");
|
||||
boolean supported = !hasNativeLibraries || abis.contains("arm64-v8a");
|
||||
return new Result(hasNativeLibraries, supported, abis);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,382 @@
|
||||
package com.qm4rs.fridabox
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.tukaani.xz.XZInputStream
|
||||
import top.niunaijun.blackbox.instrumentation.InstrumentationSettings
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.security.MessageDigest
|
||||
import java.util.Locale
|
||||
import java.util.zip.GZIPInputStream
|
||||
|
||||
enum class GadgetSource(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val repository: String
|
||||
) {
|
||||
OFFICIAL("official", "Official Frida", "frida/frida");
|
||||
|
||||
fun expectedAsset(version: String, architecture: String): String =
|
||||
"frida-gadget-$version-android-$architecture.so.xz"
|
||||
|
||||
companion object {
|
||||
fun fromId(value: String?): GadgetSource? = entries.firstOrNull { it.id == value }
|
||||
}
|
||||
}
|
||||
|
||||
data class GadgetAbi(val androidName: String, val releaseName: String) {
|
||||
companion object {
|
||||
private val supported = listOf(
|
||||
GadgetAbi("arm64-v8a", "arm64"),
|
||||
GadgetAbi("armeabi-v7a", "arm"),
|
||||
GadgetAbi("x86_64", "x86_64"),
|
||||
GadgetAbi("x86", "x86")
|
||||
)
|
||||
|
||||
fun detect(): GadgetAbi? = Build.SUPPORTED_ABIS
|
||||
.firstNotNullOfOrNull { deviceAbi -> supported.firstOrNull { it.androidName == deviceAbi } }
|
||||
}
|
||||
}
|
||||
|
||||
data class GadgetRelease(
|
||||
val source: GadgetSource,
|
||||
val version: String,
|
||||
val abi: GadgetAbi,
|
||||
val assetName: String,
|
||||
val downloadUrl: String,
|
||||
val compressedSize: Long,
|
||||
val publishedAt: String
|
||||
)
|
||||
|
||||
data class InstalledGadget(
|
||||
val source: GadgetSource,
|
||||
val version: String,
|
||||
val abi: GadgetAbi,
|
||||
val file: File,
|
||||
val sha256: String,
|
||||
val size: Long
|
||||
) {
|
||||
val identity: String get() = "${source.id}:$version:${abi.androidName}"
|
||||
}
|
||||
|
||||
data class GadgetCatalog(
|
||||
val releases: List<GadgetRelease>,
|
||||
val fromCache: Boolean,
|
||||
val hasMore: Boolean
|
||||
)
|
||||
|
||||
class GadgetManager(private val context: Context) {
|
||||
private val root = File(context.filesDir, DOWNLOAD_ROOT)
|
||||
private val cacheRoot = File(context.cacheDir, "gadget-catalogs")
|
||||
|
||||
init {
|
||||
if (InstrumentationSettings.getSelectedGadgetSource() != null
|
||||
&& InstrumentationSettings.getSelectedGadgetSource() != GadgetSource.OFFICIAL.id) {
|
||||
InstrumentationSettings.clearSelectedGadget()
|
||||
}
|
||||
}
|
||||
|
||||
fun detectedAbi(): GadgetAbi? = GadgetAbi.detect()
|
||||
|
||||
fun loadCatalog(source: GadgetSource, abi: GadgetAbi, page: Int): GadgetCatalog {
|
||||
val cache = File(cacheRoot, "${source.id}-${abi.androidName}-$page.json")
|
||||
return try {
|
||||
val json = requestText(releaseCatalogUrl(source, page))
|
||||
val releases = parseCatalog(json, source, abi)
|
||||
writeAtomically(cache, json.toByteArray(Charsets.UTF_8))
|
||||
GadgetCatalog(releases, false, JSONArray(json).length() == CATALOG_PAGE_SIZE)
|
||||
} catch (networkError: Exception) {
|
||||
if (!cache.isFile) throw networkError
|
||||
val cached = cache.readText(Charsets.UTF_8)
|
||||
val releases = parseCatalog(cached, source, abi)
|
||||
GadgetCatalog(releases, true, JSONArray(cached).length() == CATALOG_PAGE_SIZE)
|
||||
}
|
||||
}
|
||||
|
||||
fun download(release: GadgetRelease): InstalledGadget {
|
||||
val currentAbi = detectedAbi() ?: throw IOException("This device ABI is not supported")
|
||||
if (release.abi != currentAbi) throw IOException("The selected release does not match this device ABI")
|
||||
|
||||
val directory = installDirectory(release.source, release.version, release.abi).apply {
|
||||
if (!isDirectory && !mkdirs()) throw IOException("Unable to create Gadget storage")
|
||||
}
|
||||
val temporary = File(directory, "$PAYLOAD_NAME.partial")
|
||||
val destination = File(directory, PAYLOAD_NAME)
|
||||
val metadata = File(directory, METADATA_NAME)
|
||||
if (temporary.exists() && !temporary.delete()) throw IOException("Unable to replace partial download")
|
||||
|
||||
val connection = openConnection(release.downloadUrl, "application/octet-stream")
|
||||
try {
|
||||
if (connection.responseCode !in 200..299) {
|
||||
throw IOException("GitHub download failed (HTTP ${connection.responseCode})")
|
||||
}
|
||||
val compressedLength = connection.contentLengthLong
|
||||
if (compressedLength > MAX_COMPRESSED_BYTES) throw IOException("Gadget download is unexpectedly large")
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
var total = 0L
|
||||
decompressed(connection.inputStream, release.assetName).use { input ->
|
||||
FileOutputStream(temporary).use { output ->
|
||||
val buffer = ByteArray(128 * 1024)
|
||||
while (true) {
|
||||
val count = input.read(buffer)
|
||||
if (count < 0) break
|
||||
total += count
|
||||
if (total > MAX_DECOMPRESSED_BYTES) throw IOException("Expanded Gadget is unexpectedly large")
|
||||
digest.update(buffer, 0, count)
|
||||
output.write(buffer, 0, count)
|
||||
}
|
||||
output.fd.sync()
|
||||
}
|
||||
}
|
||||
if (total == 0L) throw IOException("Downloaded Gadget is empty")
|
||||
validateElf(temporary, release.abi)
|
||||
replace(temporary, destination)
|
||||
if (!destination.setReadable(true, true) || !destination.setWritable(false, false)) {
|
||||
throw IOException("Unable to secure downloaded Gadget")
|
||||
}
|
||||
val sha256 = digest.digest().joinToString("") { "%02x".format(it) }
|
||||
val record = InstalledGadget(
|
||||
release.source, release.version, release.abi, destination, sha256, total
|
||||
)
|
||||
writeMetadata(metadata, record, release.assetName)
|
||||
return record
|
||||
} catch (error: Exception) {
|
||||
temporary.delete()
|
||||
throw error
|
||||
} finally {
|
||||
connection.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
fun installed(): List<InstalledGadget> {
|
||||
val abi = detectedAbi() ?: return emptyList()
|
||||
if (!root.isDirectory) return emptyList()
|
||||
return root.walkTopDown()
|
||||
.filter { it.isFile && it.name == METADATA_NAME }
|
||||
.mapNotNull { metadata -> runCatching { readMetadata(metadata) }.getOrNull() }
|
||||
.filter { it.abi == abi && it.file.isFile }
|
||||
.filter { runCatching { validateElf(it.file, it.abi) }.isSuccess }
|
||||
.sortedWith(compareByDescending<InstalledGadget> { versionParts(it.version) }
|
||||
.thenBy { it.source.ordinal })
|
||||
.toList()
|
||||
}
|
||||
|
||||
fun selected(): InstalledGadget? {
|
||||
val path = InstrumentationSettings.getSelectedGadgetPath() ?: return null
|
||||
val selected = runCatching { File(path).canonicalFile }.getOrNull() ?: return null
|
||||
return installed().firstOrNull { runCatching { it.file.canonicalFile == selected }.getOrDefault(false) }
|
||||
}
|
||||
|
||||
fun select(gadget: InstalledGadget) {
|
||||
val currentAbi = detectedAbi() ?: throw IOException("This device ABI is not supported")
|
||||
if (gadget.abi != currentAbi || !gadget.file.isFile) {
|
||||
throw IOException("The selected Gadget is not available for this device")
|
||||
}
|
||||
validateElf(gadget.file, gadget.abi)
|
||||
if (!InstrumentationSettings.setSelectedGadget(
|
||||
gadget.file.canonicalPath,
|
||||
gadget.source.id,
|
||||
gadget.version,
|
||||
gadget.abi.androidName,
|
||||
gadget.sha256
|
||||
)) {
|
||||
throw IOException("Unable to save the selected Gadget")
|
||||
}
|
||||
}
|
||||
|
||||
fun findInstalled(release: GadgetRelease): InstalledGadget? {
|
||||
val expected = runCatching {
|
||||
File(installDirectory(release.source, release.version, release.abi), METADATA_NAME)
|
||||
}.getOrNull() ?: return null
|
||||
return runCatching { readMetadata(expected) }.getOrNull()
|
||||
?.takeIf { it.file.isFile && runCatching { validateElf(it.file, it.abi) }.isSuccess }
|
||||
}
|
||||
|
||||
private fun parseCatalog(json: String, source: GadgetSource, abi: GadgetAbi): List<GadgetRelease> {
|
||||
val releases = JSONArray(json)
|
||||
val result = ArrayList<GadgetRelease>()
|
||||
for (index in 0 until releases.length()) {
|
||||
val release = releases.optJSONObject(index) ?: continue
|
||||
if (release.optBoolean("draft") || release.optBoolean("prerelease")) continue
|
||||
val version = release.optString("tag_name").trim()
|
||||
if (!isSafeSegment(version)) continue
|
||||
val expected = source.expectedAsset(version, abi.releaseName)
|
||||
val assets = release.optJSONArray("assets") ?: continue
|
||||
for (assetIndex in 0 until assets.length()) {
|
||||
val asset = assets.optJSONObject(assetIndex) ?: continue
|
||||
val name = asset.optString("name")
|
||||
if (name != expected) continue
|
||||
val url = asset.optString("browser_download_url")
|
||||
if (!isTrustedUrl(url)) continue
|
||||
result += GadgetRelease(
|
||||
source = source,
|
||||
version = version,
|
||||
abi = abi,
|
||||
assetName = name,
|
||||
downloadUrl = url,
|
||||
compressedSize = asset.optLong("size", -1L),
|
||||
publishedAt = release.optString("published_at")
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun installDirectory(source: GadgetSource, version: String, abi: GadgetAbi): File {
|
||||
if (!isSafeSegment(version)) throw IOException("Invalid Gadget version")
|
||||
return File(root, "${source.id}${File.separator}$version${File.separator}${abi.androidName}")
|
||||
}
|
||||
|
||||
private fun writeMetadata(file: File, gadget: InstalledGadget, assetName: String) {
|
||||
val json = JSONObject()
|
||||
.put("source", gadget.source.id)
|
||||
.put("version", gadget.version)
|
||||
.put("abi", gadget.abi.androidName)
|
||||
.put("releaseAbi", gadget.abi.releaseName)
|
||||
.put("sha256", gadget.sha256)
|
||||
.put("size", gadget.size)
|
||||
.put("asset", assetName)
|
||||
.toString()
|
||||
writeAtomically(file, json.toByteArray(Charsets.UTF_8))
|
||||
}
|
||||
|
||||
private fun readMetadata(file: File): InstalledGadget {
|
||||
if (!file.isFile || file.length() > MAX_METADATA_BYTES) throw IOException("Invalid Gadget metadata")
|
||||
val json = JSONObject(file.readText(Charsets.UTF_8))
|
||||
val source = GadgetSource.fromId(json.getString("source")) ?: throw IOException("Unknown Gadget source")
|
||||
val version = json.getString("version")
|
||||
if (!isSafeSegment(version)) throw IOException("Invalid Gadget version")
|
||||
val androidAbi = json.getString("abi")
|
||||
val releaseAbi = json.getString("releaseAbi")
|
||||
val abi = GadgetAbi(androidAbi, releaseAbi)
|
||||
val payload = File(file.parentFile, PAYLOAD_NAME).canonicalFile
|
||||
val canonicalRoot = root.canonicalFile
|
||||
if (!payload.path.startsWith(canonicalRoot.path + File.separator)) throw IOException("Invalid Gadget path")
|
||||
return InstalledGadget(
|
||||
source, version, abi, payload, json.getString("sha256"), json.getLong("size")
|
||||
)
|
||||
}
|
||||
|
||||
private fun requestText(url: String): String {
|
||||
val connection = openConnection(url, "application/vnd.github+json")
|
||||
try {
|
||||
if (connection.responseCode !in 200..299) {
|
||||
throw IOException("GitHub release lookup failed (HTTP ${connection.responseCode})")
|
||||
}
|
||||
val bytes = readLimited(connection.inputStream, MAX_CATALOG_BYTES)
|
||||
return bytes.toString(Charsets.UTF_8)
|
||||
} finally {
|
||||
connection.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun openConnection(value: String, accept: String): HttpURLConnection {
|
||||
if (!isTrustedUrl(value)) throw IOException("Untrusted Gadget download URL")
|
||||
return (URL(value).openConnection() as HttpURLConnection).apply {
|
||||
connectTimeout = 15_000
|
||||
readTimeout = 45_000
|
||||
instanceFollowRedirects = true
|
||||
setRequestProperty("Accept", accept)
|
||||
setRequestProperty("User-Agent", "FridaBox/${BuildConfig.VERSION_NAME}")
|
||||
setRequestProperty("X-GitHub-Api-Version", "2022-11-28")
|
||||
}
|
||||
}
|
||||
|
||||
private fun decompressed(input: InputStream, assetName: String): InputStream = when {
|
||||
assetName.endsWith(".xz") -> XZInputStream(input)
|
||||
assetName.endsWith(".gz") -> GZIPInputStream(input, 128 * 1024)
|
||||
else -> throw IOException("Unsupported Gadget archive format")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DOWNLOAD_ROOT = "fridabox-gadgets"
|
||||
internal const val CATALOG_PAGE_SIZE = 10
|
||||
private const val PAYLOAD_NAME = "payload.so"
|
||||
private const val METADATA_NAME = "metadata.json"
|
||||
private const val MAX_METADATA_BYTES = 16L * 1024L
|
||||
private const val MAX_CATALOG_BYTES = 24L * 1024L * 1024L
|
||||
private const val MAX_COMPRESSED_BYTES = 96L * 1024L * 1024L
|
||||
private const val MAX_DECOMPRESSED_BYTES = 192L * 1024L * 1024L
|
||||
|
||||
internal fun releaseCatalogUrl(source: GadgetSource, page: Int): String {
|
||||
require(page > 0) { "Catalog page must be positive" }
|
||||
return "https://api.github.com/repos/${source.repository}/releases" +
|
||||
"?per_page=$CATALOG_PAGE_SIZE&page=$page"
|
||||
}
|
||||
|
||||
internal fun validateElf(file: File, abi: GadgetAbi) {
|
||||
val header = ByteArray(20)
|
||||
FileInputStream(file).use { input ->
|
||||
if (input.read(header) != header.size) throw IOException("Downloaded Gadget is truncated")
|
||||
}
|
||||
if (header[0].toInt() and 0xff != 0x7f || header[1] != 'E'.code.toByte()
|
||||
|| header[2] != 'L'.code.toByte() || header[3] != 'F'.code.toByte()) {
|
||||
throw IOException("Downloaded Gadget is not an ELF library")
|
||||
}
|
||||
if (header[5].toInt() != 1) throw IOException("Only little-endian Android ELF libraries are supported")
|
||||
val machine = (header[18].toInt() and 0xff) or ((header[19].toInt() and 0xff) shl 8)
|
||||
val expected = when (abi.androidName) {
|
||||
"arm64-v8a" -> 183
|
||||
"armeabi-v7a" -> 40
|
||||
"x86" -> 3
|
||||
"x86_64" -> 62
|
||||
else -> -1
|
||||
}
|
||||
if (machine != expected) throw IOException("Gadget ELF architecture does not match ${abi.androidName}")
|
||||
}
|
||||
|
||||
private fun readLimited(input: InputStream, maximum: Long): ByteArray {
|
||||
input.use {
|
||||
val output = ByteArrayOutputStream()
|
||||
val buffer = ByteArray(64 * 1024)
|
||||
var total = 0L
|
||||
while (true) {
|
||||
val count = it.read(buffer)
|
||||
if (count < 0) break
|
||||
total += count
|
||||
if (total > maximum) throw IOException("GitHub response is unexpectedly large")
|
||||
output.write(buffer, 0, count)
|
||||
}
|
||||
return output.toByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeAtomically(destination: File, bytes: ByteArray) {
|
||||
destination.parentFile?.let { if (!it.isDirectory && !it.mkdirs()) throw IOException("Unable to create storage") }
|
||||
val temporary = File(destination.parentFile, destination.name + ".partial")
|
||||
FileOutputStream(temporary).use { output ->
|
||||
output.write(bytes)
|
||||
output.fd.sync()
|
||||
}
|
||||
replace(temporary, destination)
|
||||
}
|
||||
|
||||
private fun replace(temporary: File, destination: File) {
|
||||
if (destination.exists() && !destination.delete()) throw IOException("Unable to replace ${destination.name}")
|
||||
if (!temporary.renameTo(destination)) throw IOException("Unable to install ${destination.name}")
|
||||
}
|
||||
|
||||
private fun isSafeSegment(value: String): Boolean =
|
||||
value.isNotBlank() && value.length <= 64 && value.matches(Regex("[A-Za-z0-9._-]+"))
|
||||
|
||||
private fun isTrustedUrl(value: String): Boolean = runCatching {
|
||||
val url = URL(value)
|
||||
val host = url.host.lowercase(Locale.ROOT)
|
||||
url.protocol == "https" && (host == "api.github.com" || host == "github.com"
|
||||
|| host.endsWith(".githubusercontent.com"))
|
||||
}.getOrDefault(false)
|
||||
|
||||
private fun versionParts(value: String): String = value.split('.')
|
||||
.joinToString(".") { it.toIntOrNull()?.toString()?.padStart(8, '0') ?: it }
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"interaction": {
|
||||
"type": "listen",
|
||||
"address": "127.0.0.1",
|
||||
"port": 27042,
|
||||
"on_port_conflict": "pick-next",
|
||||
"on_load": "wait"
|
||||
},
|
||||
"runtime": "qjs",
|
||||
"teardown": "minimal"
|
||||
}
|
||||
Binary file not shown.
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<gradient android:angle="315" android:startColor="#1A2942" android:centerColor="#141D30" android:endColor="#111827" />
|
||||
<corners android:radius="28dp" />
|
||||
<stroke android:width="1dp" android:color="#334862" />
|
||||
</shape>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<corners android:radius="28dp" />
|
||||
<gradient
|
||||
android:angle="90"
|
||||
android:startColor="#E0253850"
|
||||
android:centerColor="#C91A2B42"
|
||||
android:endColor="#B3122136" />
|
||||
<stroke android:width="1dp" android:color="@color/fb_glass_outline" />
|
||||
</shape>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<corners android:radius="22dp" />
|
||||
<gradient
|
||||
android:angle="135"
|
||||
android:startColor="#F02A3E58"
|
||||
android:centerColor="#E01C2D45"
|
||||
android:endColor="#D916263C" />
|
||||
<stroke android:width="1dp" android:color="@color/fb_glass_outline" />
|
||||
</shape>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<corners android:radius="24dp" />
|
||||
<gradient
|
||||
android:angle="135"
|
||||
android:startColor="#D9253850"
|
||||
android:centerColor="#B51B2B42"
|
||||
android:endColor="#9915253B" />
|
||||
<stroke android:width="1dp" android:color="@color/fb_glass_outline" />
|
||||
</shape>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_pressed="true">
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="22dp" />
|
||||
<gradient android:angle="135" android:startColor="#D564E8E8" android:endColor="#A329415D" />
|
||||
<stroke android:width="1dp" android:color="#D9FFFFFF" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="22dp" />
|
||||
<gradient android:angle="135" android:startColor="#A638516D" android:endColor="#7315263B" />
|
||||
<stroke android:width="1dp" android:color="@color/fb_glass_outline" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<corners android:radius="22dp" />
|
||||
<gradient android:angle="135" android:startColor="#59364E68" android:endColor="#33152235" />
|
||||
<stroke android:width="1dp" android:color="@color/fb_glass_outline_soft" />
|
||||
</shape>
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<gradient
|
||||
android:angle="135"
|
||||
android:startColor="#07111D"
|
||||
android:centerColor="#11162D"
|
||||
android:endColor="#080C18" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<gradient
|
||||
android:type="radial"
|
||||
android:centerX="0.88"
|
||||
android:centerY="0.08"
|
||||
android:gradientRadius="420dp"
|
||||
android:startColor="#5964E8E8"
|
||||
android:centerColor="#2464E8E8"
|
||||
android:endColor="#0064E8E8" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<gradient
|
||||
android:type="radial"
|
||||
android:centerX="0.08"
|
||||
android:centerY="0.92"
|
||||
android:gradientRadius="460dp"
|
||||
android:startColor="#4D9B87F5"
|
||||
android:centerColor="#1F9B87F5"
|
||||
android:endColor="#009B87F5" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -1,4 +0,0 @@
|
||||
<?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">
|
||||
<path android:fillColor="#FF05070B" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generated from @tabler/icons 3.45.0: icons/outline/apps.svg -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M4 5a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M4 15a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M14 15a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M14 7l6 0" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M17 4l0 6" />
|
||||
</vector>
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generated from @tabler/icons 3.45.0: icons/outline/database-off.svg -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M12.983 8.978c3.955 -.182 7.017 -1.446 7.017 -2.978c0 -1.657 -3.582 -3 -8 -3c-1.661 0 -3.204 .19 -4.483 .515m-2.783 1.228c-.471 .382 -.734 .808 -.734 1.257c0 1.22 1.944 2.271 4.734 2.74" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M4 6v6c0 1.657 3.582 3 8 3c.986 0 1.93 -.067 2.802 -.19m3.187 -.82c1.251 -.53 2.011 -1.228 2.011 -1.99v-6" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M4 12v6c0 1.657 3.582 3 8 3c3.217 0 5.991 -.712 7.261 -1.74m.739 -3.26v-4" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M3 3l18 18" />
|
||||
</vector>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generated from @tabler/icons 3.45.0: icons/outline/x.svg -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M18 6l-12 12" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M6 6l12 12" />
|
||||
</vector>
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generated from @tabler/icons 3.45.0: icons/outline/info-circle.svg -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M12 9h.01" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M11 12h1v4h1" />
|
||||
</vector>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generated from @tabler/icons 3.45.0: icons/outline/file.svg -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M14 3v4a1 1 0 0 0 1 1h4" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2" />
|
||||
</vector>
|
||||
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generated from @tabler/icons 3.45.0: icons/outline/package.svg -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M12 3l8 4.5l0 9l-8 4.5l-8 -4.5l0 -9l8 -4.5" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M12 12l8 -4.5" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M12 12l0 9" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M12 12l-8 -4.5" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M16 5.25l-8 4.5" />
|
||||
</vector>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generated from @tabler/icons 3.45.0: icons/outline/file-import.svg -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M14 3v4a1 1 0 0 0 1 1h4" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M5 13v-8a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2h-5.5m-9.5 -2h7m-3 -3l3 3l-3 3" />
|
||||
</vector>
|
||||
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generated from @tabler/icons 3.45.0: icons/outline/trash.svg -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M4 7l16 0" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M10 11l0 6" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M14 11l0 6" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3" />
|
||||
</vector>
|
||||
@@ -1,4 +1,15 @@
|
||||
<?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">
|
||||
<path android:fillColor="#FFFFFFFF" android:pathData="M3,13h4l2.4,-7.2L14,20l3,-7h4v-2h-5.3l-1.5,3.5L9.6,1L5.5,11H3z" />
|
||||
<!-- Generated from @tabler/icons 3.45.0: icons/outline/activity.svg -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M3 12h4l3 8l4 -16l3 8h4" />
|
||||
</vector>
|
||||
|
||||
@@ -1,4 +1,22 @@
|
||||
<?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">
|
||||
<path android:fillColor="#FFFFFFFF" android:pathData="M19.4,13a7.7,7.7 0,0 0,0.1 -1,7.7 7.7,0 0,0 -0.1,-1l2.1,-1.7 -2,-3.4 -2.5,1a8,8 0,0 0,-1.7 -1L15,3h-4l-0.4,2.8a8,8 0,0 0,-1.7 1l-2.5,-1 -2,3.4L6.6,11a7.7,7.7 0,0 0,-0.1 1,7.7 7.7,0 0,0 0.1,1l-2.1,1.7 2,3.4 2.5,-1a8,8 0,0 0,1.7 1L11,21h4l0.4,-2.8a8,8 0,0 0,1.7 -1l2.5,1 2,-3.4zM13,15.5a3.5,3.5 0,1 1,0 -7,3.5 3.5,0 0,1 0,7z" />
|
||||
<!-- Generated from @tabler/icons 3.45.0: icons/outline/settings.svg -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" />
|
||||
</vector>
|
||||
|
||||
@@ -1,4 +1,36 @@
|
||||
<?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">
|
||||
<path android:fillColor="#FFFFFFFF" android:pathData="M3,3h8v8H3zM13,3h8v5h-8zM13,10h8v11h-8zM3,13h8v8H3z" />
|
||||
<!-- Generated from @tabler/icons 3.45.0: icons/outline/layout-grid.svg -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M4 5a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M14 5a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M4 15a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M14 15a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
|
||||
</vector>
|
||||
|
||||
@@ -4,15 +4,18 @@
|
||||
android:id="@+id/fridabox_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/fb_background"
|
||||
android:background="@drawable/bg_liquid_glass_app"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/fb_background"
|
||||
android:elevation="8dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:elevation="0dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
@@ -20,8 +23,9 @@
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="72dp"
|
||||
android:background="@color/fb_background"
|
||||
android:layout_height="64dp"
|
||||
android:background="@drawable/bg_glass_toolbar"
|
||||
android:elevation="10dp"
|
||||
android:contentInsetStart="0dp"
|
||||
android:contentInsetEnd="0dp">
|
||||
|
||||
@@ -31,12 +35,12 @@
|
||||
android:gravity="center_vertical"
|
||||
android:layoutDirection="ltr"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="18dp"
|
||||
android:paddingEnd="18dp">
|
||||
android:paddingStart="14dp"
|
||||
android:paddingEnd="14dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_width="44dp"
|
||||
android:layout_height="44dp"
|
||||
android:contentDescription="@string/fb_brand"
|
||||
android:scaleType="centerInside"
|
||||
android:src="@drawable/logo" />
|
||||
@@ -118,29 +122,32 @@
|
||||
app:layout_constraintTop_toBottomOf="@id/app_bar"
|
||||
app:trackColor="@color/fb_surface" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/import_fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/import_fab_icon"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
android:layout_marginEnd="18dp"
|
||||
android:layout_marginBottom="18dp"
|
||||
android:letterSpacing="0"
|
||||
android:text="@string/fb_import_apk"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/fb_black"
|
||||
android:layout_marginBottom="14dp"
|
||||
android:contentDescription="@string/fb_import"
|
||||
app:backgroundTint="@color/fb_primary"
|
||||
app:icon="@drawable/ic_fb_add"
|
||||
app:iconTint="@color/fb_black"
|
||||
app:elevation="12dp"
|
||||
app:srcCompat="@drawable/ic_fb_import"
|
||||
app:tint="@color/fb_black"
|
||||
app:layout_constraintBottom_toTopOf="@id/bottom_navigation"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
android:id="@+id/bottom_navigation"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="72dp"
|
||||
android:background="@color/fb_surface"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="@drawable/bg_glass_navigation"
|
||||
android:elevation="14dp"
|
||||
app:itemActiveIndicatorStyle="@style/Widget.MaterialComponents.BottomNavigationView.Colored"
|
||||
app:itemIconTint="@color/fb_text_primary"
|
||||
app:itemRippleColor="@color/fb_glass_highlight"
|
||||
app:itemTextColor="@color/fb_text_primary"
|
||||
app:labelVisibilityMode="labeled"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@+id/nav_workspace" android:icon="@drawable/ic_fb_workspace" android:title="@string/fb_nav_workspace" />
|
||||
<item android:id="@+id/nav_gadgets" android:icon="@drawable/ic_fb_gadget" android:title="@string/fb_nav_gadgets" />
|
||||
<item android:id="@+id/nav_runtime" android:icon="@drawable/ic_fb_runtime" android:title="@string/fb_nav_runtime" />
|
||||
<item android:id="@+id/nav_settings" android:icon="@drawable/ic_fb_settings" android:title="@string/fb_nav_settings" />
|
||||
</menu>
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="fb_background">#080C14</color>
|
||||
<color name="fb_surface">#111827</color>
|
||||
<color name="fb_surface_high">#182235</color>
|
||||
<color name="fb_surface_tint">#1D2940</color>
|
||||
<color name="fb_background">#07111D</color>
|
||||
<color name="fb_surface">#18263A</color>
|
||||
<color name="fb_surface_high">#22344D</color>
|
||||
<color name="fb_surface_tint">#29415D</color>
|
||||
<color name="fb_glass_surface">#B31A2940</color>
|
||||
<color name="fb_glass_surface_strong">#E0203047</color>
|
||||
<color name="fb_glass_surface_soft">#731C3049</color>
|
||||
<color name="fb_glass_outline">#66FFFFFF</color>
|
||||
<color name="fb_glass_outline_soft">#2EFFFFFF</color>
|
||||
<color name="fb_glass_highlight">#38FFFFFF</color>
|
||||
<color name="fb_primary">#64E8E8</color>
|
||||
<color name="fb_primary_dark">#1AAFB5</color>
|
||||
<color name="fb_secondary">#9B87F5</color>
|
||||
<color name="fb_text_primary">#F8FAFC</color>
|
||||
<color name="fb_text_secondary">#9CAEC5</color>
|
||||
<color name="fb_outline">#2B3A52</color>
|
||||
<color name="fb_outline">#52708F</color>
|
||||
<color name="fb_success">#42D39A</color>
|
||||
<color name="fb_warning">#F5BF5B</color>
|
||||
<color name="fb_error">#FB7185</color>
|
||||
|
||||
@@ -3,16 +3,17 @@
|
||||
<string name="fb_brand">FridaBox</string>
|
||||
<string name="fb_brand_tagline">Mobile instrumentation workspace</string>
|
||||
<string name="fb_nav_workspace">Workspace</string>
|
||||
<string name="fb_nav_gadgets">Gadgets</string>
|
||||
<string name="fb_nav_runtime">Runtime</string>
|
||||
<string name="fb_nav_settings">Settings</string>
|
||||
<string name="fb_import_apk">Import APK</string>
|
||||
<string name="fb_hero_eyebrow">PRIVATE · NON-ROOT · ARM64</string>
|
||||
<string name="fb_hero_title">Instrument apps. Keep control.</string>
|
||||
<string name="fb_hero_body">Run trusted JavaScript agents directly on the device, connect from a computer, or launch a clean guest—per app.</string>
|
||||
<string name="fb_guest_workspace">Guest workspace</string>
|
||||
<string name="fb_guest_workspace_hint">Each guest keeps its own launch mode and agent.</string>
|
||||
<string name="fb_no_guests">Your workspace is empty</string>
|
||||
<string name="fb_no_guests_body">Import one ARM64 base APK to create a private FridaBox guest.</string>
|
||||
<string name="fb_import">Import</string>
|
||||
<string name="fb_import_from_apps">Import from apps</string>
|
||||
<string name="fb_import_from_file">Import from file</string>
|
||||
<string name="fb_installed_apps">Installed apps</string>
|
||||
<string name="fb_import_app">Import</string>
|
||||
<string name="fb_app_imported">Imported</string>
|
||||
<string name="fb_loading_apps">Loading apps…</string>
|
||||
<string name="fb_no_importable_apps">No importable apps found</string>
|
||||
<string name="fb_mode_local">On-device</string>
|
||||
<string name="fb_mode_computer">Computer</string>
|
||||
<string name="fb_mode_clean">Clean</string>
|
||||
@@ -56,4 +57,25 @@
|
||||
<string name="fb_remove_body">The virtual app, its virtual data, and its saved on-device agent will be removed from FridaBox. The Android PackageManager installation is not changed.</string>
|
||||
<string name="fb_remove">Remove</string>
|
||||
<string name="fb_details">App details</string>
|
||||
<string name="fb_gadgets_title">Gadget manager</string>
|
||||
<string name="fb_active_gadget">Active Gadget</string>
|
||||
<string name="fb_no_active_gadget_short">Not selected</string>
|
||||
<string name="fb_installed_gadgets">Installed</string>
|
||||
<string name="fb_browse_gadgets">Browse versions</string>
|
||||
<string name="fb_close">Close</string>
|
||||
<string name="fb_loading_versions">Loading versions…</string>
|
||||
<string name="fb_all_versions_loaded">All available versions loaded</string>
|
||||
<string name="fb_cached_versions">Showing cached versions</string>
|
||||
<string name="fb_use_gadget">Use this Gadget</string>
|
||||
<string name="fb_gadget_active">Active</string>
|
||||
<string name="fb_download">Download</string>
|
||||
<string name="fb_gadget_required">Download and select a Gadget before using an instrumented launch mode.</string>
|
||||
<string name="fb_gadget_selected">Gadget %1$s selected</string>
|
||||
<string name="fb_gadget_select_failed">Unable to select Gadget</string>
|
||||
<string name="fb_gadget_downloading">Downloading Gadget %1$s…</string>
|
||||
<string name="fb_gadget_downloaded">Gadget %1$s downloaded. Choose it from Installed when you want to use it.</string>
|
||||
<string name="fb_gadget_downloaded_selected">Gadget %1$s downloaded and selected.</string>
|
||||
<string name="fb_gadget_download_failed">Gadget download failed: %1$s</string>
|
||||
<string name="fb_catalog_failed_body">Unable to load compatible Gadget releases from GitHub.</string>
|
||||
<string name="fb_gadget_unsupported_title">Unsupported device ABI</string>
|
||||
</resources>
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
<item name="colorOnSurface">@color/fb_text_primary</item>
|
||||
<item name="colorError">@color/fb_error</item>
|
||||
<item name="android:colorAccent">@color/fb_primary</item>
|
||||
<item name="android:windowBackground">@color/fb_background</item>
|
||||
<item name="android:navigationBarColor">@color/fb_surface</item>
|
||||
<item name="android:windowBackground">@drawable/bg_liquid_glass_app</item>
|
||||
<item name="android:navigationBarColor">@color/fb_background</item>
|
||||
<item name="android:statusBarColor">@color/fb_background</item>
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
<item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">false</item>
|
||||
@@ -22,7 +22,7 @@
|
||||
</style>
|
||||
|
||||
<style name="ThemeOverlay.FridaBox.Dialog" parent="ThemeOverlay.MaterialComponents.MaterialAlertDialog">
|
||||
<item name="colorSurface">@color/fb_surface_high</item>
|
||||
<item name="colorSurface">@color/fb_glass_surface_strong</item>
|
||||
<item name="colorOnSurface">@color/fb_text_primary</item>
|
||||
<item name="colorPrimary">@color/fb_primary</item>
|
||||
</style>
|
||||
|
||||
@@ -37,6 +37,15 @@ public class ApkInspectorTest {
|
||||
assertFalse(result.supported);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void splitNativeLibrariesAreInspectedTogether() throws Exception {
|
||||
File base = apkWith("classes.dex");
|
||||
File arm64Split = apkWith("lib/arm64-v8a/libsample.so");
|
||||
ApkInspector.Result result = ApkInspector.inspect(base, arm64Split);
|
||||
assertTrue(result.hasNativeLibraries);
|
||||
assertTrue(result.supported);
|
||||
}
|
||||
|
||||
private File apkWith(String entry) throws Exception {
|
||||
File file = temporary.newFile("test-" + System.nanoTime() + ".apk");
|
||||
try (ZipOutputStream output = new ZipOutputStream(new FileOutputStream(file))) {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.qm4rs.fridabox
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.io.IOException
|
||||
|
||||
class GadgetManagerTest {
|
||||
@get:Rule
|
||||
val temporary = TemporaryFolder()
|
||||
|
||||
@Test
|
||||
fun releaseAssetNameMatchesOfficialRepository() {
|
||||
assertEquals(
|
||||
"frida-gadget-17.9.11-android-arm64.so.xz",
|
||||
GadgetSource.OFFICIAL.expectedAsset("17.9.11", "arm64")
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun releaseCatalogUsesTenItemPages() {
|
||||
assertEquals(
|
||||
"https://api.github.com/repos/frida/frida/releases?per_page=10&page=3",
|
||||
GadgetManager.releaseCatalogUrl(GadgetSource.OFFICIAL, 3)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun elfValidationAcceptsMatchingArchitecture() {
|
||||
val file = temporary.newFile("payload.so")
|
||||
file.writeBytes(elfHeader(183))
|
||||
GadgetManager.validateElf(file, GadgetAbi("arm64-v8a", "arm64"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun elfValidationRejectsWrongArchitecture() {
|
||||
val file = temporary.newFile("payload.so")
|
||||
file.writeBytes(elfHeader(40))
|
||||
assertThrows(IOException::class.java) {
|
||||
GadgetManager.validateElf(file, GadgetAbi("arm64-v8a", "arm64"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun elfHeader(machine: Int): ByteArray = ByteArray(20).also {
|
||||
it[0] = 0x7f
|
||||
it[1] = 'E'.code.toByte()
|
||||
it[2] = 'L'.code.toByte()
|
||||
it[3] = 'F'.code.toByte()
|
||||
it[4] = 2
|
||||
it[5] = 1
|
||||
it[18] = (machine and 0xff).toByte()
|
||||
it[19] = (machine shr 8).toByte()
|
||||
}
|
||||
}
|
||||
+13
-11
@@ -7,23 +7,19 @@
|
||||
- Android NDK 29.0.13846066 when available. This build host only had the closely
|
||||
related complete NDK 29.0.14206865, which is recorded in `docs/BASELINE.md`.
|
||||
- Python 3.10 or newer.
|
||||
- Node.js 20 or newer and npm for reproducibly building Frida 17 Java agents.
|
||||
- Node.js 20 or newer and npm for reproducibly building Frida 17 Java agents
|
||||
and generating Android vectors from the official `@tabler/icons` package.
|
||||
|
||||
Create `local.properties` with the SDK path, then fetch the pinned official
|
||||
Gadget:
|
||||
|
||||
```powershell
|
||||
python tools\fetch_frida_gadget.py
|
||||
```
|
||||
|
||||
The script uses the GitHub Releases API and Python's standard `lzma` module. The
|
||||
build fails with a direct instruction if the binary is absent. No XZ archive is
|
||||
kept.
|
||||
Create `local.properties` with the SDK path. Frida Gadget is intentionally not a
|
||||
build input and no Gadget binary is packaged in the APK. After installation, the
|
||||
user downloads and selects an ABI-compatible Official Frida build from the
|
||||
in-app **Gadgets** screen.
|
||||
|
||||
Install the pinned Java bridge/compiler and build the controller agents:
|
||||
|
||||
```powershell
|
||||
npm ci
|
||||
npm run verify:android-icons
|
||||
python tools\build_frida_agents.py
|
||||
```
|
||||
|
||||
@@ -31,6 +27,10 @@ Frida 17 no longer bundles runtime bridges into API-loaded GumJS agents. The
|
||||
compiled files under `scripts/dist/` are reproducible from the source scripts,
|
||||
`package.json`, and `package-lock.json`.
|
||||
|
||||
UI control icons are generated from pinned `@tabler/icons` sources with
|
||||
`npm run generate:android-icons`. The app `check` task runs the matching
|
||||
verification script and fails if a committed Android vector is out of sync.
|
||||
|
||||
On Windows, if the repository path contains spaces, create a no-space junction
|
||||
and set the Bcore-only native path override:
|
||||
|
||||
@@ -52,6 +52,8 @@ Build and test:
|
||||
The debug host build depends on the sample build and copies its byte-identical
|
||||
APK into generated debug assets. Generated sample APKs are not source-controlled.
|
||||
The final host output is under `app/build/outputs/apk/debug/` and is ARM64-only.
|
||||
`verifyDebugApkHasNoGadget` inspects the assembled APK and fails if a Gadget
|
||||
library or configuration is accidentally bundled again.
|
||||
|
||||
## Production release
|
||||
|
||||
|
||||
+11
-11
@@ -1,6 +1,6 @@
|
||||
# Test results
|
||||
|
||||
Validation date: 2026-07-19
|
||||
Validation date: 2026-07-27
|
||||
|
||||
## Host validation
|
||||
|
||||
@@ -12,10 +12,10 @@ The following commands completed successfully on the build host:
|
||||
.\gradlew.bat :app:assembleDebug
|
||||
.\gradlew.bat test
|
||||
.\gradlew.bat :app:check
|
||||
.\gradlew.bat :app:verifyDebugApkFridaPackaging :app:verifyInstrumentationOrdering :app:verifyDemoGuestUnmodified
|
||||
.\gradlew.bat :app:verifyDebugApkHasNoGadget :app:verifyInstrumentationOrdering :app:verifyDemoGuestUnmodified
|
||||
npm ci
|
||||
python tools\build_frida_agents.py
|
||||
python -m py_compile tools\attach_guest.py tools\forward_frida_ports.py tools\fetch_frida_gadget.py tools\build_frida_agents.py
|
||||
python -m py_compile tools\attach_guest.py tools\forward_frida_ports.py tools\build_frida_agents.py
|
||||
```
|
||||
|
||||
For this workspace path, native builds used:
|
||||
@@ -24,8 +24,12 @@ For this workspace path, native builds used:
|
||||
$env:FRIDABOX_NDK_PROJECT_DIR='D:\FridaBoxBuild\Bcore'
|
||||
```
|
||||
|
||||
Unit-test results: 16 executions passed, with zero failures, errors, or skips.
|
||||
This is eight test methods run for both debug and release variants:
|
||||
Unit tests passed with zero failures, including Gadget asset naming, ten-item
|
||||
release pagination, ELF ABI validation, and combined base/split APK inspection.
|
||||
The original suite also covers:
|
||||
|
||||
The app `check` task also verifies that all committed UI control vectors are
|
||||
byte-for-byte reproducible from pinned `@tabler/icons` 3.45.0 sources.
|
||||
|
||||
- `ApkInspectorTest`: 3 per variant;
|
||||
- `ApkIntegrityTest`: 1 per variant;
|
||||
@@ -35,13 +39,9 @@ This is eight test methods run for both debug and release variants:
|
||||
The final APK and custom verification tasks passed:
|
||||
|
||||
- output: `app/build/outputs/apk/debug/FridaBox_4.0.0_arm64-v8a-debug.apk`;
|
||||
- size: 19,977,904 bytes;
|
||||
- SHA-256: `a87af38ff7f4a54d2cdc0207ac68319a1a05ab8067b7acdeb057c6c967ed2573`;
|
||||
- packaged ABIs: ARM64 only;
|
||||
- packaged native files: `libblackbox.so`, `libfrida-gadget.so`, and
|
||||
`libfrida-gadget.config.so` under `lib/arm64-v8a/`;
|
||||
- Frida Gadget 17.16.0 SHA-256:
|
||||
`6bf149e5d1c5ec701e7b822cab57bb243f1c2a03318fa974fe373ee711a9ed9e`;
|
||||
- packaged native files do not include a Frida Gadget binary or
|
||||
Gadget configuration;
|
||||
- early-load ordering and byte-identical demo-asset checks passed.
|
||||
- pinned Frida 17 registry/bootstrap/sample agents rebuilt successfully, and
|
||||
controller help plus empty-range discovery completed successfully.
|
||||
|
||||
+25
-7
@@ -5,12 +5,30 @@ Importing an APK does not install that package into Android's real PackageManage
|
||||
|
||||
## Import an application
|
||||
|
||||
1. Open **Workspace** and tap **Import APK**.
|
||||
2. Select one ARM64 base `.apk` through Android's document picker.
|
||||
3. FridaBox validates the APK, records its SHA-256, and creates a private guest.
|
||||
1. Open **Workspace** and tap the import icon.
|
||||
2. Choose **Import from apps** to clone a launchable app already installed on
|
||||
the device, or **Import from file** to select one base `.apk` through
|
||||
Android's document picker.
|
||||
3. FridaBox validates the full installed base/split set or selected file,
|
||||
records its SHA-256 fingerprint, and creates a private guest.
|
||||
|
||||
Split-only packages (`.apks`, `.xapk`, and `.apkm`) and unsupported native ABIs
|
||||
are rejected.
|
||||
Split-only files (`.apks`, `.xapk`, and `.apkm`) and unsupported native ABIs are
|
||||
rejected. Installed split applications are imported through their system package
|
||||
so their active base and split APKs remain available to the virtual runtime.
|
||||
|
||||
## Install a Gadget
|
||||
|
||||
1. Open **Gadgets**. FridaBox detects the device ABI and only offers matching
|
||||
release assets.
|
||||
2. Tap **Browse versions**. The minimal release browser loads Official builds
|
||||
from `frida/frida` ten at a time as the list is scrolled.
|
||||
3. Download any listed version. Downloads are decompressed in private storage,
|
||||
checked as ELF libraries, and rejected when their architecture does not match.
|
||||
4. Select any installed version as active. Downloading another version does not
|
||||
replace the active selection unless no Gadget had been selected yet.
|
||||
|
||||
Instrumented launches require an active Gadget. Clean mode remains available
|
||||
without one. Gadget binaries are never bundled in the FridaBox APK.
|
||||
|
||||
## Choose a launch mode
|
||||
|
||||
@@ -34,7 +52,7 @@ Switching modes always stops the previous guest process before the next launch.
|
||||
|
||||
## On-device agents
|
||||
|
||||
Select **On-device**, tap **Select JavaScript**, review the trust warning, and
|
||||
Tap an app icon, select **On-device**, tap **Select JavaScript**, review the trust warning, and
|
||||
choose a `.js` file up to 16 MiB. The original file is not modified. The private
|
||||
copy and the private Gadget executable are made read-only before execution.
|
||||
|
||||
@@ -46,7 +64,7 @@ interceptors, and in-app overlays continue to run in the guest process.
|
||||
|
||||
The **Runtime** tab reports the latest package, process, virtual user ID, virtual
|
||||
process slot, source APK, ClassLoader, selected mode, state, and latest error.
|
||||
Use **Manage** on an app card for app details, runtime details, virtual data
|
||||
Use **Manage** in an app's launch panel for app details, runtime details, virtual data
|
||||
clearing, or removal from the private workspace.
|
||||
|
||||
Only run JavaScript agents you trust. An on-device agent executes with the same
|
||||
|
||||
Generated
+12
@@ -9,9 +9,21 @@
|
||||
"frida-java-bridge": "7.0.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tabler/icons": "3.45.0",
|
||||
"frida-compile": "19.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@tabler/icons": {
|
||||
"version": "3.45.0",
|
||||
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.45.0.tgz",
|
||||
"integrity": "sha512-jiATwV8+zGYLTZ7gMLGivCic+KtsMZXcDmufIG8umlLxoHhI6902hGYIEt0Oa9Y9SXblNzUlrisHm5jOFMxOQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/codecalm"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
{
|
||||
"name": "fridabox-agents",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"generate:android-icons": "node tools/generate_tabler_android_icons.mjs",
|
||||
"verify:android-icons": "node tools/generate_tabler_android_icons.mjs --check"
|
||||
},
|
||||
"dependencies": {
|
||||
"frida-java-bridge": "7.0.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tabler/icons": "3.45.0",
|
||||
"frida-compile": "19.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fetch and verify the pinned official Frida Gadget Android ARM64 binary."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import lzma
|
||||
import pathlib
|
||||
import shutil
|
||||
import tempfile
|
||||
import urllib.request
|
||||
|
||||
VERSION = "17.16.0"
|
||||
ASSET = f"frida-gadget-{VERSION}-android-arm64.so.xz"
|
||||
API_URL = f"https://api.github.com/repos/frida/frida/releases/tags/{VERSION}"
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||
DEST = ROOT / "app/src/main/jniLibs/arm64-v8a/libfrida-gadget.so"
|
||||
CHECKSUM = ROOT / "tools/frida-gadget-17.16.0.sha256"
|
||||
EXPECTED_SHA256 = "6bf149e5d1c5ec701e7b822cab57bb243f1c2a03318fa974fe373ee711a9ed9e"
|
||||
|
||||
|
||||
def sha256(path: pathlib.Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def release_asset_url() -> str:
|
||||
request = urllib.request.Request(
|
||||
API_URL,
|
||||
headers={"Accept": "application/vnd.github+json", "User-Agent": "FridaBox-build"},
|
||||
)
|
||||
with urllib.request.urlopen(request, timeout=30) as response:
|
||||
release = json.load(response)
|
||||
if release.get("tag_name") != VERSION:
|
||||
raise RuntimeError(f"GitHub returned unexpected release {release.get('tag_name')!r}")
|
||||
for asset in release.get("assets", []):
|
||||
if asset.get("name") == ASSET:
|
||||
return str(asset["browser_download_url"])
|
||||
raise RuntimeError(f"Release {VERSION} does not contain {ASSET}")
|
||||
|
||||
|
||||
def fetch(force: bool = False) -> str:
|
||||
if DEST.exists() and not force:
|
||||
digest = sha256(DEST)
|
||||
if digest != EXPECTED_SHA256:
|
||||
raise RuntimeError(f"Existing Gadget SHA-256 mismatch: {digest}")
|
||||
CHECKSUM.write_text(f"{digest} {DEST.name}\n", encoding="ascii")
|
||||
return digest
|
||||
DEST.parent.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.TemporaryDirectory(prefix="fridabox-") as temp_dir:
|
||||
archive = pathlib.Path(temp_dir) / ASSET
|
||||
request = urllib.request.Request(release_asset_url(), headers={"User-Agent": "FridaBox-build"})
|
||||
with urllib.request.urlopen(request, timeout=120) as response, archive.open("wb") as output:
|
||||
shutil.copyfileobj(response, output)
|
||||
temporary_output = pathlib.Path(temp_dir) / DEST.name
|
||||
with lzma.open(archive, "rb") as source, temporary_output.open("wb") as output:
|
||||
shutil.copyfileobj(source, output)
|
||||
if temporary_output.stat().st_size < 1_000_000:
|
||||
raise RuntimeError("Downloaded Gadget is unexpectedly small")
|
||||
shutil.move(str(temporary_output), DEST)
|
||||
digest = sha256(DEST)
|
||||
if digest != EXPECTED_SHA256:
|
||||
DEST.unlink(missing_ok=True)
|
||||
raise RuntimeError(f"Downloaded Gadget SHA-256 mismatch: {digest}")
|
||||
CHECKSUM.write_text(f"{digest} {DEST.name}\n", encoding="ascii")
|
||||
return digest
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--force", action="store_true", help="replace an existing Gadget")
|
||||
args = parser.parse_args()
|
||||
digest = fetch(args.force)
|
||||
print(f"Frida Gadget {VERSION}: {DEST}")
|
||||
print(f"SHA-256: {digest}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1 +0,0 @@
|
||||
6bf149e5d1c5ec701e7b822cab57bb243f1c2a03318fa974fe373ee711a9ed9e libfrida-gadget.so
|
||||
@@ -0,0 +1,69 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const packageRoot = path.join(projectRoot, 'node_modules', '@tabler', 'icons');
|
||||
const drawableRoot = path.join(projectRoot, 'app', 'src', 'main', 'res', 'drawable');
|
||||
const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
||||
const checkOnly = process.argv.includes('--check');
|
||||
|
||||
const icons = new Map([
|
||||
['layout-grid', 'ic_fb_workspace.xml'],
|
||||
['package', 'ic_fb_gadget.xml'],
|
||||
['activity', 'ic_fb_runtime.xml'],
|
||||
['settings', 'ic_fb_settings.xml'],
|
||||
['file-import', 'ic_fb_import.xml'],
|
||||
['apps', 'ic_fb_apps.xml'],
|
||||
['file', 'ic_fb_file.xml'],
|
||||
['x', 'ic_fb_close.xml'],
|
||||
['info-circle', 'ic_fb_details.xml'],
|
||||
['database-off', 'ic_fb_clear.xml'],
|
||||
['trash', 'ic_fb_remove.xml'],
|
||||
]);
|
||||
|
||||
let mismatches = 0;
|
||||
for (const [iconName, outputName] of icons) {
|
||||
const sourcePath = path.join(packageRoot, 'icons', 'outline', `${iconName}.svg`);
|
||||
const svg = fs.readFileSync(sourcePath, 'utf8');
|
||||
const paths = [...svg.matchAll(/<path\s+d="([^"]+)"\s*\/>/g)].map((match) => match[1]);
|
||||
if (paths.length === 0) throw new Error(`No SVG paths found for ${iconName}`);
|
||||
|
||||
const pathXml = paths.map((pathData) => [
|
||||
' <path',
|
||||
' android:fillColor="@android:color/transparent"',
|
||||
' android:strokeColor="@android:color/white"',
|
||||
' android:strokeWidth="2"',
|
||||
' android:strokeLineCap="round"',
|
||||
' android:strokeLineJoin="round"',
|
||||
` android:pathData="${pathData}" />`,
|
||||
].join('\n')).join('\n');
|
||||
|
||||
const output = [
|
||||
'<?xml version="1.0" encoding="utf-8"?>',
|
||||
`<!-- Generated from @tabler/icons ${packageJson.version}: icons/outline/${iconName}.svg -->`,
|
||||
'<vector xmlns:android="http://schemas.android.com/apk/res/android"',
|
||||
' android:width="24dp"',
|
||||
' android:height="24dp"',
|
||||
' android:viewportWidth="24"',
|
||||
' android:viewportHeight="24">',
|
||||
pathXml,
|
||||
'</vector>',
|
||||
'',
|
||||
].join('\n');
|
||||
const outputPath = path.join(drawableRoot, outputName);
|
||||
|
||||
if (checkOnly) {
|
||||
const current = fs.existsSync(outputPath) ? fs.readFileSync(outputPath, 'utf8') : '';
|
||||
if (current.replaceAll('\r\n', '\n') !== output) {
|
||||
console.error(`${outputName} is not synchronized with @tabler/icons`);
|
||||
mismatches += 1;
|
||||
}
|
||||
} else {
|
||||
fs.writeFileSync(outputPath, output, 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
if (mismatches > 0) process.exitCode = 1;
|
||||
else console.log(`${checkOnly ? 'Verified' : 'Generated'} ${icons.size} Android vectors from @tabler/icons ${packageJson.version}`);
|
||||
Reference in New Issue
Block a user