Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

fscrypt: Filesystem-Level Encryption

Introduction

fscrypt (pronounced “eff-ess-crypt”) is a Linux kernel library that provides filesystem-level encryption. It enables per-directory encryption policies where files in the same directory can share encryption keys but remain inaccessible without the correct key material. Used by ext4, F2FS, and UBIFS, fscrypt provides transparent encryption that integrates with standard filesystem operations while keeping key management in user space. Unlike full-disk encryption (dm-crypt), fscrypt encrypts at the directory level, allowing different keys for different users or directories on the same filesystem.

Architecture Overview

graph TD
    A[User Space] -->|ioctl / keyring| B[fscrypt Key Management]
    B --> C[fscrypt Policy Engine]
    C --> D[Per-File Encryption]
    D --> E[ext4 / F2FS / UBIFS]
    E --> F[Block Layer]
    F --> G[Storage Device]

    subgraph "Key Hierarchy"
        H[Master Key] --> I[Per-File Key]
        I --> J[Nonce + Master Key]
        J --> K[HKDF / AES-256-XTS]
    end

Encryption Modes

fscrypt supports multiple cipher modes for different purposes:

ModePurposeCipherKey Size
AES-256-XTSFile contentsAES-XTS512 bits (2 × 256)
AES-256-CTS-CBCFile namesAES-CBC-CTS256 bits
AES-128-CBCFile contents (v1)AES-CBC128 bits
AdiantumLow-end devicesAdiantum256 bits
AES-256-HCTR2File names (new)AES-HCTR2256 bits

Mode Selection

# Content encryption modes (in policies)
"fscrypt:v1"  → AES-256-XTS or AES-128-CBC
"fscrypt:v2"  → AES-256-XTS (default)

# Filename encryption modes
"fscrypt:v1"  → AES-256-CTS-CBC
"fscrypt:v2"  → AES-256-CTS-CBC or AES-256-HCTR2

Key Hierarchy

fscrypt uses a multi-level key hierarchy to derive per-file keys:

graph TD
    A[User Passphrase / Key Descriptor] --> B[Master Key]
    B --> C[KDF: HKDF-SHA512]
    C --> D[Per-File Encryption Key]
    C --> E[Per-File Naming Key]
    D --> F[AES-256-XTS for content]
    E --> G[AES-256-CTS-CBC for filenames]

    subgraph "Per-File Derivation"
        H[File nonce] --> C
        I[Policy version] --> C
    end

Key Derivation

/* fs/crypto/keysetup.c - simplified per-file key derivation */
static int fscrypt_derive_dirhash_key(struct fscrypt_info *ci,
                                       const struct fscrypt_policy *policy)
{
    u8 derived_key[FSCRYPT_MAX_KEY_SIZE];

    /* Derive per-file key using HKDF */
    return fscrypt_hkdf_expand(&ci->ci_policy,
                                FSCRYPT_HKDF_CONTEXT_FILE_ENCRYPTION_KEY,
                                ci->ci_nonce, FSCRYPT_FILE_NONCE_SIZE,
                                derived_key, ci->ci_mode->keysize);
}

Policy Versions

v1 Policies (Legacy)

v1 policies use a key descriptor (16-byte hex string) and require the master key to be provided directly:

# Set up v1 encryption policy
# 1. Generate a key identifier
echo "0123456789abcdef0123456789abcdef" | keyctl padd logon fscrypt:0123456789abcdef @u

# 2. Set policy on directory
fscryptctl set_policy 0123456789abcdef /encrypted_dir

v2 Policies (Current)

v2 policies use HKDF-based key derivation and support multiple users sharing the same policy with different keys:

# v2 policy setup
# 1. Create a v2 policy
fscryptctl setup /encrypted_dir

# 2. Add encryption key
fscryptctl key_add /encrypted_dir --key-descriptor=mykey

# 3. Lock the directory (remove key from kernel)
fscryptctl key_remove /encrypted_dir

fscrypt ioctl Interface

Setting Encryption Policy

#include <linux/fscrypt.h>
#include <sys/ioctl.h>

int set_encryption_policy(int dirfd, const unsigned char *key_id)
{
    struct fscrypt_policy_v2 policy = {
        .version = 2,
        .contents_encryption_mode = FSCRYPT_MODE_AES_256_XTS,
        .filenames_encryption_mode = FSCRYPT_MODE_AES_256_CTS,
        .flags = FSCRYPT_POLICY_FLAGS_PAD_16,
    };

    /* Copy the key identifier (16 bytes) */
    memcpy(policy.master_key_identifier, key_id,
           FSCRYPT_KEY_IDENTIFIER_SIZE);

    return ioctl(dirfd, FS_IOC_SET_ENCRYPTION_POLICY, &policy);
}

Getting Encryption Policy

int get_encryption_policy(int dirfd)
{
    struct fscrypt_get_policy_ex_arg arg = {
        .policy_size = sizeof(arg.policy),
    };

    int ret = ioctl(dirfd, FS_IOC_GET_ENCRYPTION_POLICY_EX, &arg);
    if (ret == 0) {
        printf("Policy version: %d\n", arg.policy.version);
        printf("Contents mode: %d\n",
               arg.policy.v2.contents_encryption_mode);
        printf("Filenames mode: %d\n",
               arg.policy.v2.filenames_encryption_mode);
    }
    return ret;
}

Adding Keys to the Keyring

#include <keyutils.h>

int add_fscrypt_key(const char *key_desc, const unsigned char *key,
                    size_t key_len)
{
    key_serial_t keyring;

    /* Get the user keyring */
    keyring = keyctl_search(KEY_SPEC_USER_KEYRING, "logon",
                            "fscrypt:" KEY_DESC, 0);

    /* Add the key */
    return add_key("logon", key_desc, key, key_len,
                   KEY_SPEC_USER_KEYRING);
}

Encryption of File Contents

How File Content Encryption Works

sequenceDiagram
    participant App as Application
    participant VFS as VFS
    participant FS as ext4/F2FS
    participant FSC as fscrypt
    participant Bio as Block I/O

    App->>VFS: write(data, offset)
    VFS->>FS: write_begin()
    FS->>FSC: fscrypt_encrypt_pagecache_blocks()
    FSC->>FSC: Derive per-block IV
    FSC->>FSC: AES-256-XTS encrypt
    FSC-->>FS: Encrypted blocks
    FS->>Bio: Submit encrypted blocks

Block IV Derivation

Each block within a file gets a unique IV based on its position:

/* fs/crypto/crypto.c - IV derivation */
static void fscrypt_generate_iv(u8 *iv, u64 lblk_num,
                                 const struct fscrypt_info *ci)
{
    __le64 lblk_num_le = cpu_to_le64(lblk_num);

    memset(iv, 0, FS_IV_SIZE);
    /* XOR the file nonce with the logical block number */
    BUILD_BUG_ON(FSCRYPT_FILE_NONCE_SIZE != FS_IV_SIZE);
    crypto_xor(iv, ci->ci_nonce, FSCRYPT_FILE_NONCE_SIZE);
    memcpy(iv, &lblk_num_le, sizeof(lblk_num_le));
}

Encryption of File Names

How Encrypted Names Look

# Encrypted filenames appear as base64-encoded ciphertext:
# Unencrypted: "secret-document.pdf"
# Encrypted:   "Gj3kF9xP2mN8hQ5vR7wY1bC4dT6aS0eU+iJ9kL2mN4oP6q"

# Names are padded to fixed sizes to prevent length-based analysis
# Padding: 16 bytes (FSCRYPT_POLICY_FLAGS_PAD_16)
#          32 bytes (FSCRYPT_POLICY_FLAGS_PAD_32)

Directory Listing with Keys

# With key available: normal directory listing
ls /encrypted_dir/
# file1.txt  file2.txt  subdir/

# Without key: encrypted names
# ls still works but shows ciphertext
ls /encrypted_dir/
# Gj3kF9xP2mN8hQ5vR7wY1bC4dT6aS0eU
# Hk4lG0yQ3nO9iR6sS8xZ2cD5eU7bT1fV+jK0lM3nP5qR7s

Locking (Removing Keys)

fscrypt supports directory locking by removing the key from the kernel:

# Remove key from kernel
keyctl revoke $(keyctl search @u logon "fscrypt:0123456789abcdef")

# Directory becomes locked:
# - New opens return -ENOKEY
# - Encrypted names visible (ciphertext)
# - File contents inaccessible
ls /encrypted_dir/
# Shows encrypted filenames only
cat /encrypted_dir/file1.txt
# cat: file1.txt: Required key not available

Lock/Unlock Workflow

graph TD
    A[Unlock: Add key to keyring] --> B[Normal file access]
    B --> C[Lock: Revoke key]
    C --> D[Directory locked]
    D --> E[Encrypted names visible]
    D --> F[Content access denied]
    F --> G[Unlock: Re-add key]
    G --> B

Inline Encryption

Modern storage devices support inline encryption where the storage controller handles encryption/decryption, freeing the CPU:

graph LR
    A[fscrypt key] --> B[blk-crypto]
    B --> C[Inline Encryption HW]
    C --> D[Storage Device]
    D --> E[Encrypted at rest]

Inline Crypto Configuration

/* Block layer crypto configuration */
struct blk_crypto_key {
    unsigned int crypto_mode;
    unsigned int data_unit_size;
    unsigned int size;
    unsigned int data_unit_size_bits;
    u8 raw[BLK_CRYPTO_MAX_KEY_SIZE];
};

/* Register with block layer */
int blk_crypto_init_key(struct blk_crypto_key *key,
                         const u8 *raw_key,
                         enum blk_crypto_mode_num mode,
                         unsigned int data_unit_size,
                         unsigned int dun_bytes);

DMCrypt vs fscrypt Inline

Featuredm-cryptfscrypt + inline
GranularityFull block devicePer-directory
Key managementSingle keyMultiple keys
Inline crypto
CPU overheadHigh (no inline)Low (with inline)
MetadataNot encryptedFile names encrypted

fscrypt Userspace Tools

fscryptctl

# Install
apt install fscryptctl

# Setup
mkdir /encrypted
mkfs.ext4 -O encrypt /dev/sdb1
mount /dev/sdb1 /encrypted

# Create key
head -c 64 /dev/urandom > /tmp/key
keyctl padd logon fscrypt:mykey @u < /tmp/key

# Set policy
fscryptctl set_policy --key=mykey /encrypted/private

# Lock
keyctl revoke $(keyctl search @u logon fscrypt:mykey)

fscrypt (high-level tool)

# Install
apt install fscrypt

# Setup filesystem
fscrypt setup /encrypted

# Create encrypted directory
fscrypt encrypt /encrypted/private
# Enter passphrase: *****
# Confirm: *****

# Use normally
echo "secret" > /encrypted/private/notes.txt
cat /encrypted/private/notes.txt
# secret

# Lock directory
fscrypt lock /encrypted/private

Kernel Configuration

CONFIG_FS_ENCRYPTION=y
CONFIG_FS_ENCRYPTION_ALGS=y
# Individual cipher support
CONFIG_CRYPTO_AES=y
CONFIG_CRYPTO_XTS=y
CONFIG_CRYPTO_CTS=y
CONFIG_CRYPTO_HKDF=y
CONFIG_CRYPTO_SHA512=y
# Inline encryption (optional)
CONFIG_BLK_INLINE_ENCRYPTION=y
CONFIG_BLK_INLINE_ENCRYPTION_FALLBACK=y

Supported Filesystems

Filesystemfscrypt v1fscrypt v2Inline crypto
ext4✓ (Linux 5.4+)✓ (Linux 5.9+)
F2FS✓ (Linux 5.4+)✓ (Linux 5.9+)
UBIFS-

Performance Impact

OperationOverhead (AES-NI)Overhead (no AES-NI)
Sequential read3-5%20-30%
Sequential write3-5%20-30%
Random 4K read5-8%30-50%
Random 4K write5-8%30-50%
Metadata ops< 1%2-5%
Inline crypto< 1%< 1%

Security Considerations

Threat Model

fscrypt protects against:

  • Offline attacks: Data at rest is encrypted
  • Key isolation: Different directories can use different keys
  • Authenticated encryption: v2 policies use AEAD modes

fscrypt does NOT protect against:

  • Running system attacks: Keys are in kernel memory
  • Kernel compromise: Root can access all keys
  • Side-channel attacks: Timing, power analysis

Key Erasure

# Secure key removal
keyctl revoke $(keyctl search @u logon "fscrypt:mykey")

# Verify directory is locked
ls /encrypted/private/
# Should show encrypted names only

fscrypt v2 Key Identifier

v2 policies use a 16-byte key identifier derived from the master key via HKDF:

/* fs/crypto/keysetup.c */
static int fscrypt_init_hkdf(struct fscrypt_hkdf *hkdf,
                              const u8 *master_key, unsigned int mk_size)
{
    u8 info[HKDF_CONTEXT_LEN];
    int err;

    /* Initialize HKDF with master key as IKM */
    err = crypto_shash_setkey(hkdf->hmac_tfm, master_key, mk_size);
    if (err)
        return err;

    /* Extract PRK from IKM using zero salt */
    /* PRK = HMAC-Hash(salt, IKM) where salt = 0 */

    return 0;
}

HKDF Context Types

ContextPurposeDerives
fscrypt_hkdf_context_direct_keyPer-mode keyAES-256-XTS or AES-256-CTS-CBC
fscrypt_hkdf_context_iv_ino_lblk_64_keyPer-file key (inline)IV + key for inline crypto
fscrypt_hkdf_context_dirhash_keyDirectory hash keySipHash key for dir indexing
fscrypt_hkdf_context_identifierKey identifier16-byte identifier for v2 policies
# View key identifiers in kernel keyring
keyctl show @u
# Key descriptor: logon:fscrypt:abcdef0123456789abcdef0123456789
# The 32-hex-char string is the key identifier

Directory Indexing and Casefolding

fscrypt supports casefolded directories (case-insensitive lookups):

# Create filesystem with casefolding support
mkfs.ext4 -O casefold,encrypt /dev/sdb1
mount /dev/sdb1 /encrypted

# Set casefold policy on directory
fscryptctl set_policy --casefold=utf8 /encrypted/casefold_dir

# Now lookups are case-insensitive
echo "Hello" > /encrypted/casefold_dir/FILE.txt
cat /encrypted/casefold_dir/file.txt
# Works! Case-insensitive matching
/* fs/crypto/keysetup_v2.c - dirhash key derivation */
int fscrypt_derive_dirhash_key(struct fscrypt_info *ci,
                                const struct fscrypt_policy_v2 *policy)
{
    /* Derive SipHash key for casefolded directory indexing */
    return fscrypt_hkdf_expand(&fscrypt_master_key->hkdf,
                                FSCRYPT_HKDF_CONTEXT_DIRHASH_KEY,
                                ci->ci_nonce, FSCRYPT_FILE_NONCE_SIZE,
                                ci->ci_dirhash_key,
                                FSCRYPT_DIRHASH_KEY_SIZE);
}

Policy Inheritance

fscrypt policies are inherited by child files and directories:

flowchart TD
    A["/encrypted (policy set)"] --> B["/encrypted/file1.txt (inherits policy)"]
    A --> C["/encrypted/subdir (inherits policy)"]
    C --> D["/encrypted/subdir/file2.txt (inherits)"]
    C --> E["/encrypted/subdir/nested (inherits)"]

    F["Cannot set new policy on child"] --> G["EINVAL: parent already has policy"]
# Set policy on parent directory
fscryptctl set_policy --key=mykey /encrypted/private

# All new files in this directory are automatically encrypted
# Cannot change policy on children
# Cannot remove policy once set

# Lock parent: all children become inaccessible
fscryptctl key_remove /encrypted/private
ls /encrypted/private/
# Shows encrypted names only

Multi-User Policy Sharing

v2 policies allow multiple users to share the same policy with different master keys:

flowchart TD
    A["Shared directory /shared"] --> B["User A: master_key_a"]
    A --> C["User B: master_key_b"]
    B --> D["Same policy ID, different keys"]
    C --> D
    D --> E["Both can read/write same files"]
# User A adds their key
fscryptctl key_add /shared --key-descriptor=a_key

# User B adds their key (same policy, different key)
fscryptctl key_add /shared --key-descriptor=b_key

# Both users can access files in /shared

fscrypt encrypts symlink targets the same way as filenames:

# Create symlink in encrypted directory
ln -s /etc/passwd /encrypted/private/link

# Read symlink (with key available)
readlink /encrypted/private/link
# /etc/passwd

# Without key: encrypted target
readlink /encrypted/private/link
# Gj3kF9xP2mN8hQ5vR7wY1bC4dT6aS0eU+iJ9kL2mN4oP6q

Padding and Filename Length

fscrypt pads filenames to fixed sizes to prevent length-based traffic analysis:

# Padding options in policy flags
# FSCRYPT_POLICY_FLAGS_PAD_4  — pad to 4-byte boundary
# FSCRYPT_POLICY_FLAGS_PAD_8  — pad to 8-byte boundary
# FSCRYPT_POLICY_FLAGS_PAD_16 — pad to 16-byte boundary (default)
# FSCRYPT_POLICY_FLAGS_PAD_32 — pad to 32-byte boundary

# All filenames in a directory have the same encrypted length
# (after padding and base64 encoding)
Plaintext LengthPAD_4PAD_8PAD_16PAD_32
1-4 chars32 bytes32 bytes32 bytes32 bytes
5-8 chars48 bytes32 bytes32 bytes32 bytes
9-16 chars80 bytes64 bytes32 bytes32 bytes
17-32 chars144 bytes128 bytes96 bytes32 bytes
33-64 chars272 bytes256 bytes224 bytes160 bytes

Filesystem Preparation

Creating an Encrypt-Capable Filesystem

# ext4 with encryption feature
mkfs.ext4 -O encrypt /dev/sdb1

# F2FS with encryption
mkfs.f2fs -O encrypt /dev/sdb2

# Verify encryption support
dumpe2fs -h /dev/sdb1 | grep -i feature
# Filesystem features: ... encrypt ...

# Or with F2FS
dump.f2fs /dev/sdb2 | grep -i encrypt

Kernel Requirements

# Required kernel config
CONFIG_FS_ENCRYPTION=y
CONFIG_FS_ENCRYPTION_ALGS=y

# Cipher support
CONFIG_CRYPTO_AES=y
CONFIG_CRYPTO_XTS=y         # AES-256-XTS (content encryption)
CONFIG_CRYPTO_CTS=y         # AES-256-CTS-CBC (filename encryption)
CONFIG_CRYPTO_HKDF=y        # HKDF key derivation
CONFIG_CRYPTO_SHA512=y      # HKDF hash

# Optional: inline encryption
CONFIG_BLK_INLINE_ENCRYPTION=y
CONFIG_BLK_INLINE_ENCRYPTION_FALLBACK=y

# Check runtime support
cat /proc/crypto | grep -E "xts|cts|hkdf"

Performance Deep-Dive

AES-NI Acceleration

# Check if AES-NI is available
cpuinfo | grep -i aes
# aes : AES instructions

# Benchmark encryption performance
fscryptctl benchmark
# AES-256-XTS: 2.5 GB/s (with AES-NI)
# AES-256-CTS-CBC: 2.3 GB/s (with AES-NI)
# AES-256-XTS: 150 MB/s (without AES-NI)

Per-File vs Per-Block Overhead

flowchart TD
    A["File Write"] --> B["Derive per-file key (one-time)"]
    B --> C["For each block:"]
    C --> D["Compute IV = file_nonce XOR block_number"]
    D --> E["AES-256-XTS encrypt block"]
    E --> F["Write encrypted block"]

    G["File Read"] --> H["Key already cached? (fast)"]
    H -->|Yes| I["Derive IV, decrypt block"]
    H -->|No| J["Look up key (slower)"]
    J --> I

Key Caching

/* fs/crypto/keysetup.c - key caching */

/* Keys are cached in the per-inode fscrypt_info */
struct fscrypt_info {
    struct fscrypt_master_key *ci_master_key; /* Cached master key */
    const struct fscrypt_mode *ci_mode;       /* Encryption mode */
    u8 ci_nonce[FSCRYPT_FILE_NONCE_SIZE];    /* Per-file nonce */
    struct fscrypt_encryption_key ci_enc_key; /* Derived key */
};

/* The master key is cached in a kernel keyring */
/* fscrypt_info is created on first access and cached */
/* Key derivation is O(1) after initial cache */

Android File-Based Encryption (FBE)

Android uses fscrypt for per-user encryption:

flowchart TD
    subgraph Android["Android FBE Layout"]
        CE["Credential Encrypted (CE)"]
        DE["Device Encrypted (DE)"]
        CE --> C1["User data (requires unlock)"]
        DE --> D1["Early-boot data (always available)"]
    end
# Android storage directories
/data/user/0/          # CE storage (requires unlock)
/data/user_de/0/       # DE storage (available at boot)
/data/misc/vold/       # Key storage

# Android key hierarchy
# Hardware-bound key (TEE/StrongBox)
#   -> User credential key (PIN/password)
#     -> Per-directory policy key
#       -> Per-file encryption key

Android Key Storage

/* Android stores keys in /data/misc/vold/user_keys/ */
/* Keys are wrapped with hardware-bound keys from TEE */

/* Key format: struct fscrypt_key */
struct fscrypt_key {
    u32 mode;           /* Encryption mode */
    u8 raw[FSCRYPT_MAX_KEY_SIZE];
    u32 size;
};

fscrypt vs dm-crypt Comparison

Aspectfscryptdm-crypt
GranularityPer-directoryFull block device
Key managementMultiple keys per FSSingle key per device
MetadataFilenames encryptedMetadata not encrypted
PerformanceLower overheadHigher overhead (full I/O path)
SwapNot affectedEncrypts swap too
BootWorks without initramfsRequires initramfs key setup
Hardware cryptoInline crypto supportRequires dm-crypt HW offload
Use caseMobile, multi-userLaptops, servers, compliance

Cross-References

  • ext4 - ext4 filesystem (supports fscrypt)
  • F2FS - Flash-Friendly File System (supports fscrypt)
  • Inode - File metadata structures
  • VFS - Virtual File System layer
  • dm-crypt - Full-disk encryption
  • Keyring - Kernel key management
  • Cryptography - Kernel crypto subsystem
  • Secure Boot - Boot-time integrity

Troubleshooting

Common Issues

ProblemCauseSolution
“Required key not available”Key not in keyringAdd key with fscryptctl/fscrypt
“Operation not permitted”Not root or missing capabilityRun as root or check capabilities
“Invalid argument”Wrong policy versionCheck policy version matches tool
“File exists” errorDirectory already has policyCannot change policy on existing dir
Slow performanceNo AES-NI supportCheck CPU flags for aes
Encryption not workingFeature not enabledCheck CONFIG_FS_ENCRYPTION=y

Debugging Commands

# Check if filesystem supports encryption
dumpe2fs /dev/sda1 | grep -i encrypt
# Should show: encryption

# Check kernel config
zcat /proc/config.gz | grep FS_ENCRYPTION
CONFIG_FS_ENCRYPTION=y
CONFIG_FS_ENCRYPTION_ALGS=y

# View encryption policy on directory
fscryptctl get_policy /encrypted_dir

# Check if key is in keyring
keyctl list @u

# View kernel encryption messages
dmesg | grep -i fscrypt

# Test encryption performance
fscryptctl benchmark

Recovery Procedures

# If key is lost, data is unrecoverable by design
# Prevention: backup key material securely

# Export key for backup
fscryptctl key_add /encrypted_dir --key-descriptor=mykey
keyctl pipe $(keyctl search @u logon "fscrypt:mykey") > /backup/key.bin

# Restore key from backup
keyctl padd logon fscrypt:mykey @u < /backup/key.bin

Further Reading

References