Map schema¶
schema/rosetta-map.schema.json is the canonical JSON Schema (draft-07) for
the schema_version: 5 map format. It is the single, language-neutral source of
truth for the format — this repo owns it because this repo owns the data it
describes.
The published map is a pure real→obfuscated mapping. As of schema_version: 4
the AIDL/Binder-specific fields (aidl_descriptor, aidl_txn, and the
aidl_stub / aidl_callback kind values) and the descriptive anchors[]
array were removed: no resolver read them, and finding-evidence belongs in
the signatures source, not the artifact (see Anchoring evidence types below and
schema-migration.md).
CI validates every map under maps/ against this file. It is also the reference
for field semantics when authoring a map by hand.
Clients track this schema¶
The adapters that consume these maps are clients of the schema, not owners of it:
- rosetta-frida (TypeScript) — the first-class client. It carries a validator that tracks this schema and is contract-tested against it.
- rosetta-xposed (Kotlin) — consumes the same format on-device.
Changing the format means bumping this schema first, then updating the client adapters to match — never the other way around.
Editor setup (optional)¶
In VS Code, point your settings at the schema for files under maps/ to get
autocompletion and inline validation:
// .vscode/settings.json
{
"json.schemas": [
{
"fileMatch": ["maps/**/*.json"],
"url": "./schema/rosetta-map.schema.json"
}
]
}
The canonical artifacts intentionally omit a $schema key so they stay
byte-faithful with the maps rosetta-frida emits.
Provenance fields¶
Record provenance on the map itself rather than in free text:
sources[]— which tool(s) produced which entries (tool,config,classes). Free-form provenance prose (anotesstring) was removed in v5: it has no reader in the published artifact, so it belongs in asignatures/<app>/signatures.yamlcomment, not the map.- per-class
source. signer_sha256— the lowercase-hex SHA-256 of the signing certificate(s), if you read them. This pins publisher authenticity and detects repacks. Format-checked: each value must match^[0-9a-f]{64}$— exactly 64 lowercase hex chars, no colon separators (the canonical on-disk form isabcd…, not theAB:CD:…certificate-fingerprint spelling some tools print). It may be a single string or a non-empty array of such strings (an app may present several signing certs; clients match-any). Clients enforce it fail-closed, so omit the key entirely if you don't have it — don't leave a placeholder, which would alwaysSignerMismatch.
To read it off a device, given only the package name — this locates the base APK, pulls it, and prints just the signing-certificate digest(s), one line per signer, in the canonical colon-free lowercase hex this field wants (paste the output straight in):
PKG=com.example.app; APK="${TMPDIR:-.}/base.apk"; adb pull "$(adb shell pm path "$PKG" | tr -d '\r' | sed -n 's/^package:\(.*base\.apk\)$/\1/p')" "$APK" && apksigner verify --print-certs "$APK" | awk '/^Signer .*certificate SHA-256 digest:/ && !seen[$NF]++ {print $NF}'; rm -f "$APK"
The awk filter is deliberate: it matches any Signer …certificate SHA-256
digest: line and prints the bare digest, skipping the Source Stamp Signer
certificate line apksigner also emits — the Play-distribution source stamp
is a separate cert and is not what signer_sha256 records (pasting it
would always SignerMismatch). The anchor is ^Signer rather than
^Signer #N on purpose: an app that has rotated its signing key (v3
signer lineage) makes apksigner label each cert by SDK range
(Signer (minSdkVersion=…, maxSdkVersion=…) certificate SHA-256 digest:)
instead of Signer #1, so the narrower anchor would silently print nothing.
Such an app legitimately has more than one signing cert (the current key and
its predecessors); record all of them as the signer_sha256 array so a
client matches whichever the device reports for its API level. !seen[$NF]++
de-duplicates. ${TMPDIR:-.} keeps it portable to environments without a
/tmp (e.g. Termux running on-device).
- captured_at — the date you captured the map, as an ISO YYYY-MM-DD date.
- generated_from — optional { "signatures_rev": "<git sha>" } binding the
map to the exact signatures revision it was generated from.
- status — optional lifecycle marker (active / superseded / retracted;
absent ⇒ active). When superseded, set superseded_by to the
version_code that replaces this map (the semantic validator enforces this
pairing).
- client_hints — an optional sub-object for advisory, client-specific hints
that the core resolver ignores (frida_min_version, frida_max_version). They
live under client_hints rather than at the top level so the top-level
identity keys stay clean and the hints read as non-authoritative. client_hints
is itself closed (additionalProperties: false): adding a new hint key is a
deliberate schema change (bump this schema, then the client adapters), not a
silent extension — an unrecognised key is rejected rather than ignored.
The top-level object, sources[] entries, and every class / method / field
entry are closed (additionalProperties: false): an unknown or misspelled
key (e.g. extneds, signer_sh256) is rejected rather than silently dropped.
Anchoring evidence types¶
A map is reproduced from its signatures/<app>/signatures.yaml, which pins
each class on the most rotation-stable evidence available. The evidence
taxonomy is generic-first — these are the default because they work for any
class:
- String literals — endpoint URLs, log tags,
static final Stringconstants, field/key names reached by live code. - Superclass / framework parent — an obfuscated class still extends a
non-rotating
android/androidx/javaparent or implements a stable interface. - Constants — a
static finalvalue or magic number. - Structural / cross-class — a resolved class's descriptor referenced elsewhere, or a distinctive method-table shape.
- AIDL / Binder descriptor — a niche special case when present: a
.Stubembeds its binder descriptor verbatim and it never rotates, but most classes have no AIDL contract, so it is the exception, not the rule.
This describes the authoring evidence, not on-disk schema fields — the format
itself is defined only here in schema/rosetta-map.schema.json (this section
adds no fields). See templates/signatures.template.yaml and
Contributing for the worked authoring flow.
Input bounds and key safety¶
The schema imposes defensive caps so a malformed or hostile map can't blow up a client. These exact limits are mirrored by the rosetta-frida (Zod) and rosetta-xposed clients and must stay in lockstep:
- Sizes:
classes≤ 50000 entries; per-classmethodsandfields≤ 5000 each; a method-overload array is 1–200 entries;sources≤ 100. - String lengths: obfuscated / short names ≤ 512;
signature≤ 4096;appandversion≤ 256; other free-text strings ≤ 4096. - Identifier shapes:
appmust match^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+$(a dotted Java package id — every segment must start with a letter, socom.2exampleis rejected);version_codeis an integer ≥ 0;versionmust be non-blank — the guarantee comes from two complementary constraints:minLength: 1rejects the empty string""and the\Spattern rejects an all-whitespace label like" ";signer_sha256matches^[0-9a-f]{64}$. - Reserved-key rejection: the
classes,methods, andfieldsobjects reject the keys__proto__,constructor, andprototype(prototype-pollution guard for JS clients).
Every numeric bound above is pinned in BOTH directions by the curated samples
CI checks (schema/samples/): valid/bounds-at-max.json exercises the
at-the-limit values (app/version at 256, obfuscated at 512, sources at
100, an overload array at 200) and the invalid/*-too-long / invalid/*-too-many
samples each push exactly one of those bounds one over the limit. The whole-file byte ceiling (MAX_MAP_BYTES, the maps-side
equivalent of the clients' input-byte cap) is a CI-workflow guard in
validate.yml, since JSON Schema cannot express a total-document-size or
nesting-depth limit; those two remain client-/CI-enforced rather than schema
keywords.