新聞中心
某些場景下,對于數(shù)據(jù)隱私會有較高的要求,例如,用戶系統(tǒng)的個人信息(身份證、手機(jī)號)、醫(yī)患系統(tǒng)的患者信息等,怎么用技術(shù)手段安全的保護(hù)這些敏感數(shù)據(jù)是我們開發(fā)人員需要考慮的問題。

創(chuàng)新互聯(lián)公司專注為客戶提供全方位的互聯(lián)網(wǎng)綜合服務(wù),包含不限于網(wǎng)站建設(shè)、成都網(wǎng)站制作、大冶網(wǎng)絡(luò)推廣、微信小程序開發(fā)、大冶網(wǎng)絡(luò)營銷、大冶企業(yè)策劃、大冶品牌公關(guān)、搜索引擎seo、人物專訪、企業(yè)宣傳片、企業(yè)代運(yùn)營等,從售前售中售后,我們都將竭誠為您服務(wù),您的肯定,是我們最大的嘉獎;創(chuàng)新互聯(lián)公司為所有大學(xué)生創(chuàng)業(yè)者提供大冶建站搭建服務(wù),24小時(shí)服務(wù)熱線:13518219792,官方網(wǎng)址:www.cdcxhl.com
本篇文章,將介紹 MongoDB 的客戶端字段級加密功能,英文全稱為 Client-Side Field Level Encryption,在有些地方會看到簡稱為 CSFLE,代表的是一個意思,下文有些地方也會這樣稱呼。
該功能允許開發(fā)人員將數(shù)據(jù)保存到 MongoDB 服務(wù)器之前選擇性的指定數(shù)據(jù)字段進(jìn)行加密,這些加密/解密操作都是事先在客戶端完成,與服務(wù)器通信時(shí)完全是加密的,最終只有配置了 CSFLE 客戶端才能讀取和寫入敏感數(shù)據(jù)字段。
文末列舉了幾個使用中的常見錯誤原因,如有遇到類似錯誤可以做為參考。
環(huán)境要求
MongoDB Server 選擇:MongoDB 客戶端字段級加密分為自動加密、手動加密兩種類型,自動加密社區(qū)版是不支持的,需要 MongoDB Server 4.2 企業(yè)版 或 MongoDB Atlas,學(xué)習(xí)使用推薦 MongoDB Atlas,它是在云服務(wù)器中托管的 MongoDB 服務(wù)器,不需要安裝,且提供了免費(fèi)的入門套餐是夠我們學(xué)習(xí)使用了。
驅(qū)動兼容性:使用支持 CSFLE 功能的 Node.js MongoDB 驅(qū)動程序,3.4+ 以上版本是支持的,快速入門。
libmongocrypt:客戶端字段級加密依賴 libmongocrypt,它是 MongoDB 驅(qū)動程序?qū)崿F(xiàn)客戶端加密/解密的核心組件,對應(yīng)的 Node.js NPM 包為 mongodb-client-encryption,需要注意這個包依賴于 libbson 和 libmongocrypt C 庫,需要 C++ 工具鏈,但是做為 Node.js Addons 插件,其已經(jīng)利用 prebuild 在 CI 期間做了模塊的預(yù)先編譯,直接 npm i mongodb-client-encryption 安裝即可,如果網(wǎng)絡(luò)環(huán)境問題鏈接不上 github.com 可能就很麻煩了需要手動構(gòu)建、編譯,因?yàn)閷δK的預(yù)先編譯是放在 Github 上的。
mongocryptd:客戶端加密必須要 mongocryptd 進(jìn)程啟動才能正常工作,剛開始一直遇到一個問題:MongoError: BSON field 'insert.jsonSchema' is an unknown field. This command may be meant for a mongocryptd process. 貌似就是因?yàn)?mongocryptd 進(jìn)程沒有啟動導(dǎo)致的。在 MongoDB Server 企業(yè)版中包含 mongocryptd 這個組件的,解決辦法也很簡單就是本機(jī)安裝下企業(yè)版,盡管我們這里使用的是 MongoDB Atlas 也要安裝的,安裝方法參考 docs.mongodb.com/manual/tutorial/install-mongodb-enterprise-on-os-x。
項(xiàng)目準(zhǔn)備
做一些初始化工作,安裝依賴、配置文件、創(chuàng)建一個常規(guī)的 MongoDB client。
項(xiàng)目初始化
mkdir nodejs-mongodb-client-encryption
cd nodejs-mongodb-client-encryption
npm init
npm i mongodb mongodb-client-encryption -S
配置文件
創(chuàng)建一個 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 表示密鑰保管數(shù)據(jù)庫
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 提供程序設(shè)置
local: {
key: LOCAL_MASTER_KEY,
},
};
創(chuàng)建常規(guī)
/**
* 獲取常規(guī) Mongo 客戶端
* @param {String} connectionString
* @returns
*/
function getRegularClient(connectionString) {
const client = new MongoClient(connectionString, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
return client.connect();
}
數(shù)據(jù)加密密鑰
MongoDB 驅(qū)動程序自動加密/解密時(shí)需要訪問事先創(chuàng)建的數(shù)據(jù)加密密鑰,而這個密鑰經(jīng)過程序的處理會存儲在密鑰保管數(shù)據(jù)庫的集合中,以下是創(chuàng)建一個數(shù)據(jù)加密密鑰的交互圖。
創(chuàng)建主密鑰
創(chuàng)建 MongoDB 數(shù)據(jù)加密密鑰還需要另外一個稱為 “主密鑰” 的密鑰進(jìn)行加密,下圖展示了創(chuàng)建主密鑰的流程:
主密鑰的存儲,生產(chǎn)環(huán)境 MongoDB 官方的推薦是使用密鑰管理服務(wù)(KMS):亞馬遜網(wǎng)絡(luò)服務(wù) KMS、Azure 密鑰保管庫、谷歌云平臺密鑰管理,更多內(nèi)容可閱讀 客戶端字段級加密:使用 KMS 存儲主密鑰。
學(xué)習(xí)為目的,簡單方便些可使用本地密鑰提供程序存儲主密鑰,這種方式不安全,不適合生產(chǎn)。
創(chuàng)建一個腳本文件 create-master-key.js,生成一個 96 字節(jié)的密鑰文件,并寫入到本地文件系統(tǒng)的 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 程序配置
客戶端使用如下配置發(fā)現(xiàn)主密鑰,local 表示的是使用本地主密鑰。
const LOCAL_MASTER_KEY = fs.readFileSync(config.masterKeyPath); // 讀取本地主密鑰
const kmsProviders = { // 指定 KMS 提供程序設(shè)置
local: {
key: LOCAL_MASTER_KEY,
},
};
獲取或創(chuàng)建數(shù)據(jù)加密密鑰寫一個函數(shù) getOrCreateDataKey 分別傳入創(chuàng)建的常規(guī) client、上面指定的 KMS 程序配置,該方法目的是獲取一個數(shù)據(jù)密鑰,如果不存在則創(chuàng)建,實(shí)現(xiàn)為以下幾個步驟:
- 在密鑰保管庫集合的 keyAltNames 字段上先設(shè)置唯一索引,這里創(chuàng)建的是一個部分索引,符合條件的才會創(chuàng)建。
- 檢查是否已創(chuàng)建數(shù)據(jù)加密密鑰,若創(chuàng)建則立即返回。
- 若未創(chuàng)建數(shù)據(jù)加密密鑰,向指定的密鑰保管庫集合創(chuàng)建一條新的數(shù)據(jù)密鑰。
/**
* 獲取或創(chuàng)建數(shù)據(jù)加密密鑰
* 如果已存在 dataKey 則返回,否則創(chuàng)建一條 dataKey
*/
async function getOrCreateDataKey(regularClient, kmsProviders) {
// 在密鑰保管庫集合的 keyAltNames 字段上先設(shè)置索引
await regularClient
.db(config.keyVaultDb)
.collection(config.keyVaultCollection)
.createIndex("keyAltNames", {
unique: true,
partialFilterExpression: {
keyAltNames: {
$exists: true
}
}
});
// 檢查是否已創(chuàng)建數(shù)據(jù)加密密鑰
const dataKeyInfo = await regularClient
.db(config.keyVaultDb)
.collection(config.keyVaultCollection)
.findOne({
keyAltNames: {
$in: [config.keyAltNames]
}
});
if (dataKeyInfo) { // 存在立即返回
return dataKeyInfo['_id'].toString("base64");
}
// 創(chuàng)建一條新的數(shù)據(jù)密鑰
const encryption = new ClientEncryption(regularClient, {
keyVaultNamespace: config.keyVaultNamespace,
kmsProviders,
});
const dataKey = await encryption.createDataKey('local', {
keyAltNames: [config.keyAltNames]
});
return dataKey.toString('base64');
}
驗(yàn)證數(shù)據(jù)加密密鑰是否成功創(chuàng)建
調(diào)用編寫好的方法,驗(yàn)證下數(shù)據(jù)加密密鑰是否創(chuàng)建成功。
(async () => {
let regularClient;
try {
// 創(chuàng)建常規(guī) MongoDB 客戶端
regularClient = await getRegularClient(config.connectionString);
// 獲取數(shù)據(jù)加密密鑰
const base64DataKeyId = await getOrCreateDataKey(regularClient, kmsProviders);
} catch (err) {
console.error(err);
regularClient.close();
}
})();我使用 Robo 3T 鏈接的 Atlas 集群,如果一切正常,你會看到在 encryption.__keyVault 集合中有如下一條密鑰記錄,_id 字段就是為我們需要的數(shù)據(jù)加密密鑰,使用 Base64 格式編碼。
JSON Schema 定義
Node.js 驅(qū)動程序使用 JSON Schema 定義集合需要加密的字段,文檔類型定義使用 BSON 類型。
- encryptMetadata.keyId:在根級別配置數(shù)據(jù)加密密鑰,properties 中的每個字段默認(rèn)都繼承該密鑰,除非特別指定,參考 docs.mongodb.com/manual/reference/security-client-side-automatic-json-schema/#encryptmetadata-schema-keyword。
- algorithm:指定加密算法,AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic 為確定性加密算法,對讀取操作提供了更好的支持,安全系數(shù)相對沒有**隨機(jī)加密 **AEAD_AES_256_CBC_HMAC_SHA_512-Random 高,隨機(jī)加密算法每次執(zhí)行加密都會輸出不同的值。
/**
* 使用 JSON Schema 定義集合需要加密的字段
* @param {String} base64DataKeyId
* @returns
*/
function getSchemaMap(base64DataKeyId) {
// 使用 JSON Schema 指定加密字段
const userJsonSchema = {
bsonType: 'object',
encryptMetadata: {
keyId: [new Binary(Buffer.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_256_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 客戶端驗(yàn)證讀寫操作
在有了數(shù)據(jù)加密密鑰、JSON Schema 之后可以創(chuàng)建一個支持 CSFLE 的 Mongo client,該客戶端和 MongDB 服務(wù)器交互,讀取/寫入帶有加密字段的數(shù)據(jù)。
讀寫操作流程圖
下圖展示了客戶端應(yīng)用程序和驅(qū)動程序?yàn)閷懭胱侄渭壖用軘?shù)據(jù)的一個步驟:
下圖展示了客戶端應(yīng)用程序和驅(qū)動程序?yàn)樽x取加密后字段進(jìn)行解密操作的一個過程:
創(chuàng)建 CSFLE 客戶端
創(chuàng)建 CSFLE 的 mongo client 與常規(guī) mongo client 相比較,需要多傳入 autoEncryption 對象,以下參數(shù)含義分別為:
- keyVaultNamespace:存放數(shù)據(jù)加密密鑰的密鑰保管庫集合名稱。
- kmsProviders:指定本地主密鑰。
- schemaMap:需要加密字段的一些定義。
function getCSFLEClient(schemaMap, kmsProviders) {
const secureClient = new MongoClient(config.connectionString, {
useNewUrlParser: true,
useUnifiedTopology: true,
monitorCommands: true,
autoEncryption: {
bypassAutoEncryption: true,
keyVaultNamespace: config.keyVaultNamespace,
kmsProviders,
schemaMap
}
});
return secureClient.connect();
}
(async () => {
try {
const regularClient = await getRegularClient(config.connectionString);
const base64DataKeyId = await getOrCreateDataKey(regularClient, kmsProviders);
const schemaMap = getSchemaMap(base64DataKeyId);
const csfleClient = await getCSFLEClient(schemaMap, kmsProviders);
// 執(zhí)行讀寫操作
} catch (err) {
console.error(err);
}
})();
執(zhí)行讀寫操作驗(yàn)證
在擁有 CSFLE 客戶端后,執(zhí)行一些讀寫操作,創(chuàng)建一條用戶記錄,下面的代碼和我們常規(guī)的讀寫操作沒什么區(qū)別,并且 phone 這個字段雖然是經(jīng)過加密的,我們?nèi)钥墒褂迷撟侄巫鰹樗饕?,更?查找數(shù)據(jù)。
(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 }, { upsert: true });
const result = await userColl.findOne(query);
console.log(result);
} catch (err) {
console.error(err);
}
})();當(dāng)成功插入一條記錄之后,在 Robo 3T 工具查詢該集合,可以看到需要的字段都已經(jīng)做了加密,盡管我是一個管理員能夠查看數(shù)據(jù),也無法查看這些隱私數(shù)據(jù)。
只能通過程序正確的創(chuàng)建了 CSFLE 的客戶端才能讀取出解密后的數(shù)據(jù)。
幾個常見錯誤
文中示例測試時(shí)常見的幾個錯誤,可以做為參考。
認(rèn)證失敗
遇到 Authentication failed 錯誤,基本上都是連接字符串的賬號密碼或權(quán)限錯誤,使用 MongoDB Atlas 的需要檢查下數(shù)據(jù)庫的訪問權(quán)限配置
MongoServerError: bad auth : Authentication failed.
...
ok: 0,
code: 8000,
codeName: 'AtlasError',
[Symbol(errorLabels)]: Set(0) {}
}
創(chuàng)建加密客戶端鏈接失敗
下面的報(bào)錯很簡單就是服務(wù)器鏈接不上。需要注意的是文中創(chuàng)建加密客戶端還會去鏈接本地安裝的 MongoDB 企業(yè)版 Server,在本地啟動 MongoDB 企業(yè)版 Server 時(shí)需要指定下端口 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',
servers: Map(1) { 'localhost:27020' => [ServerDescription] },
stale: false,
compatible: true,
heartbeatFrequencyMS: 10000,
localThresholdMS: 15,
logicalSessionTimeoutMinutes: undefined
},
code: undefined,
[Symbol(errorLabels)]: Set(0) {}
}
mongocryptd 進(jìn)程注意事項(xiàng)
在剛開始的環(huán)境要求里有提到過 mongocryptd 進(jìn)程,它會在這里檢查 JSON Schema 中定義的加密指令,也就是 getCSFLEClient() 傳入的 schemaMap 參數(shù),如果 mongocryptd 進(jìn)程沒有啟動,這里會一直報(bào)錯。
以下是我最開始一直遇到的一個問題,解決辦法很簡單:
- 第一步,本機(jī)安裝下企業(yè)版
- 第二步,創(chuàng)建加密的 MongoDB 客戶端時(shí),鏈接參數(shù)要設(shè)置autoEncryption.bypassAutoEncryption=true 會自動生成 mongocryptd 進(jìn)程。
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/quzhenfei/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_: 1, high_: 1632613160 },
ok: 0,
code: 4662500,
codeName: 'Location4662500',
'$clusterTime': {
clusterTime: Timestamp { _bsontype: 'Timestamp', low_: 1, high_: 1632613160 },
signature: { hash: [Binary], keyId: [Long] }
}
}
總結(jié)
MongoDB 提供的客戶端字段級自動加密,對于有數(shù)據(jù)隱私需要加密保護(hù)的還是很方便的,在配置了 CSFLE 客戶端后應(yīng)用程序在讀寫操作時(shí)和常規(guī)的客戶端讀寫操作是沒有差別的,唯一的阻礙可能是僅企業(yè)版支持。
文中我們將主密鑰存儲放在了本地的文件系統(tǒng)中,這在本地測試環(huán)境是可以的,但是生產(chǎn)環(huán)境不要用這種方式,因?yàn)槿魏文軌蛟L問您本地文件系統(tǒng)主密鑰的人都可以讀取您的數(shù)據(jù)加密密鑰,建議放在更安全的地方,例如密鑰管理系統(tǒng)(KMS)。
Reference
- docs.mongodb.com/drivers/security/client-side-field-level-encryption-guide/#e.-perform-encrypted-read-write-operations
- 基于 Mongo Shell 的手動加密 https://www.modb.pro/db/100877
- www.mongodb.com/community/forums/t/fle-mongoerror-bson-field-insert-jsonschema-is-an-unknown-field/5472/7
- www.mongodb.com/developer/how-to/client-side-field-level-encryption-csfle-mongodb-node
- mongodb.github.io/node-mongodb-native/3.4/reference/client-side-encryption/
網(wǎng)頁標(biāo)題:Node.js結(jié)合MongoDB實(shí)現(xiàn)字段級自動加密
網(wǎng)站網(wǎng)址:http://fisionsoft.com.cn/article/dhhpjdd.html


咨詢
建站咨詢
