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 ADDED
@@ -0,0 +1,252 @@
1
+ ![ti_recover](https://user-images.githubusercontent.com/57605485/133170750-20244127-1ea0-4cd0-9c67-ac5ca44f17bc.png)
2
+
3
+ ![npm version](https://img.shields.io/npm/v/titanium-apk-recover) ![npm downloads](https://img.shields.io/npm/dm/titanium-apk-recover) ![license](https://img.shields.io/npm/l/titanium-apk-recover) ![lines](https://img.shields.io/tokei/lines/github/puntorigen/ti_recover)
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.