apkdev 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apkdev/__init__.py +13 -0
- apkdev/__main__.py +5 -0
- apkdev/apkfile.py +168 -0
- apkdev/builder.py +671 -0
- apkdev/cli.py +925 -0
- apkdev/completion.py +14 -0
- apkdev/device.py +161 -0
- apkdev/inspector.py +291 -0
- apkdev/optimizer.py +58 -0
- apkdev/reverser.py +62 -0
- apkdev/sdk.py +526 -0
- apkdev/utils.py +74 -0
- apkdev-2.0.0.dist-info/METADATA +159 -0
- apkdev-2.0.0.dist-info/RECORD +17 -0
- apkdev-2.0.0.dist-info/WHEEL +5 -0
- apkdev-2.0.0.dist-info/entry_points.txt +2 -0
- apkdev-2.0.0.dist-info/top_level.txt +1 -0
apkdev/completion.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Shell completion setup for apkdev."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
@click.command("completion")
|
|
7
|
+
@click.argument("shell", type=click.Choice(["bash", "zsh", "fish"]), default="bash")
|
|
8
|
+
def completion(shell):
|
|
9
|
+
"""Generate shell completion script."""
|
|
10
|
+
from .cli import cli
|
|
11
|
+
script = cli.shell_complete(click.Context(cli), shell)
|
|
12
|
+
print(f"# Add to ~/.bashrc or ~/.{shell}rc:")
|
|
13
|
+
print(script)
|
|
14
|
+
|
apkdev/device.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Device commands — install, uninstall, pull APKs, list devices via ADB."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
|
|
11
|
+
from .utils import check_tool
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _adb_devices() -> list[dict]:
|
|
17
|
+
"""Return list of connected devices."""
|
|
18
|
+
adb = check_tool("adb")
|
|
19
|
+
r = subprocess.run([adb, "devices", "-l"], capture_output=True, text=True, timeout=10)
|
|
20
|
+
lines = r.stdout.strip().split("\n")
|
|
21
|
+
devices = []
|
|
22
|
+
for line in lines[1:]: # skip "List of devices attached"
|
|
23
|
+
parts = line.strip().split()
|
|
24
|
+
if not parts or len(parts) < 2:
|
|
25
|
+
continue
|
|
26
|
+
device = {"id": parts[0], "status": parts[1]}
|
|
27
|
+
# Parse -l fields
|
|
28
|
+
extra = " ".join(parts[2:]) if len(parts) > 2 else ""
|
|
29
|
+
if "product:" in extra:
|
|
30
|
+
for token in extra.split():
|
|
31
|
+
if ":" in token:
|
|
32
|
+
k, v = token.split(":", 1)
|
|
33
|
+
device[k] = v
|
|
34
|
+
devices.append(device)
|
|
35
|
+
return devices
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def devices() -> None:
|
|
39
|
+
"""List connected Android devices."""
|
|
40
|
+
devs = _adb_devices()
|
|
41
|
+
if not devs:
|
|
42
|
+
console.print("[yellow]No devices connected[/]")
|
|
43
|
+
console.print(" Enable USB debugging and connect your phone.")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
t = Table(title=f"Devices ({len(devs)})", border_style="cyan")
|
|
47
|
+
t.add_column("Device ID")
|
|
48
|
+
t.add_column("Status")
|
|
49
|
+
t.add_column("Product")
|
|
50
|
+
t.add_column("Model")
|
|
51
|
+
|
|
52
|
+
for d in devs:
|
|
53
|
+
status_color = "green" if d["status"] == "device" else "red"
|
|
54
|
+
t.add_row(
|
|
55
|
+
d["id"],
|
|
56
|
+
f"[{status_color}]{d['status']}[/]",
|
|
57
|
+
d.get("product", ""),
|
|
58
|
+
d.get("model", ""),
|
|
59
|
+
)
|
|
60
|
+
console.print(t)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def install_apk(apk: str, serial: Optional[str] = None) -> None:
|
|
64
|
+
"""Install APK to device via ADB."""
|
|
65
|
+
apk_path = Path(apk)
|
|
66
|
+
if not apk_path.exists():
|
|
67
|
+
console.print(f"[red]❌ File not found: {apk}[/]")
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
adb = check_tool("adb")
|
|
71
|
+
cmd = [adb]
|
|
72
|
+
if serial:
|
|
73
|
+
cmd += ["-s", serial]
|
|
74
|
+
cmd += ["install", "-r", "-t", apk]
|
|
75
|
+
|
|
76
|
+
console.print(f"[yellow]📲 Installing {apk_path.name}...[/]")
|
|
77
|
+
r = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
78
|
+
|
|
79
|
+
if r.returncode == 0:
|
|
80
|
+
console.print("[green]✅ Installed successfully![/]")
|
|
81
|
+
else:
|
|
82
|
+
console.print(f"[red]❌ Install failed[/]")
|
|
83
|
+
console.print(f"[dim]{r.stderr}[/]")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def uninstall(package: str, serial: Optional[str] = None) -> None:
|
|
87
|
+
"""Uninstall a package from device."""
|
|
88
|
+
adb = check_tool("adb")
|
|
89
|
+
cmd = [adb]
|
|
90
|
+
if serial:
|
|
91
|
+
cmd += ["-s", serial]
|
|
92
|
+
cmd += ["uninstall", package]
|
|
93
|
+
|
|
94
|
+
console.print(f"[yellow]🗑️ Uninstalling {package}...[/]")
|
|
95
|
+
r = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
96
|
+
|
|
97
|
+
if r.returncode == 0:
|
|
98
|
+
console.print(f"[green]✅ Uninstalled {package}[/]")
|
|
99
|
+
else:
|
|
100
|
+
console.print(f"[red]❌ Failed: {r.stderr.strip()}[/]")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def pull_apk(package: str, output: Optional[str] = None, serial: Optional[str] = None) -> None:
|
|
104
|
+
"""Pull APK from a device by package name."""
|
|
105
|
+
adb = check_tool("adb")
|
|
106
|
+
|
|
107
|
+
# Get APK path on device
|
|
108
|
+
prefix = [adb]
|
|
109
|
+
if serial:
|
|
110
|
+
prefix += ["-s", serial]
|
|
111
|
+
|
|
112
|
+
r = subprocess.run(prefix + ["shell", "pm", "path", package],
|
|
113
|
+
capture_output=True, text=True, timeout=10)
|
|
114
|
+
if r.returncode != 0 or "package:" not in r.stdout:
|
|
115
|
+
console.print(f"[red]❌ Package '{package}' not found on device[/]")
|
|
116
|
+
console.print(f"[dim]{r.stderr}[/]")
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
remote_path = r.stdout.strip().split("package:")[-1].strip()
|
|
120
|
+
|
|
121
|
+
dest = output or f"{package}.apk"
|
|
122
|
+
|
|
123
|
+
console.print(f"[yellow]📥 Pulling {remote_path} → {dest}...[/]")
|
|
124
|
+
r2 = subprocess.run(prefix + ["pull", remote_path, dest],
|
|
125
|
+
capture_output=True, text=True, timeout=30)
|
|
126
|
+
|
|
127
|
+
if r2.returncode == 0:
|
|
128
|
+
size = Path(dest).stat().st_size if Path(dest).exists() else 0
|
|
129
|
+
from rich.text import Text
|
|
130
|
+
console.print(f"[green]✅ Pulled → {dest}[/] [dim]({size/1024/1024:.1f} MB)[/]")
|
|
131
|
+
else:
|
|
132
|
+
console.print(f"[red]❌ Failed: {r2.stderr.strip()}[/]")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def list_packages(serial: Optional[str] = None, filter_str: Optional[str] = None) -> None:
|
|
136
|
+
"""List installed packages on device."""
|
|
137
|
+
adb = check_tool("adb")
|
|
138
|
+
cmd = [adb]
|
|
139
|
+
if serial:
|
|
140
|
+
cmd += ["-s", serial]
|
|
141
|
+
cmd += ["shell", "pm", "list", "packages"]
|
|
142
|
+
if filter_str:
|
|
143
|
+
cmd += ["|", "grep", filter_str]
|
|
144
|
+
|
|
145
|
+
import shlex
|
|
146
|
+
shell_cmd = " ".join(shlex.quote(c) for c in cmd)
|
|
147
|
+
|
|
148
|
+
r = subprocess.run(shell_cmd, capture_output=True, text=True, timeout=15, shell=True)
|
|
149
|
+
if r.returncode != 0:
|
|
150
|
+
console.print(f"[red]❌ Failed to list packages[/]")
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
pkgs = [l.replace("package:", "").strip() for l in r.stdout.strip().split("\n") if l.strip()]
|
|
154
|
+
pkgs.sort()
|
|
155
|
+
|
|
156
|
+
t = Table(title=f"Packages ({len(pkgs)})", border_style="cyan")
|
|
157
|
+
t.add_column("#", style="dim")
|
|
158
|
+
t.add_column("Package")
|
|
159
|
+
for i, p in enumerate(pkgs, 1):
|
|
160
|
+
t.add_row(str(i), p)
|
|
161
|
+
console.print(t)
|
apkdev/inspector.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""APK Inspector — extract metadata, manifest, permissions, strings from APKs."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.syntax import Syntax
|
|
10
|
+
|
|
11
|
+
from .utils import check_tool
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _run_aapt(cmd: list[str]) -> str:
|
|
17
|
+
"""Run aapt2 dump subcommand and return output."""
|
|
18
|
+
aapt = check_tool("aapt2")
|
|
19
|
+
try:
|
|
20
|
+
r = subprocess.run([aapt, "dump"] + cmd, capture_output=True, text=True, timeout=15)
|
|
21
|
+
return r.stdout
|
|
22
|
+
except subprocess.TimeoutExpired:
|
|
23
|
+
return "⚠️ Timed out"
|
|
24
|
+
except FileNotFoundError:
|
|
25
|
+
return "❌ aapt2 not found"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def inspect(apk: str) -> None:
|
|
29
|
+
"""Full APK inspection — package, version, permissions, activities, services, etc."""
|
|
30
|
+
apk_path = Path(apk)
|
|
31
|
+
if not apk_path.exists():
|
|
32
|
+
console.print(f"[red]❌ File not found: {apk}[/]")
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
raw = _run_aapt(["badging", apk])
|
|
36
|
+
|
|
37
|
+
if not raw or "ERROR" in raw.upper():
|
|
38
|
+
console.print(f"[red]❌ Could not parse APK: {apk}[/]")
|
|
39
|
+
console.print(f"[dim]{raw}[/]")
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
# Parse the badging output
|
|
43
|
+
lines = raw.split("\n")
|
|
44
|
+
|
|
45
|
+
# Extract fields
|
|
46
|
+
package = ""
|
|
47
|
+
version_name = ""
|
|
48
|
+
version_code = ""
|
|
49
|
+
min_sdk = ""
|
|
50
|
+
target_sdk = ""
|
|
51
|
+
app_label = ""
|
|
52
|
+
icon = ""
|
|
53
|
+
compile_sdk = ""
|
|
54
|
+
platform_build = ""
|
|
55
|
+
|
|
56
|
+
for line in lines:
|
|
57
|
+
# Skip comments and empty
|
|
58
|
+
if not line.strip():
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
if line.startswith("package:"):
|
|
62
|
+
# package: name='com.example.app' versionCode='1' versionName='1.0' compileSdkVersion='35'
|
|
63
|
+
import re
|
|
64
|
+
m = re.search(r"name='([^']+)'", line)
|
|
65
|
+
if m: package = m.group(1)
|
|
66
|
+
m = re.search(r"versionCode='([^']+)'", line)
|
|
67
|
+
if m: version_code = m.group(1)
|
|
68
|
+
m = re.search(r"versionName='([^']+)'", line)
|
|
69
|
+
if m: version_name = m.group(1)
|
|
70
|
+
m = re.search(r"compileSdkVersion='([^']+)'", line)
|
|
71
|
+
if m: compile_sdk = m.group(1)
|
|
72
|
+
m = re.search(r"platformBuildVersionName='([^']+)'", line)
|
|
73
|
+
if m: platform_build = m.group(1)
|
|
74
|
+
|
|
75
|
+
elif line.startswith("sdkVersion:"):
|
|
76
|
+
min_sdk = line.split(":")[1].strip().strip("'")
|
|
77
|
+
elif line.startswith("targetSdkVersion:"):
|
|
78
|
+
target_sdk = line.split(":")[1].strip().strip("'")
|
|
79
|
+
elif line.startswith("application-label:"):
|
|
80
|
+
app_label = line.split(":")[1].strip().strip("'")
|
|
81
|
+
elif line.startswith("application-icon-"):
|
|
82
|
+
icon = line.split(":")[1].strip().strip("'")
|
|
83
|
+
|
|
84
|
+
# ─── Print Info Panel ───
|
|
85
|
+
from rich.text import Text
|
|
86
|
+
from rich.layout import Layout
|
|
87
|
+
|
|
88
|
+
info = Text()
|
|
89
|
+
info.append(f"\n 📦 {package}", style="bold cyan")
|
|
90
|
+
if app_label:
|
|
91
|
+
info.append(f"\n 🏷️ {app_label}")
|
|
92
|
+
info.append(f"\n")
|
|
93
|
+
info.append(f"\n Version: {version_name} (code {version_code})")
|
|
94
|
+
info.append(f"\n SDK: min {min_sdk} / target {target_sdk}")
|
|
95
|
+
if compile_sdk:
|
|
96
|
+
info.append(f"\n Compile: SDK {compile_sdk}")
|
|
97
|
+
if platform_build:
|
|
98
|
+
info.append(f"\n Build: {platform_build}")
|
|
99
|
+
|
|
100
|
+
console.print(Panel(info, border_style="cyan", title="APK Info"))
|
|
101
|
+
|
|
102
|
+
# ─── Permissions Table ───
|
|
103
|
+
perms = [l.split(":")[1].strip().strip("'") for l in lines if l.startswith("uses-permission:")]
|
|
104
|
+
if perms:
|
|
105
|
+
t = Table(title=f"Permissions ({len(perms)})", border_style="yellow")
|
|
106
|
+
t.add_column("#", style="dim")
|
|
107
|
+
t.add_column("Permission")
|
|
108
|
+
for i, p in enumerate(perms, 1):
|
|
109
|
+
name = p.split(".")[-1] if "." in p else p
|
|
110
|
+
t.add_row(str(i), f"[yellow]{name}[/]\n[dim]{p}[/]")
|
|
111
|
+
console.print(t)
|
|
112
|
+
else:
|
|
113
|
+
console.print("[dim]No permissions[/]")
|
|
114
|
+
|
|
115
|
+
# ─── Activities Table ───
|
|
116
|
+
activities = []
|
|
117
|
+
for line in lines:
|
|
118
|
+
if line.startswith("activity:"):
|
|
119
|
+
import re
|
|
120
|
+
m = re.search(r"name='([^']+)'", line)
|
|
121
|
+
if m:
|
|
122
|
+
activities.append(m.group(1))
|
|
123
|
+
|
|
124
|
+
if activities:
|
|
125
|
+
t = Table(title=f"Activities ({len(activities)})", border_style="green")
|
|
126
|
+
t.add_column("#", style="dim")
|
|
127
|
+
t.add_column("Activity")
|
|
128
|
+
t.add_column("Launcher", style="cyan")
|
|
129
|
+
for i, act in enumerate(activities, 1):
|
|
130
|
+
is_launcher = "launchable-activity" in raw and act in raw
|
|
131
|
+
t.add_row(str(i), act, "🏠" if is_launcher else "")
|
|
132
|
+
console.print(t)
|
|
133
|
+
|
|
134
|
+
# ─── Services / Receivers / Providers ───
|
|
135
|
+
for kind, label, color in [
|
|
136
|
+
("service:", "Services", "magenta"),
|
|
137
|
+
("receiver:", "Receivers", "blue"),
|
|
138
|
+
("provider:", "Providers", "purple"),
|
|
139
|
+
]:
|
|
140
|
+
items = []
|
|
141
|
+
for line in lines:
|
|
142
|
+
if line.startswith(kind):
|
|
143
|
+
import re
|
|
144
|
+
m = re.search(r"name='([^']+)'", line)
|
|
145
|
+
if m:
|
|
146
|
+
items.append(m.group(1))
|
|
147
|
+
if items:
|
|
148
|
+
t = Table(title=f"{label} ({len(items)})", border_style=color)
|
|
149
|
+
t.add_column("#", style="dim")
|
|
150
|
+
t.add_column("Name")
|
|
151
|
+
for i, item in enumerate(items, 1):
|
|
152
|
+
t.add_row(str(i), item)
|
|
153
|
+
console.print(t)
|
|
154
|
+
|
|
155
|
+
# ─── Feature table ───
|
|
156
|
+
features = [l.split(":")[1].strip().strip("'") for l in lines if l.startswith("uses-feature:")]
|
|
157
|
+
if features:
|
|
158
|
+
t = Table(title=f"Features ({len(features)})", border_style="cyan")
|
|
159
|
+
t.add_column("#", style="dim")
|
|
160
|
+
t.add_column("Feature")
|
|
161
|
+
for i, f in enumerate(features, 1):
|
|
162
|
+
t.add_row(str(i), f)
|
|
163
|
+
console.print(t)
|
|
164
|
+
|
|
165
|
+
# ─── Supported screens ───
|
|
166
|
+
screens = [l.split(":")[1].strip().strip("'") for l in lines if l.startswith("supports-screens:")]
|
|
167
|
+
if screens:
|
|
168
|
+
console.print(f"\n[bold]Screens:[/] {', '.join(screens)}")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def manifest(apk: str) -> None:
|
|
172
|
+
"""Extract and display AndroidManifest.xml."""
|
|
173
|
+
raw = _run_aapt(["xmltree", apk, "--file", "AndroidManifest.xml"])
|
|
174
|
+
if raw:
|
|
175
|
+
syntax = Syntax(raw, "xml", theme="monokai", line_numbers=True)
|
|
176
|
+
console.print(Panel(syntax, border_style="green", title="AndroidManifest.xml"))
|
|
177
|
+
else:
|
|
178
|
+
console.print("[red]❌ Could not extract manifest[/]")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def permissions(apk: str) -> None:
|
|
182
|
+
"""List all permissions from APK."""
|
|
183
|
+
raw = _run_aapt(["permissions", apk])
|
|
184
|
+
if raw:
|
|
185
|
+
lines = [l for l in raw.split("\n") if l.strip()]
|
|
186
|
+
t = Table(title=f"Permissions ({len(lines)})", border_style="yellow")
|
|
187
|
+
t.add_column("#", style="dim")
|
|
188
|
+
t.add_column("Permission")
|
|
189
|
+
for i, p in enumerate(lines, 1):
|
|
190
|
+
t.add_row(str(i), p)
|
|
191
|
+
console.print(t)
|
|
192
|
+
else:
|
|
193
|
+
console.print("[dim]No permissions found[/]")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def strings(apk: str) -> None:
|
|
197
|
+
"""Extract resource string pool from APK."""
|
|
198
|
+
raw = _run_aapt(["strings", apk])
|
|
199
|
+
if raw:
|
|
200
|
+
lines = [l for l in raw.split("\n") if l.strip()]
|
|
201
|
+
# Show first 200 strings
|
|
202
|
+
MAX = 200
|
|
203
|
+
preview = lines[:MAX]
|
|
204
|
+
t = Table(title=f"Resource Strings ({len(lines)} total, showing {len(preview)})",
|
|
205
|
+
border_style="blue")
|
|
206
|
+
t.add_column("#", style="dim")
|
|
207
|
+
t.add_column("String")
|
|
208
|
+
for i, s in enumerate(preview, 1):
|
|
209
|
+
# Truncate long strings
|
|
210
|
+
display = s[:120] + "..." if len(s) > 120 else s
|
|
211
|
+
t.add_row(str(i), display)
|
|
212
|
+
console.print(t)
|
|
213
|
+
else:
|
|
214
|
+
console.print("[dim]No strings found[/]")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def resources(apk: str) -> None:
|
|
218
|
+
"""List resource table from APK."""
|
|
219
|
+
raw = _run_aapt(["resources", apk])
|
|
220
|
+
if raw:
|
|
221
|
+
lines = [l for l in raw.split("\n") if l.strip()]
|
|
222
|
+
MAX = 300
|
|
223
|
+
preview = lines[:MAX]
|
|
224
|
+
t = Table(title=f"Resources ({len(lines)} total, showing {len(preview)})",
|
|
225
|
+
border_style="magenta")
|
|
226
|
+
t.add_column("#", style="dim")
|
|
227
|
+
t.add_column("Resource")
|
|
228
|
+
for i, r in enumerate(preview, 1):
|
|
229
|
+
t.add_row(str(i), r[:150])
|
|
230
|
+
console.print(t)
|
|
231
|
+
else:
|
|
232
|
+
console.print("[dim]No resources found[/]")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def analyze(apk: str) -> None:
|
|
236
|
+
"""Deep analysis of APK. Uses androguard if available, falls back to aapt2."""
|
|
237
|
+
apk_path = Path(apk)
|
|
238
|
+
if not apk_path.exists():
|
|
239
|
+
console.print(f"[red]\u274c File not found: {apk}[/]")
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
# Try androguard first
|
|
243
|
+
try:
|
|
244
|
+
import androguard
|
|
245
|
+
from androguard.misc import AnalyzeAPK
|
|
246
|
+
|
|
247
|
+
console.print(f"[yellow]\U0001f50d Analyzing {apk} with androguard...[/]")
|
|
248
|
+
try:
|
|
249
|
+
a, _, _ = AnalyzeAPK(apk)
|
|
250
|
+
except Exception as e:
|
|
251
|
+
console.print(f"[red]\u274c androguard error: {e}[/]")
|
|
252
|
+
console.print("[yellow]Falling back to aapt2...[/]")
|
|
253
|
+
inspect(apk)
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
console.print(f"\n[bold cyan]\U0001f4e6 {a.get_package()}[/]")
|
|
257
|
+
console.print(f" Version: {a.get_androidversion_name()} (code {a.get_androidversion_code()})")
|
|
258
|
+
|
|
259
|
+
perms = a.get_permissions()
|
|
260
|
+
if perms:
|
|
261
|
+
t = Table(title=f"Permissions ({len(perms)})", border_style="yellow")
|
|
262
|
+
t.add_column("Permission")
|
|
263
|
+
for p in perms:
|
|
264
|
+
t.add_row(p)
|
|
265
|
+
console.print(t)
|
|
266
|
+
|
|
267
|
+
for name, items, icon in [
|
|
268
|
+
("Activities", a.get_activities(), "\U0001f3e0"),
|
|
269
|
+
("Services", a.get_services(), "\u2699"),
|
|
270
|
+
("Receivers", a.get_receivers(), "\U0001f4e1"),
|
|
271
|
+
("Providers", a.get_providers(), "\U0001f4cb"),
|
|
272
|
+
]:
|
|
273
|
+
if items:
|
|
274
|
+
console.print(f"\n[bold]{icon} {name} ({len(items)}):[/]")
|
|
275
|
+
for item in items:
|
|
276
|
+
console.print(f" \u2022 {item}")
|
|
277
|
+
|
|
278
|
+
main = a.get_main_activity()
|
|
279
|
+
if main:
|
|
280
|
+
console.print(f" \U0001f3e0 [cyan]Main:[/] {main}")
|
|
281
|
+
|
|
282
|
+
console.print(f"\n[bold]SDK:[/] min={a.get_min_sdk_version()} target={a.get_target_sdk_version()} max={a.get_max_sdk_version()}")
|
|
283
|
+
sigs = a.get_signatures()
|
|
284
|
+
if sigs:
|
|
285
|
+
console.print(f"\n[bold]Signatures:[/] {len(sigs)}")
|
|
286
|
+
for sig in sigs[:3]:
|
|
287
|
+
console.print(f" \u2022 {sig[:64]}...")
|
|
288
|
+
|
|
289
|
+
except ImportError:
|
|
290
|
+
console.print("[yellow]\U0001f50d Using aapt2-based analysis (install androguard for deeper: pip3 install androguard)[/]")
|
|
291
|
+
inspect(apk)
|
apkdev/optimizer.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Optimize — zipalign + sign APK in one step."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from .utils import check_tool
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def optimize(apk_path: str) -> None:
|
|
14
|
+
"""Zipalign + sign an APK."""
|
|
15
|
+
apk = Path(apk_path)
|
|
16
|
+
if not apk.exists():
|
|
17
|
+
console.print(f"[red]❌ File not found: {apk}[/]")
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
aligned = apk.parent / f"{apk.stem}-aligned.apk"
|
|
21
|
+
zipalign = check_tool("zipalign")
|
|
22
|
+
apksigner = check_tool("apksigner")
|
|
23
|
+
keystore = Path.home() / ".android" / "debug.keystore"
|
|
24
|
+
|
|
25
|
+
# Step 1: Zipalign
|
|
26
|
+
console.print(f"[yellow]🔧 Zipaligning → {aligned.name}...[/]")
|
|
27
|
+
r1 = subprocess.run([zipalign, "-f", "-p", "4", str(apk), str(aligned)],
|
|
28
|
+
capture_output=True, text=True, timeout=30)
|
|
29
|
+
if r1.returncode != 0:
|
|
30
|
+
console.print(f"[red]❌ Zipalign failed: {r1.stderr}[/]")
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
# Step 2: Sign
|
|
34
|
+
if not keystore.exists():
|
|
35
|
+
console.print("[yellow]🔑 Creating debug keystore...[/]")
|
|
36
|
+
keytool = check_tool("keytool")
|
|
37
|
+
subprocess.run([
|
|
38
|
+
keytool, "-genkey", "-v", "-keystore", str(keystore),
|
|
39
|
+
"-storepass", "android", "-alias", "androiddebugkey",
|
|
40
|
+
"-keypass", "android", "-keyalg", "RSA", "-keysize", "2048",
|
|
41
|
+
"-validity", "10000",
|
|
42
|
+
"-dname", "CN=Android Debug,O=Android,C=US",
|
|
43
|
+
], capture_output=True, timeout=15)
|
|
44
|
+
|
|
45
|
+
console.print(f"[yellow]🔑 Signing {aligned.name}...[/]")
|
|
46
|
+
r2 = subprocess.run([
|
|
47
|
+
apksigner, "sign", "--ks", str(keystore),
|
|
48
|
+
"--ks-key-alias", "androiddebugkey",
|
|
49
|
+
"--ks-pass", "pass:android",
|
|
50
|
+
"--key-pass", "pass:android",
|
|
51
|
+
str(aligned),
|
|
52
|
+
], capture_output=True, text=True, timeout=30)
|
|
53
|
+
|
|
54
|
+
if r2.returncode == 0:
|
|
55
|
+
size_mb = aligned.stat().st_size / 1024 / 1024
|
|
56
|
+
console.print(f"[green]✅ Optimized → {aligned}[/] [dim]({size_mb:.1f} MB)[/]")
|
|
57
|
+
else:
|
|
58
|
+
console.print(f"[red]❌ Signing failed: {r2.stderr}[/]")
|
apkdev/reverser.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Reverse Engineering tools — apktool, jadx, smali, dex2jar, APKEditor."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .utils import check_tool, run, run_java
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# ─── Known JARs ──────────────────────────────────────────────
|
|
9
|
+
SMALI_JAR = Path.home() / ".smali.jar"
|
|
10
|
+
BAKSMALI_JAR = Path.home() / ".baksmali.jar"
|
|
11
|
+
APKEDITOR_JAR = Path.home() / ".APKEditor.jar"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def apktool_decode(apk: str, output: str | None = None) -> None:
|
|
15
|
+
"""Decode APK with apktool."""
|
|
16
|
+
apktool = check_tool("apktool")
|
|
17
|
+
out = output or apk.replace(".apk", "")
|
|
18
|
+
run([apktool, "d", apk, "-o", out, "--force"])
|
|
19
|
+
print(f"✅ Decoded → {out}/")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def apktool_rebuild(directory: str, output: str | None = None) -> None:
|
|
23
|
+
"""Rebuild APK from decoded directory."""
|
|
24
|
+
apktool = check_tool("apktool")
|
|
25
|
+
out = output or f"{directory.rstrip('/')}.apk"
|
|
26
|
+
run([apktool, "b", directory, "-o", out])
|
|
27
|
+
print(f"✅ Rebuilt → {out}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def jadx_decompile(target: str, output: str | None = None) -> None:
|
|
31
|
+
"""Decompile DEX/APK to Java source with jadx."""
|
|
32
|
+
jadx = check_tool("jadx")
|
|
33
|
+
out = output or f"{Path(target).stem}-source"
|
|
34
|
+
run([jadx, "-d", out, target])
|
|
35
|
+
print(f"✅ Java source → {out}/")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def smali_assemble(smali_dir: str, output: str | None = None) -> None:
|
|
39
|
+
"""Assemble smali → dex."""
|
|
40
|
+
out = output or "out.dex"
|
|
41
|
+
run_java(str(SMALI_JAR), ["a", smali_dir, "-o", out])
|
|
42
|
+
print(f"✅ Dex → {out}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def baksmali_disassemble(dex_file: str, output: str | None = None) -> None:
|
|
46
|
+
"""Disassemble dex → smali."""
|
|
47
|
+
out = output or "smali_out"
|
|
48
|
+
run_java(str(BAKSMALI_JAR), ["d", dex_file, "-o", out])
|
|
49
|
+
print(f"✅ Smali → {out}/")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def dex2jar(dex_file: str, output: str | None = None) -> None:
|
|
53
|
+
"""Convert DEX/APK to JAR."""
|
|
54
|
+
d2j = check_tool("d2j-dex2jar")
|
|
55
|
+
out = output or f"{Path(dex_file).stem}.jar"
|
|
56
|
+
run([d2j, dex_file, "-o", out])
|
|
57
|
+
print(f"✅ JAR → {out}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def apkeditor(*args: str) -> None:
|
|
61
|
+
"""Run APKEditor with arbitrary args."""
|
|
62
|
+
run_java(str(APKEDITOR_JAR), list(args))
|