A glimpse of ext4 filesystem-level encryption

Linux 4.1 has arrived with a new feature for its popular ext4 filesystem: filesystem-level encryption!

This feature appears to have been implemented by Google since they plan to use it for future versions of Android and Chrome OS.

Android filesystem encryption currently relies on dm-crypt. Google's motivations for pushing encryption to ext4 seem:

  • To avoid using a stacked filesystem design (for better performance?).
  • To encrypt data with integrity.
  • To allow multiple users to encrypt their files with different keys on the same filesystem.

I decided to write a userspace tool to use this new feature: ext4-crypt. At the time of writing, my personal opinion is the current kernel implementation is really shaky and unstable and should not be used. Although future kernel versions will certainly come with enhancements, I think this is far from being production-ready and has been pushed prematurely into the kernel (I encountered kernel crashes by merely testing it as a simple user).

Let's move on and take a peek at the implementation.

Cryptographic implementation

This section describes the implementation as of Linux 4.1.3.

ext4 encryption works on a per-directory basis. An encryption policy is at first applied to an empty directory. The policy specifies basic information like:

  • Versioning information.
  • The cipher used for encrypting filenames.
  • The cipher used for encrypting file contents.
  • The length of padding for filenames (4, 8, 16 or 32).
  • An 8 bytes descriptor used to locate the master key in the user keyring.

On the disk structure, an encryption context is associated with the inode and stored in an extended attribute. The type of xattr used does not belong to any namespace, and you won't be able see it with a userspace tool like getfattr.

The encryption context contains the same information as the encryption policy, along with a random nonce of 16 bytes that is used to derive the encryption key. Each inode has its own random nonce, so as a result each file in the directory is encrypted with a different key.

Every time a new inode is created inside that directory, the encryption context will be inherited from the parent directory. The encryption process is only applied to three kind of files: directories, regular files and symbolic links.

For now, it is not possible to choose the cryptographic ciphers:

  • File contents will be encrypted with aes256-xts.
  • Filenames will be encrypted with aes256-cbc-cts (IV = 0) and encoded with an algorithm similar to base64.

No data integrity is implemented yet. However more cipher modes will be available in the next kernel versions, including aes256-gcm.

When an encrypted inode is accessed, the master key associated with its policy descriptor is requested from the user keyring. The key type must be logon (i.e. readable only by kernel) and the key descriptor be formatted as ext4:<policy descriptor in hexa>.

If the master key is not found, opening the file will return an access denied error. Otherwise, the encryption key is derived from the master key and the nonce using aes128-ecb. Surprisingly, there is no verification that the provided master key is actually the one that was used to encrypt the file contents in the first place.

This is an overview of the encryption process:

                                                                   AES256-CBC-CTS
                                           +--------------------+     & encode    +------------------+
                                           |ext4 dentry         +--------+-------->ENCRYPTED FILENAME|
                                           |                    |        |        |                  |
                                           +---------^----------+        |        +------------------+
                                                     |                   |
                                                     |                   |
                                           +---------v----------+    AES256-XTS   +------------------+
                                           |ext4 inode          +--------+-------->ENCRYPTED CONTENTS|
                                           |                    |        |        |                  |
                                           +--------------------+        |        +------------------+
                                                     |encryption xattr   |
                                                     |                   |
+------------------------+   policy desc   +---------v----------+        |
| USER SESSION KEYRING   <-----------------+ crypto context     |        |
|                        |                 |                    |        |
| +-------------------+  +-----------------> - policy descriptor|        |
| | ext4 policy: key  |  |   master key    | - random nonce     |        |
| +-------------------+  |                 |                    |        |
+------------------------+                 +---------+----------+        |
                                                     |                   |
                        AES128-ECB(master_key, nonce)|                   |
                                                     |                   |
                                           +---------v----------+        |
                                           | ENCRYPTION KEY     +--------+
                                           +--------------------+

Usage from userspace

To use ext4 encryption, one first needs to have a Linux 4.1+ kernel compiled with CONFIG_EXT4_ENCRYPTION.

The process of encrypting a directory is quite simple and does not require any particular privilege other than owning the directory:

  1. First ensure the directory is empty.
  2. Open the directory and send an ioctl to assign it an encryption policy.
  3. Insert the master key into the user session keyring. The key must be of logon type.

That's it. Now, every file you write inside that directory will be transparently encrypted on disk.

I wrote a userspace tool to create encrypted ext4 directories. You can download it from github.

To create an encrypted directory, simply do:

$ mkdir vault
$ ext4-crypt create vault
Enter passphrase:
Confirm passphrase:
vault: Encryption policy is now set.
$ ext4-crypt status vault
vault/: Encrypted directory
Policy version:   0
Filename cipher:  aes-256-cts
Contents cipher:  aes-256-xts
Filename padding: 4
Key descriptor:   lkVZDRI6
Key serial:       524153968

You can then check the master key is properly assigned to your keyring and is not readable:

$ keyctl show
Session Keyring
 813374732 --alswrv   1000 65534  keyring: _uid_ses.1000
 758072319 --alswrv   1000 65534   \_ keyring: _uid.1000
 524153968 --alsw-v   1000  1000   \_ logon: ext4:6c6b565a44524936

When the ext4 volume is mounted and no key is provided, filenames will appear as encrypted and reading or writing to files will be denied. You can access the directory contents by inserting the master key again into the keyring:

$ ls vault
,nCGhbNxWfdBfzffulophA  74XmRzli9dITlYBWLbpkTD  CdJOUkfjKxzOd+0zYaO0GC
$ ext-crypt attach vault
Enter passphrase:
$ ls vault
Documents  Downloads  Music

Remarks

The current implementation of ext4 encryption has a number of problems compared to the existing alternatives for disk encryption. The biggest of them in my opinion is the lack of key verification on the kernel side.

Any key you insert into the user keyring will be blindly accepted by the kernel and used for all kind of file operations. As a consequence you can read and write to files with the wrong encryption key, resulting in data corruption or just reading junk data.

This can lead to even stranger situations because filenames decrypted with the wrong key can contain slashes and null bytes, which are forbidden characters for the filesystem. Decrypted filenames can contain "/" or "/../", and you can't open them. I'm not even sure what are the actual implications of this, but that probably should never happen.

Some parts of the code also indicates a lack of testing. From fs/ext4/crypto_key.c in Linux 4.1.3:

ukp = ((struct user_key_payload *)keyring_key->payload.data);
if (ukp->datalen != sizeof(struct ext4_encryption_key)) {
    res = -EINVAL;
    goto out;
}
master_key = (struct ext4_encryption_key *)ukp->data;
BUILD_BUG_ON(EXT4_AES_128_ECB_KEY_SIZE !=
         EXT4_KEY_DERIVATION_NONCE_SIZE);
BUG_ON(master_key->size != EXT4_AES_256_XTS_KEY_SIZE); // <--- ???

Why return an error code on user-supplied data when you can just oops the kernel?

Other issues include the filesystem cache that is not invalidated when the key is unlinked and still lets you see the plaintext filenames. An encrypted directory is also left in a inconsistent state if a policy is applied and no inode is created before the device is unmounted.

I encountered some kernels crashes during the development of the tool with some basic file/key manipulations.

I am really surprised since I would have expected a very thorough code review and a lot of testing for a major filesystem like ext4.

It is probably wise to wait for the next kernel versions before using this feature. Some of these problems will hopefully be fixed by then and new features like data integrity should be added in the future.

Comments