В преддверии старта нового набора на курс «Базы данных» продолжаем публиковать серию статей про шифрование в MySQL.
В предыдущей статье этой серии (Шифрование в MySQL: хранилище ключей) мы говорили о хранилищах ключей. В этой статье мы рассмотрим, как используется главный ключ (master key), а также обсудим достоинства и недостатки шифрования методом конвертов (envelope encryption).
Идея шифрования конвертов заключается в том, что используемые для шифрования ключи (ключи табличных пространств) шифруются другим ключом (главным ключом, master key). Для шифрования данных фактически используются ключи табличных пространств. Графически это можно представить так:
Главный ключ (master key) находится в хранилище ключей (keyring), а ключи табличных пространств - в заголовках зашифрованных табличных пространств (на странице 0 табличного пространства).
На рисунке выше:
Таблица A зашифрована ключом 1 (Key 1). Ключ 1 шифруется с помощью главного ключа (master key) и хранится в зашифрованном виде в заголовке таблицы A.
Таблица B зашифрована ключом 2 (Key 2). Ключ 2 шифруется с помощью главного ключа (masker key) и хранится в зашифрованном виде в заголовке таблицы B.
И так далее.
Когда серверу необходимо расшифровать таблицу A, он получает главный ключ из хранилища, читает зашифрованный ключ 1 из заголовка таблицы A и расшифровывает ключ 1. Расшифрованный ключ 1 кэшируется в памяти сервера и используется для расшифровки таблицы A.
InnoDB
В InnoDB фактическое шифрование и дешифрование выполняются на уровне ввода-вывода. То есть страница шифруется непосредственно перед тем, как она сбрасывается на диск и расшифровывается сразу после считывания с диска.
В InnoDB шифрование работает только на уровне табличных пространств. И по умолчанию все таблицы создаются в отдельных табличных пространствах (file-per-table tablespace). Говоря иными словами, создается табличное пространство, которое может содержать только одну таблицу. Хотя вы можете создавать таблицы также и в основном табличном пространстве (general tablespace). Но в любом случае таблица всегда находится в каком-то табличном пространстве. И поскольку шифрование осуществляется на уровне табличного пространства, то оно либо полностью зашифровано, либо нет. То есть нельзя в основном табличном пространстве зашифровать только часть таблиц.
Если по какой-либо причине у вас отключен file-per-table, то все таблицы создаются внутри системного табличного пространства (system tablespace). В Percona Server for MySQL можно зашифровать системное табличное пространство с помощью переменной innodbsystablespaceencrypt или используя потоки шифрования (encryption threads), но это все еще экспериментальная функция. В MySQL этого нет.
Прежде чем двигаться дальше, нам нужно рассмотреть структуру идентификатора главного ключа (master key ID). Он состоит из UUID, KEYID и префикса "INNODBKey". Выглядит это так: INNODBKey-UUID-KEYID.
UUID - это uuid сервера с зашифрованным табличным пространством. KEYID - это просто постоянно увеличивающееся значение. При первичном создании главного ключа KEYID равен 1. При ротации ключа, когда создается новый главный ключ, KEYID = 2 и так далее. Более подробно о ротации главных ключей мы поговорим в следующих статьях этой серии.
Теперь, когда мы знаем, как выглядит идентификатор главного ключа, давайте посмотрим на заголовок зашифрованного табличного пространства. Когда табличное пространство шифруется, то информация о шифровании добавляется к заголовку. Выглядит это следующим образом:
KEY ID - это KEYID из идентификатора главного ключа, который мы уже обсуждали. UUID - это uuid сервера, который также используется в идентификаторе главного ключа. TABLESPACE KEY - ключ табличного пространства, который состоит из 256 бит, случайно сгенерированных сервером. Вектор инициализации (IV, initialization vector) также состоит из 256 случайно сгенерированных битов (хотя должен быть 128 бит). IV используется для инициализации шифрования и дешифрования AES (из 256 бит используется только 128). В конце присутствует контрольная сумма CRC32 для TABLESPACE KEY и IV.
Все это время я немного упрощал, говоря, что в заголовке есть зашифрованный ключ табличного пространства. На самом деле, ключ табличного пространства и вектор инициализации хранятся и шифруются вместе с помощью главного ключа. Помните, что перед шифрованием ключа табличного пространства и вектора инициализации, для них вычисляется CRC32.
Зачем нужен CRC32?
Если в двух словах, то для того чтобы убедиться в валидности главного ключа. После расшифровки ключа табличного пространства и вектора инициализации, вычисляется контрольная сумма и сравнивается с CRC32, хранящейся в заголовке. Если контрольные суммы совпадают, то у нас правильный главный ключ и ключ табличного пространства. В противном случае табличное пространство помечается как отсутствующее (мы все равно не сможем его расшифровать).
Вы можете спросить: в какой момент осуществляется проверка ключей? Ответ - при запуске сервера. Сервер с зашифрованными таблицами / табличными пространствами при старте считывает UUID, KEYID из заголовка и генерирует идентификатор главного ключа. Затем он получает необходимый главный ключ из хранилища (keyring), расшифровывает ключ табличного пространства и проверяет контрольную сумму. Еще раз, если контрольная сумма совпадает, то все в порядке, нет - табличное пространство помечается как отсутствующее.
Если вы читали прошлую статью этой серии (Шифрование в MySQL: хранилище ключей), то, возможно, помните, что при использовании серверного хранилища ключей, сервер при запуске получает только список идентификаторов ключей, а точнее, key id и user id, так как эта пара однозначно идентифицирует ключ. А теперь я говорю, что сервер при запуске получает все ключи, необходимые ему для проверки возможности расшифровки ключей табличных пространств. Так почему же при инициализации, в случае серверного хранилища, загружаются только keyid и userid, а не все ключи? Потому что вам могут быть не нужны все ключи. В основном это связано с ротацией главного ключа. При ротации главного ключа в хранилище создается новый главный ключ, но старые ключи не удаляются. Таким образом, в серверном хранилище ключей у вас может находиться много ключей, которые не нужны серверу и, следовательно, не извлекаются при запуске сервера.
Настало время немного поговорить о достоинствах и недостатках шифрования с использованием главного ключа. Самым большим преимуществом является то, что вам нужен только один ключ шифрования (главный ключ), который будет храниться отдельно от ваших зашифрованных данных. Это делает запуск сервера быстрым, а хранилище небольшим, что облегчает управление. И также единственный главный ключ легко перегенерировать.
Однако шифрование с помощью главного ключа имеет один большой недостаток: после того как табличное пространство зашифровано с помощью tablespace_key, оно всегда остается зашифрованным одним и тем же ключом. Ротация главного ключа здесь не помогает. Почему это недостаток? Мы знаем, что в MySQL есть баги, которые могут привести к внезапному сбою и созданию core-файла. Так как core-файл содержит дамп памяти сервера, может случиться так, что в дампе будет расшифрованный ключ табличного пространства. Что еще хуже, дешифрованные ключи табличного пространства хранятся в памяти, которая может свопиться на диск. Вы можете сказать, что это не недостаток, так как вам нужны root-права для доступа к этим файлам и разделу подкачки. Да. Но root нужен только на некоторое время. Как только кто-то получит доступ к дешифрованному ключу табличного пространства, он / она сможет продолжить использовать его для расшифровки данных, даже без прав root. Кроме того, диск может быть украден, а раздел подкачки / core-файлы можно прочитать с помощью сторонних средств. Цель TDE состоит в том, сделать его нечитаемым, даже если диск будет украден. В Percona Server for MySQL есть возможность повторного шифрования табличного пространства с новыми сгенерированными ключами. Эта функция называется потоками шифрования (encryption threads) и на момент написания этой статьи все еще является экспериментальной.