apk-patchx 7.9.2025.1__tar.gz
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.
- apk_patchx-7.9.2025.1/.gitignore +3 -0
- apk_patchx-7.9.2025.1/LICENSE +21 -0
- apk_patchx-7.9.2025.1/PKG-INFO +110 -0
- apk_patchx-7.9.2025.1/README.md +76 -0
- apk_patchx-7.9.2025.1/apk_patchx/__init__.py +14 -0
- apk_patchx-7.9.2025.1/apk_patchx/cli.py +273 -0
- apk_patchx-7.9.2025.1/apk_patchx/exceptions.py +41 -0
- apk_patchx-7.9.2025.1/apk_patchx/services/__init__.py +1 -0
- apk_patchx-7.9.2025.1/apk_patchx/services/adb.py +98 -0
- apk_patchx-7.9.2025.1/apk_patchx/services/android_sdk.py +145 -0
- apk_patchx-7.9.2025.1/apk_patchx/services/apktool.py +150 -0
- apk_patchx-7.9.2025.1/apk_patchx/services/frida.py +311 -0
- apk_patchx-7.9.2025.1/apk_patchx/services/patch_dex.py +119 -0
- apk_patchx-7.9.2025.1/apk_patchx/services/patch_smali.py +145 -0
- apk_patchx-7.9.2025.1/apk_patchx/services/signing.py +106 -0
- apk_patchx-7.9.2025.1/apk_patchx/services/split_merge.py +146 -0
- apk_patchx-7.9.2025.1/apk_patchx/utils/__init__.py +1 -0
- apk_patchx-7.9.2025.1/apk_patchx/utils/core.py +77 -0
- apk_patchx-7.9.2025.1/apk_patchx/utils/manifest.py +140 -0
- apk_patchx-7.9.2025.1/apk_patchx/utils/py.typed +0 -0
- apk_patchx-7.9.2025.1/apk_patchx/utils/versions.py +65 -0
- apk_patchx-7.9.2025.1/pyproject.toml +50 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 APKPatcher Contributors
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1,110 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: apk-patchx
|
3
|
+
Version: 7.9.2025.1
|
4
|
+
Summary: Android APK manipulation toolkit with Frida gadget injection support
|
5
|
+
Keywords: android,apk,reverse-engineering,frida,patching
|
6
|
+
Author: APKPatcher Contributors
|
7
|
+
Requires-Python: >=3.8
|
8
|
+
Description-Content-Type: text/markdown
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
10
|
+
Classifier: Environment :: Console
|
11
|
+
Classifier: Intended Audience :: Developers
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
14
|
+
Classifier: Operating System :: MacOS
|
15
|
+
Classifier: Operating System :: Microsoft :: Windows
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
22
|
+
Classifier: Topic :: Security
|
23
|
+
Classifier: Topic :: Software Development :: Build Tools
|
24
|
+
Classifier: Topic :: System :: Software Distribution
|
25
|
+
License-File: LICENSE
|
26
|
+
Requires-Dist: click>=8.0.0
|
27
|
+
Requires-Dist: requests>=2.25.0
|
28
|
+
Requires-Dist: tqdm>=4.60.0
|
29
|
+
Requires-Dist: colorama>=0.4.4
|
30
|
+
Project-URL: Homepage, https://github.com/kaifcodec/apk-patchx
|
31
|
+
Project-URL: Issues, https://github.com/kaifcodec/apk-patchx/issues
|
32
|
+
Project-URL: Repository, https://github.com/kaifcodec/apk-patchx.git
|
33
|
+
|
34
|
+
# APKPatcher
|
35
|
+
|
36
|
+
A powerful command-line tool for Android APK manipulation, including Frida gadget injection, APK decoding/building, and package management.
|
37
|
+
|
38
|
+
## Features
|
39
|
+
|
40
|
+
- **APK Management**: Pull, decode, build, and sign APK files
|
41
|
+
- **Frida Integration**: Inject Frida gadgets for runtime manipulation
|
42
|
+
- **Split APK Support**: Automatically merge split APKs into single files
|
43
|
+
- **Package Renaming**: Change APK package names
|
44
|
+
- **Auto-bootstrap**: Automatically downloads and manages required tools
|
45
|
+
|
46
|
+
## Installation
|
47
|
+
|
48
|
+
```bash
|
49
|
+
pip install apkpatcher
|
50
|
+
```
|
51
|
+
|
52
|
+
## Usage
|
53
|
+
|
54
|
+
### Pull APK from device
|
55
|
+
```bash
|
56
|
+
apkpatcher pull com.example.app
|
57
|
+
```
|
58
|
+
|
59
|
+
### Decode APK
|
60
|
+
```bash
|
61
|
+
apkpatcher decode app.apk
|
62
|
+
```
|
63
|
+
|
64
|
+
### Build APK from source
|
65
|
+
```bash
|
66
|
+
apkpatcher build app_src/
|
67
|
+
```
|
68
|
+
|
69
|
+
### Patch APK with Frida gadget
|
70
|
+
```bash
|
71
|
+
apkpatcher patch app.apk --arch arm64
|
72
|
+
```
|
73
|
+
|
74
|
+
### Rename APK package
|
75
|
+
```bash
|
76
|
+
apkpatcher rename app.apk com.newpackage.name
|
77
|
+
```
|
78
|
+
|
79
|
+
### Sign APK
|
80
|
+
```bash
|
81
|
+
apkpatcher sign app.apk
|
82
|
+
```
|
83
|
+
|
84
|
+
## Architecture Support
|
85
|
+
|
86
|
+
- ARM (`arm`)
|
87
|
+
- ARM64 (`arm64`)
|
88
|
+
- x86 (`x86`)
|
89
|
+
- x86_64 (`x86_64`)
|
90
|
+
|
91
|
+
## Requirements
|
92
|
+
|
93
|
+
- Python 3.8+
|
94
|
+
- Java Runtime Environment (JRE 8+)
|
95
|
+
- ADB (for device operations)
|
96
|
+
|
97
|
+
## Tool Management
|
98
|
+
|
99
|
+
APKPatcher automatically downloads and manages required tools in `~/.apkpatcher/tools/`:
|
100
|
+
|
101
|
+
- apktool
|
102
|
+
- Android SDK build-tools
|
103
|
+
- Platform tools (adb)
|
104
|
+
- dexpatch
|
105
|
+
- Frida gadgets
|
106
|
+
|
107
|
+
## License
|
108
|
+
|
109
|
+
MIT License - see LICENSE file for details.
|
110
|
+
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# APKPatcher
|
2
|
+
|
3
|
+
A powerful command-line tool for Android APK manipulation, including Frida gadget injection, APK decoding/building, and package management.
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
- **APK Management**: Pull, decode, build, and sign APK files
|
8
|
+
- **Frida Integration**: Inject Frida gadgets for runtime manipulation
|
9
|
+
- **Split APK Support**: Automatically merge split APKs into single files
|
10
|
+
- **Package Renaming**: Change APK package names
|
11
|
+
- **Auto-bootstrap**: Automatically downloads and manages required tools
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
```bash
|
16
|
+
pip install apkpatcher
|
17
|
+
```
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
### Pull APK from device
|
22
|
+
```bash
|
23
|
+
apkpatcher pull com.example.app
|
24
|
+
```
|
25
|
+
|
26
|
+
### Decode APK
|
27
|
+
```bash
|
28
|
+
apkpatcher decode app.apk
|
29
|
+
```
|
30
|
+
|
31
|
+
### Build APK from source
|
32
|
+
```bash
|
33
|
+
apkpatcher build app_src/
|
34
|
+
```
|
35
|
+
|
36
|
+
### Patch APK with Frida gadget
|
37
|
+
```bash
|
38
|
+
apkpatcher patch app.apk --arch arm64
|
39
|
+
```
|
40
|
+
|
41
|
+
### Rename APK package
|
42
|
+
```bash
|
43
|
+
apkpatcher rename app.apk com.newpackage.name
|
44
|
+
```
|
45
|
+
|
46
|
+
### Sign APK
|
47
|
+
```bash
|
48
|
+
apkpatcher sign app.apk
|
49
|
+
```
|
50
|
+
|
51
|
+
## Architecture Support
|
52
|
+
|
53
|
+
- ARM (`arm`)
|
54
|
+
- ARM64 (`arm64`)
|
55
|
+
- x86 (`x86`)
|
56
|
+
- x86_64 (`x86_64`)
|
57
|
+
|
58
|
+
## Requirements
|
59
|
+
|
60
|
+
- Python 3.8+
|
61
|
+
- Java Runtime Environment (JRE 8+)
|
62
|
+
- ADB (for device operations)
|
63
|
+
|
64
|
+
## Tool Management
|
65
|
+
|
66
|
+
APKPatcher automatically downloads and manages required tools in `~/.apkpatcher/tools/`:
|
67
|
+
|
68
|
+
- apktool
|
69
|
+
- Android SDK build-tools
|
70
|
+
- Platform tools (adb)
|
71
|
+
- dexpatch
|
72
|
+
- Frida gadgets
|
73
|
+
|
74
|
+
## License
|
75
|
+
|
76
|
+
MIT License - see LICENSE file for details.
|
@@ -0,0 +1,14 @@
|
|
1
|
+
"""APKPatcher - Android APK manipulation toolkit."""
|
2
|
+
|
3
|
+
__version__ = "7.9.2025.1"
|
4
|
+
__author__ = "APKPatcher Contributors"
|
5
|
+
__email__ = "contributors@apkpatcher.dev"
|
6
|
+
__license__ = "MIT"
|
7
|
+
|
8
|
+
from .exceptions import APKPatcherError, ToolNotFoundError, ValidationError
|
9
|
+
|
10
|
+
__all__ = [
|
11
|
+
"APKPatcherError",
|
12
|
+
"ToolNotFoundError",
|
13
|
+
"ValidationError"
|
14
|
+
]
|
@@ -0,0 +1,273 @@
|
|
1
|
+
"""Command-line interface for APKPatcher."""
|
2
|
+
|
3
|
+
import sys
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Optional
|
6
|
+
|
7
|
+
import click
|
8
|
+
import colorama
|
9
|
+
from colorama import Fore, Style
|
10
|
+
|
11
|
+
from . import __version__
|
12
|
+
from .exceptions import APKPatcherError
|
13
|
+
from .services.adb import ADBService
|
14
|
+
from .services.apktool import ApktoolService
|
15
|
+
from .services.frida import FridaService
|
16
|
+
from .services.signing import SigningService
|
17
|
+
from .utils.core import setup_logging, get_apkpatcher_home
|
18
|
+
|
19
|
+
colorama.init(autoreset=True)
|
20
|
+
|
21
|
+
|
22
|
+
def print_error(message: str) -> None:
|
23
|
+
"""Print error message in red."""
|
24
|
+
click.echo(f"{Fore.RED}[!] {message}{Style.RESET_ALL}", err=True)
|
25
|
+
|
26
|
+
|
27
|
+
def print_success(message: str) -> None:
|
28
|
+
"""Print success message in green."""
|
29
|
+
click.echo(f"{Fore.GREEN}[+] {message}{Style.RESET_ALL}")
|
30
|
+
|
31
|
+
|
32
|
+
def print_info(message: str) -> None:
|
33
|
+
"""Print info message in blue."""
|
34
|
+
click.echo(f"{Fore.BLUE}[*] {message}{Style.RESET_ALL}")
|
35
|
+
|
36
|
+
|
37
|
+
def print_warning(message: str) -> None:
|
38
|
+
"""Print warning message in yellow."""
|
39
|
+
click.echo(f"{Fore.YELLOW}[!] {message}{Style.RESET_ALL}")
|
40
|
+
|
41
|
+
|
42
|
+
@click.group(invoke_without_command=True)
|
43
|
+
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
|
44
|
+
@click.option("--version", is_flag=True, help="Show version and exit")
|
45
|
+
@click.pass_context
|
46
|
+
def cli(ctx: click.Context, verbose: bool, version: bool) -> None:
|
47
|
+
"""APKPatcher - Android APK manipulation toolkit."""
|
48
|
+
if version:
|
49
|
+
click.echo(f"APKPatcher {__version__}")
|
50
|
+
sys.exit(0)
|
51
|
+
|
52
|
+
if ctx.invoked_subcommand is None:
|
53
|
+
click.echo(ctx.get_help())
|
54
|
+
sys.exit(0)
|
55
|
+
|
56
|
+
ctx.ensure_object(dict)
|
57
|
+
ctx.obj["verbose"] = verbose
|
58
|
+
setup_logging(verbose)
|
59
|
+
|
60
|
+
# Initialize APKPatcher home directory
|
61
|
+
home = get_apkpatcher_home()
|
62
|
+
home.mkdir(parents=True, exist_ok=True)
|
63
|
+
|
64
|
+
if verbose:
|
65
|
+
print_info(f"APKPatcher home: {home}")
|
66
|
+
|
67
|
+
|
68
|
+
@cli.command()
|
69
|
+
@click.argument("package_name")
|
70
|
+
@click.option("--net", is_flag=True, help="Add permissive network security config")
|
71
|
+
@click.pass_context
|
72
|
+
def pull(ctx: click.Context, package_name: str, net: bool) -> None:
|
73
|
+
"""Pull APK from connected device."""
|
74
|
+
try:
|
75
|
+
verbose = ctx.obj.get("verbose", False)
|
76
|
+
adb_service = ADBService(verbose=verbose)
|
77
|
+
apktool_service = ApktoolService(verbose=verbose)
|
78
|
+
signing_service = SigningService(verbose=verbose)
|
79
|
+
|
80
|
+
print_info(f"Pulling {package_name} from device...")
|
81
|
+
apk_paths = adb_service.pull_package(package_name)
|
82
|
+
|
83
|
+
if len(apk_paths) > 1:
|
84
|
+
from .services.split_merge import SplitMergeService
|
85
|
+
merge_service = SplitMergeService(verbose=verbose)
|
86
|
+
output_path = merge_service.merge_split_apks(apk_paths, net=net)
|
87
|
+
print_success(f"Merged split APKs to: {output_path}")
|
88
|
+
else:
|
89
|
+
print_success(f"Pulled APK to: {apk_paths[0]}")
|
90
|
+
|
91
|
+
except APKPatcherError as e:
|
92
|
+
print_error(str(e))
|
93
|
+
sys.exit(1)
|
94
|
+
|
95
|
+
|
96
|
+
@cli.command()
|
97
|
+
@click.argument("apk_file", type=click.Path(exists=True, path_type=Path))
|
98
|
+
@click.option("--no-res", "-r", is_flag=True, help="Do not decode resources")
|
99
|
+
@click.option("--no-src", "-s", is_flag=True, help="Do not disassemble DEX")
|
100
|
+
@click.option("--only-main-classes", is_flag=True, help="Only disassemble main DEX classes")
|
101
|
+
@click.option("--apktool-decode-args", help="Additional apktool decode arguments")
|
102
|
+
@click.pass_context
|
103
|
+
def decode(ctx: click.Context, apk_file: Path, no_res: bool, no_src: bool,
|
104
|
+
only_main_classes: bool, apktool_decode_args: Optional[str]) -> None:
|
105
|
+
"""Decode APK file."""
|
106
|
+
try:
|
107
|
+
verbose = ctx.obj.get("verbose", False)
|
108
|
+
apktool_service = ApktoolService(verbose=verbose)
|
109
|
+
|
110
|
+
print_info(f"Decoding {apk_file}...")
|
111
|
+
output_dir = apktool_service.decode(
|
112
|
+
apk_file,
|
113
|
+
no_resources=no_res,
|
114
|
+
no_sources=no_src,
|
115
|
+
only_main_classes=only_main_classes,
|
116
|
+
extra_args=apktool_decode_args
|
117
|
+
)
|
118
|
+
print_success(f"Decoded to: {output_dir}")
|
119
|
+
|
120
|
+
except APKPatcherError as e:
|
121
|
+
print_error(str(e))
|
122
|
+
sys.exit(1)
|
123
|
+
|
124
|
+
|
125
|
+
@cli.command()
|
126
|
+
@click.argument("apk_dir", type=click.Path(exists=True, path_type=Path))
|
127
|
+
@click.option("-o", "--output", help="Output APK path", type=click.Path(path_type=Path))
|
128
|
+
@click.option("--net", is_flag=True, help="Add permissive network security config")
|
129
|
+
@click.option("--apktool-build-args", help="Additional apktool build arguments")
|
130
|
+
@click.pass_context
|
131
|
+
def build(ctx: click.Context, apk_dir: Path, output: Optional[Path],
|
132
|
+
net: bool, apktool_build_args: Optional[str]) -> None:
|
133
|
+
"""Build APK from decoded directory."""
|
134
|
+
try:
|
135
|
+
verbose = ctx.obj.get("verbose", False)
|
136
|
+
apktool_service = ApktoolService(verbose=verbose)
|
137
|
+
signing_service = SigningService(verbose=verbose)
|
138
|
+
|
139
|
+
if output is None:
|
140
|
+
output = apk_dir.with_suffix(".apk")
|
141
|
+
|
142
|
+
print_info(f"Building {apk_dir}...")
|
143
|
+
apk_path = apktool_service.build(
|
144
|
+
apk_dir,
|
145
|
+
output,
|
146
|
+
add_network_config=net,
|
147
|
+
extra_args=apktool_build_args
|
148
|
+
)
|
149
|
+
|
150
|
+
print_info("Signing APK...")
|
151
|
+
signed_path = signing_service.sign_apk(apk_path)
|
152
|
+
print_success(f"Built and signed: {signed_path}")
|
153
|
+
|
154
|
+
except APKPatcherError as e:
|
155
|
+
print_error(str(e))
|
156
|
+
sys.exit(1)
|
157
|
+
|
158
|
+
|
159
|
+
@cli.command()
|
160
|
+
@click.argument("apk_file", type=click.Path(exists=True, path_type=Path))
|
161
|
+
@click.option("--arch", "-a", required=True,
|
162
|
+
type=click.Choice(["arm", "arm64", "x86", "x86_64"]),
|
163
|
+
help="Target architecture")
|
164
|
+
@click.option("--gadget-conf", "-g", type=click.Path(exists=True, path_type=Path),
|
165
|
+
help="Frida gadget configuration file")
|
166
|
+
@click.option("--net", is_flag=True, help="Add permissive network security config")
|
167
|
+
@click.option("--no-src", "-s", is_flag=True, help="Do not disassemble DEX")
|
168
|
+
@click.option("--only-main-classes", is_flag=True, help="Only disassemble main DEX classes")
|
169
|
+
@click.option("--frida-version", help="Specific Frida version to use")
|
170
|
+
@click.option("--apktool-decode-args", help="Additional apktool decode arguments")
|
171
|
+
@click.option("--apktool-build-args", help="Additional apktool build arguments")
|
172
|
+
@click.pass_context
|
173
|
+
def patch(ctx: click.Context, apk_file: Path, arch: str, gadget_conf: Optional[Path],
|
174
|
+
net: bool, no_src: bool, only_main_classes: bool, frida_version: Optional[str],
|
175
|
+
apktool_decode_args: Optional[str], apktool_build_args: Optional[str]) -> None:
|
176
|
+
"""Patch APK with Frida gadget."""
|
177
|
+
try:
|
178
|
+
verbose = ctx.obj.get("verbose", False)
|
179
|
+
frida_service = FridaService(verbose=verbose)
|
180
|
+
|
181
|
+
print_info(f"Patching {apk_file} with Frida gadget ({arch})...")
|
182
|
+
output_path = frida_service.patch_apk(
|
183
|
+
apk_file,
|
184
|
+
arch,
|
185
|
+
gadget_config=gadget_conf,
|
186
|
+
add_network_config=net,
|
187
|
+
no_sources=no_src,
|
188
|
+
only_main_classes=only_main_classes,
|
189
|
+
frida_version=frida_version,
|
190
|
+
decode_args=apktool_decode_args,
|
191
|
+
build_args=apktool_build_args
|
192
|
+
)
|
193
|
+
print_success(f"Patched APK: {output_path}")
|
194
|
+
|
195
|
+
except APKPatcherError as e:
|
196
|
+
print_error(str(e))
|
197
|
+
sys.exit(1)
|
198
|
+
|
199
|
+
|
200
|
+
@cli.command()
|
201
|
+
@click.argument("apk_file", type=click.Path(exists=True, path_type=Path))
|
202
|
+
@click.argument("new_package")
|
203
|
+
@click.option("--net", is_flag=True, help="Add permissive network security config")
|
204
|
+
@click.pass_context
|
205
|
+
def rename(ctx: click.Context, apk_file: Path, new_package: str, net: bool) -> None:
|
206
|
+
"""Rename APK package."""
|
207
|
+
try:
|
208
|
+
verbose = ctx.obj.get("verbose", False)
|
209
|
+
apktool_service = ApktoolService(verbose=verbose)
|
210
|
+
signing_service = SigningService(verbose=verbose)
|
211
|
+
|
212
|
+
print_info(f"Renaming {apk_file} to {new_package}...")
|
213
|
+
|
214
|
+
# Decode APK
|
215
|
+
decoded_dir = apktool_service.decode(apk_file, no_resources=False, no_sources=True)
|
216
|
+
|
217
|
+
# Update apktool.yml with new package name
|
218
|
+
apktool_yml = decoded_dir / "apktool.yml"
|
219
|
+
content = apktool_yml.read_text()
|
220
|
+
|
221
|
+
# Add or update renameManifestPackage
|
222
|
+
if "renameManifestPackage:" in content:
|
223
|
+
import re
|
224
|
+
content = re.sub(r"renameManifestPackage:.*", f"renameManifestPackage: {new_package}", content)
|
225
|
+
else:
|
226
|
+
content += f"\nrenameManifestPackage: {new_package}\n"
|
227
|
+
|
228
|
+
apktool_yml.write_text(content)
|
229
|
+
|
230
|
+
# Build and sign
|
231
|
+
output_path = apk_file.parent / f"{apk_file.stem}.renamed.apk"
|
232
|
+
built_path = apktool_service.build(decoded_dir, output_path, add_network_config=net)
|
233
|
+
signed_path = signing_service.sign_apk(built_path)
|
234
|
+
|
235
|
+
print_success(f"Renamed APK: {signed_path}")
|
236
|
+
|
237
|
+
except APKPatcherError as e:
|
238
|
+
print_error(str(e))
|
239
|
+
sys.exit(1)
|
240
|
+
|
241
|
+
|
242
|
+
@cli.command()
|
243
|
+
@click.argument("apk_file", type=click.Path(exists=True, path_type=Path))
|
244
|
+
@click.pass_context
|
245
|
+
def sign(ctx: click.Context, apk_file: Path) -> None:
|
246
|
+
"""Sign APK file."""
|
247
|
+
try:
|
248
|
+
verbose = ctx.obj.get("verbose", False)
|
249
|
+
signing_service = SigningService(verbose=verbose)
|
250
|
+
|
251
|
+
print_info(f"Signing {apk_file}...")
|
252
|
+
signed_path = signing_service.sign_apk(apk_file)
|
253
|
+
print_success(f"Signed APK: {signed_path}")
|
254
|
+
|
255
|
+
except APKPatcherError as e:
|
256
|
+
print_error(str(e))
|
257
|
+
sys.exit(1)
|
258
|
+
|
259
|
+
|
260
|
+
def main() -> None:
|
261
|
+
"""Entry point for the CLI."""
|
262
|
+
try:
|
263
|
+
cli()
|
264
|
+
except KeyboardInterrupt:
|
265
|
+
print_error("Operation cancelled by user")
|
266
|
+
sys.exit(1)
|
267
|
+
except Exception as e:
|
268
|
+
print_error(f"Unexpected error: {e}")
|
269
|
+
sys.exit(1)
|
270
|
+
|
271
|
+
|
272
|
+
if __name__ == "__main__":
|
273
|
+
main()
|
@@ -0,0 +1,41 @@
|
|
1
|
+
"""Custom exceptions for APKPatcher."""
|
2
|
+
|
3
|
+
|
4
|
+
class APKPatcherError(Exception):
|
5
|
+
"""Base exception for APKPatcher."""
|
6
|
+
pass
|
7
|
+
|
8
|
+
|
9
|
+
class ToolNotFoundError(APKPatcherError):
|
10
|
+
"""Raised when a required tool is not found."""
|
11
|
+
pass
|
12
|
+
|
13
|
+
|
14
|
+
class ValidationError(APKPatcherError):
|
15
|
+
"""Raised when input validation fails."""
|
16
|
+
pass
|
17
|
+
|
18
|
+
|
19
|
+
class BuildError(APKPatcherError):
|
20
|
+
"""Raised when APK build operation fails."""
|
21
|
+
pass
|
22
|
+
|
23
|
+
|
24
|
+
class SigningError(APKPatcherError):
|
25
|
+
"""Raised when APK signing fails."""
|
26
|
+
pass
|
27
|
+
|
28
|
+
|
29
|
+
class ADBError(APKPatcherError):
|
30
|
+
"""Raised when ADB operation fails."""
|
31
|
+
pass
|
32
|
+
|
33
|
+
|
34
|
+
class FridaPatchError(APKPatcherError):
|
35
|
+
"""Raised when Frida patching fails."""
|
36
|
+
pass
|
37
|
+
|
38
|
+
|
39
|
+
class NetworkError(APKPatcherError):
|
40
|
+
"""Raised when network operation fails."""
|
41
|
+
pass
|
@@ -0,0 +1 @@
|
|
1
|
+
"""Services package for APKPatcher."""
|
@@ -0,0 +1,98 @@
|
|
1
|
+
"""ADB service for device operations."""
|
2
|
+
|
3
|
+
import re
|
4
|
+
import subprocess
|
5
|
+
from pathlib import Path
|
6
|
+
from typing import List, Optional
|
7
|
+
|
8
|
+
from ..exceptions import ADBError, ToolNotFoundError
|
9
|
+
from ..utils.core import run_command, get_apkpatcher_home
|
10
|
+
from .android_sdk import AndroidSDKService
|
11
|
+
|
12
|
+
|
13
|
+
class ADBService:
|
14
|
+
"""Service for ADB operations."""
|
15
|
+
|
16
|
+
def __init__(self, verbose: bool = False):
|
17
|
+
self.verbose = verbose
|
18
|
+
self.sdk_service = AndroidSDKService(verbose=verbose)
|
19
|
+
self._adb_path: Optional[Path] = None
|
20
|
+
|
21
|
+
@property
|
22
|
+
def adb_path(self) -> Path:
|
23
|
+
"""Get ADB executable path."""
|
24
|
+
if self._adb_path is None:
|
25
|
+
# First try system ADB
|
26
|
+
result = subprocess.run(["which", "adb"], capture_output=True, text=True)
|
27
|
+
if result.returncode == 0:
|
28
|
+
self._adb_path = Path(result.stdout.strip())
|
29
|
+
else:
|
30
|
+
# Use SDK ADB
|
31
|
+
self.sdk_service.ensure_platform_tools()
|
32
|
+
sdk_root = get_apkpatcher_home() / "tools" / "sdk"
|
33
|
+
self._adb_path = sdk_root / "platform-tools" / "adb"
|
34
|
+
|
35
|
+
if not self._adb_path.exists():
|
36
|
+
raise ToolNotFoundError("ADB not found and SDK installation failed")
|
37
|
+
|
38
|
+
return self._adb_path
|
39
|
+
|
40
|
+
def pull_package(self, package_name: str) -> List[Path]:
|
41
|
+
"""Pull APK(s) for given package name."""
|
42
|
+
# Get package path(s)
|
43
|
+
cmd = [str(self.adb_path), "shell", "pm", "path", package_name]
|
44
|
+
result = run_command(cmd, capture_output=True, text=True)
|
45
|
+
|
46
|
+
if result.returncode != 0 or not result.stdout.strip():
|
47
|
+
raise ADBError(f"Package {package_name} not found on device")
|
48
|
+
|
49
|
+
package_paths = []
|
50
|
+
for line in result.stdout.strip().split("\n"):
|
51
|
+
if line.startswith("package:"):
|
52
|
+
package_paths.append(line.replace("package:", ""))
|
53
|
+
|
54
|
+
if not package_paths:
|
55
|
+
raise ADBError(f"No package paths found for {package_name}")
|
56
|
+
|
57
|
+
# Pull each APK
|
58
|
+
pulled_apks = []
|
59
|
+
if len(package_paths) > 1:
|
60
|
+
# Split APKs
|
61
|
+
split_dir = Path(f"{package_name}_split_apks")
|
62
|
+
split_dir.mkdir(exist_ok=True)
|
63
|
+
|
64
|
+
for i, package_path in enumerate(package_paths):
|
65
|
+
apk_name = f"split_{i}.apk" if i > 0 else "base.apk"
|
66
|
+
local_path = split_dir / apk_name
|
67
|
+
|
68
|
+
cmd = [str(self.adb_path), "pull", package_path, str(local_path)]
|
69
|
+
result = run_command(cmd)
|
70
|
+
if result.returncode != 0:
|
71
|
+
raise ADBError(f"Failed to pull {package_path}")
|
72
|
+
|
73
|
+
pulled_apks.append(local_path)
|
74
|
+
else:
|
75
|
+
# Single APK
|
76
|
+
local_path = Path(f"{package_name}.apk")
|
77
|
+
cmd = [str(self.adb_path), "pull", package_paths[0], str(local_path)]
|
78
|
+
result = run_command(cmd)
|
79
|
+
if result.returncode != 0:
|
80
|
+
raise ADBError(f"Failed to pull {package_paths[0]}")
|
81
|
+
|
82
|
+
pulled_apks.append(local_path)
|
83
|
+
|
84
|
+
return pulled_apks
|
85
|
+
|
86
|
+
def is_device_connected(self) -> bool:
|
87
|
+
"""Check if device is connected."""
|
88
|
+
try:
|
89
|
+
cmd = [str(self.adb_path), "devices"]
|
90
|
+
result = run_command(cmd, capture_output=True, text=True)
|
91
|
+
|
92
|
+
lines = result.stdout.strip().split("\n")[1:] # Skip header
|
93
|
+
for line in lines:
|
94
|
+
if line.strip() and "\tdevice" in line:
|
95
|
+
return True
|
96
|
+
return False
|
97
|
+
except Exception:
|
98
|
+
return False
|