您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Model Center   Code  
会员   
   
 
     
   
 
 订阅
Node.js 结合 MongoDB 实现字段级自动加密
 
作者:五月君
   次浏览      
2023-3-16
 
编辑推荐:
本文主要介绍 MongoDB 的客户端字段级加密功能,英文全称为 Client-Side Field Level Encryption,在有些地方会看到简称为 CSFLE。 希望对您的学习有所帮助。
本文来自于微信公众号编程界,由火龙果软件Linda编辑、推荐。

某些场景下,对于数据隐私会有较高的要求,例如,用户系统的个人信息(身份证、手机号)、医患系统的患者信息等,怎么用技术手段安全的保护这些敏感数据是我们开发人员需要考虑的问题。

本篇文章,将介绍 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.__key
Vault`, // 密钥库命名空间
  keyAltNames: 'test-data-key',
  masterKeyPath: 'master-key.txt'
}
const LOCAL_MASTER_KEY = fs.rea
dFileSync(config.masterKeyPath); // 
读取本地主密钥
const kmsProviders = { // 指定 KMS
 提供程序设置
  local: {
    key: LOCAL_MASTER_KEY,
  },
};

 

创建常规 Client

/**
 * 获取常规 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.masterKey
Path); // 读取本地主密钥
const kmsProviders = { // 
指定 KMS 提供程序设置

  local: {
    key: LOCAL_MASTER_KEY,
  },
};

 

获取或创建数据加密密钥

写一个函数 getOrCreateDataKey 分别传入创建的常规 client、上面指定的 KMS 程序配置,该方法目的是获取一个数据密钥,如果不存在则创建,实现为以下几个步骤:

在密钥保管库集合的 keyAltNames 字段上先设置唯一索引,这里创建的是一个部分索引,符合条件的才会创建。

检查是否已创建数据加密密钥,若创建则立即返回。

若未创建数据加密密钥,向指定的密钥保管库集合创建一条新的数据密钥。

/**
 * 获取或创建数据加密密钥
 * 如果已存在 dataKey 则返回,
否则创建一条 dataKey
 */

async function getOrCreateDataKey
(regularClient, kmsProviders
{
  // 在密钥保管库集合的 key
AltNames 字段上先设置索引

  await regularClient
    .db(config.keyVaultDb)
    .collection(config.keyVaultCollection)
    .createIndex("keyAltNames", {
      uniquetrue,
      partialFilterExpression: {
        keyAltNames: {
          $existstrue
        }
      }
    });

  // 检查是否已创建数据加密密钥
  const dataKeyInfo = await regularClient
    .db(config.keyVaultDb)
    .collection(config.keyVaultCollection)
    .findOne({
      keyAltNames: {
        $in: [config.keyAltNames]
      }
    });
  if (dataKeyInfo) { // 存在立即返回
    return dataKeyInfo['_id'].to
String("base64");
  }

  // 创建一条新的数据密钥
  const encryption = new 
ClientEncryption(regularClient, {
    keyVaultNamespace: config.
keyVaultNamespace,
    kmsProviders,
  });
  const dataKey = await 
encryption.createDataKey('local', {
    keyAltNames: [config.keyAltNames]
  });
  return dataKey.toString('base64');
}

 

验证数据加密密钥是否成功创建

调用编写好的方法,验证下数据加密密钥是否创建成功。

(async () => {
  let regularClient;
  try {
    // 创建常规 MongoDB 客户端
    regularClient = await get
RegularClient(config.connectionString);
    // 获取数据加密密钥
    const base64DataKeyId = await get
OrCreateDataKey(regularClient, kmsProviders);
  } catch (err) {
    console.error(err);
    regularClient.close();
  }
})();

 

我使用 Robo 3T 链接的 Atlas 集群,如果一切正常,你会看到在 encryption.__keyVault 集合中有如下一条密钥记录,_id 字段就是为我们需要的数据加密密钥,使用 Base64 格式编码。

JSON Schema 定义

Node.js 驱动程序使用 JSON Schema 定义集合需要加密的字段,文档类型定义使用 BSON 类型。

encryptMetadata.keyId:在根级别配置数据加密密钥,properties 中的每个字段默认都继承该密钥,除非特别指定,参考 docs.mongodb.com/manual/reference/security-client-side-automatic-json-schema/#encryptmetadata-schema-keyword。

algorithm:指定加密算法,AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic 为确定性加密算法,对读取操作提供了更好的支持,安全系数相对没有**随机加密 **AEAD_AES_256_CBC_HMAC_SHA_512-Random 高,随机加密算法每次执行加密都会输出不同的值。

/**
 * 使用 JSON Schema 定义集合需要加密的字段
 * @param {String} base64DataKeyId
 * @returns 
 */

function getSchemaMap(base64DataKeyId{
  // 使用 JSON Schema 指定加密字段
  const userJsonSchema = {
    bsonType'object',
    encryptMetadata: {
      keyId: [new Binary(Buffe
r.from(base64DataKeyId, 'base64'), 4)]
    },
    properties: {
      phone: {
        encrypt: {
          bsonType'string',
          algorithm'AEAD_AES_256_
CBC_HMAC_SHA_512-Deterministic'

        }
      },
      password: {
        encrypt: {
          bsonType'string',
          algorithm'AEAD_AES_2
56_CBC_HMAC_SHA_512-Random'

        }
      },
      emergencyContact: {
        bsonType'object',
        properties: {
          phone: {
            encrypt: {
              bsonType'string',
              algorithm'AEAD_AES_256_
CBC_HMAC_SHA_512-Deterministic'

            }
          },
        }
      }
    }
  }

  // 将 JSON 模式映射到集合上
  const schemaMap = {
    'test.users': userJsonSchema
  };

  return schemaMap;
}

 

CSFLE 客户端验证读写操作

在有了数据加密密钥、JSON Schema 之后可以创建一个支持 CSFLE 的 Mongo client,该客户端和 MongDB 服务器交互,读取/写入带有加密字段的数据。

读写操作流程图

下图展示了客户端应用程序和驱动程序为写入字段级加密数据的一个步骤:

下图展示了客户端应用程序和驱动程序为读取加密后字段进行解密操作的一个过程:

创建 CSFLE 客户端

创建 CSFLE 的 mongo client 与常规 mongo client 相比较,需要多传入 autoEncryption 对象,以下参数含义分别为:

keyVaultNamespace:存放数据加密密钥的密钥保管库集合名称。

kmsProviders:指定本地主密钥。

schemaMap:需要加密字段的一些定义。

function getCSFLEClien
t
(schemaMap, kmsProviders
{
  const secureClient = new 
MongoClient(config.connectionString, {
    useNewUrlParsertrue,
    useUnifiedTopologytrue,
    monitorCommandstrue,
    autoEncryption: {
      bypassAutoEncryptiontrue,
      keyVaultNamespace: config.
keyVaultNamespace,
      kmsProviders,
      schemaMap
    }
  });

  return secureClient.connect();
}

(async () => {
  try {
    const regularClient = await 
getRegularClient(config.connectionString);
    const base64DataKeyId = await get
OrCreateDataKey(regularClient, kmsProviders);
    const schemaMap = getSchemaMap
(base64DataKeyId);
    const csfleClient = await get
CSFLEClient(schemaMap, kmsProviders);
    
  // 执行读写操作
  } catch (err) {
    console.error(err);
  }
})();

 

执行读写操作验证

在拥有 CSFLE 客户端后,执行一些读写操作,创建一条用户记录,下面的代码和我们常规的读写操作没什么区别,并且 phone 这个字段虽然是经过加密的,我们仍可使用该字段做为索引,更新/查找数据。

(async () => {
  try {
    // ...
    const db = csfleClient.db('test');
    const userColl = db.collection('users');
    const doc = {
      name'小张',
      phone'18800030009',
      password'123456',
      emergencyContact: {
        name'小李',
        phone'16600260023'
      }
    };
    const query = { phone: doc.phone };
    await userColl.updateOne(query,
 { $set: doc }, { upserttrue });
    const result = await user
Coll.findOne(query);
    console.log(result);
  } catch (err) {
    console.error(err);
  }
})();

 

当成功插入一条记录之后,在 Robo 3T 工具查询该集合,可以看到需要的字段都已经做了加密,尽管我是一个管理员能够查看数据,也无法查看这些隐私数据。

只能通过程序正确的创建了 CSFLE 的客户端才能读取出解密后的数据。

几个常见错误

文中示例测试时常见的几个错误,可以做为参考。

认证失败

遇到 Authentication failed 错误,基本上都是连接字符串的账号密码或权限错误,使用 MongoDB Atlas 的需要检查下数据库的访问权限配置

MongoServerError: bad auth 
: Authentication failed.
  ...
  ok: 0,
  code8000,
  codeName'AtlasError',
  [Symbol(errorLabels)]: Set(0) {}
}

 

创建加密客户端链接失败

下面的报错很简单就是服务器链接不上。需要注意的是文中创建加密客户端还会去链接本地安装的 MongoDB 企业版 Server,在本地启动 MongoDB 企业版 Server 时需要指定下端口 bin/mongod --dbpath data --logpath logs/mongo.log --port 27020。

MongoServerSelectionError: connect
 ECONNREFUSED 127.0.0.1:27020
    at Timeout._onTimeout (/Users/********
***/nodejs-mongodb-client-encryption/
node_modules/mongodb/lib/sdam/topology.js:318:38)
    at listOnTimeout (internal/timers.
js:554:17)
    at processTimers (internal/timers.
js:497:7) {
  reason: TopologyDescription {
    type'Unknown',
    serversMap(1) { 'localhost:27020'
 => [ServerDescription] },
    stalefalse,
    compatibletrue,
    heartbeatFrequencyMS10000,
    localThresholdMS15,
    logicalSessionTimeoutMinutesundefined
  },
  codeundefined,
  [Symbol(errorLabels)]: Set(0) {}
}

 

mongocryptd 进程注意事项

在刚开始的环境要求里有提到过 mongocryptd 进程,它会在这里检查 JSON Schema 中定义的加密指令,也就是 getCSFLEClient() 传入的 schemaMap 参数,如果 mongocryptd 进程没有启动,这里会一直报错。

以下是我最开始一直遇到的一个问题,解决办法很简单:

第一步,本机安装下企业版

第二步,创建加密的 MongoDB 客户端时,链接参数要设置 autoEncryption.bypassAutoEncryption=true 会自动生成 mongocryptd 进程。

writeError occurred: MongoError: BSON 
field 'insert.jsonSchema' is an unknown 
field. This command may be meant for a 
mongocryptd process.
    at MessageStream.messageHandler (/Users
/
quzhenfei/Documents/study/node_modules
/mongodb/lib/cmap/connection.js:268:20)
    at MessageStream.emit (events.js:314:20)
    at processIncomingData (/Users/quzhenfei/
Documents/study/node_modules/mongodb
/lib/cmap/message_stream.js:144:12)
    at MessageStream._write (/Users/quzhen
fei/Documents/study/node_modules/mongodb/
lib/cmap/message_stream.js:42:5)
    at writeOrBuffer (_stream_writable.js:352:12)
    at MessageStream.Writable.write 
(_stream_writable.js:303:10)
    at TLSSocket.ondata 
(_stream_readable.js:713:22)
    at TLSSocket.emit (events.js:314:20)
    at addChunk (_stream_readable.js:303:12)
    at readableAddChunk (_stream_readable.
js:279:9) {
  operationTime: Timestamp { _bsontype
'Timestamp'low_1high_1632613160 },
  ok0,
  code4662500,
  codeName'Location4662500',
  '$clusterTime': {
    clusterTime: Timestamp { _bsontype
'Timestamp'low_1high_1632613160 },
    signature: { hash: [Binary], keyId: [Long] }
  }
}

 

总结

MongoDB 提供的客户端字段级自动加密,对于有数据隐私需要加密保护的还是很方便的,在配置了 CSFLE 客户端后应用程序在读写操作时和常规的客户端读写操作是没有差别的,唯一的阻碍可能是仅企业版支持。

文中我们将主密钥存储放在了本地的文件系统中,这在本地测试环境是可以的,但是生产环境不要用这种方式,因为任何能够访问您本地文件系统主密钥的人都可以读取您的数据加密密钥,建议放在更安全的地方,例如密钥管理系统(KMS)。

 

   
次浏览       
相关文章

基于图卷积网络的图深度学习
自动驾驶中的3D目标检测
工业机器人控制系统架构介绍
项目实战:如何构建知识图谱
 
相关文档

5G人工智能物联网的典型应用
深度学习在自动驾驶中的应用
图神经网络在交叉学科领域的应用研究
无人机系统原理
相关课程

人工智能、机器学习&TensorFlow
机器人软件开发技术
人工智能,机器学习和深度学习
图像处理算法方法与实践

最新活动计划
数据建模方法与工具 12-16[北京]
基于模型系统仿真与验证 12-14 [讲座]
白盒测试技术与工具实践 12-24[线上]
LLM大模型应用与项目构建 12-26[特惠]
UML和EA进行系统分析设计 12-20[线上]
SysML建模专家 1-16[北京]
 
 
最新文章
AIGC技术与应用全解析
详解知识图谱的构建全流程
大模型升级与设计之道
自动驾驶和辅助驾驶系统
ROS机器人操作系统底层原理
最新课程
人工智能,机器学习和深度学习
人工智能与机器学习应用实战
人工智能-图像处理和识别
人工智能、机器学习& TensorFlow+Keras框架实践
人工智能+Python+大数据
成功案例
某综合性科研机构 人工智能与机器学习应用
某银行 人工智能+Python+大数据
北京 人工智能、机器学习& TensorFlow框架实践
某领先数字地图提供商 Python数据分析与机器学习
中国移动 人工智能、机器学习和深度学习