Skip to content

zfscrypt — OpenZFS encryption core

github.com/go-encryptions/zfscrypt

import "github.com/go-encryptions/zfscrypt"

Package zfscrypt implements the cryptographic primitives used by OpenZFS native (dataset-level) encryption, as added in OpenZFS 0.8 (2019) and stable since.

This package is the "math" half of ZFS encryption: KDF, MEK unwrap, per-block key derivation, and AES-CCM/GCM block decryption. The "format" half — parsing the DSL_CRYPTO_KEY bonus area and extracting per-block IV/MAC from blkptr_t — lives in the consuming filesystem driver, which already understands those on-disk structures.

Only the read path is currently implemented (the motivating use case, cloud-boot, just needs to pull /boot/vmlinuz and /boot/initrd out of an encrypted root), so Wrap/EncryptBlock are not exposed. Adding them later is straightforward — Seal is already present in the underlying AEADs.

References (OpenZFS source tree)

module/zfs/zio_crypt.c · module/zfs/dsl_crypt.c · include/sys/zio_crypt.h · include/sys/dsl_crypt.h

Algorithms & key schedule

Stage Algorithm
Wrapping-key derivation PBKDF2-HMAC-SHA1(passphrase, salt, iters, 32)
MEK unwrap AES-128/192/256-CCM or GCM AEAD Open
Per-block key derivation HKDF-SHA512(mek, salt=blockSalt, info=algName, klen)
Block decryption AES-128/192/256-CCM (via ccm) or AES-GCM (stdlib)
Metadata auth HMAC-SHA512, truncated to 32 bytes

SHA1 is intentional

The wrapping-key KDF uses PBKDF2-HMAC-SHA1, matching what OpenZFS shipped in 0.8. It is part of the on-disk format, not a security choice — do not "upgrade" it without a corresponding format change.

Suites

Suite is a uint8 matching the on-disk crypt_algorithm field of a DSL_CRYPTO_KEY object:

Constant Value Key length
SuiteInherit 0 — (resolved by the format layer)
AES128CCM 1 16
AES192CCM 2 24
AES256CCM 3 32
AES128GCM 4 16
AES192GCM 5 24
AES256GCM 6 32

Helpers: Suite.KeyLen() int, Suite.IsCCM() bool, Suite.String() string.

Fixed sizes (ZFS uses 12-byte IVs and 16-byte MACs uniformly):

const (
    IVSize         = 12 // per-block / unwrap IV
    MACSize        = 16 // authentication tag
    WrappingKeyLen = 32 // always AES-256 wraps the MEK
    WrappedKeySize = 64 // 32-byte MEK || 32-byte HMAC key
)

API

// DeriveWrappingKey derives the 32-byte wrapping key from the user
// passphrase via PBKDF2-HMAC-SHA1. iters and salt come from the
// DSL_CRYPTO_KEY object.
func DeriveWrappingKey(passphrase, salt []byte, iters int) ([]byte, error)

// Unwrap decrypts the wrapped (MEK || HMAC-key) blob with the
// wrapping key, returning the 32-byte MEK and 32-byte HMAC key.
// suite selects CCM vs GCM; ad must equal the AD the pool used at
// wrap time, or Open fails the tag check.
func Unwrap(suite Suite, wrappingKey, iv, mac, wrapped, ad []byte) (mek, hmacKey []byte, err error)

// DeriveBlockKey derives the per-block data key from the MEK via
// HKDF-SHA512 (salt = per-block randomiser, info = suite.String()).
func DeriveBlockKey(suite Suite, mek, salt []byte) ([]byte, error)

// DecryptBlock authenticates and decrypts one ZFS block with a
// previously-derived per-block key.
func DecryptBlock(suite Suite, key, iv, mac, ciphertext, ad []byte) ([]byte, error)

// HMAC returns HMAC-SHA512 over data, truncated to 32 bytes, using
// the dataset's HMAC key — for metadata that bypasses the AEAD
// layer (e.g. the per-block salt).
func HMAC(hmacKey, data []byte) []byte

Read path

// 1. Derive the wrapping key from the passphrase.
wk, err := zfscrypt.DeriveWrappingKey(passphrase, kdfSalt, iters)

// 2. Unwrap the master encryption key (and its HMAC sibling).
mek, hmacKey, err := zfscrypt.Unwrap(suite, wk, wrapIV, wrapMAC, wrapped, wrapAD)

// 3. Per encrypted block: derive its key, then decrypt.
blockKey, err := zfscrypt.DeriveBlockKey(suite, mek, blockSalt)
pt, err := zfscrypt.DecryptBlock(suite, blockKey, blockIV, blockMAC, ciphertext, blockAD)

The IV, MAC, salt and AAD inputs are supplied by the format driver after it parses the DSL_CRYPTO_KEY object and the encrypted block's blkptr_t. Passing the wrong AAD surfaces as a tag-verification failure, not as garbage plaintext.

Internals

decryptAEAD builds the right AEAD for the suite — ccm.NewCCM(block, 16, 12) for CCM suites or cipher.NewGCMWithNonceSize(block, 12) for GCM — then splices ciphertext || mac back together (ZFS stores the tag separately in the blkptr) and calls Open with the IV as the nonce.

Dependencies

Standard library (crypto/aes, crypto/cipher, crypto/hkdf, crypto/hmac, crypto/pbkdf2, crypto/sha1, crypto/sha512) plus this org's own ccm for the CCM suites. No golang.org/x/crypto.

Requires Go 1.25 or newer.