[Из песочницы] Шифрование в EXT4. How It Works?
1. Как это работает
Для начала необходимо освоить несколько полезных команд
Форматирование тома с опцией шифрования
# mkfs.ext4 -O encrypt /dev/xxx
Включение опции шифрования на существующий том
# tune2fs -O encrypt /dev/xxx
Создание ключа шифрования
# mount /dev/xxx /mnt/xxx
$ e4crypt add_key
Enter passphrase (echo disabled):
Added key with descriptor [8e679e4449bb9235]
При создании ключа том с поддержкой шифрования должен быть примонтирован, иначе e4crypt выдаст ошибку «No salt values available». Если примонтировано несколько томов с опцией encrypt, то будут созданы ключи для каждого. Утилита e4crypt входит в состав e2fsprogs.
Ключи добавляются в Linux Kernel Keyring [1].
Чтение списка ключей
$ 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
Ключи, используемые для шифрования, имеют тип «logon». Содержимое (payload) ключей такого типа недоступно из пространства пользователя — keyctl команды read, pipe, print вернут ошибку. В данном примере у ключа префикс «ext4», но может быть и «fscrypt». Если keyctl отсутствует в системе, то необходимо установить пакет keyutils.
Создание зашифрованной директории
$ mkdir /mnt/xxx/encrypted_folder
$ e4crypt set_policy 8e679e4449bb9235 /mnt/xxx/encrypted_folder/
Key with descriptor [8e679e4449bb9235] applied to /mnt/xxx/encrypted_folder/.
Здесь в команду set_policy передается дескриптор созданного ключа без указания префикса (ext4) и типа (logon). Одним и тем же ключом можно зашифровать несколько директорий. Для шифрования разных директорий можно использовать разные ключи. Чтобы узнать, каким ключом зашифрована директория, необходимо выполнить команду:
$ e4crypt get_policy /mnt/xxx/encrypted_folder/
/mnt/xxx/encrypted_folder/: 8e679e4449bb9235
Установить другую политику безопасности на зашифрованную директорию не получится:
$ 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/].
Зато такую директорию можно беспрепятственно удалить:
$ 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/
$
Шифрование файла
$ 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
Имена файлов в директории и содержимое файла будут доступны, пока в хранилище ключей существует ключ, которым была зашифрована директория. После аннулирования ключа доступ к директории будет сильно ограничен:
$ 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)
Ключ аннулирован, читаем содержимое директории:
$ 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
Имя файла уже абырвалг. Но всё-таки попробуем прочитать файл:
$ cat /mnt/xxx/encrypted_folder/BhqTNRNHDBwpa9S1qCaXwC
cat: /mnt/xxx/encrypted_folder/BhqTNRNHDBwpa9S1qCaXwC: Required key not available
NOTE: в Ubuntu 17.04 (kernel 4.10.0–19) директория остается доступной после удаления ключа до перемонтирования.
$ 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
Директория зашифрована ключом с дескриптором »8e679e4449bb9235». Ключ отсутствует в хранилище. Несмотря на это, директория и содержимое файла в свободном доступе.
$ 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
Перемонтирование:
# 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. Изменения в файловой системе
В Суперблоке: набор опций s_feature_incompat на томе с поддержкой шифрования содержит флаг EXT4_FEATURE_INCOMPAT_ENCRYPT,
s_encrypt_algos[4] — хранит алгоритмы шифрования; на данный момент это:
s_encrypt_algos[0] = EXT4_ENCRYPTION_MODE_AES_256_XTS;
s_encrypt_algos[1] = EXT4_ENCRYPTION_MODE_AES_256_CTS;
s_encrypt_pw_salt — также задается при форматировании.
В айноде: i_flags содержит флаг EXT4_ENCRYPT_FL и именно по нему можно определить, что объект зашифрован.
Структура зашифрованной директории
Чтобы прочитать содержимое директории, нужно по ее айноду определить ее местоположение на диске.
1. Определение номера айнода:
$ 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. Поиск айнода в таблице айнодов.
Айнод 14 принадлежит 0-й группе, поэтому необходимо прочитать таблицу дескрипторов 0-й группы и найти в ней номер блока таблицы айнодов. Таблица дескрипторов 0-й группы находится в кластере, следующим за суперблоком:
# dd if=/dev/xxx of=gdt bs=4096 count=1 skip=1
Рис. 1. Таблица дескрипторов 0-й группы
Вначале пропускаем номера кластеров битмапа блоков и битмапа айнодов, номер кластера начала таблицы айнодов читаем по смещению 8 байт от начала таблицы — 0×00000424 (1060) в BigEndian формате. Айнод директории = 14, при размере айнода в 256 байт в таблице он будет находиться по смещению 0×0D00 от ее начала. Таким образом, достаточно прочитать только 1-й кластер таблицы айнодов:
# dd if=/dev/xxx of=itable bs=4096 count=1 skip=1060
Рис. 2. Айнод зашифрованной директории.
В айноде определяем начало поля i_block[]. Т.к. это ext4, то в первых 2 байтах i_block находится заголовок дерева экстентов — 0xF30A. Далее можно увидеть номер блока, в котором хранится зашифрованная директория — 0×00000402 (1026). (На рисунке выделено не всё поле i_block, а только информативные 24 байта — остальные 36 байт заполнены нулями.)
3. Чтение блока директории:
# dd if=/dev/xxx of=dirdata bs=4096 count=1 skip=1026
Рис. 3. Дамп зашифрованной директории.
Подробнее: первые две entry (выделены красным) — это записи ».» и »…», соответственно, текущая и родительская директории. У текущей директории айнод 0×0000000E, длина записи 0×000C байт, количество символов в имени файла — 01 и тип entry 02 — это директория. Далее следует имя директории, выровненное по 4-байтовой границе — 2E000000 (2E соответствует символу ».» — точка).
Следующая, родительская директория, имеет айнод 0×00000002 (корневая директория), аналогичная длина записи 0×000C, в имени 02 символа, тип также 02, после чего идет имя директории — 2E2E0000 (две точки).
Наконец, последняя entry в данной директории имеет айнод 0×0000000F, размер записи 0×0FDC, количество символов в имени 0×10, тип 01 — это и есть зашифрованный файл. Как видно его имя не соответствует созданному my_secrets.txt. К тому же, в исходном имени файла всего 14 символов, а не 16 как здесь.
NOTE: особенно внимательные читатели с калькулятором могли заметить, что т.к. зашифрованный файл является последней entry в директории, то его размер записи должен ссылаться на границу блока. Однако, 0×1000 — 0xC — 0xC = 0xFE8, а не 0xFDC. Это связано с тем, что том создавался с опцией «metadata_csum», которая задается по умолчанию, начиная с Ubuntu 16.10. При включении этой опции в конце каждого блока директории создается 12-байтовая структура, содержащая контрольную сумму этого блока.
4. Чтение зашифрованного файла.
Из дампа директории определяем, что файл имеет айнод 15 (0xF). Ищем его в таблице айнодов и аналогично определяем его положение на диске:
Рис. 4. Айнод зашифрованного файла.
Читаем содержимое кластера 0×0000AA00 (43520)
# dd if=/dev/xxx of=filedata bs=4096 count=1 skip=43520
Рис. 5. Содержимое зашифрованного файла
И это совсем не соответствует записанной в файл информации. Настоящий размер файла можно прочитать в поле i_size айнода (отмечен синим прямоугольником на рис. 4): 0×00000017 — именно столько было записано командой echo «My secret file content» + символ перевода строки 0×0A.
3. Расшифровка
Расшифровка имени файла
Согласно EXT4 Encryption Design Document [2] расшифровка имен файлов выполняется в два этапа:
1. DerivedKey = AES-128-ECB (data=MasterKey, key=DirNonce);
2. EncFileName = AES-256-CBC-CTS (data=DecFileName, key=DerivedKey);
Т.е. на первом этапе надо получить ключ для расшифровки. Для этого используются данные Мастер-ключа, созданного при добавлении ключа в keyring, которые шифруются по AES-ECB 128-битным ключом DirNonce. На втором этапе используется фиксированный вектор инициализации (IV), заполненный нулями. Для AES-ECB вектор инициализации не нужен.
Что такое DirNonce? В айноде зашифрованной директории есть extended attribute.
Рис. 6. Айнод зашифрованной директории и его extended attribute
При размере айнода в 256 байт в структуре остается около сотни неиспользуемых байт (0×100 — EXT2_GOOD_OLD_INODE_SIZE — i_extra_size), в которых можно хранить информацию (красная область на рис. 6). Как видно по заголовку 0xEA020000 в первых четырех байтах этой области, здесь хранится extended attribute с индексом 09, данные которого смещены на 0×40 байт от заголовка и имеют размер 0×1C. Область данных поделена на 3 зоны: в первой (01 01 04 00) записаны алгоритмы, по которым был зашифрован айнод. Во второй — хранится 8 байт (8E 67 9E 44 49 BB 92 35), повторяющие дескриптор ключа. В третьей — содержится 16-байтовый одноразовый код (нонс [3]), используемый при шифровании Мастер-ключа.
Таким образом, для расшифровки имени файла, необходимо:
1) прочитать значение безымянного extended attribute директории с индексом 9 — получаем нонс директории;
2) по алгоритму AES-ECB зашифровать данные Мастер-ключа, используя в качестве ключа 128 бит нонса директории;
3) по алгоритму AES-CBC-CTS расшифровать имя файла, используя в качестве ключа первые 256 бит (половину) ключа, полученного на предыдущем этапе.
Расшифровка содержимого файла
Выполняется аналогично процедуре расшифровки имени файла, за исключением того, что в качестве нонса используется значение extended attribute, полученное из айнода файла. И вместо CBC содержимое дешифруется по алгоритму AES-XTS с полным 64-байтовым ключом. В качестве IV используется Logical Block Offset относительно начала файла
Рис. 7. Айнод зашифрованного файла и его extended attribute.
Сравнивая значение extended attribute зашифрованного файла и директории, можно заметить, что их нонсы различаются, в то время как алгоритмы шифрования и дескрипторы ключей совпадают (желтая и синяя зоны на рисунках).
Содержимое файлов шифруется постранично, поэтому для расшифровки контента обязательно использовать целый кластер файла (4K), а не размер, указанный в поле i_size айнода.
4. Реализация
Реализация дешифратора выполнена на основе Linux Kernel Crypto API [4]. В цепочке используется два вида шифраторов в зависимости от того, что прописано в /proc/crypto для алгоритмов ebc (aes), cts (cbc (aes)), xts (aes). Рассматриваем ядро 4.10.0–19: шифр ebc реализуется через blkcipher, cts (cbc) и xts — через skcipher:
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
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;
}
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;
}
#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: В версиях ядра младше 4.4 отсутствует функция user_key_payload. Данные ключа можно прочитать непосредственно из struct key* keyring_key.
Расшифровка имени файла
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;
Расшифровка контента
Для упрощения опущена работа с памятью. Предположим, 2 x PAGE_SIZE нам дали на стеке.
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;
Используемые заголовочные файлы (актуально для 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. Результаты
Исходные данные:
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
};
Закодированное имя файла enc_file_name получено из дампа директории (рис. 3).
Нонс директории nonce_dir получен из дампа айнода директории (рис. 6)
Нонс файла nonce_file получен из дампа айнода файла (рис. 7)
Мастер-ключ показан здесь полностью для наглядности. Его можно получить при отладке e4crypt:
Результат работы созданного драйвера
[2] EXT4 Encryption Design Document, docs.google.com/document/d/1ft26lUQyuSpiu6VleP70_npaWdRfXFoNnB8JYnykNTg/edit
[3] Wikipedia — Nonce, ru.wikipedia.org/wiki/Nonce
[4] Linux Kernel Crypto API, www.kernel.org/doc/html/latest/crypto/index.html
Комментарии (1)
1 мая 2017 в 16:30
+1↑
↓
Необходимо отметить, что для всего этого требуется e2fsprogs версии >=1.43, а в текущем LTS Ubuntu (16.04) мы имеем 1.42.13 и при попытке включить криптацию получимtune2fs -O encrypt /dev/vda1 tune2fs 1.42.13 (17-May-2015) Invalid filesystem option set: encrypt
Так что версия ядра нам никак не поможет, если мы не станем разводить у себя помойку и тащить столь важный пакет не из родных репозиториев.
Таким образом в серьезных серверных дистрибутивах, а не в тестовых версиях (каковыми являются у Ubuntu non-LTS), мы получим эту возможность только с выходом Debian 9(где e2fsprogs 1.43.4), а для Ubuntu только в 18.04 LTS.