Этот пост был первоначально опубликован в блоге ParagonIE и размещен здесь с их разрешения.
Нам [ParagonIE] часто задают один и тот же вопрос (или какой-то ремикс на него).
Этот вопрос время от времени появляется в трекерах ошибок библиотек с открытым исходным кодом . Это была одна из «странных проблем», о которых говорилось в моем выступлении в B-Sides Orlando (« Построение защищенных решений для странных проблем» ), и мы ранее посвятили ему небольшой раздел в одном из наших официальных документов .
Вы знаете, как искать в полях базы данных , но вопрос в том, как мы можем надежно зашифровать поля базы данных, но по-прежнему использовать эти поля в поисковых запросах?
Наше безопасное решение довольно простое, но путь между большинством команд, задающих этот вопрос, и открытием нашего простого решения чреват опасностями: плохой дизайн, академические исследовательские проекты, вводящий в заблуждение маркетинг и плохое моделирование угроз .
Если вы спешите, смело переходите к решению .
На пути к поисковому шифрованию
Давайте начнем с простого сценария (который может быть особенно актуален для многих приложений местного правительства или здравоохранения):
- Вы создаете новую систему, которая должна собирать номера социального страхования (SSN) от своих пользователей.
- Правила и здравый смысл диктуют необходимость шифрования пользовательских SSN в состоянии покоя.
- Сотрудники должны иметь возможность просматривать учетные записи пользователей, учитывая их SSN.
Давайте сначала рассмотрим недостатки с очевидными ответами на эту проблему.
Небезопасные (или иным образом опрометчивые) ответы
Нерандомизированное шифрование
Наиболее очевидным ответом для большинства команд (особенно команд, в которых нет экспертов по безопасности или криптографии) было бы сделать что-то вроде этого:
<?php
class InsecureExampleOne
{
protected $db;
protected $key;
public function __construct(\PDO $db, string $key = '')
{
$this->db = $db;
$this->key = $key;
}
public function searchByValue(string $query): array
{
$stmt = $this->db->prepare('SELECT * FROM table WHERE column = ?');
$stmt->execute([
$this->insecureEncryptDoNotUse($query)
]);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
protected function insecureEncryptDoNotUse(string $plaintext): string
{
return \bin2hex(
\openssl_encrypt(
$plaintext,
'aes-128-ecb',
$this->key,
OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING
)
);
}
}
В приведенном выше фрагменте один и тот же открытый текст всегда создает один и тот же зашифрованный текст при шифровании одним и тем же ключом. Но что касается режима ECB, то каждый 16-байтовый блок шифруется отдельно , что может иметь некоторые крайне печальные последствия .
Формально эти конструкции не являются семантически безопасными: если вы зашифруете большое сообщение, вы увидите повторение блоков в зашифрованном тексте.
Для обеспечения безопасности шифрование должно быть неотличимо от случайного шума для всех, кто не владеет ключом дешифрования. Небезопасные режимы включают режим ECB и режим CBC со статическим (или пустым) IV.
Вы хотите недетерминированное шифрование, что означает, что каждое сообщение использует уникальный одноразовый номер или вектор инициализации, который никогда не повторяется для данного ключа.
Экспериментальные Академические Проекты
Существует много научных исследований по таким темам, как гомоморфные , раскрывающие порядок и сохраняющие порядок методы шифрования.
Как бы ни была интересна эта работа, текущие проекты еще недостаточно безопасны для использования в производственной среде.
Например, шифрование, раскрывающее порядок, пропускает достаточно данных для вывода открытого текста .
Гомоморфные схемы шифрования часто переупаковывают уязвимости (практические атаки с использованием выбранного шифротекста) в качестве функций.
- Ненадежный RSA гомоморфен относительно умножения .
- Если вы умножите зашифрованный текст на целое число, полученный вами открытый текст будет равен исходному сообщению, умноженному на то же самое целое число. Существует несколько возможных атак против неополненного RSA , поэтому в реальном мире RSA использует заполнение (хотя часто это небезопасный режим заполнения ).
- AES в режиме счетчика гомоморфен относительно XOR .
- Вот почему однократное повторное использование нарушает конфиденциальность вашего сообщения в режиме CTR (и потоковых шифров не-ЯМР в целом).
Как мы уже упоминали в предыдущем сообщении в блоге , когда речь заходит о криптографии реального мира, конфиденциальность без целостности — это то же самое, что и отсутствие конфиденциальности . Что происходит, если злоумышленник получает доступ к базе данных, изменяет шифротексты и изучает поведение приложения при расшифровке?
Существует потенциал для постоянных исследований в области криптографии, которые позволят в один прекрасный день создать инновационный дизайн шифрования, который не отменяет десятилетий исследований в области безопасных криптографических примитивов и конструкций криптографических протоколов. Тем не менее, мы еще не там, и вам не нужно вкладывать деньги в ненужно сложный исследовательский прототип, чтобы решить эту проблему.
Нечестное упоминание: расшифровывать каждую строку
Я не ожидаю, что большинство инженеров придут к этому решению без следа сарказма. Плохая идея в том, что, поскольку вам необходимо безопасное шифрование (см. Ниже), единственным выходом для вас является запрос каждого зашифрованного текста в базе данных, а затем итерация по ним, расшифровка их по одному и выполнение операции поиска в коде приложения.
Если вы пойдете по этому пути, вы откроете свое приложение для атак отказа в обслуживании. Это будет медленно для ваших законных пользователей. Это ответ циника, и вы можете сделать это намного лучше, как мы продемонстрируем ниже.
Безопасное шифрование с возможностью поиска стало проще
Давайте начнем с того, что одним махом избежим всех проблем, описанных в небезопасном / опрометчивом разделе: все шифротексты будут результатом аутентифицированной схемы шифрования , предпочтительно с большими одноразовыми номерами (генерируемыми из безопасного генератора случайных чисел ).
При использовании аутентифицированной схемы шифрования шифротексты являются недетерминированными (одно и то же сообщение и ключ, но отличаются от одноразового номера, дает другой зашифрованный текст) и защищены тегом аутентификации. Некоторые подходящие варианты включают в себя: XSalsa20-Poly1305, XChacha20-Poly1305 и (при условии, что он не сломан до завершения CAESAR) NORX64-4-1. Если вы используете NaCl или libsodium, вы можете просто использовать crypto_secretbox
Следовательно, наши шифротексты неотличимы от случайного шума и защищены от атак с выбранным шифротекстом . Вот как безопасное, скучное шифрование должно быть.
Однако это представляет собой непосредственную проблему: мы не можем просто зашифровать произвольные сообщения и запросить в базе данных соответствующие шифротексты. К счастью, есть умный обходной путь.
Важное замечание: модель угрозы. Использование шифрования
Прежде чем начать, убедитесь, что шифрование действительно делает ваши данные более безопасными. Важно подчеркнуть, что «зашифрованное хранилище» не является решением для защиты приложения CRUD, уязвимого для внедрения SQL. Решение реальной проблемы (т.е. предотвращение внедрения SQL ) — единственный путь.
Если шифрование является подходящим элементом управления безопасностью для реализации, это означает, что криптографические ключи, используемые для шифрования / дешифрования данных, не доступны для программного обеспечения базы данных. В большинстве случаев имеет смысл хранить сервер приложений и сервер баз данных на отдельном оборудовании.
Реализация буквального поиска зашифрованных данных
Возможный вариант использования: хранение номеров социального страхования, но возможность их запрашивать.
Чтобы хранить зашифрованную информацию и все еще использовать открытый текст в запросах SELECT, мы будем следовать стратегии, которую мы называем слепой индексацией . Общая идея заключается в том, чтобы хранить хеш-код с ключом (например, HMAC) открытого текста в отдельном столбце. Важно, чтобы слепой индексный ключ отличался от ключа шифрования и был неизвестен серверу базы данных.
Для очень конфиденциальной информации вместо простого HMAC вам понадобится использовать алгоритм растяжения ключа (PBKDF2-SHA256, scrypt, Argon2) с ключом, действующим как статическая соль, чтобы замедлить попытки перечисления. В любом случае нас не беспокоят атаки «грубой силы» в автономном режиме, если только злоумышленник не сможет получить ключ (который не должен храниться в базе данных).
Поэтому, если ваша схема таблицы выглядит следующим образом (в варианте PostgreSQL):
CREATE TABLE humans (
humanid BIGSERIAL PRIMARY KEY,
first_name TEXT,
last_name TEXT,
ssn TEXT, /* encrypted */
ssn_bidx TEXT /* blind index */
);
CREATE INDEX ON humans (ssn_bidx);
Вы должны хранить зашифрованное значение в humans.ssn
Слепой индекс незашифрованного SSN попадет в humans.ssn_bidx
Наивная реализация может выглядеть так:
<?php
/* This is not production-quality code.
* It's optimized for readability and understanding, not security.
*/
function encryptSSN(string $ssn, string $key): string
{
$nonce = random_bytes(24);
$ciphertext = sodium_crypto_secretbox($ssn, $nonce, $key);
return bin2hex($nonce . $ciphertext);
}
function decryptSSN(string $ciphertext, string $key): string
{
$decoded = hex2bin($ciphertext);
$nonce = mb_substr($decoded, 0, 24, '8bit');
$cipher = mb_substr($decoded, 24, null, '8bit');
return sodium_crypto_secretbox_open($cipher, $nonce, $key);
}
function getSSNBlindIndex(string $ssn, string $indexKey): string
{
return bin2hex(
sodium_crypto_pwhash(
32,
$ssn,
$indexKey,
SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE,
SODIUM_CRYPTO_PWHASH_MEMLIMIT_MODERATE
)
);
}
function findHumanBySSN(PDO $db, string $ssn, string $indexKey): array
{
$index = getSSNBlindIndex($ssn, $indexKey);
$stmt = $db->prepare('SELECT * FROM humans WHERE ssn_bidx = ?');
$stmt->execute([$index]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
Более подробное подтверждение концепции включено в дополнительный материал для моей презентации B-Sides Orlando 2017 . Он выпущен под лицензией Creative Commons CC0, которая для большинства людей означает то же самое, что и «общественное достояние».
Анализ безопасности и ограничения
В зависимости от вашей конкретной модели угроз, это решение оставляет два вопроса, на которые необходимо ответить, прежде чем его можно будет принять:
- Это безопасно для использования, или это утечка данных, как сито?
- Каковы ограничения на его полезность? (Этот вид уже ответил.)
Учитывая приведенный выше пример, предполагая, что ваш ключ шифрования и ваш слепой индексный ключ разделены, оба ключа хранятся на веб-сервере, и у сервера базы данных нет никакого способа получить эти ключи, тогда любой злоумышленник, который только скомпрометирует сервер базы данных ( но не веб-сервер) сможет узнать, только если несколько строк имеют номер социального страхования, но не то, что является общим SSN. Эта утечка повторяющихся записей необходима для того, чтобы индексация была возможной, что, в свою очередь, позволяет выполнять быстрые запросы SELECT из предоставленного пользователем значения.
Кроме того, если злоумышленник способен как наблюдать / изменять открытые тексты как обычный пользователь приложения, так и наблюдать слепые индексы, хранящиеся в базе данных, он может использовать это в атаке с выбранным открытым текстом , где он повторяет каждое возможное значение как пользователь а затем коррелируйте с результирующим значением слепого индекса. Это более практично в сценарии HMAC, чем, например, в сценарии Argon2. Для значений высокой энтропии или низкой чувствительности (не SSN) физика грубой силы может быть на нашей стороне.
Гораздо более практичной атакой для такого преступника будет замена значений из одной строки в другую, а затем обычный доступ к приложению, что откроет открытый текст, если не был использован отдельный ключ для строки (например, hash_hmac('sha256', $rowID, $masterKey, true)
Лучшей защитой здесь является использование режима AEAD (передача первичного ключа в качестве дополнительных связанных данных), чтобы шифротексты были привязаны к определенной строке базы данных. (Это не помешает злоумышленникам удалить данные, что является гораздо более сложной задачей .)
По сравнению с объемом информации, просочившейся из других решений, модели угроз большинства приложений должны посчитать это приемлемым компромиссом. Пока вы используете аутентифицированное шифрование для шифрования и либо HMAC (для слепой индексации нечувствительных данных), либо алгоритм хеширования паролей (для слепой индексации конфиденциальных данных), легко рассуждать о безопасности вашего приложения.
Однако у него есть одно очень серьезное ограничение: оно работает только для точных совпадений. Если две строки различаются бессмысленным образом, но всегда выдают разные криптографические хеш-значения, то поиск одной из них никогда не приведет к другой. Если вам нужно выполнить более сложные запросы, но при этом не использовать ключи дешифрования и значения в виде открытого текста в руках сервера базы данных, нам нужно проявить изобретательность.
Стоит также отметить, что, хотя HMAC / Argon2 может помешать злоумышленникам, не обладающим ключом, узнать значения открытого текста того, что хранится в базе данных, он может раскрыть метаданные (например, два, казалось бы, не связанных между собой человека имеют общий адрес улицы) о реальный мир.
Реализация нечеткого поиска зашифрованных данных
Возможный вариант использования: шифрование юридических имен людей и возможность поиска только с частичными совпадениями.
Давайте основываемся на предыдущем разделе, где мы создали слепой индекс, который позволяет запрашивать базу данных для точных совпадений.
На этот раз вместо добавления столбцов в существующую таблицу мы собираемся сохранить дополнительные значения индекса в объединенной таблице.
CREATE TABLE humans (
humanid BIGSERIAL PRIMARY KEY,
first_name TEXT, /* encrypted */
last_name TEXT, /* encrypted */
ssn TEXT, /* encrypted */
);
CREATE TABLE humans_filters (
filterid BIGSERIAL PRIMARY KEY,
humanid BIGINT REFERENCES humans (humanid),
filter_label TEXT,
filter_value TEXT
);
/* Creates an index on the pair. If your SQL expert overrules this, feel free to omit it. */
CREATE INDEX ON humans_filters (filter_label, filter_value);
Причиной этого изменения является нормализация наших структур данных. Вы можете обойтись, просто добавив столбцы в существующую таблицу, но это, вероятно, будет грязно.
Следующее изменение заключается в том, что мы собираемся хранить отдельный, отдельный слепой индекс для каждого столбца для каждого вида запросов, который нам нужен (каждый со своим собственным ключом). Например:
- Нужен поиск без учета регистра, который игнорирует пробелы?
- Сохраните слепой индекс
preg_replace('/[^a-z]/', '', strtolower($value))
- Сохраните слепой индекс
- Нужно запросить первую букву их фамилии?
- Сохраните слепой индекс
strtolower(mb_substr($lastName, 0, 1, $locale))
- Сохраните слепой индекс
- Нужно сопоставить «существа с этим письмом, заканчивается этим письмом»?
- Сохраните слепой индекс
strtolower($string[0] . $string[-1])
- Сохраните слепой индекс
- Нужно запросить первые три буквы их фамилии и первую букву их имени?
- Ты угадал! Создайте другой индекс на основе частичных данных.
У каждого индекса должен быть свой ключ, и нужно приложить немало усилий, чтобы не дать слепым индексам подмножеств открытого текста передать утечку реальных значений открытого текста преступнику, умеющему разгадывать кроссворды. Создавайте индексы только для серьезных бизнес-задач и агрессивно регистрируйте доступ к этим частям вашего приложения.
Торговая память на время
До сих пор все проектные предложения были в пользу того, чтобы позволить разработчикам писать тщательно продуманные запросы SELECT, в то же время сводя к минимуму количество вызовов подпрограммы дешифрования. Как правило, именно здесь останавливается поезд, и цели большинства людей были достигнуты.
Однако существуют ситуации, когда умеренный удар по производительности в поисковых запросах является приемлемым, если это означает сохранение большого дискового пространства.
Трюк здесь прост: обрежьте ваши слепые индексы, например, до 16, 32 или 64 бит и рассматривайте их как фильтр Блума :
- Если участвующие в запросе слепые индексы соответствуют заданной строке, данные, вероятно, совпадают.
- Код вашего приложения должен будет выполнить расшифровку для каждой строки-кандидата и затем обслуживать только фактические соответствия.
- Если участвующие в запросе слепые индексы не соответствуют заданной строке, то данные определенно не совпадают.
Может также стоить преобразовать эти значения из строки в целое число, если ваш сервер базы данных в конечном итоге будет хранить его более эффективно.
Вывод
Я надеюсь, что я адекватно продемонстрировал, что не только возможно создать систему, которая использует безопасное шифрование, позволяя быстрые запросы (с минимальной утечкой информации против очень привилегированных злоумышленников), но что возможно создать такую систему просто, из компоненты, предоставляемые современными криптографическими библиотеками с очень небольшим количеством клея.
Если вы заинтересованы в реализации зашифрованного хранилища базы данных в своем программном обеспечении, мы будем рады предоставить вам и вашей компании наши консультационные услуги. Свяжитесь с ParagonIE, если вы заинтересованы.