Inspect an NSKeyedArchiver Object Graph with REA
On this page
Short answer: use rea inspect-keyed-archive archive.bin --json to see the archive’s named roots, object table and references. A UID is an index into that table; it is not the object itself. REA reads the serialized graph without instantiating its classes. It cannot tell you what a custom class would do when an application decodes it.
This is useful when a plist contains $archiver, $objects and $top, but an ordinary plist viewer leaves you with a list of numeric UIDs. The macOS app walkthrough introduces this command as part of a broader investigation. Here we follow one small graph and then page its object table.
We tested the commands below with REA 6.8.0 and Node.js 26.12.1 on macOS arm64. REA 6.8.0 requires Node.js ^22.23.4 || ^24.22.1 || >=26.12.1. The archive is a deliberately hand-built, synthetic binary plist: it demonstrates the serialized structure, not a file generated by a real application or a claim that Foundation would accept the custom self-reference as a decoded object.
Make a small archive
Python 3’s plistlib can write binary plist UID values. This creates three table entries: the nil sentinel, a dictionary and a string. The dictionary points to itself through peer.
python3 - <<'PY'
import plistlib
archive = {
'$archiver': 'NSKeyedArchiver',
'$version': 100000,
'$objects': [
'$null',
{'name': plistlib.UID(2), 'peer': plistlib.UID(1)},
'demo',
],
'$top': {'root': plistlib.UID(1)},
}
with open('demo.archive', 'wb') as f:
plistlib.dump(archive, f, fmt=plistlib.FMT_BINARY, sort_keys=False)
PY
Use a sample you own, or a copy of a file you are authorized to inspect. For the following commands, install REA as described in the setup guide and check rea --version.
Inspect the roots and object table
rea inspect-keyed-archive demo.archive --json > archive.json
The tested output reported archive_format: binary-plist, total_objects: 3 and total_references: 3 inside normalized_result. The relevant fields, shortened for readability, were:
{
"roots": {"root": {"UID": 1}},
"objects": [
{"id": 0, "kind": "nil", "value": "$null"},
{"id": 1, "kind": "dictionary", "value": {"name": {"UID": 2}, "peer": {"UID": 1}}},
{"id": 2, "kind": "scalar", "value": "demo"}
]
}
Start at roots.root: UID 1 selects the dictionary. Its name points to object 2, the string demo; its peer points back to object 1. The separate references list records those edges, including the self-reference. Do not recursively expand references without tracking visited IDs: a cycle would otherwise keep you walking in circles.
On macOS, plutil -p demo.archive gives a useful independent view of the raw plist. Its printed UID wrapper includes process-specific addresses; compare the value numbers, not those addresses. REA’s evidence also contains an archive SHA-256, so you can identify the exact bytes inspected.
Page without losing object IDs
For a large archive, request a small slice:
rea inspect-keyed-archive demo.archive --offset 1 --limit 1 --json
In this sample the page contains object ID 1, not a renumbered object 0. It reports offset: 1, next_offset: 2 and truncated: true. Request the next slice with --offset 2 --limit 1. That page contains object 2 and has next_offset: null; in the tested result truncated still says true, so use next_offset to decide whether another page exists rather than assuming truncated alone is a next-page cursor. References are reported separately from the selected object slice: a referenced target need not be in the current page.
If an archive has multiple named entries in $top, select one explicitly:
rea inspect-keyed-archive demo.archive --root root --json
The sample has only root. Asking for --root absent returned invalid_request with the available root name. A --limit 0 request failed argument validation; use a positive limit.
What the graph does and does not establish
The parser reports serialized fields, object IDs, references and class names when present. It does not instantiate archive classes or establish custom class semantics. UID 0 can represent nil, but the serialized value alone cannot distinguish an ordinary nil from a conditional reference encoded as UID 0. A missing field stays missing; it is not evidence that an application would use a particular default.
Keep the original file and its digest, inspect an authorized copy, and treat the graph as static evidence. If you need to understand how an app uses a field at runtime, inspect the relevant decoding code separately; the archive by itself does not show that execution.
Next steps
- Reverse engineer a macOS app with REA to find candidate resources and binaries.
- Inspect an iOS provisioning profile for a different plist-based signing artifact.