Практический опыт работы с запросным шифрованием MongoDB и Node.js.

автор vadim


В MongoDB 6 появилась возможность запрашивать зашифрованные данные в базе данных. Данные шифруются на протяжении всего пути: при вставке, хранении и запросе. Это означает новый уровень безопасности данных, которые остаются безопасными даже при использовании в базе данных. Только клиентское приложение способно расшифровать данные. База данных вообще не содержит ключей к зашифрованным данным, но все же поддерживает запросы к этим данным.

Таким образом, шифрование с возможностью запроса MongoDB удаляет хранилище данных и его инфраструктуру как цели атаки. Эта квазимагическая возможность требует некоторой дополнительной настройки приложений. В этой статье показано, как настроить среду разработки для работы с запросным шифрованием MongoDB в приложении Node.js.

Мастер-ключи и ключи шифрования данных

В запрашиваемом шифровании MongoDB используются два типа ключей: главный ключ клиента (CMK) и ключи шифрования данных (DEK). Они работают вместе, чтобы защитить ваши данные. DEK используется для шифрования данных в базе данных, а CMK используется для шифрования и дешифрования DEK, добавляя уровень защиты. CMK — более чувствительный ключ. Если CMK скомпрометирован, ваши данные уязвимы для компрометации. Если CMK или DEK потерян или недоступен, клиентское приложение не сможет расшифровать данные.

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

Создать CMK

Первым шагом является создание вашего CMK. Вы можете сделать это с помощью инструмента командной строки openssl, как показано в листинге 1.

Листинг 1. Генерация локального ключа

openssl rand 96 > cmk.txt

Создать DEK

Мы создадим простую программу Node.js для обработки нашего CMK, создадим DEK и вставим DEK в специальную зашифрованную коллекцию в MongoDB, называемую ключевое хранилище. Шифрование с возможностью запроса содержит в этой коллекции DEK, зашифрованный CMK. При вызове зашифрованных данных приложения ваше клиентское приложение извлекает DEK из хранилища ключей, расшифровывает DEK с помощью CMK, а затем использует расшифрованный DEK для взаимодействия с экземпляром базы данных.

Приходится много шифрования, но, опять же, идея состоит в том, чтобы обеспечить безопасность DEK с помощью CMK.

Документы MongoDB предоставляют более полную версию приложения, используемого для создания таблицы хранилища ключей DEK. Мы построим минимум, чтобы попытаться видеть лес сквозь деревья.

Создайте новое приложение NPM, набрав npm init. Вы можете принять все значения по умолчанию. Теперь создайте два новых js-файла с именами make-dek.js и Insert.js и добавьте строки в файл package.json, как показано в листинге 2.

Листинг 2. Сценарий makeDEK

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "makeDEK": "node ./make-dek.js",
    "insert": "node ./insert.js"
}

Теперь вы можете запустить makeDEK.js, введя npm run makeDEK и npm run insert в командной строке. (Но эти команды пока ничего не делают.)

Добавить зависимости

Для следующих шагов нам понадобятся два установленных пакета. Введите команды из листинга 3, чтобы установить их.

Листинг 3. Добавление зависимостей MondoDB

npm install mongodb
npm install mongodb-client-encryption

Настройте Атлас MongoDB

Для этого урока мы будем использовать Atlas, управляемый сервис MongoDB. На момент написания этой статьи для создания кластера MongoDB 6 в Atlas вам понадобится выделенный кластер платного уровня. (Обратите внимание, что с помощью сервера сообщества MongoDB можно использовать шифрование с возможностью запроса в ручном режиме.)

Вы можете создать бесплатную учетную запись Atlas здесь. Отсюда легко настроить кластер (оставив имя Cluster0) и создать пользователя с аутентификацией по паролю. Просто убедитесь, что вы выбрали «Выделенный кластер», когда у вас появится такой выбор.

Обратите внимание, что для выполнения этих действий у пользователя должна быть роль AtlasAdmin. Вы можете установить роль пользователя, перейдя в «Доступ к базе данных» в консоли MongoDB и нажав «Изменить» рядом с вашим пользователем. Затем в раскрывающемся меню «Встроенная роль» выберите AtlasAdmin.

На следующих шагах мы будем использовать имя пользователя и пароль для доступа к кластеру Atlas.

Обратите внимание (предупреждение безопасности):

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

Добавить общую библиотеку шифрования

MongoDB поддерживает два стиля шифрования с возможностью запроса: автоматическое и ручное. Auto проще, позволяя клиенту MongoDB согласовывать шифрование за вас. Чтобы использовать auto, вам понадобится общая библиотека шифрования MongoDB, доступная здесь. В раскрывающемся списке справа выберите crypt_shared, укажите свою операционную систему и используйте последнюю версию, как показано на снимке экрана 1. (Вы также введите адрес электронной почты, чтобы принять лицензию.)

Скриншот 1. Загрузите пакет crypt_shared.

общий доступ к склепу mongodb ИДГ

Теперь поместите этот файл в удобное место и разархивируйте его. В каталоге, созданном при извлечении, вы найдете файл /lib/mongo_crypt_v1.so. Это тот, который нам нужен. Запишите путь, поскольку он понадобится вам позже, когда мы установим в листинге 4 и листинге 5.

Код make-dek.js

Теперь мы готовы написать код для файла make-dek.js. Это будет небольшое приложение, которое настраивает коллекцию хранилища ключей и саму зашифрованную коллекцию. Эти две коллекции работают совместно, обеспечивая сохранение, запрос и извлечение данных из зашифрованной коллекции. (Более подробно это описано в документации MongoDB.) Содержимое make-dek.js показано в листинге 4.

Листинг 4. make-dek.js

const { MongoClient, Binary } = require("mongodb");
const { ClientEncryption } = require("mongodb-client-encryption");

const keyVaultNamespace = "encryption.__keyvault";
const secretDB = "secretDB";
const secretCollection = "secretCollection";
const uri = "mongodb+srv://<ATLAS_USERNAME>:<ATLAS_PASSWORD>@cluster0.444xyz.mongodb.net/?retryWrites=true&w=majority";

async function run() {
   const keyVaultClient = new MongoClient(uri);
   await keyVaultClient.connect();
   const keyVaultDB = keyVaultClient.db("encryption");
   await keyVaultDB.dropDatabase();
   const keyVaultColl = keyVaultDB.collection("__keyvault");
   await keyVaultColl.createIndex(
      { keyAltNames: 1 },
      { unique: true, partialFilterExpression: { keyAltNames: { $exists: true } } }
   );
   const localMasterKey = require("fs").readFileSync("./cmk.txt");
   const kmsProviders = { local: { key: localMasterKey } };
   const clientEnc = new ClientEncryption(keyVaultClient, {
      keyVaultNamespace: keyVaultNamespace,
      kmsProviders: kmsProviders
   });
   const dek = await clientEnc.createDataKey("local", { keyAltNames: ["dek"] });
   const encryptedFieldsMap = {
      ["secretDB.secretCollection"]: {
         fields: [
            {
               keyId: dek,
               path: "secretField",
               bsonType: "int",
               queries: { queryType: "equality" },
            }
         ]
      }
   };
   const extraOptions = { cryptSharedLibPath: "<MONGO_CRYPT_LIB_PATH>" };
   const encClient = new MongoClient(uri, {
      autoEncryption: {
         keyVaultNamespace,
         kmsProviders,
         extraOptions,
         encryptedFieldsMap
      }
   });

   await encClient.connect();
   const newEncDB = encClient.db(secretDB);
   await newEncDB.dropDatabase();
   await newEncDB.createCollection(secretCollection);
   await keyVaultClient.close();
   await encClient.close();
   console.log("Successfully created DEK and encrypted collection.");
}

run().catch(console.dir);

В листинге 4 рассказывается о двух коллекциях: encryption.__keyvault и secretDB.secretCollection. Эти две коллекции используются вместе для поддержки шифрования с возможностью запроса.

secretDB.secretCollection отели фактические бизнес-данные. encryption.__keyvault коллекция содержит зашифрованные ключи шифрования данных, используемые на secretDB.secretCollection. Используются два MongoClient. Зашифрованный клиент (encClient) настроен с использованием DEK, созданного незашифрованным keyVaultClient. ДЭК установлен на encryptedFieldsMap.keyId поле, которое используется для настройки encClient.

encryptedFieldsMap содержит дополнительную информацию для зашифрованного клиента encClientчто является стандартом MongoClient набор с autoEncrypted поле заполнено. encryptedFieldsMap сообщает клиенту, какие поля зашифрованы (с помощью свойства пути), в данном случае secretField. Если queries свойство не установлено, поле будет зашифровано, но недоступно для запроса. На момент написания статьи только equality поддерживается как queryType.

Обратите внимание, что ClientEncryption объект (clientEnc) используется для генерации DEK. clientEnc объект использует keyVaultClient вместе с keyVaultNameSpace (encryption.__keyvault) и kmsProvider.

kmsProvider — это локальный поставщик ключей, который указывает на случайное число, которое мы сгенерировали в командной строке. Он также используется autoEncryption мы установили на encClient клиент. (Напоминание: не используйте локальный kmsProvider в производстве.)

Вставка и запрос данных

Теперь у нас есть инфраструктура для вставки и запроса данных в secretDB.secretCollection.secretField. Это делается с помощью клавиш в encryption.__keyvault. В листинге 5 представлен урезанный пример выполнения этой операции с двумя полями: незашифрованное поле. string на nonsecretField и зашифрованный int на secretField.

Листинг 5. Вставка и запрос к зашифрованным и незашифрованным полям

const { MongoClient, Binary } = require("mongodb");

const localMasterKey = require("fs").readFileSync("./cmk.txt");
const kmsProviders = { local: { key: localMasterKey } };
const uri = "mongodb+srv://<ATLAS_USERNAME>:
<ATLAS_PASSWORD>@cluster0.444xyz.mongodb.net/?retryWrites=true&w=majority"

async function run() {
const unencryptedClient = new MongoClient(uri);
await unencryptedClient.connect();
const keyVaultClient = unencryptedClient.db("encryption").collection("__keyvault");
const dek = await keyVaultClient.findOne({ "keyAltNames": "dek" });

const encryptedFieldsMap = {
["secretDB.secretCollection"]: {
fields: [
{
keyId: dek._id,
path: "secretField",
bsonType: "int",
queries: { queryType: "equality" }
}
]
}
};
const extraOptions = { cryptSharedLibPath: "<MONGO_CRYPT_LIB_PATH>" };
const encryptedClient = new MongoClient(uri, {
autoEncryption: {
keyVaultNamespace: "encryption.__keyvault",
kmsProviders: kmsProviders,
extraOptions: extraOptions,
encryptedFieldsMap: encryptedFieldsMap
}
});
await encryptedClient.connect();
try {
const unencryptedColl = unencryptedClient.db("secretDB").collection("secretCollection");
const encryptedColl = encryptedClient.db("secretDB").collection("secretCollection");
await encryptedColl.insertOne({
secretField: 42,
nonsecretField: "What is the secret to life, the universe and everything?"
});
console.log(await unencryptedColl.findOne({ nonsecretField: /universe/ }));
console.log(await encryptedColl.findOne({ "secretField":42 })
);
} finally {
await unencryptedClient.close();
await encryptedClient.close();
}
}

run().catch(console.dir);

В листинге 5 мы создаем два клиента MongoDB: незашифрованный клиент и зашифрованный клиент. С unencryptedClient мы получаем доступ к хранилищу ключей encryption.__keyvault который мы создали с помощью make-dek.js в листинге 4, и извлекаем сохраненный там DEK. Затем мы используем DEK для построения encryptedFieldsMapкоторый также содержит путь, тип и параметры запросов для секретного поля.

Затем мы создаем зашифрованный клиент, указывая пространство имен хранилища ключей (encryption.__keyvault), kmsProvider (который снова является локальным файлом cmk.txt), extraOptions указывая на общую библиотеку шифрования, которую мы скачали из MongoDB, и encryptedFieldsMap.

Затем мы используем encryptedClient вставить в secretDB.secretCollection коллекция, с. secretField и nonsecretField установлен на int и stringсоответственно.

Наконец, мы запрашиваем данные. Сначала мы используем незашифрованный клиент — на этот раз указанный secretDB.secretCollection — для запроса с помощью nonsecretField и выведите результат. Результат покажет, что secretField является зашифрованным текстом, а nonsecretField является открытым текстом. Дело в том, что незашифрованный клиент может запрашивать и использовать обычные поля как обычно.

Зашифрованный клиент затем используется для запроса secretFieldи когда этот результат выводится, все поля, включая secretField видны. Это демонстрирует, что encryptedClient имеет полный доступ ко всем полям.

Обратите внимание, что secretDB.secretCollection также хранит метаданные в __safeContent__ поле. Убедитесь, что вы не изменяете это или коллекцию хранилища ключей, иначе все может работать не так, как ожидалось.

Шифрование, которое вы можете запросить

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

Related Posts

Оставить комментарий