Node.js 结合 MongoDB 实现字段级自动加密
某些场景下,对于数据隐私会有较高的要求,例如,用户系统的个人信息(身份证、手机号)、医患系统的患者信息等,怎么用技术手段安全的保护这些敏感数据是我们开发人员需要考虑的问题。
本篇文章,将介绍 MongoDB 的客户端字段级加密功能,英文全称为 Client-Side Field Level
Encryption,在有些地方会看到简称为 CSFLE,代表的是一个意思,下文有些地方也会这样称呼。
该功能允许开发人员将数据保存到 MongoDB
服务器之前选择性的指定数据字段进行加密,这些加密/解密操作都是事先在客户端完成,与服务器通信时完全是加密的,最终只有配置了 CSFLE
客户端才能读取和写入敏感数据字段。
文末列举了几个使用中的常见错误原因,如有遇到类似错误可以做为参考。
环境要求
MongoDB Server 选择:MongoDB 客户端字段级加密分为自动加密、手动加密两种类型,自动加密社区版是不支持的,需要 MongoDB
Server 4.2 企业版 或 MongoDB Atlas,学习使用推荐 MongoDB Atlas,它是在云服务器中托管的 MongoDB
服务器,不需要安装,且提供了免费的入门套餐是够我们学习使用了。
驱动兼容性:使用支持 CSFLE 功能的 Node.js MongoDB 驱动程序,3.4 以上版本是支持的,快速入门。
libmongocrypt:客户端字段级加密依赖 libmongocrypt,它是 MongoDB 驱动程序实现客户端加密/解密的核心组件,对应的
Node.js NPM 包为 mongodb-client-encryption,需要注意这个包依赖于 libbson 和 libmongocrypt C
库,需要 C 工具链,但是做为 Node.js Addons 插件,其已经利用 prebuild 在 CI 期间做了模块的预先编译,直接 npm i
mongodb-client-encryption 安装即可,如果网络环境问题链接不上 github.com
可能就很麻烦了需要手动构建、编译,因为对模块的预先编译是放在 Github 上的。
mongocryptd:客户端加密必须要 mongocryptd 进程启动才能正常工作,刚开始一直遇到一个问题:MongoError: BSON
field insert.jsonSchema is an unknown field. This command may be meant for a
mongocryptd process. 貌似就是因为 mongocryptd 进程没有启动导致的。在 MongoDB Server 企业版中包含
mongocryptd 这个组件的,解决办法也很简单就是本机安装下企业版,尽管我们这里使用的是 MongoDB Atlas 也要安装的,安装方法参考
docs.mongodb.com/manual/tutorial/install-mongodb-enterprise-on-os-x。
项目准备
做一些初始化工作,安装依赖、配置文件、创建一个常规的 MongoDB client。
项目初始化mkdir nodejs-mongodb-client-encryption
cd nodejs-mongodb-client-encryption
npm init
npm i mongodb mongodb-client-encryption -S配置文件
创建一个 index.js 文件,核心代码逻辑都在该文件编写,
// index.js
const base64 = require(uuid-base64);
const { MongoClient, Binary } = require(mongodb);
const { ClientEncryption } = require(mongodb-client-encryption);
const fs = require(fs);
// 配置
const config = {
connectionString: ${替换为自己的 MongoDB 链接字符串},
keyVaultDb: encryption, // encryption 表示密钥保管数据库
keyVaultCollection: __keyVault, // __keyVault 表示集合
keyVaultNamespace: `encryption.__keyVault`, // 密钥库命名空间
keyAltNames: test-data-key,
masterKeyPath: master-key.txt
}
const LOCAL_MASTER_KEY = fs.readFileSync(config.masterKeyPath); // 读取本地主密钥
const kmsProviders = { // 指定 KMS 提供程序设置
local: {
key: LOCAL_MASTER_KEY,
},
};创建常规 /**
* 获取常规 Mongo 客户端
* @param {String} connectionString
* @returns
*/
function getRegularClient(connectionString) {
const client = new MongoClient(connectionString, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
return client.connect();
}
数据加密密钥
MongoDB
驱动程序自动加密/解密时需要访问事先创建的数据加密密钥,而这个密钥经过程序的处理会存储在密钥保管数据库的集合中,以下是创建一个数据加密密钥的交互图。
创建主密钥
创建 MongoDB 数据加密密钥还需要另外一个称为 “主密钥” 的密钥进行加密,下图展示了创建主密钥的流程:
主密钥的存储,生产环境 MongoDB 官方的推荐是使用密钥管理服务(KMS):亚马逊网络服务 KMS、Azure
密钥保管库、谷歌云平台密钥管理,更多内容可阅读 客户端字段级加密:使用 KMS 存储主密钥。
学习为目的,简单方便些可使用本地密钥提供程序存储主密钥,这种方式不安全,不适合生产。
创建一个脚本文件 create-master-key.js,生成一个 96 字节的密钥文件,并写入到本地文件系统的 master-key.txt
文件中。
// create-master-key.js
const fs = require(fs);
const crypto = require(crypto);
try {
fs.writeFileSync(master-key.txt, crypto.randomBytes(96));
} catch (err) {
console.error(err);
}指定 KMS 程序配置
客户端使用如下配置发现主密钥,local 表示的是使用本地主密钥。
const LOCAL_MASTER_KEY = fs.readFileSync(config.masterKeyPath); // 读取本地主密钥
const kmsProviders = { // 指定 KMS 提供程序设置
local: {
key: LOCAL_MASTER_KEY,
},
};
获取或创建数据加密密钥写一个函数 getOrCreateDataKey 分别传入创建的常规 client、上面指定的 KMS
程序配置,该方法目的是获取一个数据密钥,如果不存在则创建,实现为以下几个步骤:
在密钥保管库集合的 keyAltNames 字段上先设置唯一索引,这里创建的是一个部分索引,符合条件的才会创建。检查是否已创建数据加密密钥,若创建则立即返回。若未创建数据加密密钥,向指定的密钥保管库集合创建一条新的数据密钥。/**
* 获取或创建数据加密密钥
* 如果已存在 dataKey 则返回,否则创建一条 dataKey
*/
async function getOrCreateDataKey(regularClient, kmsProviders) {
// 在密钥保管库集合的 keyAltNames 字段上先设置索引
await regularClient
.db(config.keyVaultDb)
.collection(config.keyVaultCollection)
.createIndex("keyAltNames", {
unique: true,
partialFilterExpression: {
keyAltNames: {
$exists: true
}
}
});
// 检查是否已创建数据加密密钥
const dataKeyInfo = await regularClient
.db(config.keyVaultDb)
.collection(config.keyVaultCollection)
.findOne({
keyAltNames: {
$in: [config.keyAltNames]
}
});
if (dataKeyInfo) { // 存在立即返回
return dataKeyInfo[_id].toString("base64");
}
// 创建一条新的数据密钥
const encryption = new ClientEncryption(regularClient, {
keyVaultNamespace: config.keyVaultNamespace,
kmsProviders,
});
const dataKey = await encryption.createDataKey(local, {
keyAltNames: [config.keyAltNames]
});
return dataKey.toString(base64);
}验证数据加密密钥是否成功创建
调用编写好的方法,验证下数据加密密钥是否创建成功。
(async () =