titanium-apk-recover 2.2.0
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.
- package/README.md +252 -0
- package/dist/chunk-ML2DCRT6.js +1063 -0
- package/dist/chunk-ML2DCRT6.js.map +1 -0
- package/dist/cli.cjs +1086 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.js +90 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +1126 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +340 -0
- package/dist/index.d.ts +340 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/package.json +82 -0
package/README.md
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
   
|
|
4
|
+
|
|
5
|
+
Recover the source code from almost any APK built with [Appcelerator Titanium](https://titaniumsdk.com/), whether it was compiled in **development** or **distribution** (encrypted) mode — including the newer **`ti.cloak`** (`.bin`) encryption.
|
|
6
|
+
|
|
7
|
+
It ships as both a modern, promise-based **library** (TypeScript types included, ESM + CommonJS) and a **command-line tool**, and runs **entirely in JavaScript** — no JDK, apktool or jadx required.
|
|
8
|
+
|
|
9
|
+
> As featured in my blog post: [How recoverable is an APK's source code made with Titanium?](https://pabloschaffner.cl/2017/02/01/how-recoverable-is-an-apk-source-code-made-with-titanium/)
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- **Node.js >= 18**
|
|
14
|
+
|
|
15
|
+
That's it. Since **v2.1.0** everything (APK unzip, binary manifest parsing, DEX
|
|
16
|
+
parsing and AES decryption) runs in pure JS, so no Java/JDK installation is
|
|
17
|
+
needed.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# as a CLI
|
|
23
|
+
npm install -g titanium-apk-recover
|
|
24
|
+
|
|
25
|
+
# or as a dependency
|
|
26
|
+
npm install titanium-apk-recover
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
> Formerly published as `ti_recover`. The CLI still exposes a `ti_recover`
|
|
30
|
+
> command alias for continuity.
|
|
31
|
+
|
|
32
|
+
## CLI usage
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# recover a project into ./out
|
|
36
|
+
titanium-apk-recover myapp.apk ./out
|
|
37
|
+
|
|
38
|
+
# just inspect what's inside (no files written)
|
|
39
|
+
titanium-apk-recover info myapp.apk
|
|
40
|
+
|
|
41
|
+
# emit machine-readable JSON
|
|
42
|
+
titanium-apk-recover myapp.apk ./out --json
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
By default the CLI **reconstructs** an openable Titanium project (sources under
|
|
46
|
+
`Resources/`, plus a generated `tiapp.xml`). Pass `--no-reconstruct` to keep the
|
|
47
|
+
raw recovered layout instead.
|
|
48
|
+
|
|
49
|
+
### Commands & options
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
titanium-apk-recover <apk> <outdir> Recover source code and assets (default command)
|
|
53
|
+
titanium-apk-recover info <apk> Print Titanium metadata about an APK
|
|
54
|
+
|
|
55
|
+
Options (recover):
|
|
56
|
+
--no-reconstruct keep the flat recovered layout instead of a Titanium project
|
|
57
|
+
--keep-tmp keep the temporary working directory
|
|
58
|
+
--tmp-dir <dir> temporary working directory (default: "_tmp")
|
|
59
|
+
--json print recovery info as JSON
|
|
60
|
+
-q, --quiet suppress progress output
|
|
61
|
+
-d, --debug verbose logging
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Exit codes: `0` success, `1` error, `2` the APK was not built with Titanium.
|
|
65
|
+
|
|
66
|
+
## Library usage
|
|
67
|
+
|
|
68
|
+
### One-shot helper
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { recover } from "titanium-apk-recover";
|
|
72
|
+
|
|
73
|
+
const result = await recover({
|
|
74
|
+
apk: "myapp.apk",
|
|
75
|
+
outDir: "./out",
|
|
76
|
+
reconstruct: true, // produce an openable Titanium project (default: false)
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (result.recovered) {
|
|
80
|
+
console.log(result.info); // TitaniumInfo
|
|
81
|
+
console.log(result.files); // [{ name, bytes }, ...]
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
CommonJS works too:
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
const { recover } = require("titanium-apk-recover");
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Step-by-step with the `TiRecover` class
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { TiRecover } from "titanium-apk-recover";
|
|
95
|
+
|
|
96
|
+
const ti = new TiRecover({ apk: "myapp.apk", outDir: "./out" });
|
|
97
|
+
|
|
98
|
+
await ti.init(); // unzip the APK (pure JS)
|
|
99
|
+
if (await ti.test()) { // is it a Titanium app?
|
|
100
|
+
await ti.extract(); // recover sources into memory
|
|
101
|
+
const info = await ti.info(); // Titanium metadata (call after extract)
|
|
102
|
+
await ti.reconstruct(); // optional: rebuild a Titanium project layout
|
|
103
|
+
await ti.writeToDisk(); // write recovered sources to outDir
|
|
104
|
+
await ti.copyAssets(); // copy images/resources + manifest
|
|
105
|
+
}
|
|
106
|
+
await ti.clean(); // remove the temporary working directory
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### API
|
|
110
|
+
|
|
111
|
+
| Method | Description |
|
|
112
|
+
| --- | --- |
|
|
113
|
+
| `new TiRecover(config)` | Create an instance. Config: `apk`, `apkDir`, `outDir`, `tmpDir`, `debug`. |
|
|
114
|
+
| `init()` | Unzips the APK in pure JS (or reuses a provided extracted `apkDir`). |
|
|
115
|
+
| `test()` | Resolves `true` if the APK was built with Titanium (dev or distribution). |
|
|
116
|
+
| `extract()` | Recovers sources into memory; resolves a `MemorySource` map. |
|
|
117
|
+
| `info()` | Resolves `TitaniumInfo` (package, versions, mode, engine, Alloy, files). Call after `extract()`. |
|
|
118
|
+
| `reconstruct()` | Rebuilds sources into an openable Titanium project (`Resources/` + `tiapp.xml`). |
|
|
119
|
+
| `writeToDisk()` | Writes in-memory sources to `outDir` (`.js` files are beautified). |
|
|
120
|
+
| `copyAssets()` | Copies the APK's images/resources and `AndroidManifest.xml` to `outDir`. |
|
|
121
|
+
| `clean()` | Removes the temporary working directory. |
|
|
122
|
+
|
|
123
|
+
Pure, JVM-free helpers are also exported for advanced use and testing:
|
|
124
|
+
`parseManifest`, `parseBinaryManifest`, `readAssetCrypt`, `decryptRange`,
|
|
125
|
+
`decryptRanges`, `parseRanges`, `parseAssetBuffer`, `decodeJavaInt`,
|
|
126
|
+
`detectAlloy`, `extractStringChunks`, `extractRanges`, `extractByteArrayFields`,
|
|
127
|
+
`extractCloakSalt`, `instructionWidth`, `deriveCloakKey`, `decryptCloakAsset`,
|
|
128
|
+
`pickCloakKey`, `isProbablyText`, `buildInfo`, `buildReconstruct` and
|
|
129
|
+
`buildTiappXml`.
|
|
130
|
+
|
|
131
|
+
## How it works
|
|
132
|
+
|
|
133
|
+
Everything runs in pure JS:
|
|
134
|
+
|
|
135
|
+
- The APK is unzipped with [`fflate`](https://www.npmjs.com/package/fflate).
|
|
136
|
+
- The binary `AndroidManifest.xml` is parsed with
|
|
137
|
+
[`adbkit-apkreader`](https://www.npmjs.com/package/adbkit-apkreader) for the
|
|
138
|
+
package id and versions (and re-serialised to readable XML in the output).
|
|
139
|
+
- **Development-mode** APKs ship plain JS/JSON/XML sources under
|
|
140
|
+
`assets/Resources`, which are read directly.
|
|
141
|
+
- **Distribution-mode** APKs store all sources as a single AES-encrypted blob.
|
|
142
|
+
Titanium's generated `AssetCryptImpl` class holds that blob and the per-file
|
|
143
|
+
byte ranges in its bytecode. titanium-apk-recover reads `classes*.dex` with
|
|
144
|
+
[`libdex-ts`](https://www.npmjs.com/package/libdex-ts) and walks the
|
|
145
|
+
`initAssetsBytes()` / `initAssets()` instruction streams to lift the blob and
|
|
146
|
+
ranges directly, then decrypts each file with `node:crypto`
|
|
147
|
+
(`aes-128-ecb`, key = the blob's last 16 bytes).
|
|
148
|
+
|
|
149
|
+
### The newer `ti.cloak` scheme
|
|
150
|
+
|
|
151
|
+
Recent Titanium SDKs replaced the static `AssetCryptImpl` blob with a scheme
|
|
152
|
+
that stores each source as an encrypted `Resources/<name>.bin` asset, decrypted
|
|
153
|
+
at runtime with `AES/CBC/PKCS5Padding` (IV = a hardcoded `salt`) and a key
|
|
154
|
+
produced by the native `libti.cloak.so` (see
|
|
155
|
+
[issue #9](https://github.com/puntorigen/ti_recover/issues/9) and
|
|
156
|
+
[#6](https://github.com/puntorigen/ti_recover/issues/6)).
|
|
157
|
+
|
|
158
|
+
Since **v2.2.0** titanium-apk-recover recovers these too, entirely in JS:
|
|
159
|
+
|
|
160
|
+
- the `salt` (IV) is lifted from `AssetCryptImpl.<clinit>` (its `byte[]`
|
|
161
|
+
`fill-array-data` payload) in the DEX;
|
|
162
|
+
- the AES key is derived from the fixed key block the build embeds in every
|
|
163
|
+
bundled `lib/<abi>/libti.cloak.so` (the key is `salt XOR xor`, where `xor` is
|
|
164
|
+
assembled from four slices of that block);
|
|
165
|
+
- each `.bin` is decrypted with `node:crypto` (`aes-128-cbc`), transparently
|
|
166
|
+
gunzipping any compressed payloads.
|
|
167
|
+
|
|
168
|
+
The derived key is confirmed by trial-decrypting a sample asset, so
|
|
169
|
+
titanium-apk-recover tries every bundled ABI and only proceeds when one produces
|
|
170
|
+
valid output. If the APK ships no `libti.cloak.so` (e.g. an ABI-split APK missing
|
|
171
|
+
the native lib), the key can't be derived and titanium-apk-recover reports it
|
|
172
|
+
with a clear error instead of producing garbage.
|
|
173
|
+
|
|
174
|
+
## Development
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
npm install # install deps
|
|
178
|
+
npm run build # bundle ESM + CJS + types into dist/
|
|
179
|
+
npm test # run the vitest suite (pure JS, no JVM)
|
|
180
|
+
npm run lint # eslint
|
|
181
|
+
npm run typecheck # tsc --noEmit
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Updates
|
|
185
|
+
|
|
186
|
+
### version 2.2.0
|
|
187
|
+
|
|
188
|
+
- **Recovers the newer `ti.cloak` (`.bin`) encryption scheme** in pure JS
|
|
189
|
+
(previously detected-but-unsupported):
|
|
190
|
+
- lifts the hardcoded `salt` (AES-CBC IV) from `AssetCryptImpl.<clinit>`'s
|
|
191
|
+
`fill-array-data` payload in the DEX;
|
|
192
|
+
- derives the AES key from the fixed key block embedded in the bundled
|
|
193
|
+
`lib/<abi>/libti.cloak.so` (`key = salt XOR xor`);
|
|
194
|
+
- decrypts every `Resources/*.bin` asset with `node:crypto` `aes-128-cbc`,
|
|
195
|
+
transparently gunzipping compressed payloads, and confirms the key by trial
|
|
196
|
+
decryption across all bundled ABIs.
|
|
197
|
+
- Still reports a clear error when the native `libti.cloak.so` (or the salt) is
|
|
198
|
+
absent and the key therefore can't be derived.
|
|
199
|
+
|
|
200
|
+
### version 2.1.0
|
|
201
|
+
|
|
202
|
+
- **Removed the Java/JDK dependency entirely** — recovery now runs in pure JS.
|
|
203
|
+
- APK unzip via `fflate` (replaces the `apk_unpack` apktool step).
|
|
204
|
+
- Binary `AndroidManifest.xml` parsing via `adbkit-apkreader` (replaces the apktool text decode); a readable manifest is still written to the output.
|
|
205
|
+
- Distribution-mode asset data (encrypted blob + per-file ranges) is lifted directly from `classes*.dex` with `libdex-ts` and a small DEX instruction walker (replaces the jadx decompile of `AssetCryptImpl`).
|
|
206
|
+
- AES decryption via `node:crypto` `aes-128-ecb` (replaces `javax.crypto`); DEX stores real strings, so the old `commons-lang` string-unescape step is gone.
|
|
207
|
+
- Dropped the optional `java` and `apk_unpack` dependencies and the bundled ~13 MB `java/` directory (apktool + jadx JARs).
|
|
208
|
+
- Detects and clearly reports APKs using the newer, statically-unrecoverable `ti.cloak` / `.bin` encryption scheme.
|
|
209
|
+
- Multidex aware (scans `classes.dex`, `classes2.dex`, …).
|
|
210
|
+
|
|
211
|
+
### version 2.0.0
|
|
212
|
+
|
|
213
|
+
- Full rewrite in **TypeScript** with an ESM + CommonJS dual build and shipped type definitions.
|
|
214
|
+
- New **promise-based API** (`TiRecover` class + `recover()` helper); the old callback API has been removed (breaking change).
|
|
215
|
+
- New **commander-based CLI** with `--help`, an `info` subcommand, `--json`, `--no-reconstruct`, `--keep-tmp`, `--quiet`, `--debug` flags and proper exit codes.
|
|
216
|
+
- Implemented the previously pending **`reconstruct()`** (rebuild an openable Titanium project) and **`info()`** (Titanium metadata) methods.
|
|
217
|
+
- Single shared JVM instance and async, non-blocking filesystem I/O.
|
|
218
|
+
- The native `java` bridge and `apk_unpack` are now **optional dependencies**, so installs no longer fail on machines without a JDK.
|
|
219
|
+
- Added a **test suite** (vitest) with synthetic fixtures.
|
|
220
|
+
|
|
221
|
+
### version 1.1.1
|
|
222
|
+
|
|
223
|
+
- now assets are put on the correct directories.
|
|
224
|
+
|
|
225
|
+
### version 1.0.9
|
|
226
|
+
|
|
227
|
+
- updated to latest apk_unpack to use jadx.
|
|
228
|
+
- now resources and manifest are also copied to outputdir.
|
|
229
|
+
|
|
230
|
+
### version 1.0.6
|
|
231
|
+
|
|
232
|
+
- added ability to recover APKs created in development mode.
|
|
233
|
+
|
|
234
|
+
### version 1.0.5
|
|
235
|
+
|
|
236
|
+
- improved readability of CLI, added prettifier to source code, and bugfix several issues.
|
|
237
|
+
|
|
238
|
+
### version 1.0.4
|
|
239
|
+
|
|
240
|
+
- fixed tmp dir location bug. Now CLI works ok.
|
|
241
|
+
|
|
242
|
+
### version 1.0.2-3
|
|
243
|
+
|
|
244
|
+
- added delay before decrypting files, to account for slower hdd disks.
|
|
245
|
+
|
|
246
|
+
### version 1.0.1
|
|
247
|
+
|
|
248
|
+
- fixed console debug.
|
|
249
|
+
|
|
250
|
+
### version 1.0.0
|
|
251
|
+
|
|
252
|
+
- first version.
|