📜 ⬆️ ⬇️

Encryption in EXT4. How it works?

image Paranoia is not treated! But not prosecuted. Therefore, Linux Kernel 4.1 adds support for encrypting the ext4 file system at the level of individual files and directories. You can only encrypt an empty directory. All files that will be created in such a directory will also be encrypted. Only file names and contents are encrypted, metadata is not encrypted, inline data (when file data not exceeding 60 bytes in size is stored in the inode) is not supported in files. Since the file content is decrypted directly in memory, encryption is available only when the cluster size is the same as PAGE_SIZE, i.e. equals 4K.

1. How it works


First you need to learn some useful commands.

Formatting the volume with the encryption option

# mkfs.ext4 -O encrypt /dev/xxx 

Enable encryption option on existing volume
')
 # tune2fs -O encrypt /dev/xxx 

Creating an 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 an error “No salt values ​​available”. If multiple volumes are mounted with the encrypt option, keys for each will be created. The e4crypt utility is included with e2fsprogs.

Keys are added to the Linux Kernel Keyring [1].

Reading the 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 available 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 the keyctl is not in the system, then you must install the keyutils package.

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 key handle is passed to the set_policy command without specifying the prefix (ext4) and type (logon). The same key can encrypt several directories. To encrypt different directories, you can use different keys. To find out which key the directory is encrypted with, you need to run the command:

 $ e4crypt get_policy /mnt/xxx/encrypted_folder/ /mnt/xxx/encrypted_folder/: 8e679e4449bb9235 

Install another security policy on the encrypted directory does not work:

 $ 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 a key exists in the keystore with which the directory has been encrypted. After canceling the key, 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) 

Key revoked, 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: on Ubuntu 17.04 (kernel 4.10.0-19), the directory remains available after removing the key before remounting it.

 $ 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 file contents 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 in the file system


In the Superblock: the s_feature_incompat options set on the encryption-enabled volume contains the EXT4_FEATURE_INCOMPAT_ENCRYPT flag,
s_encrypt_algos [4] - stores encryption algorithms; at the moment it 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 the inode: i_flags contains the EXT4_ENCRYPT_FL flag and it is from it that one can determine that the object is encrypted.

Encrypted directory structure

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 inode in the inode table.

Aynod 14 belongs to the 0th group, therefore it is necessary to read the table of descriptors of the 0th group and find in it the block number of the inode table. The 0th group descriptor table is located in the cluster following the superblock:

 # dd if=/dev/xxx of=gdt bs=4096 count=1 skip=1 

image
Fig. 1. Table of descriptors of the 0th group

First, we skip the cluster number of the bitmap block and the inode bitmap, the cluster number of the beginning of the inode table is read by offset 8 bytes from the beginning of the table - 0x00000424 (1060) in BigEndian format. Anode directory = 14, with an inode size of 256 bytes in the table, it will be at offset 0x0D00 from its beginning. Thus, it suffices to read only the 1st cluster of the inode table:

 # dd if=/dev/xxx of=itable bs=4096 count=1 skip=1060 

image
Fig. 2. Inode encrypted directory.

In inode, we define the start of the i_block [] field. Since is ext4, then in the first 2 bytes of i_block there is an extents tree header - 0xF30A. Then you can see the block number in which the encrypted directory is stored - 0x00000402 (1026). (The figure does not highlight the entire i_block field, 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 

image
Fig. 3. Dump the encrypted directory.

Details: the first two entries (highlighted in red) are the entries “.” And “..”, respectively, the current and parent directories. The current directory has an inode 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 directory name, aligned on a 4-byte boundary - 2E000000 (2E corresponds to the symbol '.' - dot).

The next parent directory has an inode 0x00000002 (root directory), a similar record length 0x000C, 02 characters in the name, type also 02, followed by the directory name 2E2E0000 (two dots).

Finally, the last entry in this directory has an inode 0x0000000F, the entry size is 0x0FDC, the number of characters in the name is 0x10, type 01 - this is the encrypted file. As you can see, his name does not match the created my_secrets.txt. In addition, the original file name is only 14 characters, and not 16 as here.

NOTE: especially attentive readers with a calculator may have noticed that If the encrypted file is the last entry in the directory, then its record size should refer to the block boundary. However, 0x1000 - 0xC - 0xC = 0xFE8, not 0xFDC. This is due to the fact that the volume was created with the option "metadata_csum", 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 an encrypted file.

From the directory dump, we determine that the file has an inin 15 (0xF). We look for it in the inode table and determine its position on the disk in the same way:

image
Fig. 4. Inode encrypted file.

Read the contents of the cluster 0x0000AA00 (43520)

 # dd if=/dev/xxx of=filedata bs=4096 count=1 skip=43520 

image
Fig. 5. The contents of the encrypted file

And this does not correspond to the information recorded in the file. The actual file size can be read in the i_size inode field (marked with a blue rectangle in Figure 4): 0x00000017 - this is exactly how much was written by the echo command “My secret file content” + the newline character 0x0A.

3. Decryption


File name decryption

According to EXT4 Encryption Design Document [2], the decryption of file names is performed in two steps:

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 by adding the key to the keyring, which are encrypted using the AES-ECB 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? In the inode of the encrypted directory there is a extended attribute.

image
Fig. 6. Anino encrypted directory and its extended attribute

With an inode size of 256 bytes, about a hundred unused bytes (0x100 - EXT2_GOOD_OLD_INODE_SIZE - i_extra_size) remain in the structure, in which information can be stored (red area in Fig. 6). As can be seen 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 have a size of 0x1C. The data area is divided into 3 zones: the first (01 01 04 00) contains algorithms for which the inode was encrypted. In the second, 8 bytes (8E 67 9E 44 49 BB 92 35) are stored, repeating the key descriptor. The third one contains a 16-byte one-time code (nonce [3]) used for encrypting the Master Key.

Thus, to decrypt the file name, you must:

1) read the value of the nameless extended attribute directory with the index 9 — we get the non-directory;
2) using the AES-ECB algorithm, encrypt Master Key data using 128 bits of non-directories 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 the key.

Decrypt file content

It is performed in the same way as the file name decryption procedure, except that the extended attribute value obtained from the inode of the file is used as the nonce. And instead of CBC, the content is decrypted using the AES-XTS algorithm with a full 64-byte key. Logical Block Offset is used as IV for the beginning of the file

image
Fig. 7. Inode 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 notes are different, while the encryption algorithms and key descriptors are the same (the yellow and blue zones in the figures).

The contents of the files are encrypted page by page, so for decrypting the content you must use the whole file cluster (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]. Two types of encoders are used in the chain, 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 via blkcipher, cts (cbc) and xts - via 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

Implementation of the encoder 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; } 


The implementation of the encoder through 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; } 


Read data (payload) 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 younger than 4.4, the user_key_payload function is missing. Key data can be read directly from struct key * keyring_key.

File name decryption

 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; 

Content decryption

To simplify the omitted work with memory. 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; 

Headers used (valid for 4.10.0-19)

 #include <linux/kernel.h> #include <linux/module.h> #include <linux/scatterlist.h> #include <linux/fscrypto.h> 

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 the dump directory (Fig. 3).
The nonce of the nonce_dir directory is obtained from the dump of the inode directory (Fig. 6)
The nonce of the nonce_file file is obtained from the dump of the inode file (Fig. 7)

The master key is shown here fully for clarity. It can be obtained when debugging e4crypt:

image

The result of the created driver

image

Source: https://habr.com/ru/post/327682/


All Articles