Перейти к содержимому

Шифрование данных в базе - ключ, поле, миграция

Храним чужие ключи, токены и другие секреты в базе зашифрованными. Разбираем общий ключ, шифрованное поле сущности, переход старой колонки и его ограничения.

Механика

Шифрование ядра держится на едином ключе из файла настроек продукта. Один и тот же ключ обслуживает шифрованные поля сущностей и защищённые куки платформы.

Ключ шифрования закрывают признаком только для чтения. Так его нельзя поменять из административной части, а случайная перезапись настроек не превращает данные в нечитаемый набор байтов.

Менять ключ после шифрования данных категорически нельзя. Старые значения расшифровать станет нечем, поэтому в кластере один и тот же ключ ставят на всех узлах сразу.

Шифрование ядра опирается на расширение криптографии в самом PHP. Без него поля просто не работают, и проверка доступности - обязательный шаг установщика модуля.

Шифрование ядра недетерминированное по своей природе. Одно и то же значение при каждом сохранении даёт разный результат, поэтому искать по зашифрованной колонке обычным сравнением невозможно.

Одна колонка не умеет держать шифрованные и обычные значения одновременно. Тип поля в описании сущности выбирают по состоянию колонки, а не задают жёстко.

Переход существующей колонки на шифрование идёт строго в три шага. Сначала увеличивают размер колонки, потом шифруют все записи порциями, и только потом включают режим шифрования.

Размер колонки считают заранее по формуле из официальной документации. Шифротекст заметно длиннее исходного значения, и под десяток байтов данных нужна колонка почти в сто байтов.

Для уникального секрета на каждую запись в ядре есть отдельный тип поля. Он сам генерирует значение при пустом поле и сам кодирует данные перед шифрованием.

Прямое симметричное шифрование остаётся для случаев вне ORM-сущностей. Там ключ передаёт сам разработчик, а результат перед записью в базу приводят к текстовому виду.

Отдельно стоит решить, что вообще подлежит шифрованию. Чужие ключи доступа и токены интеграций - да, а данные, по которым нужен поиск, шифруют только с пониманием, чем заменить поиск по ним.

Второй вопрос - где хранится сам ключ и кто имеет к нему доступ. Ключ в том же репозитории, что и код, защищает ровно до первого доступа к репозиторию, а не от кражи копии базы.

Шаги

  1. Убедиться, что ключ шифрования задан в настройках и закрыт от случайной правки.
  2. Проверить доступность криптографии в окружении перед использованием шифрованных полей.
  3. Увеличить размер колонки под шифротекст по формуле из официальной документации.
  4. Зашифровать существующие записи порциями, закрыв таблицу от посторонней записи.
  5. Включить режим шифрования колонки только после обработки всех записей.
  6. Проверить чтение и запись через сущность ORM, а не запросом к таблице.

Код

Смотрим ключ шифрования:

/bitrix/.settings.php
'crypto' => ['value' => ['crypto_key' => 'строка из 32 символов'], 'readonly' => true],
// ключ генерируют случайной строкой, а не придумывают руками
// на всех узлах кластера ключ должен быть одинаковым
// тот же ключ обслуживает защищённые куки платформы

Ключ - единственное, что превращает шифротекст обратно в данные. Его потеря означает потерю данных, а его смена после шифрования - то же самое.

Проверяем доступность шифрования:

use Bitrix\Main\ORM\Fields\CryptoField;
if (!CryptoField::cryptoAvailable()) {
throw new \RuntimeException('нет ключа шифрования или расширения криптографии');
}
// проверку ставят в установщике модуля и в миграциях

Проверка доступности спасает установщик модуля на чужом незнакомом окружении. Без ключа или без расширения криптографии установка иначе падает в середине, оставляя модуль в половинчатом состоянии.

Описываем поле сущности:

public static function getMap(): array
{
return [
new \Bitrix\Main\ORM\Fields\IntegerField('ID', ['primary' => true, 'autocomplete' => true]),
static::cryptoEnabled('TOKEN')
? new CryptoField('TOKEN') // колонка уже зашифрована
: new \Bitrix\Main\ORM\Fields\StringField('TOKEN'),
];
}

Тип поля в описании выбирают по текущему состоянию колонки. Жёстко заданный шифрованный тип на колонке со старыми открытыми значениями делает их нечитаемыми при первой же выборке.

Шифруем существующие записи порциями:

$last = 0;
while ($rows = TokenTable::getList(['filter' => ['>ID' => $last],
'order' => ['ID' => 'ASC'], 'limit' => 200])->fetchAll()) {
foreach ($rows as $row) {
TokenTable::update($row['ID'], ['TOKEN' => $row['TOKEN']]); // перезапись значения и есть шифрование
$last = $row['ID'];
}
}

Перезапись значения через сущность ORM и есть само шифрование. Порции нужны по той же причине, что и в любой массовой операции: на десятках тысяч записей одним запросом это не проходит.

Включаем режим шифрования:

TokenTable::enableCrypto('TOKEN'); // только после обработки всех записей
// после вызова запись шифруется автоматически, а чтение расшифровывает

Включение режима фиксирует новое состояние колонки в служебных настройках. Сделать это до окончания миграции - значит получить смесь зашифрованных и открытых значений в одной колонке.

Заводим секрет на каждую запись:

use Bitrix\Main\ORM\Fields\SecretField;
new SecretField('API_KEY', ['secret_length' => 32]);
// значение генерируется само, если поле оставили пустым
// длина секрета задаётся параметром поля при описании сущности

Такое поле удобно для ключей доступа и одноразовых токенов. Оно и генерирует значение, и приводит его к текстовому виду перед шифрованием, избавляя от ручной работы.

Считаем размер колонки под шифротекст:

$len = 32; // длина исходного значения
$newLen = (int)ceil(($len + 16 + 32) * 1.5); // режим по умолчанию
printf("нужна колонка не меньше %d байт\n", $newLen);
// под десяток байтов данных требуется колонка почти в сотню байтов

Расчёт делают до миграции, а не после первой обрезанной записи. Увеличение колонки на большой таблице - отдельная операция, которую планируют в тихие часы вместе с самой миграцией.

Ограничения

По зашифрованной колонке нельзя искать записи обычным сравнением. Одинаковые значения дают разный шифротекст, поэтому поиск строят по отдельному признаку, а не по самому секрету.

Сортировка по такой зашифрованной колонке тоже бессмысленна. Порядок шифротекста никак не связан с порядком самих исходных значений.

Шифрование не защищает от доступа к самому работающему сайту. Код с ключом расшифрует данные для любого, кто получил выполнение на сервере.

Резервная копия базы без ключа шифрования бесполезна. Ключ хранят и переносят вместе с копией, но отдельно от неё, иначе восстановление ничего не даст.

Типичные проблемы

После включения шифрования данные читаются как мусор.

Режим шифрования включили до того, как зашифровали все существующие записи. Одна колонка не может держать зашифрованные и открытые значения одновременно.

Значения обрезаются при сохранении.

Размер колонки в базе не увеличен под шифротекст. Он заметно длиннее исходного значения, и нужный размер считают заранее по формуле.

На втором узле кластера данные не расшифровываются.

Ключ шифрования на узлах кластера оказался разным. Один и тот же ключ обязан стоять на всех серверах одного проекта.

Установщик модуля падает на чужом сервере.

Нет ключа или расширения криптографии, а проверка доступности так и не сделана. Такую проверку ставят до первого обращения к шифрованному полю.

Поиск по секрету перестал работать.

Шифрование недетерминированное, и обычное сравнение по значению больше не находит нужные записи. Ищут по отдельному открытому признаку записи, а вовсе не по самому секрету.

Частые вопросы

Что шифровать, а что нет?

Чужие ключи доступа, токены и секреты интеграций. Персональные данные шифруют осознанно: по ним обычно нужен поиск, а он по зашифрованной колонке невозможен.

Можно ли поменять ключ шифрования?

Только вместе с расшифровкой всех данных старым ключом и повторным шифрованием новым. Простая замена ключа делает существующие значения нечитаемыми.

Как искать по зашифрованному полю?

По отдельной колонке с признаком: хешем, последними символами или ссылкой на владельца. Само зашифрованное значение для поиска не годится.

Чем поле-секрет отличается от обычного шифрованного?

Оно само генерирует значение при пустом поле и кодирует данные перед шифрованием. Это удобно для ключей доступа, которые никто не вводит руками.

Нужно ли шифровать пароли пользователей?

Нет, пароли не шифруют, а хранят в виде необратимого хеша, и платформа делает это сама. Шифрование нужно там, где значение потом требуется прочитать.

Смежное

Первоисточник