与 MongoDB 客户端字段级加密集成

¥Integrating with MongoDB Client Side Field Level Encryption

客户端字段级加密,简称 CSFLE,是一种在 MongoDB 中以加密格式存储数据的工具。例如,CSFLE 意味着 MongoDB 不会将 name 属性存储为纯文本字符串,而是将 name 存储为加密缓冲区。对于无权解密数据的客户端来说,生成的文档将类似于以下内容。

¥Client Side Field Level Encryption, or CSFLE for short, is a tool for storing your data in an encrypted format in MongoDB. For example, instead of storing the name property as a plain-text string, CSFLE means MongoDB will store your document with name as an encrypted buffer. The resulting document will look similar to the following to a client that doesn't have access to decrypt the data.

{
  "_id" : ObjectId("647a3207661e3a3a1bc3e614"),
  "name" : BinData(6,"ASrIv7XfokKwiCUJEjckOdgCG+u6IqavcOWX8hINz29MLvcKDZ4nnjCnPFZG+0ftVxMdWgzu6Vdh7ys1uIK1WiaPN0SqpmmtL2rPoqT9gfhADpGDmI60+vm0bJepXNY1Gv0="),
  "__v" : 0
}

你可以在 MongoDB CSFLE 文档这篇关于 Node.js 中的 CSFLE 的博文 上阅读有关 CSFLE 的更多信息。

¥You can read more about CSFLE on the MongoDB CSFLE documentation and this blog post about CSFLE in Node.js.

Mongoose 中的自动 FLE

¥Automatic FLE in Mongoose

Mongoose 支持加密模式的声明 - 当连接到模型时,这些模式会在底层利用 MongoDB 的客户端字段级加密或可查询加密。Mongoose 在实例化 MongoClient 时自动生成 encryptedFieldsMapschemaMap,并在写入时加密字段,在读取时解密字段。

¥Mongoose supports the declaration of encrypted schemas - schemas that, when connected to a model, utilize MongoDB's Client Side Field Level Encryption or Queryable Encryption under the hood. Mongoose automatically generates either an encryptedFieldsMap or a schemaMap when instantiating a MongoClient and encrypts fields on write and decrypts fields on reads.

加密类型

¥Encryption types

MongoDB 有两种不同的自动加密实现:客户端字段级加密 (CSFLE) 和可查询加密 (QE)。参见 选择使用中的加密方法

¥MongoDB has two different automatic encryption implementations: client side field level encryption (CSFLE) and queryable encryption (QE).\ See choosing an in-use encryption approach.

声明加密模式

¥Declaring Encrypted Schemas

以下模式声明了两个属性:namessnssn 使用可查询加密进行加密,并配置为用于相等性查询:

¥The following schema declares two properties, name and ssn. ssn is encrypted using queryable encryption, and is configured for equality queries:

const encryptedUserSchema = new Schema({ 
  name: String,
  ssn: { 
    type: String, 
    // 1
    encrypt: { 
      keyId: '<uuid string of key id>',
      queries: 'equality'
    }
  }
  // 2
}, { encryptionType: 'queryableEncryption' });

要将字段声明为加密,你必须:

¥To declare a field as encrypted, you must:

  1. 在模式定义中使用加密元数据注释字段

    ¥Annotate the field with encryption metadata in the schema definition

  2. 为模式选择一种加密类型,并根据加密类型配置模式。

    ¥Choose an encryption type for the schema and configure the schema for the encryption type

并非所有模式类型都支持 CSFLE 和 QE。有关受支持的 BSON 类型的概述,请参阅 MongoDB 文档。

¥Not all schematypes are supported for CSFLE and QE. For an overview of supported BSON types, refer to MongoDB's documentation.

注册模型

¥Registering Models

加密模式可以注册到全局 Mongoose 对象或特定连接上,只要模型在连接建立之前注册即可:

¥Encrypted schemas can be registered on the global mongoose object or on a specific connection, so long as models are registered before the connection is established:

// specific connection
const GlobalUserModel = mongoose.model('User', encryptedUserSchema);

// specific connection
const connection = mongoose.createConnection();
const UserModel = connection.model('User', encryptedUserSchema);

连接和配置加密选项

¥Connecting and configuring encryption options

Mongoose 中的字段级加密通过生成 MongoDB 驱动程序对连接上每个加密模型所期望的加密模式来工作。当模型的连接建立时,这将自动发生。

¥Field level encryption in Mongoose works by generating the encryption schema that the MongoDB driver expects for each encrypted model on the connection. This happens automatically when the model's connection is established.

可查询加密和 CSFLE 需要与 MongoDB 加密使用文档 中概述的所有配置相同,但 schemaMap 或 encryptedFieldsMap 选项除外。

¥Queryable encryption and CSFLE require all the same configuration as outlined in the MongoDB encryption in-use documentation, except for the schemaMap or encryptedFieldsMap options.

const keyVaultNamespace = 'client.encryption';
const kmsProviders = { local: { key } };
await connection.openUri(`mongodb://localhost:27017`, {
  // Configure auto encryption
  autoEncryption: {
    keyVaultNamespace: 'datakeys.datakeys',
    kmsProviders
  }
});

连接建立后,Mongoose 的操作将照常进行。写入操作在发送到服务器之前由 MongoDB 驱动程序自动加密,读取操作在从服务器获取文档后由驱动程序解密。

¥Once the connection is established, Mongoose's operations will work as usual. Writes are encrypted automatically by the MongoDB driver prior to sending them to the server and reads are decrypted by the driver after fetching documents from the server.

鉴别器

¥Discriminators

加密模型也支持鉴别器:

¥Discriminators are supported for encrypted models as well:

const connection = createConnection();

const schema = new Schema({
  name: {
    type: String, encrypt: { keyId }
  }
}, {
  encryptionType: 'queryableEncryption'
});

const Model = connection.model('BaseUserModel', schema);
const ModelWithAge = model.discriminator('ModelWithAge', new Schema({
  age: {
    type: Int32, encrypt: { keyId: keyId2 }
  }
}, {
  encryptionType: 'queryableEncryption'
}));

const ModelWithBirthday = model.discriminator('ModelWithBirthday', new Schema({
  dob: {
    type: Int32, encrypt: { keyId: keyId3 }
  }
}, {
  encryptionType: 'queryableEncryption'
}));

在生成加密模式时,Mongoose 会将同一命名空间中声明的所有鉴别器合并在一起。因此,不支持使用不同类型的相同密钥声明鉴别器。此外,同一命名空间的所有鉴别器必须共享相同的加密类型。 - 无法在同一模型上同时为 CSFLE 和可查询加密配置鉴别器。

¥When generating encryption schemas, Mongoose merges all discriminators together for all of the discriminators declared on the same namespace. As a result, discriminators that declare the same key with different types are not supported. Furthermore, all discriminators for the same namespace must share the same encryption type - it is not possible to configure discriminators on the same model for both CSFLE and Queryable Encryption.

管理数据密钥

¥Managing Data Keys

Mongoose 提供了一个便捷的 API 来获取配置为管理密钥库中数据密钥的 ClientEncryption 对象。可以使用 Model.clientEncryption() 辅助函数获取客户端加密:

¥Mongoose provides a convenient API to obtain a ClientEncryption object configured to manage data keys in the key vault. A client encryption can be obtained with the Model.clientEncryption() helper:

const connection = createConnection();

const schema = new Schema({
  name: {
    type: String, encrypt: { keyId }
  }
}, {
  encryptionType: 'queryableEncryption'
});

const Model = connection.model('BaseUserModel', schema);
await connection.openUri(`mongodb://localhost:27017`, {
  autoEncryption: {
    keyVaultNamespace: 'datakeys.datakeys',
    kmsProviders: { local: '....' }
  }
});

const clientEncryption = Model.clientEncryption();

Mongoose 中的手动 FLE

¥Manual FLE in Mongoose

首先,你需要安装 mongodb-客户端加密 npm 包。这是 MongoDB 用于设置加密密钥的官方包。

¥First, you need to install the mongodb-client-encryption npm package. This is MongoDB's official package for setting up encryption keys.

npm install mongodb-client-encryption

你还需要确保已安装 mongocryptd。mongocryptd 是一个独立于 MongoDB 服务器的进程,你需要运行它才能使用字段级加密。你可以自己运行 mongocryptd,或者确保它位于系统 PATH 上,并且 MongoDB Node.js 驱动程序将为你运行它。你可以在这里阅读有关 mongocryptd 的更多信息

¥You also need to make sure you've installed mongocryptd. mongocryptd is a separate process from the MongoDB server that you need to run to work with field level encryption. You can either run mongocryptd yourself, or make sure it is on the system PATH and the MongoDB Node.js driver will run it for you. You can read more about mongocryptd here.

设置并运行 mongocryptd 后,首先需要创建一个新的加密密钥,如下所示。请记住,以下示例是一个帮助你入门的简单示例。以下示例中的加密密钥不安全;MongoDB 建议使用 KMS

¥Once you've set up and run mongocryptd, first you need to create a new encryption key as follows. Keep in mind that the following example is a simple example to help you get started. The encryption key in the following example is insecure; MongoDB recommends using a KMS.

const { ClientEncryption } = require('mongodb');
const mongoose = require('mongoose');

run().catch(err => console.log(err));

async function run() {
  /* Step 1: Connect to MongoDB and insert a key */

  // Create a very basic key. You're responsible for making
  // your key secure, don't use this in prod :)
  const arr = [];
  for (let i = 0; i < 96; ++i) {
    arr.push(i);
  }
  const key = Buffer.from(arr);

  const keyVaultNamespace = 'client.encryption';
  const kmsProviders = { local: { key } };

  const uri = 'mongodb://127.0.0.1:27017/mongoose_test';
  const conn = await mongoose.createConnection(uri, {
    autoEncryption: {
      keyVaultNamespace,
      kmsProviders
    }
  }).asPromise();
  const encryption = new ClientEncryption(conn.getClient(), {
    keyVaultNamespace,
    kmsProviders,
  });

  const _key = await encryption.createDataKey('local', {
    keyAltNames: ['exampleKeyName'],
  });
}

拥有加密密钥后,你可以使用 schemaMap 创建单独的 Mongoose 连接,该连接定义使用 JSON 结构语法加密哪些字段,如下所示。

¥Once you have an encryption key, you can create a separate Mongoose connection with a schemaMap that defines which fields are encrypted using JSON schema syntax as follows.

/* Step 2: connect using schema map and new key */
await mongoose.connect('mongodb://127.0.0.1:27017/mongoose_test', {
  // Configure auto encryption
  autoEncryption: {
    keyVaultNamespace,
    kmsProviders,
    schemaMap: {
      'mongoose_test.tests': {
        bsonType: 'object',
        encryptMetadata: {
          keyId: [_key]
        },
        properties: {
          name: {
            encrypt: {
              bsonType: 'string',
              algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'
            }
          }
        }
      }
    }
  }
});

通过上述连接,如果你创建一个名为 '测试' 并使用 'tests' 集合的模型,则任何文档的 name 属性都将被加密。

¥With the above connection, if you create a model named 'Test' that uses the 'tests' collection, any documents will have their name property encrypted.

// 'super secret' will be stored as 'BinData' in the database,
// if you query using the `mongo` shell.
const Model = mongoose.model('Test', mongoose.Schema({ name: String }));
await Model.create({ name: 'super secret' });