PEFS (Private Encrypted File System) is a kernel level stacked cryptographic filesystem for FreeBSD.

PEFS crypto primitives

This document was created from an article written by Gleb Kurtsou

Supported data encryption algorithms: AES and Camellia (with 128, 192 and 256 bits key sizes). Adding another block cipher with 128 block size should be trivial.

Filename encryption

File names are always encrypted using AES-128 in CBC mode with zero IV. Encrypted file name consists of a random per file tweak, checksum and the name itself:

XBase64(checksum || E(tweak || filename))

Checksum is VMAC of encrypted tweak and file name:

checksum = VMAC(E(tweak || filename))

Both the checksum and the tweak have 64 bit length. Stronger / larger lengths are not advisable because the filenames are limited by the file system name length limits (255 bytes); with multibyte encodings like UTF-8, the total available space might be very small (e.g. for non-ASCII European languages using 2 bytes per UTF-8 character, the process might leave only 80-90 characters for the actual user-specified filename).

Filenames are encrypted in AES-CBC mode with zero IV (and padded with zeros to cipher block boundary). First encryption block contains pseudo random tweak which makes all encrypted filenames unique due to CBC mode chaining. Main reason for not providing alternatives to the name encryption algorithm is to keep the design simple.

Encrypt-then-Authenticate construction is used. It allows checking if the name was encrypted by a given key without performing decryption. VMAC was chosen because of it performance characteristics and its ability to produce 64 bit MAC (without truncation of original result like in HMAC case). There is no real "authentication" performed; it's designed to be just a strong checksum. Breaking VMAC wouldn't result in breaking encrypted data. This checksum's main purpose is to be able to find a key the file is encrypted with.

Encrypted directory/socket/device names also contain tweak but it's used solely to randomize first CBC block and keep name structure uniform.

File data encryption

Files are encrypted to the same length as their original size. PEFS uses block ciphers (AES, Camellia) operating in XTS mode, in which a 64 bit per-file tweak value concatenated with 64 bit file offset form the XTS tweak. All encryption operations are performed on 4096 byte sectors ("data unit" in XTS notion). The idea behind using a tweak is to get unique, per file ciphertext. Incomplete sectors are also encrypted according to XTS standard, but encryption of blocks smaller then 128 bits is not defined for XTS. In this situation (e.g. the "remainder" of a file if the file size is not a multiple of 4 KiB when the remainder is also smaller than 16 bytes), CTR mode is used with IV being XTS tweak value for the sector combined with sector length. Sparse files are detected by examining if all 4096 bytes in a sector are 0 before decryption (in which case they are not decrypted but 0-filled sector is returned).

Other information

4 different keys are used with cryptographic primitives: one for filename encryption, one for VMAC and two keys for data encryption as required by XTS. All these keys are derived from the 512 bit user supplied key using HKDF algorithm based on HMAC-SHA512 (IETF draft). The kernel part expects cryptographically strong key from userspace. This key is generated with PBKDF on using HMAC-SHA512 from passphrase.

Standard implementations of ciphers are used, but not opencrypto framework. Opencrypto is not used mainly because it lacks full support for XTS mode (it is currently not able to encrypt incomplete / "short" sectors). It is also rather heavy weight (extra initialization and memory allocations) so using it may even hurt performance.

PEFS supports multiple keys, mixing files encrypted with different keys in single directory, transparent (unencrypted) mode, key chaining (adding a series of keys by entering just one of them) and more.

Key chains

Chain is a series of keys. Chains are stored in a database (.pefs.db file). Each database entry consists of two keys: parent key and child key.

Consider the following chain: k1 -> k2 -> k3.
4 database entries are used to represent it, '0' denotes end of chain marker; entries are represented in the form (parent key => child key):

(k1 => k2)
(k2 => k3)
(k3 => 0)

If user enters k1 passphrase (with 'addkey -c' command or similar), all 3 keys will be retrieved from database added added to file system: k1, k2, k3.

Entering k3 passphrase will result in a single k3 key. Such use case emulates storing single key for personal encrypted directory.

Database may contain independent key chains:

a1 -> a2
b1 -> b2 -> b3
c1

Using key chains one can emulate access levels, e.g. in first example k1 is considered the most secure key and k3 - the less: k1 = top-secret, k2 = personal, k3 = anti-notebook-theft

All database entries are stored encrypted in a way that child key can be decrypted only by parent key.

Technical details

Technically database is a standard btree(3) database with entries indexed by KEYID(key). It may be described as a dictionary of the following form:

database[KEYID(key)] = data || mac
data = AES128-CTR(KEK(key), child_key || key_param)
mac = HMAC-SHA512(KEK(key), data)

Entry correctness is guarantied by Message Authentication Code (mac).

HMAC-SHA512 is used to calculate both Key Encryption Key (KEK) and KEYID:

KEYID(key) = HMAC-SHA512(key, CONST_KEYID)
KEK(key) = HMAC-SHA512(key, CONST_KEK)

While adding key chain with addkey -c first parent key is generated from user provided passphrase:

key = PKCS5V2(passphrase)

Child key is than looked up in database: child_key = database[KEYID(key)].child_key. The procedure repeats recursively until end of chain marker is found.

Besides first parent key parameters (algorithm and key size) can also be restored from database entry (key_param field), i.e. there is no need for user to specify algorithm and key size used every time; all keys can have different parameters.

Tutorial: Encrypting a directory

Let's encrypt ~/private.enc directory and mount it to ~/private. Note that PEFS supports mounting on the same directory, separate directories used for clarity.

# mkdir ~/private ~/private.enc

1. Save encryption key

PEFS doesn't store any metadata (as encryption keys) within file system, for end user it means that passphrase he or she enters is not saved by file system and there is no way to verify its correctness. To achieve such functionality userlevel pefs utility has support for key chains (series of keys). The key chain consisting of a single key can be used to emulate traditional key storage.

addchain command adds an entry to key chain database (.pefs.db file), database file will be created if it doesn't exist.
-f option disables file system type check: to be accessible afterwards database has to be created on plain (not encrypted) file system.
-Z option specifies that there is no child key in chain being created, i.e. entry consists only of single parent key.

# pefs addchain -f -Z ~/private.enc
Enter parent key passphrase:
Reenter parent key passphrase:

Key chain database file was created:

# ls -A ~/private.enc
.pefs.db

2. Mount file system with saved encryption key

PEFS file system doesn't require a key to be mounted. After mount file system remains in read-only mode until key is added.

# pefs mount ~/private.enc ~/private

addkey -c option is used to allow only keys from key chain database. Without it any key can be added to a file system.

# pefs addkey -c ~/private
Enter passphrase:


CategoryHowTo CategoryProject

PEFS (last edited 2022-06-08T01:05:57+0000 by KubilayKocak)