npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

titanium-apk-recover

v2.3.1

Published

Recover the source code from almost any APK made with Appcelerator Titanium (development, distribution or ti.cloak mode).

Readme

npm version npm downloads license lines

Recover the source code from almost any APK built with Appcelerator Titanium, whether it was compiled in development or distribution (encrypted) mode — including the newer ti.cloak (.bin) encryption.

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.

As featured in my blog post: How recoverable is an APK's source code made with Titanium?

Requirements

  • Node.js >= 18

That's it. Since v2.1.0 everything (APK unzip, binary manifest parsing, DEX parsing and AES decryption) runs in pure JS, so no Java/JDK installation is needed.

Install

# as a CLI
npm install -g titanium-apk-recover

# or as a dependency
npm install titanium-apk-recover

Formerly published as ti_recover. The CLI still exposes a ti_recover command alias for continuity.

CLI usage

# recover a project into ./out
titanium-apk-recover myapp.apk ./out

# just inspect what's inside (no files written)
titanium-apk-recover info myapp.apk

# emit machine-readable JSON
titanium-apk-recover myapp.apk ./out --json

By default the CLI reconstructs an openable Titanium project (sources under Resources/, plus a generated tiapp.xml). Pass --no-reconstruct to keep the raw recovered layout instead.

Commands & options

titanium-apk-recover <apk> <outdir>   Recover source code and assets (default command)
titanium-apk-recover info <apk>       Print Titanium metadata about an APK

Options (recover):
  --no-reconstruct   keep the flat recovered layout instead of a Titanium project
  --keep-tmp         keep the temporary working directory
  --tmp-dir <dir>    temporary working directory (default: "_tmp")
  --json             print recovery info as JSON
  -q, --quiet        suppress progress output
  -d, --debug        verbose logging

Exit codes: 0 success, 1 error, 2 the APK was not built with Titanium.

Library usage

One-shot helper

import { recover } from "titanium-apk-recover";

const result = await recover({
  apk: "myapp.apk",
  outDir: "./out",
  reconstruct: true, // produce an openable Titanium project (default: false)
});

if (result.recovered) {
  console.log(result.info);   // TitaniumInfo
  console.log(result.files);  // [{ name, bytes }, ...]
}

CommonJS works too:

const { recover } = require("titanium-apk-recover");

Step-by-step with the TiRecover class

import { TiRecover } from "titanium-apk-recover";

const ti = new TiRecover({ apk: "myapp.apk", outDir: "./out" });

await ti.init();                 // unzip the APK (pure JS)
if (await ti.test()) {           // is it a Titanium app?
  await ti.extract();            // recover sources into memory
  const info = await ti.info();  // Titanium metadata (call after extract)
  await ti.reconstruct();        // optional: rebuild a Titanium project layout
  await ti.writeToDisk();        // write recovered sources to outDir
  await ti.copyAssets();         // copy images/resources + manifest
}
await ti.clean();                // remove the temporary working directory

API

| Method | Description | | --- | --- | | new TiRecover(config) | Create an instance. Config: apk, apkDir, outDir, tmpDir, debug. | | init() | Unzips the APK in pure JS (or reuses a provided extracted apkDir). | | test() | Resolves true if the APK was built with Titanium (dev or distribution). | | extract() | Recovers sources into memory; resolves a MemorySource map. | | info() | Resolves TitaniumInfo (package, versions, mode, engine, Alloy, files). Call after extract(). | | reconstruct() | Rebuilds sources into an openable Titanium project (Resources/ + tiapp.xml). | | writeToDisk() | Writes in-memory sources to outDir (.js files are beautified). | | copyAssets() | Copies the APK's images/resources and AndroidManifest.xml to outDir. | | clean() | Removes the temporary working directory. |

Pure, JVM-free helpers are also exported for advanced use and testing: parseManifest, parseBinaryManifest, readAssetCrypt, decryptRange, decryptRanges, parseRanges, parseAssetBuffer, decodeJavaInt, detectAlloy, extractStringChunks, extractRanges, extractByteArrayFields, extractCloakSalt, instructionWidth, extractKeyBlock, deriveKeyFromBlock, cloakKeyCandidates, deriveCloakKey, decryptCloakAsset, pickCloakKey, isProbablyText, buildInfo, buildReconstruct and buildTiappXml.

How it works

Everything runs in pure JS:

  • The APK is unzipped with fflate.
  • The binary AndroidManifest.xml is parsed with adbkit-apkreader for the package id and versions (and re-serialised to readable XML in the output).
  • Development-mode APKs ship plain JS/JSON/XML sources under assets/Resources, which are read directly.
  • Distribution-mode APKs store all sources as a single AES-encrypted blob. Titanium's generated AssetCryptImpl class holds that blob and the per-file byte ranges in its bytecode. titanium-apk-recover reads classes*.dex with libdex-ts and walks the initAssetsBytes() / initAssets() instruction streams to lift the blob and ranges directly, then decrypts each file with node:crypto (aes-128-ecb, key = the blob's last 16 bytes).

The newer ti.cloak scheme

Recent Titanium SDKs replaced the static AssetCryptImpl blob with a scheme that stores each source as an encrypted Resources/<name>.bin asset, decrypted at runtime with AES/CBC/PKCS5Padding (IV = a hardcoded salt) and a key produced by the native libti.cloak.so (see issue #9 and #6).

Since v2.2.0 titanium-apk-recover recovers these too, entirely in JS:

  • the salt (IV) is lifted from AssetCryptImpl.<clinit> (its byte[] fill-array-data payload) in the DEX;
  • the AES key is derived from the fixed KEY_BLOCK the build embeds in every bundled lib/<abi>/libti.cloak.so (the key is salt XOR xor, where xor is assembled from four slices of that block). Because the block's file offset varies per ABI/SDK/compile flags, its location is resolved from the ELF's exported KEY_BLOCK symbol (a small built-in ELF32/ELF64 parser), falling back to the known per-ABI offsets — see issue #13;
  • each .bin is decrypted with node:crypto (aes-128-cbc), transparently gunzipping any compressed payloads.

Every candidate key is confirmed by trial-decrypting a sample asset, so titanium-apk-recover tries each bundled ABI (and each candidate offset) and only proceeds when one produces valid output. If the APK ships no libti.cloak.so (e.g. an ABI-split APK missing the native lib), the key can't be derived and titanium-apk-recover reports it with a clear error instead of producing garbage.

Development

npm install        # install deps
npm run build      # bundle ESM + CJS + types into dist/
npm test           # run the vitest suite (pure JS, no JVM)
npm run lint       # eslint
npm run typecheck  # tsc --noEmit

Updates

version 2.3.1

  • docs: fixed the broken blog-post link in the README.

version 2.3.0

  • More robust ti.cloak key extraction (issue #13, thanks @j4k0xb). The KEY_BLOCK offset inside libti.cloak.so is no longer hardcoded to the arm64 value (0x2008) — it is resolved from the ELF's exported KEY_BLOCK symbol via a small built-in ELF32/ELF64 parser, so armeabi-v7a, x86, x86_64 and ABI-split APKs now work too. Known per-ABI offsets remain as a fallback, and every candidate is still validated by trial decryption.
  • New exported helpers: extractKeyBlock, deriveKeyFromBlock, cloakKeyCandidates.

version 2.2.2

  • Added a header banner (logo + wordmark + tagline) to the README / npm page. No code changes.

version 2.2.1

  • Renamed the npm package to titanium-apk-recover (previously ti_recover) for better discoverability. The published package now lives under titanium-apk-recover; the CLI keeps a ti_recover command alias for continuity. No API changes.

version 2.2.0

  • Recovers the newer ti.cloak (.bin) encryption scheme in pure JS (previously detected-but-unsupported):
    • lifts the hardcoded salt (AES-CBC IV) from AssetCryptImpl.<clinit>'s fill-array-data payload in the DEX;
    • derives the AES key from the fixed key block embedded in the bundled lib/<abi>/libti.cloak.so (key = salt XOR xor);
    • decrypts every Resources/*.bin asset with node:crypto aes-128-cbc, transparently gunzipping compressed payloads, and confirms the key by trial decryption across all bundled ABIs.
  • Still reports a clear error when the native libti.cloak.so (or the salt) is absent and the key therefore can't be derived.

version 2.1.0

  • Removed the Java/JDK dependency entirely — recovery now runs in pure JS.
    • APK unzip via fflate (replaces the apk_unpack apktool step).
    • Binary AndroidManifest.xml parsing via adbkit-apkreader (replaces the apktool text decode); a readable manifest is still written to the output.
    • 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).
    • 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.
  • Dropped the optional java and apk_unpack dependencies and the bundled ~13 MB java/ directory (apktool + jadx JARs).
  • Detects and clearly reports APKs using the newer, statically-unrecoverable ti.cloak / .bin encryption scheme.
  • Multidex aware (scans classes.dex, classes2.dex, …).

version 2.0.0

  • Full rewrite in TypeScript with an ESM + CommonJS dual build and shipped type definitions.
  • New promise-based API (TiRecover class + recover() helper); the old callback API has been removed (breaking change).
  • New commander-based CLI with --help, an info subcommand, --json, --no-reconstruct, --keep-tmp, --quiet, --debug flags and proper exit codes.
  • Implemented the previously pending reconstruct() (rebuild an openable Titanium project) and info() (Titanium metadata) methods.
  • Single shared JVM instance and async, non-blocking filesystem I/O.
  • The native java bridge and apk_unpack are now optional dependencies, so installs no longer fail on machines without a JDK.
  • Added a test suite (vitest) with synthetic fixtures.

version 1.1.1

  • now assets are put on the correct directories.

version 1.0.9

  • updated to latest apk_unpack to use jadx.
  • now resources and manifest are also copied to outputdir.

version 1.0.6

  • added ability to recover APKs created in development mode.

version 1.0.5

  • improved readability of CLI, added prettifier to source code, and bugfix several issues.

version 1.0.4

  • fixed tmp dir location bug. Now CLI works ok.

version 1.0.2-3

  • added delay before decrypting files, to account for slower hdd disks.

version 1.0.1

  • fixed console debug.

version 1.0.0

  • first version.