Encryption in EXT4. How it works

1. How it works
First you need to learn some useful commands.
Formatting a volume with an encryption option
# mkfs.ext4 -O encrypt /dev/xxx
Enabling the encryption option on an existing volume
# tune2fs -O encrypt /dev/xxx
Create Encryption Key
# mount /dev/xxx /mnt/xxx
$ e4crypt add_key
Enter passphrase (echo disabled):
Added key with descriptor [8e679e4449bb9235]
When creating a key, a volume with encryption support must be mounted, otherwise e4crypt will generate a “No salt values available” error. If several volumes with the encrypt option are mounted, then keys for each will be created. The e4crypt utility is part of e2fsprogs.
Keys are added to Linux Kernel Keyring [1].
Reading a list of keys
$ keyctl show
Session Keyring
771961813 --alswrv 1000 65534 keyring: _uid_ses.1000
771026675 --alswrv 1000 65534 \_ keyring: _uid.1000
803843970 --alsw-v 1000 1000 \_ logon: ext4:8e679e4449bb9235
The keys used for encryption are of type “logon”. The content (payload) of keys of this type is not accessible from user space - the keyctl of the read, pipe, print command will return an error. In this example, the key has the prefix “ext4”, but it can also be “fscrypt”. If keyctl is not present on the system, then the keyutils package must be installed.
Creating an encrypted directory
$ mkdir /mnt/xxx/encrypted_folder
$ e4crypt set_policy 8e679e4449bb9235 /mnt/xxx/encrypted_folder/
Key with descriptor [8e679e4449bb9235] applied to /mnt/xxx/encrypted_folder/.
Here, the set_policy command is passed the handle of the created key without specifying the prefix (ext4) and type (logon). Multiple directories can be encrypted with the same key. You can use different keys to encrypt different directories. To find out which key the directory is encrypted, you need to run the command:
$ e4crypt get_policy /mnt/xxx/encrypted_folder/
/mnt/xxx/encrypted_folder/: 8e679e4449bb9235
Installing a different security policy on an encrypted directory will fail:
$ e4crypt add_key
Enter passphrase (echo disabled):
Added key with descriptor [9dafe822ae6e7994]
$ e4crypt set_policy 9dafe822ae6e7994 /mnt/xxx/encrypted_folder/
Error [Invalid argument] setting policy.
The key descriptor [9dafe822ae6e7994] may not match the existing encryption context for directory [/mnt/xxx/encrypted_folder/].
But such a directory can be easily removed:
$ rm -rf /mnt/xxx/encrypted_folder/
$ ll /mnt/xxx
total 24
drwxr-xr-x 3 user user 4096 Apr 21 15:14 ./
drwxr-xr-x 4 root root 4096 Mar 29 15:30 ../
drwx------ 2 root root 16384 Apr 17 12:41 lost+found/
$
File encryption
$ echo "My secret file content" > /mnt/xxx/encrypted_folder/my_secrets.txt
$ cat /mnt/xxx/encrypted_folder/my_secrets.txt
My secret file content
$ ll /mnt/xxx/encrypted_folder/
total 12
drwxr-xr-x 2 user user 4096 Apr 20 14:25 ./
drwxr-xr-x 5 user user 4096 Apr 20 14:15 ../
-rw-r--r-- 1 user user 23 Apr 20 14:26 my_secrets.txt
The file names in the directory and the contents of the file will be available as long as there is a key in the keystore with which the directory has been encrypted. After the key is revoked, access to the directory will be severely limited:
$ keyctl revoke 803843970
$ keyctl show
Session Keyring
771961813 --alswrv 1000 65534 keyring: _uid_ses.1000
771026675 --alswrv 1000 65534 \_ keyring: _uid.1000
803843970: key inaccessible (Key has been revoked)
The key is canceled, read the contents of the directory:
$ ll /mnt/xxx/encrypted_folder/
total 12
drwxr-xr-x 2 user user 4096 Apr 20 14:25 ./
drwxr-xr-x 5 user user 4096 Apr 20 14:15 ../
-rw-r--r-- 1 user user 23 Apr 20 14:26 BhqTNRNHDBwpa9S1qCaXwC
The file name is already abyrvalg. But still try to read the file:
$ cat /mnt/xxx/encrypted_folder/BhqTNRNHDBwpa9S1qCaXwC
cat: /mnt/xxx/encrypted_folder/BhqTNRNHDBwpa9S1qCaXwC: Required key not available
NOTE: in Ubuntu 17.04 (kernel 4.10.0-19), the directory remains accessible after removing the key before remounting.
$ keyctl show
Session Keyring
771961813 --alswrv 1000 65534 keyring: _uid_ses.1000
771026675 --alswrv 1000 65534 \_ keyring: _uid.1000
$ e4crypt get_policy /mnt/xxx/encrypted_folder/
/mnt/xxx/encrypted_folder/: 8e679e4449bb9235
The directory is encrypted with the key with the descriptor “8e679e4449bb9235”. The key is not in the repository. Despite this, the directory and contents of the file are freely available.
$ ll /mnt/xxx/encrypted_folder/
total 12
drwxr-xr-x 2 user user 4096 Apr 20 14:25 ./
drwxr-xr-x 5 user user 4096 Apr 20 14:15 ../
-rw-r--r-- 1 user user 23 Apr 20 14:26 my_secrets.txt
$ cat /mnt/xxx/encrypted_folder/my_secrets.txt
My secret file content
Remounting:
# umount /dev/xxx
# mount /dev/xxx /mnt/xxx
$ ll /mnt/xxx/encrypted_folder/
total 12
drwxr-xr-x 2 user user 4096 Apr 20 14:25 ./
drwxr-xr-x 5 user user 4096 Apr 20 14:15 ../
-rw-r--r-- 1 user user 23 Apr 20 14:26 BhqTNRNHDBwpa9S1qCaXwC
2. Changes to the file system
In the Superblock: the set of options s_feature_incompat on the volume with encryption support contains the flag EXT4_FEATURE_INCOMPAT_ENCRYPT,
s_encrypt_algos [4] - stores encryption algorithms; at the moment this is:
s_encrypt_algos [0] = EXT4_ENCRYPTION_MODE_AES_256_XTS;
s_encrypt_algos [1] = EXT4_ENCRYPTION_MODE_AES_256_CTS;
s_encrypt_pw_salt - also set during formatting.
In inode: i_flags contains the EXT4_ENCRYPT_FL flag and it is from it that you can determine that the object is encrypted.
The structure of the encrypted directory
To read the contents of a directory, you need to determine its location on the disk by its inode.
1. Determination of inode number:
$ stat /mnt/xxx/encrypted_folder/
File: /mnt/xxx/encrypted_folder/
Size: 4096 Blocks: 8 IO Block: 4096 directory
Device: 811h/2065d Inode: 14 Links: 2
2. Search for inodes in the inodes table.
Ainod 14 belongs to the 0th group, so you need to read the descriptor table of the 0th group and find the block number of the inodo table in it. The descriptor table of group 0 is in the cluster following the superblock:
# dd if=/dev/xxx of=gdt bs=4096 count=1 skip=1

Fig. 1. Descriptor table of group 0
First, skip the cluster numbers of the bitmap blocks and the bitmap of inodes, read the cluster number of the beginning of the inode table at an offset of 8 bytes from the beginning of the table - 0x00000424 (1060) in BigEndian format. The inode of the directory = 14, with the inode size of 256 bytes in the table, it will be at offset 0x0D00 from its beginning. Thus, it is enough to read only the 1st cluster of the inodo table:
# dd if=/dev/xxx of=itable bs=4096 count=1 skip=1060
Fig. 2. Ainod encrypted directory.
In inode, determine the beginning of the i_block [] field. Because is ext4, then in the first 2 bytes of i_block is the header of the extent tree - 0xF30A. Next, you can see the block number in which the encrypted directory is stored - 0x00000402 (1026). (In the picture, not the entire i_block field is highlighted, but only informative 24 bytes - the remaining 36 bytes are filled with zeros.)
3. Reading the directory block:
# dd if=/dev/xxx of=dirdata bs=4096 count=1 skip=1026
Fig. 3. Dump the encrypted directory.
More: the first two entries (highlighted in red) are the entries “.” And “..”, respectively, the current and parent directories. The current inod directory is 0x0000000E, the record length is 0x000C bytes, the number of characters in the file name is 01, and the entry type 02 is the directory. The following is the name of the directory, aligned on a 4-byte boundary - 2E000000 (2E corresponds to the character '.' - period).
The next parent directory has the inode 0x00000002 (root directory), the same record length is 0x000C, the name is 02 characters, the type is also 02, followed by the name of the directory - 2E2E0000 (two points).
Finally, the last entry in this directory has the inode 0x0000000F, the record size is 0x0FDC, the number of characters in the name is 0x10, type 01 - this is the encrypted file. As you can see, its name does not match the created my_secrets.txt. In addition, in the original file name there are only 14 characters, not 16 as here.
NOTE: especially attentive readers with a calculator may have noticed that since the encrypted file is the last entry in the directory, then its record size must refer to the border of the block. However, 0x1000 - 0xC - 0xC = 0xFE8, not 0xFDC. This is because the volume was created with the “metadata_csum” option, which is set by default starting with Ubuntu 16.10. When this option is enabled, a 12-byte structure is created at the end of each directory block containing the checksum of this block.
4. Reading the encrypted file.
From the dump of the directory we determine that the file has inode 15 (0xF). We look for it in the table of inodes and similarly determine its position on the disk:
Fig. 4. Ainod encrypted file.
Read the contents of the cluster 0x0000AA00 (43520)
# dd if=/dev/xxx of=filedata bs=4096 count=1 skip=43520
Fig. 5. The contents of the encrypted file
And this does not correspond at all to the information recorded in the file. The real file size can be read in the i_size inode field (marked with a blue rectangle in Fig. 4): 0x00000017 - this is how much was written with the echo command “My secret file content” + line feed character 0x0A.
3. Decryption
Decryption of the file name
According to the EXT4 Encryption Design Document [2] decryption of file names is carried out in two stages:
1. DerivedKey = AES-128-ECB (data = MasterKey, key = DirNonce);
2. EncFileName = AES-256-CBC-CTS (data = DecFileName, key = DerivedKey);
Those. At the first stage, you need to get the key for decryption. To do this, use the data of the Master key created when adding the key to keyring, which is encrypted using AES-ECB with a 128-bit DirNonce key. The second stage uses a fixed initialization vector (IV) filled with zeros. For AES-ECB, an initialization vector is not needed.
What is DirNonce? There is an extended attribute in the inode of the encrypted directory.

Fig. 6. Aynod of the encrypted directory and its extended attribute
With an inode size of 256 bytes, about a hundred unused bytes remain in the structure (0x100 - EXT2_GOOD_OLD_INODE_SIZE - i_extra_size), in which information can be stored (red area in Fig. 6). As you can see from the header 0xEA020000 in the first four bytes of this area, the extended attribute with index 09 is stored here, the data of which is offset by 0x40 bytes from the header and has a size of 0x1C. The data area is divided into 3 zones: in the first (01 01 04 00), the algorithms by which the inode was encrypted are recorded. In the second, 8 bytes (8E 67 9E 44 49 BB 92 35) are stored, repeating the key handle. The third one contains a 16-byte one-time code (nons [3]) used when encrypting the Master key.
Thus, to decrypt the file name, you must:
1) read the value of the nameless extended attribute of the directory with index 9 - we get the directory nons;
2) using the AES-ECB algorithm, encrypt the data of the Master key using 128 bits of directory nonsense as the key;
3) using the AES-CBC-CTS algorithm, decrypt the file name using the first 256 bits (half) of the key obtained in the previous step as a key.
Decryption of the contents of the file
It is carried out similarly to the decryption of the file name, except that the extended attribute value obtained from the inode of the file is used as a nonse. And instead of CBC, the content is decrypted using the AES-XTS algorithm with a full 64-byte key. Logical Block Offset relative to the beginning of the file is used as IV
. 7. Ainod of the encrypted file and its extended attribute.
Comparing the value of the extended attribute of the encrypted file and the directory, you can see that their non-names are different, while the encryption algorithms and key descriptors are the same (yellow and blue zones in the figures).
The content of the files is encrypted page by page, therefore, to decrypt the content, it is necessary to use a whole cluster of file (4K), and not the size specified in the i_size inode field.
4. Implementation The
implementation of the decoder is based on the Linux Kernel Crypto API [4]. The chain uses two types of encryptors, depending on what is written in / proc / crypto for the ebc (aes), cts (cbc (aes)), xts (aes) algorithms. Consider the kernel 4.10.0-19: the ebc cipher is implemented through blkcipher, cts (cbc) and xts through skcipher:
$ cat / proc / crypto
$ cat / proc / crypto
name: ecb (aes)
driver: ecb (aes-aesni)
module: kernel
priority: 300
internal: no
type: blkcipher
blocksize: 16
min keysize: 16
max keysize: 32
ivsize: 0
geniv: default
name : cts (cbc (aes))
driver: cts (cbc-aes-aesni)
module: kernel
priority: 400
internal: no
type: skcipher
async: yes
blocksize: 16
min keysize: 16
max keysize: 32
ivsize: 16
chunksize: 16
name: xts (aes)
driver: xts-aes-aesni
module: aesni_intel
priority: 401
internal: no
type: skcipher
async: yes
blocksize: 16
min keysize: 32
max keysize: 64
ivsize: 16
chunksize: 16
name: ecb (aes)
driver: ecb (aes-aesni)
module: kernel
priority: 300
internal: no
type: blkcipher
blocksize: 16
min keysize: 16
max keysize: 32
ivsize: 0
geniv: default
name : cts (cbc (aes))
driver: cts (cbc-aes-aesni)
module: kernel
priority: 400
internal: no
type: skcipher
async: yes
blocksize: 16
min keysize: 16
max keysize: 32
ivsize: 16
chunksize: 16
name: xts (aes)
driver: xts-aes-aesni
module: aesni_intel
priority: 401
internal: no
type: skcipher
async: yes
blocksize: 16
min keysize: 32
max keysize: 64
ivsize: 16
chunksize: 16
Encryptor implementation via blkcipher
typedef enum { ENCRYPT, DECRYPT } cipher_mode;
static int do_blkcrypt(const u8* cipher, const u8* key, u32 key_len,
void* iv, void* dst, void* src, size_t src_len, cipher_mode mode)
{
int res;
struct crypto_blkcipher* blk;
struct blkcipher_desc desc;
struct scatterlist sg_src, sg_dst;
blk = crypto_alloc_blkcipher(cipher, 0, 0);
if (IS_ERR(blk))
{
printk(KERN_WARNING "Failed to initialize blkcipher mode %s\n", cipher);
return PTR_ERR(blk);
}
res = crypto_blkcipher_setkey(blk, key, key_len);
if (res)
{
printk(KERN_WARNING "Failed to set key. len=%#x\n", key_len);
crypto_free_blkcipher(blk);
return res;
}
crypto_blkcipher_set_iv(blk, iv, 16);
sg_init_one(&sg_src, src, src_len);
sg_init_one(&sg_dst, dst, src_len);
desc.tfm = blk;
desc.flags = 0;
if (mode == ENCRYPT)
res = crypto_blkcipher_encrypt(&desc, &sg_dst, &sg_src, src_len);
else
res = crypto_blkcipher_decrypt(&desc, &sg_dst, &sg_src, src_len);
crypto_free_blkcipher(blk);
return res;
}
Encryptor implementation via skcipher
struct tcrypt_result {
struct completion completion;
int err;
};
static void crypt_complete_cb(struct crypto_async_request* req, int error)
{
struct tcrypt_result* res = req->data;
if (error == -EINPROGRESS)
return;
res->err = error;
complete(&res->completion);
}
static int do_skcrypt(const u8* cipher, const u8* key, u32 key_len,
void* iv, void* dst, void* src, size_t src_len, cipher_mode mode)
{
struct scatterlist src_sg, dst_sg;
struct crypto_skcipher* tfm;
struct skcipher_request* req = 0;
struct tcrypt_result crypt_res;
int res = -EFAULT;
tfm = crypto_alloc_skcipher(cipher, 0, 0);
if (IS_ERR(tfm))
{
printk(KERN_WARNING "Failed to initialize skcipher mode %s\n", cipher);
res = PTR_ERR(tfm);
tfm = NULL;
goto out;
}
req = skcipher_request_alloc(tfm, GFP_NOFS);
if (!req)
{
printk(KERN_WARNING "Couldn't allocate skcipher handle\n");
res = -ENOMEM;
goto out;
}
skcipher_request_set_callback(req, CRYPTO_TFM_REQ_MAY_BACKLOG | CRYPTO_TFM_REQ_MAY_SLEEP,
crypt_complete_cb, &crypt_res);
if (crypto_skcipher_setkey(tfm, key, key_len))
{
printk(KERN_WARNING "Failed to set key\n");
res = -EINVAL;
goto out;
}
sg_init_one(&src_sg, src, src_len);
sg_init_one(&dst_sg, dst, src_len);
skcipher_request_set_crypt(req, &src_sg, &dst_sg, src_len, iv);
init_completion(&crypt_res.completion);
if (mode == ENCRYPT)
res = crypto_skcipher_encrypt(req);
else
res = crypto_skcipher_decrypt(req);
switch (res)
{
case 0: break;
case -EINPROGRESS:
case -EBUSY:
wait_for_completion(&crypt_res.completion);
if (!res && !crypt_res.err)
{
reinit_completion(&crypt_res.completion);
break;
}
default:
printk("Skcipher %scrypt returned with err = %d, result %#x\n",
mode == ENCRYPT ? "en" : "de", res, crypt_res.err);
break;
}
out:
if (tfm)
crypto_free_skcipher(tfm);
if (req)
skcipher_request_free(req);
return res;
}
Reading data (payload) of the master key
#define MASTER_KEY_SIZE 64
static int GetMasterKey(const u8* descriptor, u8* raw)
{
struct key* keyring_key = NULL;
const struct user_key_payload* ukp;
struct fscrypt_key* master_key;
keyring_key = request_key(&key_type_logon, descriptor, NULL);
if (IS_ERR(keyring_key))
return -EINVAL;
if (keyring_key->type != &key_type_logon)
{
printk_once(KERN_WARNING "%s: key type must be 'logon'\n", __func__);
return -EINVAL;
}
down_read(&keyring_key->sem);
ukp = user_key_payload(keyring_key);
master_key = (struct fscrypt_key*)ukp->data;
up_read(&keyring_key->sem);
if (master_key->size != MASTER_KEY_SIZE)
{
printk(KERN_WARNING "Wrong Master key size %#x\n", master_key->size);
return -EINVAL;
}
memcpy(raw, master_key->raw, master_key->size);
return 0;
}
NOTE: In kernel versions earlier than 4.4, the user_key_payload function is missing. Key data can be read directly from struct key * keyring_key.
Decoding file name
int err;
u8 iv[16] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
u8 nonce_dir[16] = { ... };
u8 master_key[64], derived_key[64];
u8 dec_file_name[] = { ... };
u8 enc_file_name[sizeof(dec_file_name)];
err = do_blkcrypt("ecb(aes)", nonce_dir, 16, iv, derived_key, master_key,
MASTER_KEY_SIZE, ENCRYPT);
if (err)
return err;
err = do_skcrypt("cts(cbc(aes))", derived_key, MASTER_KEY_SIZE / 2, iv,
dec_file_name, enc_file_name, sizeof(dec_file_name), DECRYPT);
return err;
Decryption of content
To simplify, the work with memory is omitted. Suppose 2 x PAGE_SIZE was given to us on the stack.
u8 nonce_file[16] = { ... };
u8 enc_file_data[PAGE_SIZE] = { ... };
u8 dec_file_data[PAGE_SIZE];
err = do_blkcrypt("ecb(aes)", nonce_file, 16, iv, derived_key, master_key,
MASTER_KEY_SIZE, ENCRYPT);
if (err)
return err;
err = do_skcrypt("xts(aes)", derived_key, MASTER_KEY_SIZE, iv,
dec_file_data, enc_file_data, PAGE_SIZE, DECRYPT);
return err;
Used header files (relevant for 4.10.0-19)
#include
#include
#include
#include
Makefile
obj-m += ciphertest.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
5. Results
Initial data:
u8 master_key[MASTER_KEY_SIZE] = {
0xa5, 0xb5, 0xc9, 0x23, 0x02, 0x14, 0xfc, 0xf7,
0x28, 0xdc, 0x90, 0x25, 0x24, 0x9e, 0xe6, 0xbc,
0x7c, 0xa8, 0xf8, 0xe1, 0x94, 0xf6, 0x67, 0x32,
0x33, 0xc4, 0xc1, 0xe8, 0x78, 0x59, 0xab, 0xfb,
0xae, 0xb0, 0xbf, 0x5d, 0x2c, 0x69, 0xc3, 0x8f,
0x51, 0x37, 0x26, 0x3f, 0xd1, 0xce, 0x37, 0xef,
0x3f, 0x80, 0xe3, 0x2d, 0xd5, 0xfd, 0x78, 0x45,
0x62, 0xf3, 0xa5, 0x24, 0x6b, 0xcf, 0x4a, 0x88
};
u8 enc_file_name[] = {
0x41, 0xa8, 0x4e, 0x4d, 0xd4, 0x1c, 0x43, 0x00,
0xa7, 0x5a, 0x2f, 0xd5, 0xaa, 0xa0, 0x5d, 0xb0
};
u8 nonce_dir[] = {
0x37, 0xba, 0x14, 0x16, 0x3e, 0xa8, 0xd5, 0x48,
0xd1, 0x3c, 0xb5, 0x6a, 0x01, 0xb7, 0x7c, 0x41
};
u8 nonce_file[] = {
0x61, 0x63, 0xb8, 0x31, 0xf4, 0xf5, 0xfc, 0x99,
0x1e, 0x3c, 0xf1, 0x8a, 0x23, 0xaf, 0x1e, 0xa8
};
The encoded file name enc_file_name is obtained from a dump of the directory (Fig. 3).
The nons of the nonce_dir directory are received from the dump of the inode of the directory (Fig. 6) The
nons of the nonce_file directory are received from the dump of the inode of the file (Fig. 7) The
master key is shown here completely for clarity. It can be obtained by debugging e4crypt:
The result of the created driver
References
[1] KERNEL KEY RETENTION SERVICE, www.kernel.org/doc/Documentation/security/keys.txt
[2] EXT4 Encryption Design Document, docs.google.com/document/d/1ft26lUQyuSpiu6VleP70_npaWdRfXFoNnB8JYnykiaTce_cep
] , ru.wikipedia.org/wiki/Nonce
[4] Linux Kernel Crypto API, www.kernel.org/doc/html/latest/crypto/index.html
[2] EXT4 Encryption Design Document, docs.google.com/document/d/1ft26lUQyuSpiu6VleP70_npaWdRfXFoNnB8JYnykiaTce_cep
] , ru.wikipedia.org/wiki/Nonce
[4] Linux Kernel Crypto API, www.kernel.org/doc/html/latest/crypto/index.html