fscrypt: Per-File Encryption
Key hierarchy, HKDF derivation, and transparent encryption in ext4/F2FS/Btrfs
Overview
fscrypt provides per-directory transparent encryption at the VFS layer. Unlike dm-crypt (full disk), fscrypt encrypts individual files and directories, allowing: - Multiple users to have separate encrypted directories with different keys - Selective encryption (not all files need to be encrypted) - Fast key revocation (remove key → files become inaccessible)
Supported filesystems: ext4, F2FS, UBIFS, Btrfs (5.14+)
Application read()/write()
↓
Page cache (plaintext while key is present)
↓ (encrypt on writeback, decrypt on readahead)
fscrypt layer
↓
Block device (ciphertext)
Key hierarchy
fscrypt uses a two-level key hierarchy:
User passphrase / TPM / hardware key
↓ PBKDF2 or Argon2 (userspace)
Master key (256-bit, type: ext4/logon)
↓ HKDF-SHA512 (kernel)
├── File encryption key (per-file, derived from master + file nonce)
├── Filename encryption key (for directory entries)
└── IV (per-block, derived from file nonce + block number)
Each encrypted directory has a policy that records: - Which master key to use (by key descriptor or key identifier) - Which encryption mode (AES-256-XTS for contents, AES-256-CTS for filenames) - Key derivation version (v1 trusted keyring, v2 HKDF)
Setting up encryption (API v2)
# Step 1: Enable encryption on the filesystem (ext4):
tune2fs -O encrypt /dev/sda1
# or at mkfs time:
mkfs.ext4 -O encrypt /dev/sda1
# Step 2: Add master key to kernel keyring:
# Generate a 512-bit key:
dd if=/dev/urandom bs=64 count=1 of=/tmp/master_key 2>/dev/null
# Add to session keyring:
keyctl add logon fscrypt:$(xxd -p /tmp/master_key | head -c 16) \
/tmp/master_key @s
# Or using the fscrypt tool:
apt install fscrypt
fscrypt setup /dev/sda1
fscrypt encrypt /home/user/private/ --user=user
Kernel ioctl API
#include <linux/fscrypt.h>
/* Step 1: Add master key to filesystem keyring */
struct fscrypt_add_key_arg {
struct fscrypt_key_specifier key_spec; /* identifies the key */
__u32 raw_size; /* 16-64 bytes for AES, 64 bytes for HKDF */
__u32 key_id; /* output: kernel-assigned ID */
__u32 __reserved[8];
__u8 raw[]; /* key material */
};
struct fscrypt_key_specifier key_spec = {
.type = FSCRYPT_KEY_SPEC_TYPE_IDENTIFIER, /* v2: HKDF-based */
};
int key_fd = open("/", O_RDONLY);
ioctl(key_fd, FS_IOC_ADD_ENCRYPTION_KEY, &add_key_arg);
/* Returns key_spec.u.identifier (16-byte hash of key) */
/* Step 2: Set encryption policy on a directory */
struct fscrypt_policy_v2 policy = {
.version = FSCRYPT_POLICY_V2,
.contents_encryption_mode = FSCRYPT_MODE_AES_256_XTS,
.filenames_encryption_mode = FSCRYPT_MODE_AES_256_CTS,
.flags = FSCRYPT_POLICY_FLAGS_PAD_32,
.master_key_identifier = /* key_spec.u.identifier */,
};
int dir_fd = open("/home/user/private", O_RDONLY);
ioctl(dir_fd, FS_IOC_SET_ENCRYPTION_POLICY, &policy);
/* From now on, all new files in this directory are encrypted */
Key derivation internals
/* fs/crypto/hkdf.c */
/* HKDF-SHA512 is used to derive all per-file keys: */
/* File content key: */
/* Input keying material (IKM) = master_key */
/* Info = "fscrypt\0" + mode + filesystem UUID + file nonce */
fscrypt_hkdf_expand(&ci->ci_master_key->mk_secret.hkdf,
HKDF_CONTEXT_PER_FILE_ENC_KEY,
info, infolen,
derived_key, mode->keysize);
/* File nonce: random 16 bytes stored in inode on disk */
/* Every file gets a unique nonce → unique key even if same master key */
/* Per-file key stored in: */
struct fscrypt_inode_info {
struct fscrypt_master_key *ci_master_key;
union fscrypt_iv ci_iv; /* per-block IV base */
struct crypto_skcipher *ci_enc_key; /* actual cipher */
/* ... */
};
IV generation (per-block)
/* For AES-256-XTS (contents encryption, default per-file key): */
/* IV = block_number (logical block index within the file, 0-based) */
/* The per-file key is derived from master key + file nonce via HKDF */
/* For filename encryption (AES-256-CTS-CBC): */
/* IV = 0 (CTS-CBC, not ECB — provides diffusion across the filename) */
/* → same filename in same directory always encrypts the same way */
/* → directory listing is possible (if you have the key), order leaks */
/* For policy flag FSCRYPT_POLICY_FLAG_IV_INO_LBLK_64: */
/* IV = inode_number (high bits) | block_number (low bits), packed 64-bit */
/* Enables hardware inline encryption (one key per filesystem) */
/* Note: FSCRYPT_POLICY_FLAG_IV_INO_LBLK_32 uses SipHash for a 32-bit IV */
Inline encryption hardware
# Check for inline encryption support:
cat /sys/block/nvme0n1/queue/crypto_capabilities 2>/dev/null
# AES-256-XTS, key size 64, data unit size 512/1024/2048/4096
# Mount with inline encryption:
mount -o inlinecrypt /dev/nvme0n1p1 /data
# Kernel programs NVMe keyslot; block layer does encryption in hardware
# CPU only sets up keys — no per-block crypto overhead
# Check if inlinecrypt is active:
cat /proc/mounts | grep inlinecrypt
Key revocation and locking
/* Remove master key from filesystem (lock encrypted directories): */
ioctl(fs_fd, FS_IOC_REMOVE_ENCRYPTION_KEY, &key_spec);
/* All pages of encrypted files are evicted from page cache */
/* Inode's ci_enc_key is freed → files become unreadable */
/* On next access attempt: -ENOKEY (key not available) */
# Using fscrypt tool:
fscrypt lock /home/user/private/ # lock (remove key from kernel)
fscrypt unlock /home/user/private/ # unlock (re-add key)
# Check encryption status:
fscrypt status /home/user/private/
# "/home/user/private": encrypted
# Policy version: 2
# Encryption mode: AES-256-XTS (contents)
# AES-256-CTS (filenames)
# Key: unlocked (key identifier: abc123...)
fscrypt vs dm-crypt
| fscrypt | dm-crypt | |
|---|---|---|
| Granularity | Per-directory | Full block device |
| Multiple keys | Yes | No (one key per device) |
| Key revocation | Hot-revoke (pages evicted) | Unmount required |
| Metadata encryption | Filenames only | All metadata |
| Performance | Slightly higher CPU | AES-NI nearly free |
| Use case | Multi-user (Android FBE, shared laptops) | Single-user laptop, server |
| Filesystem | ext4/F2FS/Btrfs | Any FS on top |
Observability
# Check if a file is encrypted:
fscryptctl get_policy /home/user/private/secret.txt
# Encryption policy for /home/user/private/secret.txt:
# Policy version: 2
# Master key identifier: abc1234567890def
# Contents encryption mode: AES-256-XTS
# Filenames encryption mode: AES-256-CTS
# Flags: PAD_32
# List all master keys in a filesystem:
ioctl(fs_fd, FS_IOC_GET_ENCRYPTION_KEY_STATUS, &status)
# (or use fscrypt status)
# dmesg for crypto errors:
dmesg | grep -E "fscrypt|crypto"
# BPF trace key lookups:
bpftrace -e 'kprobe:fscrypt_get_encryption_info { @[comm] = count(); }'
Further reading
- dm-crypt — full-disk encryption alternative
- xattr — fscrypt stores policy in
trusted.fscryptxattr - Linux Audit — audit fscrypt key operations
- Credentials — per-user keyrings for master keys
fs/crypto/— fscrypt implementationDocumentation/filesystems/fscrypt.rst