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
- Variables
- func AESKeyUnwrap(kek, wrapped []byte) ([]byte, error)
- func AESKeyWrap(kek, plaintext []byte) ([]byte, error)
- func AddArgon2idPassphrase(rw blockRW, existingPassphrase, newPassphrase []byte, params Argon2idParams) error
- func AddInstitutionalKey(rw blockRW, existingPassphrase []byte, pub *rsa.PublicKey) error
- func AddPassphrase(rw blockRW, existingPassphrase, newPassphrase []byte) error
- func AddRecoveryKey(rw blockRW, existingPassphrase, recoveryKey []byte) error
- func BuildKEKBlob(userUUID [16]byte, flags uint64, wrappedKEK []byte, iterations uint32, ...) ([]byte, error)
- func BuildVEKBlob(volumeUUID [16]byte, flags uint64, wrappedVEK []byte, salt []byte) ([]byte, error)
- func DecryptContainerKeybag(ciphertext []byte, containerUUID [16]byte, paddr uint64) ([]byte, error)
- func DecryptKeybagBlocks(ciphertext []byte, uuid [16]byte, paddr uint64, blockSize uint32) ([]byte, error)
- func DecryptVolumeBlock(ciphertext []byte, vek []byte, paddr uint64) ([]byte, error)
- func DecryptVolumeBlocks(ciphertext []byte, vek []byte, paddr uint64, blockSize uint32) ([]byte, error)
- func DecryptVolumeKeybag(ciphertext []byte, volumeUUID [16]byte, paddr uint64) ([]byte, error)
- func Detect(path string) (bool, error)
- func DetectFrom(rw io.ReaderAt) (bool, error)
- func EncryptContainerKeybag(plaintext []byte, containerUUID [16]byte, paddr uint64) ([]byte, error)
- func EncryptKeybagBlocks(plaintext []byte, uuid [16]byte, paddr uint64, blockSize uint32) ([]byte, error)
- func EncryptVolumeBlock(plaintext []byte, vek []byte, paddr uint64) ([]byte, error)
- func EncryptVolumeBlocks(plaintext []byte, vek []byte, paddr uint64, blockSize uint32) ([]byte, error)
- func EncryptVolumeKeybag(plaintext []byte, volumeUUID [16]byte, paddr uint64) ([]byte, error)
- func IsRecoveryKeyUUID(u [16]byte) bool
- func PackKeybagBlock(entries []KeybagEntry) []byte
- func ParseKEKBlob(der []byte) (kekBlobParse, error)
- func ParseVEKBlob(der []byte) (vekBlobParse, error)
- type Argon2idParams
- type Device
- func Format(path string, passphrase []byte) (*Device, error)
- func FormatArgon2id(path string, passphrase []byte, params Argon2idParams) (*Device, error)
- func FormatArgon2idOn(rw blockRW, passphrase []byte, params Argon2idParams) (*Device, error)
- func FormatOn(rw blockRW, passphrase []byte) (*Device, error)
- func Open(path string, passphrase []byte) (*Device, error)
- func OpenFrom(rw blockRW, passphrase []byte) (*Device, error)
- func OpenFromLimited(rw blockRW, passphrase []byte, lim Limits) (*Device, error)
- func OpenFromWithPrivateKey(rw blockRW, priv *rsa.PrivateKey) (*Device, error)
- func OpenLimited(path string, passphrase []byte, lim Limits) (*Device, error)
- func OpenWithPrivateKey(path string, priv *rsa.PrivateKey) (*Device, error)
- type KeybagEntry
- type Limits
- type SectorCipher
- type VolumeKey
Constants ¶
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).
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 ¶
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.
var ErrLimit = errors.New("apfs: the keybag asks for more than the limits allow")
ErrLimit is a locker whose parameters a Limits refuses.
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 ¶
AESKeyUnwrap is a public alias of aesKeyUnwrap.
func AESKeyWrap ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
Detect reports whether the data at path looks like an APFS container. It checks for the APFS container superblock magic "NXSB".
func DetectFrom ¶
DetectFrom reports whether rw looks like an APFS container by checking the NX superblock magic at offset 32 of block 0.
func EncryptContainerKeybag ¶
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 ¶
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 ¶
EncryptVolumeKeybag is the inverse of DecryptVolumeKeybag.
func IsRecoveryKeyUUID ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
type KeybagEntry ¶
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 ¶
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.
type VolumeKey ¶ added in v0.2.0
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
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.
