How Apple DMG hashes are extracted
A technical walkthrough of how Hash Extractor parses encrypted Apple disk images to produce a crackable $dmg$ hash for John the Ripper.
Hash Extractor parses encrypted Apple disk images entirely in the browser — no upload, no server — and emits the $dmg$ hash that John the Ripper understands. This post walks through exactly how that parsing works, from magic-byte detection to the final hash string, for both on-disk layouts macOS has used.
Two layouts, one format name
Apple has used two distinct binary layouts for encrypted .dmg files. Both produce a $dmg$ hash, but the version number in the hash reflects which layout was found:
| Version | Magic bytes | Position |
|---|---|---|
| v1 | cdsaencr | Last 8 bytes of the file |
| v2 | encrcdsa | First 8 bytes of the file |
Detection checks v2 first (a starts_with on the buffer), then v1 (a tail comparison), and falls back to file extension when neither magic is found.
v1: the tail-anchored header
In the v1 layout the entire encryption header lives at the end of the file. The parser subtracts a fixed header size of 1276 bytes from the file length to find the header base h, then reads fields at these offsets relative to h:
| Offset | Size | Field |
|---|---|---|
h + 48 | 4 bytes BE | PBKDF2 iteration count (iter) |
h + 52 | 4 bytes BE | Salt length (salt_len) |
h + 56 | salt_len bytes | Salt |
h + 136 | 4 bytes BE | Wrapped AES key length (waes_len) |
h + 140 | waes_len bytes | Wrapped AES key |
h + 436 | 4 bytes BE | Wrapped HMAC-SHA1 key length (hmac_len) |
h + 440 | hmac_len bytes | Wrapped HMAC-SHA1 key |
The cdsaencr magic sits at the very end of the header (bytes h + 1268 through h + 1275), which is also the last 8 bytes of the file.
v1 hash format
$dmg$1*<saltlen>*<salt_hex>*<waes_len>*<waes_hex>*<hmac_len>*<whmac_hex>*<iter>
Breaking down a real v1 example
The unit test in dmg.rs constructs a minimal v1 image with:
iter = 1000salt_len = 20, salt bytes0x01..0x14waes_len = 32, AES key bytes0x00..0x1fhmac_len = 48, HMAC key bytes0x00..0x2f
That produces a hash beginning with:
$dmg$1*20*0102030405060708090a0b0c0d0e0f1011121314*32*...
and ending with *1000. Reading left to right:
1— v1 layout20— 20-byte salt0102...1314— the 20 salt bytes in hex32— 32-byte wrapped AES key- (wrapped AES key hex)
- (wrapped HMAC-SHA1 length)
- (wrapped HMAC-SHA1 hex)
1000— PBKDF2 iterations
John the Ripper uses PBKDF2-HMAC-SHA1 with the provided salt and iteration count to derive a candidate key, then attempts to unwrap the AES key blob. A successful unwrap confirms the password.
v2: the front-anchored header with data chunks
The v2 layout places the encrcdsa magic at byte 0. Beyond the header fields, the cracker also needs two plaintext data chunks from the encrypted image to verify a candidate password — those chunks are read directly from the file at computed offsets.
Header fields
All integers are big-endian:
| Offset | Size | Field |
|---|---|---|
| 56 | 8 bytes | datasize — total encrypted data size in bytes |
| 64 | 8 bytes | dataoffset — byte offset where encrypted data begins |
| 104 | 4 bytes | KDF iteration count (iter) |
| 108 | 4 bytes | salt_len |
| 112 | up to 32 bytes | Salt (capped at 32 bytes) |
| 148 | 32 bytes | blob_enc_iv — IV for the encrypted key blob |
| 196 | 4 bytes | keyblob_size |
| 200 | up to 64 bytes | keyblob_field (hex-encoded, zero-padded to keyblob_size * 2 chars) |
Computing the data chunks
The cracker needs two AES blocks from the encrypted payload to test a decryption attempt. Their positions are derived from datasize and dataoffset:
cno = ceil(datasize / 4096) - 2
data_size = datasize - cno * 4096
chunk1 starts at: dataoffset + cno * 4096 (length: data_size bytes)
chunk2 starts at: dataoffset (length: 4096 bytes)
cno is the second-to-last 4096-byte block index. chunk2 is always the very first 4096-byte block of the encrypted data. Both are embedded verbatim in the hash as hex strings.
v2 hash format
$dmg$2*<saltlen>*<salt_hex>*32*<iv_hex>*<keyblob_size>*<keyblob_hex>*<cno>*<data_size>*<chunk1_hex>*1*<chunk2_hex>*<iter>
The 32 before <iv_hex> is the fixed IV length. The 1 before <chunk2_hex> is a literal constant in the format (chunk2 is always one 4096-byte block).
No hashcat mode
Unlike many other formats, there is no dedicated hashcat mode for $dmg$. John the Ripper with --format=dmg is the standard cracker for both v1 and v2 hashes.
Cracking a DMG hash with John
Once you have the hash from Hash Extractor:
echo '$dmg$1*20*0102030405060708090a0b0c0d0e0f1011121314*32*...*1000' > hash.txt
john --format=dmg --wordlist=/usr/share/wordlists/rockyou.txt hash.txt
The same command works for v2 hashes — John detects the version from the $dmg$2* prefix automatically.
Why client-side WASM matters
Encrypted disk images can contain sensitive data. Uploading a .dmg to a remote service to extract its hash exposes the file before you have even started cracking. Hash Extractor runs the entire parser — Rust compiled to WebAssembly — inside your browser. The image bytes never leave your machine; only the hash string (which contains no plaintext data) is shown to you. This mirrors what dmg2john does on the command line, but with no toolchain to install and no file transfer.
Summary
| v1 | v2 | |
|---|---|---|
| Magic position | EOF | SOF |
| Magic value | cdsaencr | encrcdsa |
| KDF | PBKDF2-HMAC-SHA1 | PBKDF2-HMAC-SHA1 |
| Data chunks in hash | No | Yes (chunk1 + chunk2) |
| Hash prefix | $dmg$1* | $dmg$2* |
| Cracker | John --format=dmg | John --format=dmg |
Both layouts are handled automatically: drop a .dmg into Hash Extractor and it detects the magic, picks the correct parser, and emits a ready-to-crack hash string.