MongoDBの暗号化をGoで実装してみた(Client-Side Field Level Encryption)


初めに

当記事はこちらを参考に執筆しています。
自分の振り返り用でもあるので、ある程度の雑さはご勘弁を。

対象

  • mongoのfleについてざっくりと理解したい。
  • とりあえず動かして理解したい。
  • mongoDBの操作に慣れている
  • Go言語なんとなく分かる

環境

  • macOS Big Sur 11.6.1
  • mongodb-communitity
  • go 1.16.10 darwin/amd64
  • MongoDB Go driver 1.2+
  • libmongocrypt
  • mongocryptd
    ↑ libmongocryptとmongocryptdが暗号化に使うライブラリ。

CSFLE(Client-Side Field Level Encryption)とは

With field level encryption, you can choose to encrypt certain fields within a document, client-side, while leaving other fields as plain text. This is particularly useful because when viewing a CSFLE document with the CLI, Compass, or directly within Altas, the encrypted fields will not be human readable. When they are not human readable, if the documents should get into the wrong hands, those fields will be useless to the malicious user. However, when using the MongoDB language drivers while using the same encryption keys, those fields can be decrypted and are queryable within the application.
参考: https://www.mongodb.com/developer/how-to/field-level-encryption-fle-mongodb-golang/

-> 要するに、MongoDBのフィールド単位での暗号化のことです。

実装内容

とりあえず動かした人はこちらをコピペしたら動くかと思います。(dbの設定などは自分の環境に合わせてください)
次節から一つずつ解説していきます。

package main

import (
	"context"
	"crypto/rand"
	"encoding/base64"
	"fmt"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)

var (
	ctx          = context.Background()
	kmsProviders map[string]map[string]interface{}
)

// データキーの生成
func createDataKey() primitive.Binary {
	// mongoに接続
	kvClient, err := mongo.Connect(ctx, options.Client().ApplyURI("<MONGO_URI>").SetAuth(
		options.Credential{
			Username:   "root",
			Password:   "password",
			AuthSource: "admin",
		}))
	if err != nil {
		panic(err)
	}

	// データキー生成に使用する乱数を生成(本来はAWS KMSなどを使用する)
	localKey := make([]byte, 96)
	if _, err := rand.Read(localKey); err != nil {
		panic(err)
	}
	kmsProviders = map[string]map[string]interface{}{
		"local": {
			"key": localKey,
		},
	}

	clientEncryptionOpts := options.ClientEncryption().SetKeyVaultNamespace("keyvault.datakeys").SetKmsProviders(kmsProviders)

	// データキー用のdbクライアントを生成
	clientEncryption, err := mongo.NewClientEncryption(kvClient, clientEncryptionOpts)
	if err != nil {
		panic(err)
	}
	defer clientEncryption.Close(ctx)

	// データキーの生成
	dataKeyId, err := clientEncryption.CreateDataKey(ctx, "local", options.DataKey().SetKeyAltNames([]string{"example"}))
	if err != nil {
		panic(err)
	}
	return dataKeyId
}

func readSchemaMap(dataKeyIdBase64 string) bson.M {
	content :=
		`{
			"fle-example.people": {
				"encryptMetadata": {
					"keyId": [
						{
							"$binary": 
								{
									"base64": "%s",
									"subType": "04"
								}
						}
					]
				},
				"properties": {
					"accountNumber": {
						"encrypt": {
							"bsonType": "string",
							"algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
						}
					}
				},
				"bsonType": "object"
			}
		}`
	schema := fmt.Sprintf(content, dataKeyIdBase64)
	var doc bson.M
	if err := bson.UnmarshalExtJSON([]byte(schema), false, &doc); err != nil {
		panic(err)
	}
	return doc
}

// 暗号化用のdbクライアントを生成
func createEncryptedClient(schemaMap bson.M) *mongo.Client {
	mongocryptdOpts := map[string]interface{}{
		"mongodcryptdBypassSpawn": true,
	}

	// 自動暗号化のオプション
	autoEncryptionOpts := options.AutoEncryption().
		SetKeyVaultNamespace("keyvault.datakeys").
		SetKmsProviders(kmsProviders).
		SetSchemaMap(schemaMap).
		SetExtraOptions(mongocryptdOpts)

	// 暗号化用のdbクライアントを生成
	mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI("<MONGO_URL>").SetAuth(options.Credential{
		Username:   "root",
		Password:   "password",
		AuthSource: "admin",
	}).SetAutoEncryptionOptions(autoEncryptionOpts))
	if err != nil {
		panic(err)
	}

	return mongoClient
}

func main() {
	// データキーの生成
	dataKey := createDataKey()

	// スキーママップを定義
	schemaMap := readSchemaMap(base64.StdEncoding.EncodeToString(dataKey.Data))

	// 暗号化用のdbクライアントを生成
	client := createEncryptedClient(schemaMap)

	defer client.Disconnect(ctx)
	collection := client.Database("fle-example").Collection("people")

	// insert
	if _, err := collection.InsertOne(context.TODO(), bson.M{"name": "TAROOO", "accountNumber": "123456"}); err != nil {
		panic(err)
	}

	// find
	var people interface{}
	err := collection.FindOne(context.TODO(), bson.M{"accountNumber": "123456"}).Decode(&people)
	if err != nil {
		panic(err)
	}
	fmt.Printf("結果:%v", people)
}

// 起動コマンド
// go run -tags cse main.go

1. データキーの生成

// データキーの生成
func createDataKey() primitive.Binary {
	// mongoに接続
	kvClient, err := mongo.Connect(ctx, options.Client().ApplyURI("<MONGO_URI>").SetAuth(
		options.Credential{
			Username:   "root",
			Password:   "password",
			AuthSource: "admin",
		}))
	if err != nil {
		panic(err)
	}

	// データキー生成に使用する乱数を生成(本来はAWS KMSなどを使用するのが望ましい)
	localKey := make([]byte, 96)
	if _, err := rand.Read(localKey); err != nil {
		panic(err)
	}
	kmsProviders = map[string]map[string]interface{}{
		"local": {
			"key": localKey,
		},
	}

	clientEncryptionOpts := options.ClientEncryption().SetKeyVaultNamespace("keyvault.datakeys").SetKmsProviders(kmsProviders)

	// データキー用のdbクライアントを生成
	clientEncryption, err := mongo.NewClientEncryption(kvClient, clientEncryptionOpts)
	if err != nil {
		panic(err)
	}
	defer clientEncryption.Close(ctx)

	// データキーの生成
	dataKeyId, err := clientEncryption.CreateDataKey(ctx, "local", options.DataKey().SetKeyAltNames([]string{"example"}))
	if err != nil {
		panic(err)
	}
	return dataKeyId
}

func main() {
	dataKey := createDataKey()
}

createDataKey関数では、初めにMongoDBに接続します。

接続が成功したら、暗号化オプションとして、鍵の保管場所とKMSプロバイダを定義します。例では保管先のdb名をkeyvault, コレクション名をdatakeysとしています。その後に定義されているkmsProvidersにはローカルの鍵情報を格納します。

データキーの生成時にSetKeyAltNames([]string{"example"})を設定していますが、これは_idで参照する必要がないようにするための一意な代替名です。KeyAltNameはAEAD_AES_256_CBC_HMAC_SHA_512-Randomを使用する場合に有効であることに注意してください(後に説明します)。

createDataKey関数を実行すると、キー情報がMongoDB内に作成されます。

2. スキーママップの定義

func readSchemaMap(dataKeyIdBase64 string) bson.M {
	content :=
		`{
			"fle-example.people": {
				"encryptMetadata": {
					"keyId": [
						{
							"$binary": 
								{
									"base64": "%s",
									"subType": "04"
								}
						}
					]
				},
				"properties": {
					"accountNumber": {
						"encrypt": {
							"bsonType": "string",
							"algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
						}
					}
				},
				"bsonType": "object"
			}
		}`
	schema := fmt.Sprintf(content, dataKeyIdBase64)
	var doc bson.M
	if err := bson.UnmarshalExtJSON([]byte(schema), false, &doc); err != nil {
		panic(err)
	}
	return doc
}

func main() {
	// スキーママップを定義
	schemaMap := readSchemaMap(base64.StdEncoding.EncodeToString(dataKey.Data))
}

次はreadSchemaMapの処理を説明します。

変数contentに格納されているjsonデータがスキーママップです。ドキュメント内のどのフィールドを暗号化するかを定義しています。
例ではfle-exampleデータベース内のpeopleドキュメントのaccountNumberフィールドを暗号化しています。

AEAD_AES_256_CBC_HMAC_SHA_512-Deterministicとは暗号化のアルゴリズムです。詳しくはこちらを参照してください。

3. 暗号化用のdbクライアントを生成

// 暗号化用のdbクライアントを生成
func createEncryptedClient(schemaMap bson.M) *mongo.Client {
	mongocryptdOpts := map[string]interface{}{
		"mongodcryptdBypassSpawn": true,
	}

	// 自動暗号化のオプション
	autoEncryptionOpts := options.AutoEncryption().
		SetKeyVaultNamespace("keyvault.datakeys").
		SetKmsProviders(kmsProviders).
		SetSchemaMap(schemaMap).
		SetExtraOptions(mongocryptdOpts)

	// 暗号化用のdbクライアントを生成
	mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI("<MONGO_URL>").SetAuth(options.Credential{
		Username:   "root",
		Password:   "password",
		AuthSource: "admin",
	}).SetAutoEncryptionOptions(autoEncryptionOpts))
	if err != nil {
		panic(err)
	}

	return mongoClient
}

func main() {
	// 暗号化用のdbクライアントを生成
	client := createEncryptedClient(schemaMap)

	defer client.Disconnect(ctx)
	collection := client.Database("fle-example").Collection("people")

	// insert
	if _, err := collection.InsertOne(context.TODO(), bson.M{"name": "TAROOO", "accountNumber": "123456"}); err != nil {
		panic(err)
	}
	
	// find
	var people interface{}
	err := collection.FindOne(context.TODO(), bson.M{"accountNumber": "123456"}).Decode(&people)
	if err != nil {
		panic(err)
	}
	fmt.Printf("結果:%v", people)
	// 結果:[{_id ObjectID("624970b82b0608643c27d9c5")} {name TAROOO} {accountNumber 123456}]%
}

createEncryptedClientでは、今までの説明で定義してきた情報をもとに、暗号化用のクライアントを生成しています。生成されてクライアントを使ってDBに接続することによって、対象のオブジェクトが自動で暗号化、複合化されます。

最後にmain関数でinsertとfindを行なって終了です。

最後に

ここまでざっくり説明してきましたが、もっと詳しく知りたい方は下記を参考にしてみてください。