Census of iOS-backup implementations
A comparison of ios-backup-core against independent implementations, run
2026-08-21. Recorded here rather than in validation.md because it is a
method with limits worth stating, not a result.
The question
Does ios-backup-core disagree with any independent implementation on a
load-bearing format decision, and does each disagreement indicate a defect in
our code, in theirs, or a legitimate divergence?
How the population was enumerated, and why it is a floor
Two differently-shaped instruments, because one instrument's blind spot is invisible from inside it.
Instrument 1 — GitHub repository search, ten query phrasings, union of 23
repositories. It missed every implementation already known to exist:
jsharkey13/iphone_backup_decrypt, avibrazil/iOSbackup,
dinosec/iphone-dataprotection, mvt-project/mvt, datatags/mount-ios-backup.
Repo search matches names and descriptions, so it finds projects that describe
themselves as backup tools and misses libraries that merely are one. Reported
here as a failed instrument, not as a population.
Instrument 2 — GitHub code search on format-internal constants (WPKY,
DPSL, BackupKeyBag, ManifestKey). A parser cannot implement the keybag
without these strings whatever it calls itself, so this does not depend on
self-description. It surfaced ~153 repositories touching keybag or manifest
internals, of which perhaps 25 are genuine implementations (the rest are LLVM
and AWS-SDK forks matching the tokens incidentally).
This is a floor, not a census, and the reason is mechanical: every query returned exactly 60 results, which is the API's page limit. A capped query yields a lower bound on the population and never a total. No proportion, percentage or "most implementations" claim is made anywhere in this document, because the denominator is unknown.
The corpus actually read
Nine implementations, chosen for lineage diversity rather than count.
Implementations descended from iphone-dataprotection are not independent
evidence of one another — agreement among five of them is one observation, not
five.
| Implementation | Language | Lineage |
|---|---|---|
dinosec/iphone-dataprotection |
Python | the root reference |
datatags/mount-ios-backup |
Python | vendors iphone-dataprotection |
mvt-project/mvt |
Python | delegates to iOSbackup |
avibrazil/iOSbackup |
Python | independent-ish |
jsharkey13/iphone_backup_decrypt |
Python | independent-ish |
jfarley248/iTunes_Backup_Reader |
Python | independent-ish |
MaxiHuHe04/iTunes-Backup-Explorer |
Java | independent |
richinfante/ibackuptool2 |
JS/TS | independent |
kusaanko/iOSBackupBrowser |
Java | independent |
fox-it/dissect.target |
Python | independent (Fox-IT) |
philsmd/itunes_backup2hashcat |
Perl | independent (keybag only) |
Where we agree
Every implementation read agrees with ios-backup-core on: the keybag TLV
grammar; the header-vs-class UUID/WRAP rule; the class-only tag set
(CLAS WRAP WPKY KTYP PBKY); the two-round derivation
(PBKDF2-SHA256 over DPSL/DPIC, then PBKDF2-SHA1 over SALT/ITER); the
WRAP & 2 passcode test; the RFC 3394 integrity check; the 40-byte wrapped-key
width; the ManifestKey 4-byte little-endian class prefix; the all-zero IV for
Manifest.db; and EncryptionKey as NS.data[4:] behind an NSKeyedArchiver
UID.
Where we deliberately differ
| Decision | Population | Ours | Why |
|---|---|---|---|
| Plaintext length | strip PKCS#7 | truncate to recorded Size, PKCS#7 only as fallback |
the manifest states the length; padding is not evidence (ADR-0003) |
| Decrypted data | written to disk (MVT, iOSbackup) |
held in memory | no plaintext copy of evidence with an untracked lifetime (ADR-0002) |
| Encryption detection | probe whether Manifest.db parses (MVT) |
declaration and probe, kept separately | distinguishes encrypted from corrupt, which a probe alone cannot (ADR-0009) |
| Partial keybag unwrap | fail whole (iphone-dataprotection) |
recover every class that unwraps | a damaged keybag should yield what it can |
| Malformed input | exceptions / silent skips | typed errors carrying the offending value | ADR-0012 |
Defects the comparison found in our code
Three, all of the same family — a refusal counted as zero — and all now fixed with regression tests:
- Path traversal via
fileID(from MVT).Path::joinreplaces the whole path on an absolute argument, so afileIDof/etc/passwdaddressed the examiner's filesystem. → ADR-0008,core/tests/path_traversal.rs. - Absent
Sizeread as zero (frommount-ios-backup). A row recording noSizetruncated a real file to nothing and returnedOk. → ADR-0003 as amended,core/tests/missing_size.rs. - Manifest rows silently dropped (from this census).
fileIDstored as a BLOB is legal — SQLite enforces affinity, not type — and such a row vanished from the tree with no trace. →core/tests/row_loss.rs,Backup::unreadable_rows().
The third is the worst of the three: the loss was at the collection stage, so no analyzer could flag it and no examiner could know to look.
What this comparison cannot support
- It is not a census. The population is capped at the API page limit, so no proportion is claimed and "most implementations" appears nowhere.
- Comparing against source is T2, not T1. Executing another implementation
on the same artifact and reconciling the output would be T1; that needs a real
backup and its password, which is the gap
validation.mdalready records. - The regex matrix understates.
itunes_backup2hashcatscored zero on every dimension and in fact implements the KDF — its Perl usesindex($data, "SALT", …), which the patterns could not see. That is a Type II in the instrument, and it means every absent cell in that matrix is unreliable. The agreements above rest on reading the source, not on the grep. - Elusion is unmeasured. No sample was drawn from the ~130 repositories the code search surfaced and this corpus did not read, so the rate of disagreements missed is unknown. "We agree with the population" is bounded to the eleven implementations listed.