rea-reverse

Reverse Engineer a HarmonyOS HAP App Package with REA

Written for REA 6.7.0Published by rea-reverseUpdated 7 min read
On this page

A HarmonyOS app ships as a HAP: a HarmonyOS Ability Package. Under the suffix it is an ordinary ZIP archive, but its contents are HarmonyOS-specific. Instead of Android’s classes.dex, a HAP carries ArkTS compiled to Ark bytecode (.abc). Instead of a single AndroidManifest.xml, it carries a module.json (the Stage model) or a config.json (the older FA model) that declares the module’s abilities. Alongside those sit a compiled resource index, native .so libraries, and signing material.

REA, the open-source project by morluto, has a capability for exactly this file: project-harmony-application-graph. It takes authenticated inventory Evidence for a .hap, .hsp or App Pack (.app) and projects it into one HarmonyOS-shaped structure: which paths are manifests, which are bytecode, which are resources, which are native libraries, which runtime families are present, and where a managed-to-native bridge might cross. Every entry carries the SHA-256 of its exact bytes.

Who this is for: developers shipping on HarmonyOS who want a repeatable map of their own .hap, and anyone studying how OpenHarmony apps are assembled. You need to be comfortable in a terminal and know roughly what a ZIP looks like from the inside.

What this capability does, and what it does not. Be clear about this before you start, because it shapes what the output means. The projection is execution-free and deterministic: it reads exact paths and content hashes and classifies them by path and format. It does not decode module.json, does not enumerate your abilities by name, does not parse Ark bytecode, and does not verify a signature. In REA’s own words, “manifest, module, resource, and signing semantics require a dedicated HarmonyOS provider; this projection reports exact inventory paths and hashes only.” So this is the structural, evidence-grade first pass — the skeleton you anchor deeper work to — not a module.json pretty-printer.

If you have analyzed an Android APK with REA, the shape will feel familiar: it is the HarmonyOS sibling of project-android-application-graph.

Respect the license. Analyze apps you built, open-source OpenHarmony projects, or packages whose license permits it. This article only reads metadata and content hashes; it launches nothing. See REA’s DISCLAIMER.md.

Reverse Engineer a HarmonyOS HAP App Package with REASilent walkthrough with English captions.

Prerequisites

  1. REA 6.7.0: npm install --global [email protected], or prefix commands with npx rea-agents@latest. Connect it to your agent with npx rea-agents setup (/getting-started-rea-mcp/).
  2. No HarmonyOS SDK, DevEco Studio or emulator. A HAP is a ZIP, and both steps below — inspect-artifact and project-harmony-application-graph — are pure parsing in TypeScript. They run on macOS, Linux or Windows with nothing but Node and REA. project-harmony-application-graph has no side effects at all: it only produces Evidence.
  3. A .hap to look at. Build one in DevEco Studio, or take an OpenHarmony sample app you compiled yourself.

We verified the command surface and behavior for this article against the npm [email protected] tarball on macOS. We did not have a signed production HarmonyOS app to publish findings from, so rather than invent module names or counts, the run below uses a minimal HAP we assembled by hand — a module.json, one .abc, a resource index, one ELF .so and a certificate entry, zipped and renamed to demo.hap. The field values are real output from REA 6.7.0 on that file; only the sample is synthetic, and it is labelled as such.

Step 1: Inventory the HAP

The projection does not open the archive itself. It consumes inventory Evidence, so you produce that first with inspect-artifact:

rea inspect-artifact /absolute/path/demo.hap --json > inspection.json

inspect-artifact returns the complete content-addressed inventory: every archive entry with its logical path, declared size, SHA-256 and detected format, plus integrity observations and a list of suggested next probes. It recognizes the .hap suffix as a HarmonyOS package root (.hsp and, from verified ZIP magic, .app too), which is what lets the HarmonyOS projection accept it in the next step.

One honest limitation travels with every ZIP inventory and is worth internalizing now: it lists central-directory entries only. Bytes that are not entries — a ZIP comment, or a signing block inserted before the central directory — are not occurrences. That matters for signing, as we will see.

Step 2: Project the HarmonyOS graph

project-harmony-application-graph takes a single JSON argument: a workflow object with an inventory_evidence array. The inventory Evidence that inspect-artifact produced is nested one level down in its result, so lift it out:

jq '{inventory_evidence: [.normalized_result.substeps[].evidence]}' \
  inspection.json > harmony-input.json

rea project-harmony-application-graph ./harmony-input.json --json > harmony-graph.json

You can pass the JSON inline instead of a file path, but a file is easier to re-run. The command is strict about its input: if you feed it inventory Evidence whose root format is not hap, hsp or app-pack, it refuses with a TypeError naming the format it got, rather than guessing.

Step 3: Read the structure

Every field in the result is derived deterministically from the inventory. Pull the parts that matter:

jq '.normalized_result | {root_format, packaging_model, runtime_families, coverage}' harmony-graph.json
jq '.normalized_result.components | map_values(map(.path))' harmony-graph.json

On our minimal HAP this returned:

{
  "root_format": "hap",
  "packaging_model": "stage",
  "runtime_families": ["ark", "native"],
  "coverage": { "status": "complete-within-inventory", "inventory_complete": true }
}
{
  "manifests":        ["module.json"],
  "bytecode":         ["ets/modules.abc"],
  "resources":        ["resources.index", "resources/base/element/string.json"],
  "native_libraries": ["libs/arm64-v8a/libentry.so"],
  "javascript":       [],
  "signing":          ["META-INF/CERT.cer"]
}

Each component is more than a path. A bytecode entry, for example, comes back as its path, a content-addressed artifact_id, the SHA-256 of its bytes, and a detected format — our ets/modules.abc reported format: "file", while libs/arm64-v8a/libentry.so reported format: "elf" because REA read its ELF magic rather than trusting the .so suffix. The result also carries a projection_id prefixed hmp_, the root_sha256 of the whole package, and source_evidence_ids tying the projection back to the inventory it came from. Nothing here is a name you supplied; it is all read from bytes.

Here is how to read each field:

packaging_model is a path-presence heuristic, and REA says so. It claims stage only when a module.json is present and fa only from a config.json. Our sample has module.json, hence stage. Treat this as a strong hint, not a verdict — unusual layouts can carry both.

components sorts every inventoried file into six buckets by path and format: manifests (module.json, config.json, pack.info), bytecode (.abc, .pa), resources (resources.index and the resources/ tree), native_libraries (anything detected as ELF, or matching libs/<abi>/*.so), javascript (.js/.mjs/.cjs or a detected JS bundle), and signing (certificate-entry suffixes under META-INF/). This is the map of the package.

runtime_families summarizes what runs inside. Bytecode present adds ark (the ArkCompiler runtime), any ELF adds native, JavaScript adds javascript, and path conventions can also surface react-native, flutter or unity. Our sample reports ["ark", "native"].

bridge_candidates pairs managed bytecode with native libraries as hypotheses about where ArkTS calls into C/C++ through N-API. On our file it proposed one pair — ets/modules.abc ↔ libs/arm64-v8a/libentry.so, basis napi-library-convention. These are path-based guesses to investigate, not decoded N-API declarations or observed calls.

coverage tells you whether absence means anything. complete-within-inventory with inventory_complete: true means every inventoried occurrence resolved to content, so an empty bucket is a real absence within what was inventoried. A partial status means you cannot conclude anything from a missing component.

Reading the limitations before you trust a conclusion

The projection returns a limitations array, and it is not boilerplate — each line closes off a specific wrong conclusion. Two deserve attention.

An empty signing list does not mean the package is unsigned. The signing bucket lists META-INF certificate-entry suffixes only. The signature a HarmonyOS packaging tool actually applies lives in the ZIP signing block, which sits before the central directory — and as Step 1 noted, non-entry bytes are not inventory occurrences. So a HAP can be properly signed and still show an empty signing list here. Verifying the signature is a separate job.

A .abc suffix does not prove valid Ark bytecode, and runtime_families is inferred from formats and paths, not from parsing. The buckets tell you what the package is shaped like, not that every file is well-formed.

REA also notes that it does not identify HarmonyOS .har shared libraries, because .har is also the suffix for an HTTP Archive JSON file, so that call would need verified ZIP bytes.

HSP and App Pack

The same two steps handle the other HarmonyOS package types. An HSP (Harmony Shared Package) inventories and projects exactly like a HAP; root_format comes back hsp. An App Pack (.app) is the distribution bundle that wraps several .hap/.hsp modules. When root_format is app-pack, the result adds an app_pack_children array listing the packaged modules by path. Note the boundary: those children are listed, not recursively opened. To go inside a child module, inventory and project it on its own.

Troubleshooting

project-harmony-application-graph refuses with a format error. It only accepts hap, hsp or app-pack inventory Evidence. If you pointed inspect-artifact at a .zip or a loose directory, its root format is not HarmonyOS and the projection rejects it by design. Rename to the correct suffix only if the file genuinely is that package type.

jq returns null for the evidence. The inventory Evidence is at .normalized_result.substeps[].evidence in the inspect-artifact output. Check the top-level keys of inspection.json if your REA version nests it differently.

Every component bucket is empty but coverage is complete. Confirm you inventoried the HAP itself and not an outer wrapper. Then take the empty buckets at face value — within a complete inventory, empty means absent.

Next steps

This projection gives you the authenticated skeleton. Where you go next depends on what the structure revealed: