【実践編】MongoDBの検索可能暗号の使い方を解説！！

**@kenmaro**です。
普段は主に**秘密計算、準同型暗号などの記事について投稿しています**。
秘密計算に関連するまとめの記事に関しては以下をご覧ください。

> - [秘密計算エンジニアを始めて２年半が経った。](https://qiita.com/kenmaro/items/48059dd60f79e80f5ef8)
> - [秘密計算エンジニアを始めて3年が経った。](https://qiita.com/kenmaro/items/a1d90a074d552b7bf886)

#### 勝手に秘密計算アドベントカレンダーについて

この記事は

https://qiita.com/kenmaro/items/3e8a124ce4306fe040dc

の「２日目」の記事
としようかと思っています。
興味のある方はアドベントカレンダー参加してみませんか？
ご連絡お待ちしております。

## 概要

前回の記事では、MongoDBが検索可能暗号を正式にサポートし、ベータ版として提供を開始した、
という記事について少し解説しました。

https://qiita.com/kenmaro/items/7d4a6c7b18bd4305f1fb


今回は、このMongoDBが、検索可能暗号を正式にサポートし、ベータ版として提供を開始した、
という記事について解説を行いました。



おさらいになりますが、
MongoDB は言わずと知れたSaaS型のDBサービスです。
ドキュメント指向データベース、もしくはNoSQLとも言われるタイプのデータベースです。

https://www.mongodb.com/cloud/atlas/lp/try2?utm_content=rlsavisitor&utm_source=google&utm_campaign=gs_apac_rlsamulti_search_core_brand_atlas_desktop_rlsa&utm_term=mongodb&utm_medium=cpc_paid_search&utm_ad=e&utm_ad_campaign_id=14412646476&adgroup=131761130812&gclid=CjwKCAjw7vuUBhBUEiwAEdu2pBQ4IOlTXk-xf7_B_8edqZnsQkBERhlLtOKG2R_LyIXIhRr3gskXlRoCag4QAvD_BwE

今記事では、実際にMongoDBの検索可能暗号を使ってみる、
つまりドキュメントにあるクイックスタートを追ってみる

ことにチャレンジしてみようと思います。



## 忙しい人のために

- MongoDBという世界最大規模のクラウド型DBが、検索可能暗号をサポートし、ベータ版として公開したよ
- この記事では実際に検索可能暗号DBを使ってみるよ
- AWS のKMSとの連携ができることを確認したよ
- 暗号化したいフィールドに対して暗号鍵を生成してみたよ
- データをインサートして暗号状態で保存されていることを確認できたよ
- 暗号化されていないフィールドに対して一致クエリを投げると復号して返してくれることを確認できたよ
- 暗号化されているフィールドに対して一致クエリを投げるとエラーになってハマったので一旦検証を終了したよ

- 使ったコードはgithubに上げているよ

https://github.com/kenmaro3/mongodb-encrypt




それでは、やっていきます。


## 初めに

https://www.mongodb.com/docs/upcoming/core/queryable-encryption/tutorials/#std-label-qe-tutorial-automatic-encryption

前回の記事でお伝えしたように、
検索可能暗号DBでは外部の鍵マネジメントシステムの利用が必須となっています。

上を見る限り
AWS、Azure、GCPの三大クラウドプロバイダが利用できそうですが、
今回はAWSを使ってやってみようと思います。

というわけで、

https://www.mongodb.com/docs/upcoming/core/queryable-encryption/tutorials/aws/aws-automatic/#before-you-get-started

ここをフォローすることになりそうです。

また、準備として

https://www.mongodb.com/docs/upcoming/core/queryable-encryption/install/#std-label-qe-install

こちらから始めます。


## 準備 + エラー集

先ほどのRequirements に従って必要なものを揃えていきます。

- Automatic Encryption Shared Library

暗号化に必要なライブラリのバイナリを取得し、展開します。
今回は`tmp_lib`というフォルダに格納しています、
こちらを後続のプログラムからパス指定する必要があります。

```bash
mkdir tmp_lib && cd tmp_lib
wget https://downloads.mongodb.com/osx/mongo_crypt_shared_v1-macos-x86_64-enterprise-6.0.0-rc8.tgz?_ga=2.37189024.1258991149.1654659347-125648093.1654659347&_gac=1.150727364.1654660103.CjwKCAjw7vuUBhBUEiwAEdu2pBQ4IOlTXk-xf7_B_8edqZnsQkBERhlLtOKG2R_LyIXIhRr3gskXlRoCag4QAvD_BwE

tar -xzvf mongo_crypt_shared_v1-macos-x86_64-enterprise-6.0.0-rc8.tgz
```

- MongoDB driver Compatible with Queryable Encryption

MongoDBのドライバーを取得します。



### バージョンはきちんと従う

Atlas は6.0以上で（Dedicated) で立ち上げておく必要があります。
最初ケチってSharedでデプロイしたところ、バージョンが5系しか対応しておらず、時間を溶かしましたので、
Dedicated で6.0以上できちんとデプロイしましょう。
ちなみに、latest バージョンで立ち上げた場合も、コネクションエラーが起きてしまったため、6.0で立ち上げた方が無難かと思います。


### チュートリアルコードに注意

https://github.com/mongodb/docs/tree/master/source/includes/queryable-encryption/sample_apps/build/node-fle-2/aws/reader

こちらを結局はコピペすることになりますが、そのまま使うとエラーになりますので注意。



```bash
ReferenceError: keyVaultColl is not defined

これは
const keyVaultColl = keyVaultClient.db(eDB).collection(eKV);
が記載漏れしています。
```


```bash
npm install mongodb-client-encryption

gitレポジトリにある
package.json
に従い、
npm install 
しましょう。
"mongodb-client-encryption": "^2.2.0-alpha.0"
が必要です。
```

```bash
TypeError: A crypt_shared override path was specified

クリプトライブラリのパスは、
  const extraOptions = {
    cryptSharedLibPath: "/Users/xxx/Downloads/tmp_lib/lib/mongo_crypt_v1.dylib",
    cryptSharedRequired: true,
  };

というように指定します。
```

#### user権限のエラー
```bash
MongoServerError: user is not allowed to do action [dropDatabase] on [Cluster0.]
```

MongoDBコンソールのタブから、security　--> user にadmin権限を付与して解決しました。
![Screen Shot 2022-06-09 at 13.51.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/519792/709441b5-ad85-e31a-854e-fde5ffc1807a.png)



```bash
MongoServerError: BSON field 'create.clusteredIndex' is the wrong type 'object', expected types '[bool, long, int, decimal, double']
```
MongoDBのAtlasのバージョンによるエラーでした、６系にきちんとすると消えました。



#### コネクションエラー
```bash
MongoServerSelectionError: connect ECONNREFUSED 127.0.0.1:27020
```

MongoDBのAtlasのバージョンによるエラーでした、６系にきちんとすると消えました。


#### MongoDB X NodeJSに不慣れな人は簡単に勉強が必要

https://stackoverflow.com/questions/46523321/mongoerror-connect-econnrefused-127-0-0-127017

https://reffect.co.jp/windows/mac-mongodb-install

```
brew tap mongodb/brew
brew install mongodb-community
brew services start mongodb-community
```


```bash
brew install mongodb-atlas-cli
```


## プログラムの解説

使用した 

- `make_data_key.js`
- `insert_encrypted_document.js`

について言葉で少しだけ解説します。


### make_data_key.js
は、名前の通り、

- KMSのマスターキー
- 暗号化したいフィールドのコンフィギュレーション
から、フィールドを暗号化するための鍵を生成するプログラムです。

```javascript
const eDB = "encryption";
const eKV = "__keyVault";
const keyVaultNamespace = `${eDB}.${eKV}`;
const secretDB = "Cluster0";
const secretCollection = "patients";
```
この記述は、
`encryption.__keyVault`
というコレクションに必要な鍵を生成し、

実際に格納する（暗号化対象の）データは
`Cluster0.patients`
というコレクションに格納することを表しています。

```javascript
  const clientEnc = new ClientEncryption(keyVaultClient, {
    keyVaultNamespace: keyVaultNamespace,
    kmsProviders: kmsProviders,
  });
  const dek1 = await clientEnc.createDataKey(provider, {
    masterKey: masterKey,
    keyAltNames: ["dataKey1"],
  });
  const dek2 = await clientEnc.createDataKey(provider, {
    masterKey: masterKey,
    keyAltNames: ["dataKey2"],
  });
  const dek3 = await clientEnc.createDataKey(provider, {
    masterKey: masterKey,
    keyAltNames: ["dataKey3"],
  });
  const dek4 = await clientEnc.createDataKey(provider, {
    masterKey: masterKey,
    keyAltNames: ["dataKey4"],
  });
```

このように鍵を作り、

```javascript
  const encryptedFieldsMap = {
    [`${secretDB}.${secretCollection}`]: {
      fields: [
        {
          keyId: dek1,
          path: "phoneNumber",
          bsonType: "string",
          queries: { queryType: "equality" },
        },
        {
          keyId: dek2,
          path: "medications",
          bsonType: "array",
        },
        {
          keyId: dek3,
          path: "patientRecord.ssn",
          bsonType: "string",
          queries: { queryType: "equality" },
        },
        {
          keyId: dek4,
          path: "patientRecord.billing",
          bsonType: "object",
        },
      ],
    },
  };
```

このように暗号化したいフィールドに対してそれぞれの鍵を割り振っています。
また、`queries`という箇所で、`equality`を指定することで、
このフィールドに対して一致検索をすることを明示しています。
これらがコンフィギュレーションになっています。

実行すると、以下のように鍵が生成されているはずです。（MongoDB Compass を使って確認しています。）

![Screen Shot 2022-06-09 at 14.17.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/519792/c714b6e3-abe5-93f3-2e21-cdc78aa88cc9.png)



### insert_encrypted_document.js

次に、このプログラムですが、
実際に暗号化対象のデータを一件インサートし、
そのインサートされたデータに対して

- 鍵の存在を知らないクライアント(unencryptedClient)
- 鍵の存在を知っているクライアント(encryptedClient)

の二つのクライアントからクエリを投げ、それぞれ

- 暗号化されているデータ
- 復号されたデータ

が返ってくることを示すプログラムです。

具体的には、

```javascript
    await encryptedColl.insertOne({
      firstName: "Jon",
      lastName: "Doe",
      patientId: 12345678,
      address: "157 Electric Ave.",
      phoneNumber: "012-3456-7890",
      patientRecord: {
        ssn: "987-65-4320",
        billing: {
          type: "Visa",
          number: "4111111111111111",
        },
      },
      medications: ["Atorvastatin", "Levothyroxine"],
    });
```
により一件の患者のデータを挿入しています。
データを挿入すると実際にこのように暗号化されています。

![Screen Shot 2022-06-09 at 14.17.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/519792/5edb929d-0db6-656e-80f7-a532143270a1.png)


このうち、make_data_key.js で指定した

- phoneNumber
- patientRecord.ssn
- patientRecord.billing
- medications

が暗号化の対象となっています。

さらに、

```javascript
await encryptedColl.findOne({ firstName: "Jon" })
await encryptedColl.findOne({ phoneNumber: "012-3456-7890" })
```
などでクエリを送信することができます。
上はfirstNameという暗号化されていないフィールドに対してのクエリであり、
下はphoneNumberという暗号化されているフィールドに対してのクエリです。

クエリの結果は、それぞれ

![Screen Shot 2022-06-09 at 14.20.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/519792/7182f9a5-1ba4-59a5-e156-ae92bf4910fc.png)

![Screen Shot 2022-06-09 at 14.20.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/519792/8eb51f33-81b4-e5d6-fbe4-cb50423dc31b.png)

encryptedClientを経由すると、確かに復号されてデータを確認できることがわかります。



## 検証結果

検証の結果、

- AWSのKMSで作ったマスター鍵との連携
- 暗号化したいフィールドの指定、コンフィギュレーション
- データのインサート
- インサートされたデータが暗号化されていることの確認
- 暗号化されていないフィールドに対しての一致検索、結果が復号されていることの確認

ができましたが、肝心の

- 暗号化されているフィールドに対しての一致検索

をしようとした時のエラーが解決できず、一旦終了という結果になりました。


## 使ってみた感想

暗号化されているフィールドに対して一致検索をする、
つまり検索可能暗号をつかって暗号状態でクエリを投げて動くことを確かめたかったのですが、
後に書くエラーにより実行できず、時間が溶けそうだったので一旦諦めました。
おそらく私の問題だとは思うので、次また時間を見つけてチャレンジしてみようと思います。

その他のKMSとの連携や、フィールドに対して鍵を生成するプロセス、
データのインサートなどは（比較的）スムーズにいき、使いやすいなと思いました。

MongoDBクライアントはNodeJSのみ現在サポートされているようで、
今まで使ったことがある人はスムーズに検証できそうです。
私はNodeJSは経験がありますが、MongoDBをNodeJSから使ったことは過去にあったかないかわからないくらいのレベルから検証したので、理解するまでに少し時間がかかりました。

クライアントのライブラリ、暗号化に使うライブラリなどはOSSになっており、
エラーが出た時もソースコードを読むことでデバッグができる形にはなっていたため、
開発者に対しては使いやすいなと感じました。

また、公式のGitレポジトリに載っているプログラムが普通に初歩的なエラーを吐いたので、
まだ機能としても準備段階ではあるのだなと思いました。
結果的にこのエラーはコードをよく読めば解決できるものではあったのですが、、。



## まとめ

使ったコードはgithubにあげています。

https://github.com/kenmaro3/mongodb-encrypt

今回は、前回の記事に引き続き
MongoDB 6.0.0 以降に搭載された「検索可能暗号」機能の
実運用についてチュートリアルコードを追ってみました。

一番やりたかった暗号フィールドに対しての一致検索ができなかったのがとても悔しいですが、
おそらく自分の理解が足りていないのが原因だと思うのでまたチャレンジして記事をアップデートしてみようと思います。

今回の記事に関しては機能に対する説明などはあまり書いていませんが、
前回の記事に関しては検索可能暗号について考察を書いているので、開発者以外の方はそちらを読んでいただければと思います。

MongoDBは個人的に好きなサービスなので、これからも追っていこうと思っています。

また、このようなセキュリティに関連したリサーチャーや、開発者を米国で募集されているようなので、
興味のある方はコンタクトをとってみてはいかがでしょうか、
トップの開発者２人のツイッターはこちらのようです。

Senny Kamara さん

https://twitter.com/senykam

Tarik Moataz さん
https://twitter.com/tarikmoataz



というわけで今回はこの辺で。



@kenmaro