Как я зашифровал базу данных, нигде не сохраняя ключи.
Проблемы)
Представьте себе следующий (кошмарный) сценарий: несмотря на все ваши усилия по обеспечению безопасности вашего сервера, кто-то взломал его и украл копию всей вашей базы данных, полную личных, конфиденциальных данных о ваших пользователях.
Однако, если вы храните поля базы данных в зашифрованном виде (по крайней мере, конфиденциальные данные), все, что у злоумышленников будет, это не поддающиеся расшифровке строки байтов — конфиденциальность ваших пользователей в безопасности.
Но подождите: чтобы ваш сервер мог хранить и извлекать данные, ему необходимо знать ключ шифрования! И вы не можете хранить этот ключ в самой базе данных — или где-нибудь на жестком диске сервера, если на то пошло — иначе злоумышленники также получат ключ, когда они взломают сервер.
И есть еще одна проблема: хранение полей в зашифрованном состоянии сделало бы невозможным поиск — мы не можем создать индексную базу данных для поиска полей, содержащих, скажем, слово «ипотека», если поля зашифрованы.
И нет, создание индекса на основе исходных незашифрованных данных не сработает — если злоумышленник увидит, что заданное зашифрованное поле имеет индексы, указывающие на него, указывающие на то, что оно содержит слова «ипотека», «платеж», ‘Январь’ и ‘2015’, то содержание поля стало бы достаточно очевидным — даже без его расшифровки.
К счастью, есть способы обойти эти две проблемы.
Решение первой проблемы — сгенерировать ключ из пароля (который нигде не хранится).
Для этого нам нужен метод генерации криптографического ключа, который полностью детерминированный — ввод одного и того же пароля миллион раз приведет к выводу одного и того же ключа миллион раз — и, одновременно, совершенно непредсказуемый — злоумышленник не должен угадать ключ, не зная пароля, из которого он был сгенерирован.
Такой метод называется «функция деривации ключей» — или, для краткости, KDF.
К счастью, такая функция, называемая ПБКДФ2 широко доступен — включен в OpenSSL и, следовательно, может использоваться на таких платформах, как Node.js, PHP и т. д., из коробки.
PBKDF2 работает, «растягивая» строки — он берет пароль (или любую строку), которую вы ему даете, и выполняет с ним тысячи сложных преобразований, пока не создаст последовательность байтов любой требуемой длины, без очевидной связи с паролем. исходная строка.
Вам нужно будет вводить пароль на свой веб-сервер каждый раз, когда вы его запускаете (поскольку он нигде не хранится); он будет использовать пароль для генерации ключа шифрования и с этого момента использовать этот ключ, не сохраняя его.
Решение второй проблемы заключается в создании слепых индексов.
Идея состоит в том, чтобы вычислить хэш из условий поиска, а затем использовать хэш для индексации.
Например: предположим, мы хотим найти, какие из (зашифрованных) полей содержат слово «ипотека».
Во-первых, мы вычисляем хэш от слова: скажем, «ипотека» -> 14231297424532579.
Затем мы создаем индекс, в котором перечислены все поля, содержащие слово, хэш которого равен числу 14231297424532579 — без сохранения фактического слова «ипотека».
т.е. «Слово, хэш которого равен 14231297424532579, содержится в строках 34, 156, 1240,…»
Это называется «слепой индекс» — поскольку в нем фактически не хранится слово «ипотека» — злоумышленник, получивший базу данных, не сможет выяснить из этого индекса, какие слова содержатся в каких полях.
Наконец, всякий раз, когда нам нужно найти строки, содержащие слово «ипотека», мы снова вычисляем его хэш (то есть число 14231297424532579), а затем ищем его в слепом индексе.
Для того, чтобы это работало, нам нужна функция хеширования — для этого мы можем использовать ту же функцию растяжения строки (PBKDF2), которую мы использовали для генерации ключа из пароля — мы просто растягиваем слово в 6-байтовую строку (6 это максимальное количество байтов, которое криптобиблиотека примет для преобразования в целое число), затем интерпретируйте указанную строку как целое число, которое мы можем использовать в качестве хэша.
Мы можем добавить дополнительный уровень безопасности, используя секретную соль для генерации хэша — и сгенерировать этот хэш из того же пароля, который мы использовали для генерации ключа. Таким образом, злоумышленник не сможет выяснить, какое слово хешируется до 14231297424532579 при атаке по словарю — не зная пароля.
Вот пример (на Node.js, но можно портировать на любой язык)
Во-первых, мы импортируем криптобиблиотеку (которую Node предоставляет из коробки):
const crypto = require('crypto');
Затем мы пишем вспомогательную функцию для растяжения строки; Я использую алгоритм sha512 с сотней тысяч итераций — дает очень хорошие результаты без чрезмерной загрузки процессора.
function stretchString(s, salt, outputLength){
return crypto.pbkdf2Sync(s, salt, 100000, outputLength, 'sha512');
}
Таким образом, все, что нам нужно для растяжения строки, это (помимо самой строки), используемая соль и количество байтов, которые мы хотим получить для вывода.
Затем с помощью нашего stretchString
мы генерируем как наш криптографический ключ, так и очень хорошую соль для использования в наших слепых индексах, и все это только из пароля.
function keyFromPassword(password){
const keyPlusHashingSalt = stretchString(password, 'salt', 24 + 48);
return {
cipherKey: keyPlusHashingSalt.slice(0,24),
hashingSalt: keyPlusHashingSalt.slice(24)
};
}
Теперь мы можем использовать сгенерированный ключ для шифрования любых данных:
function encrypt(key, sourceData){
const iv = Buffer.alloc(16, 0);
const cipher = crypto.createCipheriv('aes-192-cbc', key.cipherKey, iv);
let encrypted = cipher.update(sourceData, 'binary', 'binary');
encrypted += cipher.final('binary');
return encrypted;
}
А потом тем же (симметричным) ключом расшифровать его обратно:
function decrypt(key, encryptedData){
const iv = Buffer.alloc(16, 0);
const decipher = crypto.createDecipheriv('aes-192-cbc', key.cipherKey, iv);
let decrypted = decipher.update(encryptedData, 'binary', 'binary');
decrypted += decipher.final('binary');
return decrypted;
}
Теперь все, что нам нужно, это функция для вычисления целочисленных хэшей, которые можно использовать для слепого индексирования:
function hash(key, sourceData){
const hashBuffer = stretchString(sourceData, key.hashingSalt, 6);
return hashBuffer.readUIntLE(0,6);
}
Вот и все! вот полный код:
const crypto = require('crypto');
function stretchString(s, salt, outputLength){
return crypto.pbkdf2Sync(s, salt, 100000, outputLength, 'sha512');
}
function keyFromPassword(password){
const keyPlusHashingSalt = stretchString(password, 'salt', 24 + 48);
return {
cipherKey: keyPlusHashingSalt.slice(0,24),
hashingSalt: keyPlusHashingSalt.slice(24)
};
}
function encrypt(key, sourceData){
const iv = Buffer.alloc(16, 0);
const cipher = crypto.createCipheriv('aes-192-cbc', key.cipherKey, iv);
let encrypted = cipher.update(sourceData, 'binary', 'binary');
encrypted += cipher.final('binary');
return encrypted;
}
function decrypt(key, encryptedData){
const iv = Buffer.alloc(16, 0);
const decipher = crypto.createDecipheriv('aes-192-cbc', key.cipherKey, iv);
let decrypted = decipher.update(encryptedData, 'binary', 'binary');
decrypted += decipher.final('binary');
return decrypted;
}
function hash(key, sourceData){
const hashBuffer = stretchString(sourceData, key.hashingSalt, 6);
return hashBuffer.readUIntLE(0,6);
}
const key = keyFromPassword('Our password');
const encryptedTest = encrypt(key, 'This is a test');
console.log( decrypt(key, encryptedTest) );
console.log( hash(key, 'This is another test') );