Como usar a criptografia no nível do campo do lado do cliente (CSFLE) do MongoDB com Node.js
Joe Karlsson12 min read • Published Jan 10, 2022 • Updated Sep 23, 2022
Avalie esse Tutorial
Você já precisou desenvolver um aplicativo que armazena dados confidenciais, como números de cartão de crédito ou de previdência social? Esse é um caso de uso muito comum para bancos de dados, e pode ser difícil salvar esses dados de forma segura. Felizmente para nós, existem alguns recursos de segurança incríveis que vêm com o MongoDB. Por exemplo, você deve saber que, com o MongoDB, você pode aproveitar:
- Regras baseadas em rede e usuário, que permite aos administradores conceder e restringir permissões de nível de coleção para os usuários.
- Criptografia dos seus dados em repouso, que criptografa os arquivos do banco de dados em disco.
- Criptografia de transporte usando TLS/SSL que criptografa os dados pela rede.
- E agora você pode até ter criptografia no lado do cliente, conhecida como criptografia em nível de campo do lado do cliente (CSFLE).
O diagrama a seguir é uma lista dos recursos de segurança do MongoDB oferecidos e as possíveis vulnerabilidades de segurança que eles abordam:
A criptografia no nível do campo no lado do cliente permite que os engenheiros especifiquem os campos de um documento que deve ser mantido criptografado. Os dados confidenciais são criptografados/descriptografados de forma transparente pelo cliente e só são comunicados de e para o servidor de forma criptografada. Esse mecanismo mantém os campos de dados especificados seguros de forma criptografada no servidor e na rede. Embora todos os clientes tenham acesso aos campos de dados não confidenciais, somente os clientes CSFLE configurados adequadamente podem ler e gravar os campos de dados confidenciais.
Nesta postagem, projetaremos um cliente Node.js que pode ser usado para armazenar com segurança campos selecionados como parte de um aplicativo médico.
Existem alguns requisitos que devem ser atendidos antes de tentar usar a criptografia no nível do campo do lado do cliente (CSFLE) com o driver Node.js.
- MongoDB Atlas 4.2+ ou MongoDB Server 4.2 Empresarial
- Driver de nó do MongoDB 3.6.2+
Este tutorial se concentrará na criptografia automática. Embora este tutorial use o MongoDB Atlas, você precisará usar a versão 4.2 ou mais recente para MongoDB Atlas ou MongoDB Enterprise Edition. Você não poderá usar criptografia automática em nível de campo com o MongoDB Community Edition.
A suposição é que você esteja familiarizado com o desenvolvimento de aplicativos Node.js que usam MongoDB. Se quiser se atualizar, dê uma olhada na série de início rápido que publicamos sobre o tópico.
Devido aos requisitosdo libmongocrypt e do mongocryptd, vale a pena revisar como instalá-los e configurá-los. Exploraremos a instalação no macOS, mas consulte a documentação de libmongocrypt e mongocryptd para seu sistema operacional específico.
A libmongocrypt é necessária para a criptografia automática em nível de campo, pois é o componente responsável por executar a criptografia ou descriptografia dos dados no cliente com o MongoDB 4.2-drivers de nó compatíveis. Agora, atualmente existem algumas soluções para instalar a bibliotecalibmongocrypt no macOS. No entanto, o mais fácil é com Homebrew. Se você tiver o Homebrew instalado, poderá instalar alibmongocrypt com o seguinte comando:
1 brew install mongodb/brew/libmongocrypt
Tive um problema com o libmongocrypt quando tentei executar meu código, porque o libmongocrypt estava tentando se vincular estaticamente ao libmongocrypt em vez de se vincular dinamicamente. Enviei um problema para a equipe para corrigir esse problema, mas, para corrigi-lo, tive que correr:
1 export BUILD_TYPE=dynamic
O mongocryptd é necessário para a criptografia automática em nível de campo e está incluído como componente no pacote MongoDB Enterprise Server . Omongocryptd é responsável apenas por oferecer suporte à criptografia automática no nível do campo no lado do cliente e não executa criptografia ou descriptografia.
Consulte a documentação sobre como obter o bináriomongocryptd, pois cada sistema operacional tem etapas diferentes.
Para macOS, você deve baixar o MongoDB Enterprise Edition no Centro de Download do MongoDB . Você pode consultar as instruções de instalação da Enterprise Edition para instalar o macOS, mas a essência da instalação envolve extrair o arquivo TAR e movê-los para o diretório apropriado .
Nesse ponto, todos os componentes apropriados para criptografia em nível de campo do lado do cliente devem estar instalados ou disponíveis. Certifique-se de que você esteja executando o MongoDB Enterprise em seu cliente enquanto estiver usando o CSFLE, mesmo se estiver salvando seus dados no Atlas.
Vamos começar configurando todos os arquivos e dependências de que precisaremos. Em um novo diretório, crie os seguintes arquivos, executando o seguinte comando:
1 touch clients.js helpers.js make-data-key.js
Certifique-se de inicializar um novo projeto NPM, pois usaremos várias dependências NPM.
1 npm init --yes
E Go instalar todos os pacotes que usaremos agora.
1 npm install -S mongodb mongodb-client-encryption node-gyp
Observação: a base de código completa para este projeto pode ser encontrada aqui: https://github.com/JoeKarlson/client-side-field-level-encryption-csfle-mongodb-node-demo
A CSFLE (Client-Side Field Level Encryption) do MongoDB usa uma estratégia de criptografia chamada criptografia de envelope, na qual as chaves usadas para criptografar/descriptografar dados (chamadas de chaves de criptografia de dados) são criptografadas com outra chave (chamada de chave mestra). O diagrama a seguir mostra como a chave mestra é criada e armazenada:
Aviso
O provedor de chaves locais não é adequado para produção.
O provedor de chaves local é um método inseguro de armazenamento e, portanto, não é recomendado se você planeja usar o CSFLE na produção. Em vez disso, configure uma chave mestra em um sistema de gerenciamento de chaves (KMS) que armazene e descriptografe as chaves de criptografia de dados remotamente.
Para saber como usar um KMS em sua implementação CSFLE, leia o guia Criptografia em nível de campo do lado do cliente: use um KMS para armazenar a chave mestra.
1 // clients.js 2 3 const fs = require("fs") 4 const mongodb = require("mongodb") 5 const { ClientEncryption } = require("mongodb-client-encryption") 6 const { MongoClient, Binary } = mongodb 7 8 module.exports = { 9 readMasterKey: function (path = "./master-key.txt") { 10 return fs.readFileSync(path) 11 }, 12 CsfleHelper: class { 13 constructor({ 14 kmsProviders = null, 15 keyAltNames = "demo-data-key", 16 keyDB = "encryption", 17 keyColl = "__keyVault", 18 schema = null, 19 connectionString = "mongodb://localhost:27017", 20 mongocryptdBypassSpawn = false, 21 mongocryptdSpawnPath = "mongocryptd" 22 } = {}) { 23 if (kmsProviders === null) { 24 throw new Error("kmsProviders is required") 25 } 26 this.kmsProviders = kmsProviders 27 this.keyAltNames = keyAltNames 28 this.keyDB = keyDB 29 this.keyColl = keyColl 30 this.keyVaultNamespace = `${keyDB}.${keyColl}` 31 this.schema = schema 32 this.connectionString = connectionString 33 this.mongocryptdBypassSpawn = mongocryptdBypassSpawn 34 this.mongocryptdSpawnPath = mongocryptdSpawnPath 35 this.regularClient = null 36 this.csfleClient = null 37 } 38 39 /** 40 * In the guide, https://docs.mongodb.com/ecosystem/use-cases/client-side-field-level-encryption-guide/, 41 * we create the data key and then show that it is created by 42 * retreiving it using a findOne query. Here, in implementation, we only 43 * create the key if it doesn't already exist, ensuring we only have one 44 * local data key. 45 * 46 * @param {MongoClient} client 47 */ 48 async findOrCreateDataKey(client) { 49 const encryption = new ClientEncryption(client, { 50 keyVaultNamespace: this.keyVaultNamespace, 51 kmsProviders: this.kmsProviders 52 }) 53 54 await this.ensureUniqueIndexOnKeyVault(client) 55 56 let dataKey = await client 57 .db(this.keyDB) 58 .collection(this.keyColl) 59 .findOne({ keyAltNames: { $in: [this.keyAltNames] } }) 60 61 if (dataKey === null) { 62 dataKey = await encryption.createDataKey("local", { 63 keyAltNames: [this.keyAltNames] 64 }) 65 return dataKey.toString("base64") 66 } 67 68 return dataKey["_id"].toString("base64") 69 } 70 }
O script a seguir gera uma chave mestra de 96-byte, gerenciada localmente, e a salva em um arquivo chamado master-key.txt no diretório a partir do qual o script é executado, além de salvá-la em nosso sistema improvisado de gerenciamento de chaves no Atlas.
1 // make-data-key.js 2 3 const { readMasterKey, CsfleHelper } = require("./helpers"); 4 const { connectionString } = require("./config"); 5 6 async function main() { 7 const localMasterKey = readMasterKey() 8 9 const csfleHelper = new CsfleHelper({ 10 kmsProviders: { 11 local: { 12 key: localMasterKey 13 } 14 }, 15 connectionString: "PASTE YOUR MONGODB ATLAS URI HERE" 16 }) 17 18 const client = await csfleHelper.getRegularClient() 19 20 const dataKey = await csfleHelper.findOrCreateDataKey(client) 21 console.log("Base64 data key. Copy and paste this into clients.js\t", dataKey) 22 23 client.close() 24 } 25 26 main().catch(console.dir)
Depois de salvar esse código, execute o seguinte para gerar e salvar nossas chaves.
1 node make-data-key.js
E você deve obter essa saída no terminal. Certifique-se de salvar esta chave, pois a usaremos em nossa próxima etapa.
Também é uma boa ideia verificar se esses dados foram salvos corretamente. Go para seus clusters no Atlas e navegue até suas collection. Você deverá ver uma nova chave salva na collectionencryption.__keyVault.
Sua chave deve ter este formato:
1 { 2 "_id": "UUID('27a51d69-809f-4cb9-ae15-d63f7eab1585')", 3 "keyAltNames": ["demo-data-key"], 4 "keyMaterial": "Binary('oJ6lEzjIEskH...', 0)", 5 "creationDate": "2020-11-05T23:32:26.466+00:00", 6 "updateDate": "2020-11-05T23:32:26.466+00:00", 7 "status": "0", 8 "masterKey": { 9 "provider": "local" 10 } 11 }
Com a chave de dados criada, estamos em um ponto no tempo em que precisamos descobrir quais campos devem ser criptografados em um documento e quais campos devem ser deixados como texto sem formatação. A maneira mais fácil de fazer isso é com um mapa de esquema.
Um mapa de esquema para criptografia é JSON estendido e pode ser adicionado diretamente ao código-fonte Go ou carregado de um arquivo externo. Do ponto de vista da manutenção, o carregamento de um arquivo externo é mais fácil de manter.
A tabela a seguir ilustra o modelo de dados do Sistema de Gerenciamento de Assistência Médica.
tipo de campo | Algoritmo de criptografia | Tipo de JSON |
---|---|---|
Nome | Não criptografado | String |
SSN | Determinístico(a) | Int |
Tipo sanguíneo | Aleatório | String |
Registros médicos | Aleatório | Array |
Seguro: número da apólice | Determinístico(a) | Int (incorporado dentro do objeto de seguro) |
Seguro: fornecedor | Não criptografado | string (incorporado dentro do objeto de seguro) |
Vamos adicionar uma função ao nosso métodoCSFLEhelper no arquivo helper.js para que nosso aplicativo saiba quais campos precisam ser criptografados e descriptografados .
1 if (dataKey === null) { 2 throw new Error( 3 "dataKey is a required argument. Ensure you've defined it in clients.js" 4 ) 5 } 6 return { 7 "medicalRecords.patients": { 8 bsonType: "object", 9 // specify the encryptMetadata key at the root level of the JSON Schema. 10 // As a result, all encrypted fields defined in the properties field of the 11 // schema will inherit this encryption key unless specifically overwritten. 12 encryptMetadata: { 13 keyId: [new Binary(Buffer.from(dataKey, "base64"), 4)] 14 }, 15 properties: { 16 insurance: { 17 bsonType: "object", 18 properties: { 19 // The insurance.policyNumber field is embedded inside the insurance 20 // field and represents the patient's policy number. 21 // This policy number is a distinct and sensitive field. 22 policyNumber: { 23 encrypt: { 24 bsonType: "int", 25 algorithm: "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic" 26 } 27 } 28 } 29 }, 30 // The medicalRecords field is an array that contains a set of medical record documents. 31 // Each medical record document represents a separate visit and specifies information 32 // about the patient at that that time, such as their blood pressure, weight, and heart rate. 33 // This field is sensitive and should be encrypted. 34 medicalRecords: { 35 encrypt: { 36 bsonType: "array", 37 algorithm: "AEAD_AES_256_CBC_HMAC_SHA_512-Random" 38 } 39 }, 40 // The bloodType field represents the patient's blood type. 41 // This field is sensitive and should be encrypted. 42 bloodType: { 43 encrypt: { 44 bsonType: "string", 45 algorithm: "AEAD_AES_256_CBC_HMAC_SHA_512-Random" 46 } 47 }, 48 // The ssn field represents the patient's 49 // social security number. This field is 50 // sensitive and should be encrypted. 51 ssn: { 52 encrypt: { 53 bsonType: "int", 54 algorithm: "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic" 55 } 56 } 57 } 58 }
Tudo bem, agora temos o JSON schema e as chaves de criptografia necessárias para criar um cliente MongoDB habilitado para CSFLE. Vamos recapitular como nosso cliente funcionará. Nosso cliente MongoDB habilitado para CSFLE consultará nossos dados criptografados e o processomongocryptd será iniciado automaticamente por padrão. O mongocryptd lida com as seguintes responsabilidades:
- Valida as instruções de criptografia definidas no JSON schema e sinaliza os campos referenciados para criptografia em operações de leitura e gravação.
- Impede que operações não suportadas sejam executadas em campos criptografados.
Para criar o cliente habilitado para CSFLE, precisamos instanciar um objeto cliente MongoDB padrão com as configurações adicionais de criptografia automática com o seguinte trecho de código:
1 async getCsfleEnabledClient(schemaMap = null) { 2 if (schemaMap === null) { 3 throw new Error( 4 "schemaMap is a required argument. Build it using the CsfleHelper.createJsonSchemaMap method" 5 ) 6 } 7 const client = new MongoClient(this.connectionString, { 8 useNewUrlParser: true, 9 useUnifiedTopology: true, 10 monitorCommands: true, 11 autoEncryption: { 12 // The key vault collection contains the data key that the client uses to encrypt and decrypt fields. 13 keyVaultNamespace: this.keyVaultNamespace, 14 // The client expects a key management system to store and provide the application's master encryption key. 15 // For now, we will use a local master key, so they use the local KMS provider. 16 kmsProviders: this.kmsProviders, 17 // The JSON Schema that we have defined doesn't explicitly specify the collection to which it applies. 18 // To assign the schema, they map it to the medicalRecords.patients collection namespace 19 schemaMap 20 } 21 }) 22 return await client.connect() 23 }
Se a conexão for bem-sucedida, o cliente é retornado.
Agora temos um cliente habilitado para CSFLE e podemos testar se o cliente pode executar consultas que atendam aos nossos requisitos de segurança.
O diagrama a seguir mostra as etapas seguidas pelo aplicativo cliente e pelo driver para executar uma gravação de dados criptografados em nível de campo:
Precisamos escrever uma função em nosso clients.js para criar um novo registro de paciente com o seguinte trecho de código:
Observação: os clientes que não têm o CSFLE configurado inserirão dados não criptografados. Recomendamos usar a validação de esquema do lado do servidor para impor gravações criptografadas para campos que devem ser criptografados.
1 const { readMasterKey, CsfleHelper } = require("./helpers"); 2 const { connectionString, dataKey } = require("./config"); 3 4 const localMasterKey = readMasterKey() 5 6 const csfleHelper = new CsfleHelper({ 7 // The client expects a key management system to store and provide the application's master encryption key. For now, we will use a local master key, so they use the local KMS provider. 8 kmsProviders: { 9 local: { 10 key: localMasterKey 11 } 12 }, 13 connectionString, 14 }) 15 16 async function main() { 17 let regularClient = await csfleHelper.getRegularClient() 18 let schemeMap = csfleHelper.createJsonSchemaMap(dataKey) 19 let csfleClient = await csfleHelper.getCsfleEnabledClient(schemeMap) 20 21 let exampleDocument = { 22 name: "Jon Doe", 23 ssn: 241014209, 24 bloodType: "AB+", 25 medicalRecords: [ 26 { 27 weight: 180, 28 bloodPressure: "120/80" 29 } 30 ], 31 insurance: { 32 provider: "MaestCare", 33 policyNumber: 123142 34 } 35 } 36 37 const regularClientPatientsColl = regularClient 38 .db("medicalRecords") 39 .collection("patients") 40 const csfleClientPatientsColl = csfleClient 41 .db("medicalRecords") 42 .collection("patients") 43 44 // Performs the insert operation with the csfle-enabled client 45 // We're using an update with an upsert so that subsequent runs of this script 46 // don't insert new documents 47 await csfleClientPatientsColl.updateOne( 48 { ssn: exampleDocument["ssn"] }, 49 { $set: exampleDocument }, 50 { upsert: true } 51 ) 52 53 // Performs a read using the encrypted client, querying on an encrypted field 54 const csfleFindResult = await csfleClientPatientsColl.findOne({ 55 ssn: exampleDocument["ssn"] 56 }) 57 console.log( 58 "Document retreived with csfle enabled client:\n", 59 csfleFindResult 60 ) 61 62 // Performs a read using the regular client. We must query on a field that is 63 // not encrypted. 64 // Try - query on the ssn field. What is returned? 65 const regularFindResult = await regularClientPatientsColl.findOne({ 66 name: "Jon Doe" 67 }) 68 console.log("Document retreived with regular client:\n", regularFindResult) 69 70 await regularClient.close() 71 await csfleClient.close() 72 } 73 74 main().catch(console.dir)
O diagrama a seguir mostra as etapas realizadas pelo aplicativo cliente e pelo driver para consultar e descriptografar dados criptografados em nível de campo:
Podemos executar queries em documentos com campos criptografados usando métodos de driver padrão do MongoDB. Quando um médico realiza uma consulta no Medical Care Management System para procurar um paciente pelo seu SSN, o driver descriptografa os dados do paciente antes de retorná-los:
1 { 2 "_id": "5d6ecdce70401f03b27448fc", 3 "name": "Jon Doe", 4 "ssn": 241014209, 5 "bloodType": "AB+", 6 "medicalRecords": [ 7 { 8 "weight": 180, 9 "bloodPressure": "120/80" 10 } 11 ], 12 "insurance": { 13 "provider": "MaestCare", 14 "policyNumber": 123142 15 } 16 }
Se você tentar consultar seus dados com um MongoDB que não esteja configurado com a chave correta, é isso que você verá:
E você deve ver seus dados gravados em seu banco de dados MongoDB Atlas:
Se você tiver problemas ao executar seu código localmente, desenvolvemos uma imagem do Docker que você pode usar para ajudá-lo a configurar rapidamente ou solucionar problemas de configuração local. Você pode baixar o código aqui. Certifique-se de ter o docker configurado localmente antes de executar o código. Você pode baixar o Docker aqui.
- Alterar diretórios para o diretório Docker.
1 cd docker - Construa a imagem do Docker com um nome de tag. Dentro deste diretório, execute:
1 docker build . -t mdb-csfle-example Isso criará uma imagem Docker com o nome de tag mdb-csfle-example. - Execute o seguinte comando para executar a imagem do Docker:
1 docker run -tih csfle mdb-csfle-example O comando acima executará uma imagem Docker com tag mdb-csfle-example e fornecerá csfle como seu nome de host. - Quando estiver dentro do Docker container, você pode seguir as etapas abaixo para executar o exemplo de código NodeJS.
1 $ export MONGODB_URL="mongodb+srv://USER:PWD@EXAMPLE.mongodb.net/dbname?retryWrites=true&w=majority" 2 3 $ node ./example.js Observação: se você estiver se conectando ao MongoDB Atlas, certifique-se de Configure Allowlist Entries.
Queríamos desenvolver um sistema que armazenasse com segurança registros médicos confidenciais para os pacientes. Também queríamos fortes garantias de acesso e segurança a dados que não dependessem de usuários individuais. Depois de pesquisar as opções disponíveis, determinamos que a criptografia em nível de campo do lado do cliente do MongoDB atende aos requisitos e decidimos implementá-la em seu aplicativo. Para implementar o CSFLE, fizemos o seguinte:
1. Criou uma chave de encriptação mestra gerenciada localmente
Uma chave mestra gerenciada localmente nos permitiu desenvolver rapidamente o aplicativo cliente sem dependências externas e evitar o vazamento acidental de credenciais de produção confidenciais.
2. Gerou uma chave de dados criptografada com a chave mestra
O CSFLE usa criptografia de envelope, então geramos uma chave de dados que criptografa e descriptografa cada campo e, em seguida, criptografamos a chave de dados usando uma chave mestra. Isso nos permite armazenar a chave de dados criptografada no MongoDB para que ela seja compartilhada com todos os clientes e, ao mesmo tempo, impedir o acesso de clientes que não têm acesso à chave mestre.
3. Criou um JSON Schema
CSFLE pode criptografar e descriptografar campos automaticamente com base em um JSON schema fornecido que especifica quais campos criptografar e como criptografá-los.
4. Queries testadas e validadas com o cliente CSFLE
Testamos sua implementação de CSFLE inserindo e consultando documentos com campos criptografados. Em seguida, validamos que os clientes sem o CSFLE ativado não podiam ler os dados criptografados.
Neste guia, armazenamos a chave mestra em seu sistema de arquivos local. Como as chaves de criptografia de dados podem ser lidas por qualquer pessoa que tenha acesso direto à chave mestra, recomendamos enfaticamente que você use um local de armazenamento mais seguro, como um sistema de gerenciamento de chaves (KMS).
Para obter mais informações sobre a criptografia no nível do campo do lado do cliente no MongoDB, confira os Docs no manual do servidor:
- Para obter informações adicionais sobre a API CSFLE do MongoDB, consulte a documentação oficial do driver Node.js
- Questões? Comentários? Queremos muito nos conectar com você. Participe da conversa nos fóruns da MongoDB Community.