apfs — APFS FileVault 2
github.com/go-fde/apfs is pure-Go read/write support for APFS FileVault 2
full-disk encryption. The on-disk format is little-endian. It reads both Apple's
exact on-disk shape (what diskutil apfs encryptVolume produces) and a legacy
self-consistent shape produced by this package's own Format helpers.
Key hierarchy
APFS FileVault 2 protects volumes with AES-XTS block-level encryption:
passphrase
│ PBKDF2-SHA256
▼
KEK (Key Encryption Key) ─── AES-KW (RFC 3394) ──▶ wrapped KEK (Container Key Bag)
KEK ─── AES-KW (RFC 3394) ──▶ wrapped VEK (Container Key Bag)
VEK (Volume Encryption Key)
│ AES-XTS (per 512-byte sector)
▼
plaintext blocks
Key bag entries live at the location named by the NX container superblock's
nx_keylocker field (offset 1296). Two on-disk shapes are supported, dispatched
on the nx_flags & NX_CRYPTO_SW bit through the same apfs.Open /
apfs.OpenFrom entry points:
- Apple shape (
NX_CRYPTO_SWset): the keybag is encrypted at rest with AES-XTS-128 keyed oncontainerUUID || containerUUID; decrypted bytes follow Apple'sapfs_obj_phys+apfs_kb_lockerlayout. The container keybag's tag=2 entry carries an ASN.1 VEKBLOB; the volume keybag's tag=3 entry carries an ASN.1 KEKBLOB. - Legacy self-consistent shape (
NX_CRYPTO_SWclear): the keybag is plaintext on disk; tag=3 encodes KDF parameters (PBKDF2-SHA256 or Argon2id) plus the wrapped KEK inline, tag=2 holds the raw AES-KW(KEK, VEK) ciphertext. Used by this package'sFormat/FormatArgon2idhelpers and the test suite.
Supported features
| Feature | Status |
|---|---|
| APFS NX container superblock parsing | ✅ (nx_keylocker at +1296, nx_flags) |
| Apple-shape container keybag parsing (NX_CRYPTO_SW path) | ✅ |
| Apple-shape volume keybag parsing | ✅ |
| Legacy self-consistent keybag parsing | ✅ (auto-dispatched on nx_flags & 0x4) |
| Key derivation: PBKDF2-SHA256 | ✅ |
| Key derivation: Argon2id | ✅ (package-defined locker layout) |
| AES Key Wrap / Unwrap (RFC 3394) | ✅ |
Cipher: AES-128-XTS (vek_size = 32) |
✅ |
Cipher: AES-256-XTS (vek_size = 64) |
✅ |
| At-rest keybag XTS encryption / decryption | ✅ |
| ASN.1 VEKBLOB / KEKBLOB build + parse | ✅ |
| Keybag block builder | ✅ (PackKeybagBlock — Apple's exact obj_phys + apfs_kb_locker shape) |
| Personal Recovery Key (PRK) | ✅ (passphrase-style locker with a well-known UUID) |
| Institutional Recovery Key (IRK) | ✅ (RSA-OAEP wrapping of the KEK) |
| T2 / Secure Enclave mediated keys | ❌ (hardware access required) |
| Encrypted metadata (APFS snapshots, etc.) | ❌ (payload encryption only) |
Apple-shape encrypted-container support
Beyond the legacy format, this package exposes the byte-level primitives needed
to build and parse keybags in Apple's exact on-disk shape — the layout
diskutil apfs encryptVolume produces. The recipe was reverse-engineered
byte-for-byte against two independently-encrypted Apple reference DMGs and is
locked in by parity tests:
TestVEKBlob_HMACAgainstAppleReference— re-computes the HMAC over Apple's reference VEKBLOB and asserts byte-equality with Apple's stored value.TestKeybagChain_PassphraseUnlocksVEK— builds a complete Apple-shape container + volume keybag pair, then walks the structure end-to-end with only the passphrase + the two UUIDs + the two paddrs, recovering the VEK byte-for-byte — the same chain Apple'sapfs.kextwalks on mount.
The recipe (validated against Apple's reference):
- At-rest XTS: AES-XTS-128, key =
uuid || uuid(container UUID for the container keybag, volume UUID for the volume keybag), 512-byte XTS sectors, tweak =paddr × 8 + sector_index. - Keybag block layout: 32-byte
obj_phys(type0x6b657973"syek", sealed Fletcher-64 cksum) + 16-byteapfs_kb_locker(version=2, nkeys, nbytes including the 16-byte header AND the trailing 16-byte alignment pad) + 16-byte-aligned entries. - VEKBLOB / KEKBLOB: ASN.1 DER with HMAC-SHA256 keyed by
SHA-256(\x01\x16\x20\x17\x15\x05 || salt)over the[3]inner envelope. The[2]field is an 8-byte OCTET STRING (Apple's opaqueinfo_t), not a minimal-length INTEGER. The inner[3]contains the AES-KW(KEK, VEK) RFC-3394 ciphertext (40 bytes for a 32-byte VEK).
Usage
Open an encrypted APFS volume image
import "github.com/go-fde/apfs"
dev, err := apfs.Open("/dev/disk2s2", []byte("my passphrase"))
if err != nil {
log.Fatal(err)
}
defer dev.Close()
// Read decrypted blocks. Offsets are absolute byte offsets in the device,
// aligned to 512-byte sector boundaries.
buf := make([]byte, 4096)
_, err = dev.ReadAt(buf, 0)
// Write encrypted blocks.
_, err = dev.WriteAt(buf, 0)
Create a new APFS FDE container
Format and FormatOn write a minimal APFS NX superblock and key bag to an
existing file or block device, then return an open *Device ready for payload
I/O. Container parameters:
| Parameter | Value |
|---|---|
| Block size | 4096 bytes |
| NX superblock | block 0 |
| Key bag | block 1 (referenced via nx_keylocker at NX SB offset 1296) |
| Payload start | block 2 (byte offset 8192) |
| VEK / KEK size | 32 bytes each (AES-256) |
| Key derivation | PBKDF2-SHA256, 1000 iterations, 16-byte salt |
| Key wrapping | AES Key Wrap (RFC 3394) |
| Cipher | AES-256-XTS, 512-byte sectors |
This is the legacy self-consistent format (nx_flags & NX_CRYPTO_SW clear,
plaintext keybag). To produce the Apple-shape format byte-for-byte, use
go-filesystems/apfs.FormatContainerEncrypted (or …GPT), which sits on top of
this package.
// The file must exist before calling Format.
f, _ := os.Create("disk.apfs")
f.Close()
dev, err := apfs.Format("disk.apfs", []byte("passphrase"))
if err != nil { log.Fatal(err) }
defer dev.Close()
// WriteAt offset is absolute from the start of the container.
// Payload starts at block 2 = byte offset 2 × 4096 = 8192.
dev.WriteAt(myData, 2*4096)
FormatOn accepts the same blockRW interface as OpenFrom, enabling
container creation inside a QCOW2 virtual disk.
Detect an APFS container
ok, err := apfs.Detect("/path/to/disk.img")
if ok {
fmt.Println("this is an APFS container")
}
Layer on top of another block device (e.g. QCOW2)
OpenFrom accepts any value satisfying:
interface {
io.ReaderAt
WriteAt([]byte, int64) (int, error)
io.Closer
}
import (
apfsfde "github.com/go-fde/apfs"
image_qcow2 "github.com/go-diskimages/qcow2"
)
qdev, err := image_qcow2.OpenDevice("disk.qcow2")
if err != nil { log.Fatal(err) }
// apfsDev.Close() also closes qdev.
apfsDev, err := apfsfde.OpenFrom(qdev, []byte("passphrase"))
if err != nil {
qdev.Close()
log.Fatal(err)
}
defer apfsDev.Close()
Block addressing
ReadAt/WriteAt use absolute byte offsets from the start of the
underlying device (block 0 of the APFS container). Reads and writes must be
aligned to 512-byte sector boundaries with lengths that are multiples of 512
bytes — constraints imposed by AES-XTS, which operates on fixed 512-byte
sectors. The XTS tweak for a sector is byteOffset / 512, matching Apple's
kernel com.apple.filesystems.apfs kext.
Recovery keys
- Personal Recovery Key (PRK) — a regular passphrase locker tagged with the
well-known UUID
EBC6C064-0000-11AA-AA11-00306543ECAC. Add one withAddRecoveryKey(rw, existing, prk); unlock by passing it as the passphrase. - Institutional Recovery Key (IRK) — an RSA keypair held by an MDM
administrator. The KEK is wrapped with RSA-OAEP-SHA256 under the public key
(
AddInstitutionalKey); unlock later with the private key viaOpenWithPrivateKey.
Argon2id format
Apple does not document an on-disk layout for Argon2id-protected lockers. This
package defines a self-consistent layout (kdfType, time/memory/parallelism
costs, salt, wrapped KEK) used by FormatArgon2id / FormatArgon2idOn and
AddArgon2idPassphrase.
On-disk format references
- Apple File System Reference (developer.apple.com)
- "Infiltrate the Vault: Security Analysis and Decryption of Lion Full Disk Encryption" — Ligh, Walters et al. (PasswordsCon 2012)
apfs.handIOKit/storage/APFS/APFSConstants.hfrom the Darwin xnu tree- RFC 3394 — AES Key Wrap Algorithm
Note
hdiutil create -encryption AES-256 is DMG-envelope (UDIF) encryption,
not APFS FDE — a different layer entirely.