Help us shape the future of Go! Take the 2026 Go Developer Survey

apfs

package module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 31, 2026 License: BSD-3-Clause Imports: 17 Imported by: 3

README

go-fde/apfs

apfs

ci Go Reference

Pure-Go read/write support for APFS FileVault 2 full-disk encryption.

Overview

APFS FileVault 2 protects volumes using AES-XTS block-level encryption. The key hierarchy is:

passphrase
    │  PBKDF2-SHA256
    ▼
  KEK (Key Encryption Key)  ─── AES-KW (RFC 3394) ──▶  wrapped KEK
                                                          stored in
                                                        Container Key Bag

  KEK ─── AES-KW (RFC 3394) ──▶  wrapped VEK
                                  stored in
                                Container Key Bag

  VEK (Volume Encryption Key)
      │  AES-XTS (per 512-byte sector)
      ▼
  plaintext blocks

Key bag entries are stored at the location named by the NX container superblock's nx_keylocker field (offset 1296). Two on-disk shapes are supported:

  • Apple shape (selected when nx_flags & NX_CRYPTO_SW): keybag is encrypted at rest with AES-XTS-128 keyed on containerUUID || containerUUID; decrypted bytes follow Apple's apfs_obj_phys + apfs_kb_locker layout. The container keybag's tag=2 entry carries an ASN.1 VEKBLOB; the volume keybag's tag=3 entry carries an ASN.1 KEKBLOB. See Apple-shape encrypted-container support (F-2) below.
  • Legacy self-consistent shape (selected when the NX_CRYPTO_SW bit is clear): keybag is plaintext on disk; tag=3 entry encodes KDF parameters (PBKDF2-SHA256 or Argon2id) plus the wrapped KEK inline, tag=2 entry holds the raw AES-KW(KEK, VEK) ciphertext. Used by this package's Format / FormatArgon2id helpers and by the test suite. See Argon2id format below for that layout.

unlockVEK dispatches between the two shapes based on the nx_flags & NX_CRYPTO_SW bit; callers go through the same apfs.Open / apfs.OpenFrom public entry points either way.

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 (pre-F-2 format) ✅ (auto-dispatched on nx_flags & 0x4)
Key derivation: PBKDF2-SHA256 ✅
Key derivation: Argon2id ✅ (package-defined locker layout, see Argon2id format below)
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 ✅ (DecryptKeybagBlocks / EncryptKeybagBlocks for any block size from 4096 to 65536; EncryptContainerKeybag, EncryptVolumeKeybag, DecryptContainerKeybag, DecryptVolumeKeybag for 4096)
ASN.1 VEKBLOB / KEKBLOB build + parse ✅ (BuildVEKBlob, BuildKEKBlob, ParseVEKBlob, ParseKEKBlob)
Keybag block builder ✅ (PackKeybagBlock — emits Apple's exact obj_phys + apfs_kb_locker shape)
Personal Recovery Key (PRK) ✅ (passphrase-style locker tagged with a well-known UUID)
Institutional Recovery Key (IRK) ✅ (RSA-OAEP wrapping of the KEK; private key required to unlock)
T2 / Secure Enclave mediated keys ❌ (hardware access required)
Encrypted metadata (APFS snapshots, etc.) ❌ (payload encryption only)

Apple-shape encrypted-container support (F-2)

Beyond the legacy self-consistent format produced by Format / FormatArgon2id, this package now 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:

  • TestVEKBlob_HMACAgainstAppleReference — re-computes the HMAC over Apple's reference VEKBLOB using our computeKeybagHMAC and asserts byte-equality with Apple's stored value.
  • TestKeybagChain_PassphraseUnlocksVEK — builds a complete Apple- shape container + volume keybag pair (using BuildVEKBlob, BuildKEKBlob, PackKeybagBlock, EncryptContainerKeybag, EncryptVolumeKeybag), then walks the structure end-to-end with only the passphrase + the two UUIDs + the two paddrs, recovering the VEK byte-for-byte. Same chain Apple's apfs.kext walks on mount.

The recipe (validated against Apple's reference 2026-05-09):

  • 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 × blockSize/512 + sector_index: paddr × 8 for the common 4096-byte block (apfs-fuse KeyMgmt.cpp, cs_factor = blocksize / 0x200). DecryptKeybagBlocks, EncryptKeybagBlocks, DecryptVolumeBlocks and EncryptVolumeBlocks take the container's block size; the unlock path passes it. The functions without one (DecryptContainerKeybag and its siblings) are the 4096-byte case.
  • Keybag block layout: 32-byte obj_phys (type = 0x6b657973 "syek", sealed Fletcher-64 cksum) + 16-byte apfs_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 opaque info_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).
  • unlockVEK dispatch: nx_flags & NX_CRYPTO_SW (bit 0x4) selects the Apple-shape unlock walk; clear bit selects the legacy PBKDF2-locker path. Both work transparently through the apfs.Open / apfs.OpenFrom public API.

Usage

Open an encrypted APFS volume image
import "github.com/go-fde/apfs"

// Open and unlock an APFS encrypted volume.
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.
// Reads and writes must be 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 is clear and the keybag is plaintext on disk. Use pkg/go-filesystems/apfs.FormatContainerEncrypted (or …GPT) for the Apple-shape format, which produces output structurally identical to diskutil apfs encryptVolume.

// 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 any value satisfying 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 err != nil {
    log.Fatal(err)
}
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()

buf := make([]byte, 512)
apfsDev.ReadAt(buf, 0)
Opening an untrusted image: limits

The keybag is read from the image, so it says how much work unlocking costs. Without bounds, a crafted locker could ask for 2³² PBKDF2 iterations (hours of CPU) or 4 TiB of Argon2 memory, or it could crash the program: an Argon2 parallelism of 256 becomes 0 in the uint8 that golang.org/x/crypto/argon2 takes, and argon2 panics on 0. Every locker and KEKBLOB is therefore checked before any key is derived for it.

  • Always: Argon2 1 to 255 threads, at least 8 KiB of memory per thread (RFC 9106 §3.1), at most 4 GiB. At least one iteration. A wrapped key of at most 72 bytes (a 64-byte key plus AES-KW's 8, RFC 3394).
  • The caller's Limits: PBKDF2 iterations, Argon2 passes and memory. Open and OpenFrom use DefaultLimits: 2²⁶ iterations, 1000 passes, 4 GiB, generous for keybags macOS calibrates on fast machines. A program opening images from elsewhere (an upload, a browser) can tighten them with OpenLimited / OpenFromLimited.

If no locker opens and one was refused, the error wraps ErrLimit. If lockers were tried and none opened, it wraps ErrWrongPassphrase. Anything else is a structural error in the image.

The image geometry is bounded too: a block size must be a power of two from 4096 to 65536 (NX_MINIMUM_BLOCK_SIZE / NX_MAXIMUM_BLOCK_SIZE), a keybag at most 16 blocks, and a block address must fit in an int64 byte offset. The DER blobs (VEKBLOB, KEKBLOB) are parsed with every length checked against what is left, and fuzzed (FuzzParseBlobs, FuzzUnlockVolumeKey).

dev, err := apfs.OpenFromLimited(rw, passphrase, apfs.Limits{
	MaxPBKDF2Iterations: 1 << 22,
	MaxArgon2Time:       16,
	MaxArgon2MemoryKiB:  1 << 20, // 1 GiB
})
The volume key without a Device: UnlockVolumeKey and SectorCipher

A Device decrypts every byte it reads. That is right for this package's legacy shape, but not for a real FileVault container: there the container metadata (superblock, checkpoints, object maps, volume superblock) is plaintext, and only file-system tree nodes whose object-map entry has OMAP_VAL_ENCRYPTED and file extents are encrypted with the VEK (Apple File System Reference, "Encryption"). A file-system reader needs the key and a cipher it can aim at single blocks:

vk, err := apfs.UnlockVolumeKey(r, passphrase, apfs.DefaultLimits)
// vk.VEK, vk.VolumeUUID (zero for the legacy shape)
sc, err := apfs.NewSectorCipher(vk.VEK)
// a B-byte block at paddr: data units of 512 bytes, tweak paddr*(B/512)
err = sc.Decrypt(block, paddr*uint64(len(block)/apfs.SectorSize))

go-filesystems/apfs (OpenFDE, OpenFDEFromBlockDevice) reads FileVault volumes this way.

Block addressing

ReadAt and 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 and have lengths that are multiples of 512 bytes. These constraints are imposed by AES-XTS, which operates on fixed 512-byte sectors.

The XTS tweak value for a given sector is byteOffset / 512, matching the behaviour of Apple's kernel com.apple.filesystems.apfs kext.

Compatibility

Reading

apfs.Open(path, passphrase) opens both shapes transparently:

  • Apple-shape containers produced by diskutil apfs encryptVolume on macOS 10.13 High Sierra and later. unlockVEK decrypts the container keybag with the container UUID, walks to the volume keybag, parses the ASN.1 KEKBLOB / VEKBLOB, derives the KEK from the passphrase via PBKDF2-SHA256, and unwraps the VEK.
  • Legacy self-consistent containers produced by this package's Format / FormatArgon2id helpers (plaintext keybag, proprietary PBKDF2 / Argon2id locker). Used by tests and by callers that don't need byte-level Apple compatibility.

Both paths support AES-128-XTS (vek_size = 32) and AES-256-XTS (vek_size = 64) at the volume layer.

Writing

This package's Format produces the legacy self-consistent shape. To produce an Apple-shape encrypted container that matches diskutil apfs encryptVolume byte-for-byte, use pkg/go-filesystems/apfs.FormatContainerEncrypted (or FormatContainerEncryptedGPT) which sits on top of this package and wires the additional container-level metadata (NX SB nx_keylocker + nx_flags, checkpoint ephemerals, FQ trees, APSB encryption flags). See COMPAT.md cell F-2 for the full recipe and parity test.

Note: hdiutil create -encryption AES-256 is DMG-envelope encryption (UDIF), NOT APFS FDE — different layer entirely.

Argon2id format

Apple's APFS reference does not publicly document an on-disk layout for Argon2id-protected lockers. This package defines and uses the following self-consistent layout for the data field of a kbTagVolumePassphrase keybag entry when kdfType = 0x0001:

uint16 kdfType       (0x0001)
uint16 padding       (0)
uint32 timeCost      (Argon2id "t" parameter)
uint32 memoryKiB     (Argon2id "m" parameter, in KiB)
uint16 parallelism   (Argon2id "p" parameter)
uint16 saltLen
[saltLen bytes salt]
[remaining bytes = wrapped KEK]

Use FormatArgon2id / FormatArgon2idOn to create a container with an Argon2id-protected locker, or AddArgon2idPassphrase to add an Argon2id locker to an existing container.

Personal Recovery Key (PRK)

A PRK is a regular passphrase locker tagged with the well-known UUID EBC6C064-0000-11AA-AA11-00306543ECAC (the same UUID Apple uses for the recovery user on HFS+ FileVault). Use AddRecoveryKey(rw, existing, prk) to add one to a container; the resulting keybag entry can be unlocked by passing the recovery key as the passphrase to Open/OpenFrom.

Institutional Recovery Key (IRK)

An IRK is an RSA keypair held by an MDM administrator. The container's KEK is wrapped with RSA-OAEP-SHA256 under the public key. The keybag entry uses tag kbTagInstitutionalKey = 0x0004 with the layout:

[32 bytes  SHA-256 fingerprint of the RSA public key DER]
[N  bytes  RSA-OAEP-SHA256 ciphertext of the KEK]

The fingerprint lets the unlock path locate the matching entry when several IRK keys coexist in the same keybag.

priv, _ := rsa.GenerateKey(rand.Reader, 2048)
apfs.AddInstitutionalKey(rw, []byte("primary passphrase"), &priv.PublicKey)

// later, with the private key only:
dev, err := apfs.OpenWithPrivateKey("disk.apfs", priv)

On-disk format references

  • Apple File System Reference (developer.apple.com/support/downloads)
  • "Infiltrate the Vault: Security Analysis and Decryption of Lion Full Disk Encryption" — Ligh, Walters et al. (PasswordsCon 2012)
  • apfs.h and IOKit/storage/APFS/APFSConstants.h from the Darwin xnu open-source tree
  • RFC 3394 — Advanced Encryption Standard (AES) Key Wrap Algorithm

Documentation

Overview

Package apfs provides pure-Go support for APFS FileVault 2 full-disk encryption (FDE). It implements the APFS key bag format and AES-XTS block-level decryption as documented in the Apple File System Reference.

Overview

APFS FileVault 2 encrypts each block of a volume using AES-XTS with a Volume Encryption Key (VEK). The VEK is stored encrypted inside a Key Encryption Key (KEK), which in turn is wrapped using AES key-wrap (RFC 3394) with a key derived from the user's passphrase via PBKDF2-SHA256.

This package reads the APFS container key bag, locates the matching passphrase key-bag entry, unwraps the KEK and then the VEK, and exposes a Device that transparently decrypts blocks on ReadAt and encrypts on WriteAt.

Usage

dev, err := apfs.Open("/dev/disk2s1", []byte("my passphrase"))
if err != nil { log.Fatal(err) }
defer dev.Close()

// ReadAt / WriteAt offsets are block-absolute (relative to offset 0 of the
// underlying device, not the payload start).
buf := make([]byte, 4096)
_, err = dev.ReadAt(buf, 0)

Supported ciphers

  • AES-128-XTS (128-bit VEK, used when key_size = 16)
  • AES-256-XTS (256-bit VEK, used when key_size = 32)

Both use the block's absolute sector index (offset / 512) as the XTS tweak.

Supported unlock paths

  • PBKDF2-SHA256 passphrase locker (the default, see Format).
  • Argon2id passphrase locker (see FormatArgon2id, AddArgon2idPassphrase).
  • Personal Recovery Key (see AddRecoveryKey; unlock with Open using the recovery key as the passphrase).
  • Institutional Recovery Key with RSA-OAEP wrapping (see AddInstitutionalKey, OpenWithPrivateKey).

Limitations

  • Read-only iSCSI / remote devices: only ReadAt is guaranteed stable.
  • T2 / Secure Enclave mediated keys require hardware access.
  • Argon2id and IRK locker layouts are package-defined; Apple does not publicly document on-disk formats for them, so containers produced by this package using those mechanisms are not interoperable with macOS.

Index

Constants

View Source
const (
	KBTagVolumeKey           = uint16(kbTagVolumeKey)
	KBTagVolumeUnlockRecords = uint16(kbTagVolumePassphrase) // 3
	KBTagVolumePassphrase    = uint16(kbTagVolumePassphrase) // 3 (alias)
)

Public mirrors of the unexported keybag-tag constants. The 0x0003 tag is overloaded by Apple: in the *container* keybag it's KB_TAG_VOLUME_UNLOCK_RECORDS (data = apfs_prange to the volume keybag); in the *volume* keybag it's KB_TAG_VOLUME_PASSPHRASE (data = KEKBLOB DER).

View Source
const SectorSize = sectorSize

SectorSize is the AES-XTS data-unit size APFS software encryption uses: 512 bytes, whatever the container block size. A block of B bytes at physical address paddr is B/512 consecutive data units whose tweaks start at paddr*(B/512) (apfs-fuse ApfsVolume::ReadBlocks, cs_factor).

Variables

View Source
var DefaultLimits = Limits{
	MaxPBKDF2Iterations: 1 << 26,
	MaxArgon2Time:       1000,
	MaxArgon2MemoryKiB:  argon2MaxMemoryKiB,
}

DefaultLimits are what Open and OpenFrom use: generous for keybags written by macOS (FileVault calibrates its PBKDF2 count to the machine) and by this package.

View Source
var ErrLimit = errors.New("apfs: the keybag asks for more than the limits allow")

ErrLimit is a locker whose parameters a Limits refuses.

View Source
var ErrWrongPassphrase = errors.New("apfs: wrong passphrase")

ErrWrongPassphrase is returned when at least one passphrase locker was tried and none of them unwrapped with the passphrase given. It is distinct from ErrLimit (a locker the Limits refused before running its KDF) and from structural errors (no keybag, a malformed one).

Functions

func AESKeyUnwrap

func AESKeyUnwrap(kek, wrapped []byte) ([]byte, error)

AESKeyUnwrap is a public alias of aesKeyUnwrap.

func AESKeyWrap

func AESKeyWrap(kek, plaintext []byte) ([]byte, error)

AESKeyWrap is a public alias of aesKeyWrap. It implements RFC 3394 AES Key Wrap; the format-time encrypted-container writer in pkg/go-filesystems/apfs uses it to wrap the KEK with a passphrase- derived key and the VEK with the KEK.

func AddArgon2idPassphrase

func AddArgon2idPassphrase(rw blockRW, existingPassphrase, newPassphrase []byte, params Argon2idParams) error

AddArgon2idPassphrase rewrites the keybag to add an additional passphrase locker that uses Argon2id (kdfType = 0x0001) for key derivation.

func AddInstitutionalKey

func AddInstitutionalKey(rw blockRW, existingPassphrase []byte, pub *rsa.PublicKey) error

AddInstitutionalKey rewrites the keybag to add an Institutional Recovery Key locker. existingPassphrase recovers the KEK and pub is used to RSA-OAEP encrypt the KEK. The entry UUID is the SHA-256 fingerprint of the public key DER (truncated to 16 bytes) so multiple IRKs can coexist without collision.

func AddPassphrase

func AddPassphrase(rw blockRW, existingPassphrase, newPassphrase []byte) error

AddPassphrase rewrites the keybag to add an additional regular passphrase locker (PBKDF2). Useful when a container needs more than one passphrase to unlock — for example a personal one and a shared one.

func AddRecoveryKey

func AddRecoveryKey(rw blockRW, existingPassphrase, recoveryKey []byte) error

AddRecoveryKey rewrites the keybag of an existing container to add a Personal Recovery Key (PRK) locker. unlock recovers the master KEK with the supplied existingPassphrase (or recovery key) and re-wraps the same KEK under the new recoveryKey using PBKDF2-SHA256. The PRK locker is tagged with the well-known recoveryKeyUUID so callers can distinguish it.

func BuildKEKBlob

func BuildKEKBlob(userUUID [16]byte, flags uint64, wrappedKEK []byte, iterations uint32, pbkdf2Salt []byte, hmacSalt []byte) ([]byte, error)

BuildKEKBlob returns the DER-encoded KEKBLOB Apple stores in the volume keybag's KB_TAG_VOLUME_PASSPHRASE entry. wrappedKEK must be the AES-KW ciphertext of the KEK under the PBKDF2-derived key.

hmacSalt is the 8-byte salt that feeds the outer HMAC key derivation (kbhmacKey). pbkdf2Salt is the salt PBKDF2 was called with — typically 16 bytes per Apple's reference. The two salts are independent.

func BuildVEKBlob

func BuildVEKBlob(volumeUUID [16]byte, flags uint64, wrappedVEK []byte, salt []byte) ([]byte, error)

BuildVEKBlob returns the 124-byte (typical) DER-encoded VEKBLOB Apple stores in the container keybag's KB_TAG_VOLUME_KEY entry. It computes the HMAC-SHA256 over the inner keyblob (including the [3] CONSTRUCTED tag/length prefix) automatically.

salt must be 8 bytes (Apple's convention). wrappedKey is the AES-KW(KEK, VEK) RFC-3394 ciphertext (8-byte IV + ciphertext = 40 bytes for a 32-byte VEK).

func DecryptContainerKeybag

func DecryptContainerKeybag(ciphertext []byte, containerUUID [16]byte, paddr uint64) ([]byte, error)

DecryptContainerKeybag decrypts a raw container-keybag block (or a contiguous run of them) read at paddr from a software-encrypted APFS container, using only the container UUID. This is the recipe Apple's apfs.kext applies to bytes pointed to by the NX SuperBlock's nx_keylocker field — see pkg/go-filesystems/apfs/probe_apple_keybag_darwin_test.go for the empirical validation that produced this implementation.

ciphertext length must be a multiple of the APFS block size (4096). The volume keybag, by contrast, is encrypted with the VEK rather than the container UUID — call decryptKeybagAtRest directly with the VEK in that path (or use the higher-level unlock flow once it lands).

func DecryptKeybagBlocks added in v0.3.0

func DecryptKeybagBlocks(ciphertext []byte, uuid [16]byte, paddr uint64, blockSize uint32) ([]byte, error)

DecryptKeybagBlocks decrypts keybag blocks of blockSize bytes read at paddr, keyed by uuid: the container's for the container keybag, the volume's for the volume keybag. The XTS data units are 512 bytes, and the first is paddr × blockSize/512 -- the container's block size, not 4096 (apfs-fuse KeyMgmt.cpp, KeyManager::DecryptBlocks: cs_factor = GetBlocksize() / 0x200, uno = block * cs_factor). blockSize is a power of two from 4096 to 65536 (NX_MINIMUM_BLOCK_SIZE to NX_MAXIMUM_BLOCK_SIZE).

func DecryptVolumeBlock

func DecryptVolumeBlock(ciphertext []byte, vek []byte, paddr uint64) ([]byte, error)

DecryptVolumeBlock is the inverse of EncryptVolumeBlock.

func DecryptVolumeBlocks added in v0.3.0

func DecryptVolumeBlocks(ciphertext []byte, vek []byte, paddr uint64, blockSize uint32) ([]byte, error)

DecryptVolumeBlocks decrypts volume blocks of blockSize bytes read at paddr with the VEK: the same data units as DecryptKeybagBlocks.

func DecryptVolumeKeybag

func DecryptVolumeKeybag(ciphertext []byte, volumeUUID [16]byte, paddr uint64) ([]byte, error)

DecryptVolumeKeybag decrypts a raw volume-keybag block using the volume UUID. The volume keybag is referenced by the container keybag's tag=3 (KB_TAG_VOLUME_UNLOCK_RECORDS) entry (which carries an apfs_prange to its on-disk location). Apple's apfs.kext applies the same `uuid || uuid` AES-XTS recipe as for the container keybag, but keyed on the volume UUID rather than the container UUID.

func Detect

func Detect(path string) (bool, error)

Detect reports whether the data at path looks like an APFS container. It checks for the APFS container superblock magic "NXSB".

func DetectFrom

func DetectFrom(rw io.ReaderAt) (bool, error)

DetectFrom reports whether rw looks like an APFS container by checking the NX superblock magic at offset 32 of block 0.

func EncryptContainerKeybag

func EncryptContainerKeybag(plaintext []byte, containerUUID [16]byte, paddr uint64) ([]byte, error)

EncryptContainerKeybag is the inverse of DecryptContainerKeybag. The future FormatContainerEncrypted writer in pkg/go-filesystems/apfs uses this to seal the container keybag before writing it to disk.

func EncryptKeybagBlocks added in v0.3.0

func EncryptKeybagBlocks(plaintext []byte, uuid [16]byte, paddr uint64, blockSize uint32) ([]byte, error)

EncryptKeybagBlocks is the inverse of DecryptKeybagBlocks.

func EncryptVolumeBlock

func EncryptVolumeBlock(plaintext []byte, vek []byte, paddr uint64) ([]byte, error)

EncryptVolumeBlock encrypts a 4 KiB volume metadata or file-data block at rest using the VEK. Apple's apfs.kext applies this layer to every block reachable from the volume superblock (APSB, volume OMAP, FS-tree root, snap-meta / extent-ref trees, and file data blocks) when the container has NX_CRYPTO_SW set.

The recipe is identical to the keybag at-rest XTS layer: 512-byte XTS sectors with tweak = paddr × 8 + sector_index_within_block. The only difference is the key — the VEK rather than a UUID-derived value. vek must be 32 bytes (AES-128-XTS) or 64 bytes (AES-256-XTS).

func EncryptVolumeBlocks added in v0.3.0

func EncryptVolumeBlocks(plaintext []byte, vek []byte, paddr uint64, blockSize uint32) ([]byte, error)

EncryptVolumeBlocks is the inverse of DecryptVolumeBlocks.

func EncryptVolumeKeybag

func EncryptVolumeKeybag(plaintext []byte, volumeUUID [16]byte, paddr uint64) ([]byte, error)

EncryptVolumeKeybag is the inverse of DecryptVolumeKeybag.

func IsRecoveryKeyUUID

func IsRecoveryKeyUUID(u [16]byte) bool

IsRecoveryKeyUUID reports whether u matches the well-known PRK UUID stamped by this package on Personal Recovery Key keybag entries.

func PackKeybagBlock

func PackKeybagBlock(entries []KeybagEntry) []byte

PackKeybagBlock builds a 4096-byte plaintext keybag block carrying the supplied entries, with the proper 32-byte obj_phys header (o_type = APFS_OBJECT_TYPE_MEDIA_KEYBAG / 0x6b657973, oid = 0 matching Apple's reference, sealed Fletcher-64 cksum) and 16-byte apfs_kb_locker. Entries are 16-byte aligned per Apple's apfs-fuse reference.

The returned block is *plaintext* — call EncryptContainerKeybag or EncryptVolumeKeybag (with the appropriate UUID and paddr) before writing to disk.

func ParseKEKBlob

func ParseKEKBlob(der []byte) (kekBlobParse, error)

ParseKEKBlob decodes a KEKBLOB (the data field of a volume keybag's KB_TAG_VOLUME_PASSPHRASE entry) and returns the inner UUID, the AES-KW(derivedKey, KEK) ciphertext, and the PBKDF2 iteration count and salt needed to derive the unwrap key from a passphrase.

func ParseVEKBlob

func ParseVEKBlob(der []byte) (vekBlobParse, error)

ParseVEKBlob decodes a VEKBLOB (the data field of a container keybag's KB_TAG_VOLUME_KEY entry) and returns the inner volume UUID and AES-KW(KEK, VEK) ciphertext. The HMAC field is not verified here — apfs.kext checks it and we trust that path; if a corrupt blob ever makes it to AESKeyUnwrap the unwrap will fail.

Types

type Argon2idParams

type Argon2idParams struct {
	TimeCost    uint32
	MemoryKiB   uint32
	Parallelism uint16
}

Argon2idParams selects time/memory/parallelism for an Argon2id locker. Zero fields fall back to the defaults above.

type Device

type Device struct {
	// contains filtered or unexported fields
}

Device is an unlocked APFS encrypted device. Its ReadAt and WriteAt methods transparently decrypt / encrypt blocks using the Volume Encryption Key.

func Format

func Format(path string, passphrase []byte) (*Device, error)

Format creates a new APFS FDE container at path, writing the NX superblock and key bag blocks and protecting them with passphrase. The file must already exist. Returns an opened Device ready for payload I/O.

func FormatArgon2id

func FormatArgon2id(path string, passphrase []byte, params Argon2idParams) (*Device, error)

FormatArgon2id is the Argon2id-protected counterpart of Format. The key bag will contain a single Argon2id locker (kdfType = 0x0001) protecting the container's KEK.

func FormatArgon2idOn

func FormatArgon2idOn(rw blockRW, passphrase []byte, params Argon2idParams) (*Device, error)

FormatArgon2idOn is the FormatOn equivalent for Argon2id-protected containers.

func FormatOn

func FormatOn(rw blockRW, passphrase []byte) (*Device, error)

FormatOn creates a new APFS FDE container on rw, writing the NX superblock and key bag blocks and protecting them with passphrase. Returns an opened Device ready for payload I/O.

func Open

func Open(path string, passphrase []byte) (*Device, error)

Open opens the APFS encrypted volume at path and unlocks it using passphrase. The path may be a raw disk device or an image file.

func OpenFrom

func OpenFrom(rw blockRW, passphrase []byte) (*Device, error)

OpenFrom unlocks an APFS encrypted volume from any read-write-closable block device. This allows the package to be layered on top of, e.g., a QCOW2 device. Close on the returned Device also closes rw.

func OpenFromLimited

func OpenFromLimited(rw blockRW, passphrase []byte, lim Limits) (*Device, error)

OpenFromLimited is OpenFrom with lim instead of DefaultLimits.

func OpenFromWithPrivateKey

func OpenFromWithPrivateKey(rw blockRW, priv *rsa.PrivateKey) (*Device, error)

OpenFromWithPrivateKey is the OpenFrom equivalent for IRK unlock.

func OpenLimited

func OpenLimited(path string, passphrase []byte, lim Limits) (*Device, error)

OpenLimited is Open with lim instead of DefaultLimits.

func OpenWithPrivateKey

func OpenWithPrivateKey(path string, priv *rsa.PrivateKey) (*Device, error)

OpenWithPrivateKey opens an APFS FDE container at path and unlocks it using the supplied RSA private key against any institutional recovery key locker it carries.

func (*Device) Close

func (d *Device) Close() error

Close releases the underlying file or device.

func (*Device) ReadAt

func (d *Device) ReadAt(p []byte, off int64) (int, error)

ReadAt reads data from the underlying device starting at byte offset off. If the volume is encrypted, the data is decrypted before being returned. The offset is absolute (relative to the start of the device).

func (*Device) WriteAt

func (d *Device) WriteAt(p []byte, off int64) (int, error)

WriteAt encrypts p and writes it to the underlying device at byte offset off. The offset is absolute (relative to the start of the device).

type KeybagEntry

type KeybagEntry struct {
	UUID [16]byte
	Tag  uint16
	Data []byte
}

KeybagEntry is the public form of a single keybag entry consumed by PackKeybagBlock. Tag values are the package constants KBTagVolumeKey (2) and KBTagVolumeUnlockRecords / KBTagVolumePassphrase (both 3 — the meaning depends on which keybag the entry lives in).

type Limits

type Limits struct {
	MaxPBKDF2Iterations uint32
	MaxArgon2Time       uint32
	MaxArgon2MemoryKiB  uint32
}

Limits bound the work a keybag may make Open do. A keybag is read from the image, so it is untrusted: without bounds a locker can ask for 2^32 PBKDF2 iterations (hours), 4 TiB of Argon2 memory, or a parallelism of 256 that becomes 0 once converted to the uint8 argon2 takes -- a panic in golang.org/x/crypto/argon2.

Structural bounds always apply: Argon2 1 to 255 threads (what argon2 takes), at least 8 KiB of memory per thread (RFC 9106 §3.1), at most 4 GiB; at least one iteration; a wrapped key of at most 64 bytes plus AES-KW's 8 (RFC 3394). The cost bounds are the caller's.

type SectorCipher added in v0.2.0

type SectorCipher struct {
	// contains filtered or unexported fields
}

SectorCipher is AES-XTS over 512-byte data units with an explicit 64-bit data-unit number as the tweak. It is what a reader needs to decrypt only the blocks APFS encrypts -- file-system tree nodes whose object-map entry carries OMAP_VAL_ENCRYPTED, and file extents (tweak base = crypto_id) -- while reading the plaintext container metadata raw, which a Device (that decrypts every byte) cannot do.

A SectorCipher is safe for concurrent use.

func NewSectorCipher added in v0.2.0

func NewSectorCipher(vek []byte) (*SectorCipher, error)

NewSectorCipher returns a SectorCipher keyed with vek: 32 bytes for AES-128-XTS (key1 = vek[:16], key2 = vek[16:]) or 64 for AES-256-XTS.

func (*SectorCipher) Decrypt added in v0.2.0

func (s *SectorCipher) Decrypt(p []byte, first uint64) error

Decrypt decrypts p in place. p must be a whole number of 512-byte data units; p[0:512] uses tweak first, the next unit first+1, and so on. The unit number wraps modulo 2^64 like the uint64 it is on disk.

func (*SectorCipher) Encrypt added in v0.2.0

func (s *SectorCipher) Encrypt(p []byte, first uint64) error

Encrypt is the inverse of Decrypt.

type VolumeKey added in v0.2.0

type VolumeKey struct {
	VEK        []byte
	VolumeUUID [16]byte
}

VolumeKey is what UnlockVolumeKey recovers: the volume encryption key and the UUID of the volume it belongs to (from the container keybag). The UUID is zero for the legacy keybag shape this package's Format writes, which does not name a volume.

func UnlockVolumeKey added in v0.2.0

func UnlockVolumeKey(r io.ReaderAt, passphrase []byte, lim Limits) (VolumeKey, error)

UnlockVolumeKey walks the keybags of the container in r (container keybag -> volume keybag -> KEK -> VEK, Apple File System Reference, "Accessing Encrypted Objects") with passphrase and returns the volume key, without wrapping r in a Device. lim bounds the key derivation a keybag can ask for.

Errors: ErrLimit (wrapped) when a locker asks for more than lim allows and none opened; ErrWrongPassphrase (wrapped) when lockers were tried and none opened; anything else is a structural error in the image.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL