この記事について
GCPを利用して、下記のようなアプリケーションの開発を目指しています。
- 機密情報を暗号化する
- 暗号化した機密情報をデータストアに保存する
- 保存された機密情報を復号し、処理に利用する
そこで、簡単な処理を実装し、暗号化・復号に用いるCloud KMSや、
データの保存に用いるFirestoreのキャッチアップを行っていました。
しかし、Cloud KMSを用いた復号でつまずいたので、その内容と解決策についての記事です。
どこでつまずいたか
Cloud KMSを用いたデータの暗号化と復号の方法に関しては、公式のドキュメントに記載されています。
このドキュメントでは、ファイルを経由したデータの暗号化・復号の方法が解説されています。
具体的には、
- ファイルに平文を記述
- ファイルに記述された平文を読み込み、暗号化
- 暗号化されたテキストをファイルに記述
- ファイルに記述された暗号化テキストを読み込み、復号
- 復号して取得した平文をファイルに記述
という方法です。
自分の場合は、Cloud KMSが正しく動作することを手っ取り早く確認したかったことと、
実際の利用時には暗号化されたテキストを、ファイルではなくデータベースに保存しようと考えていたため、
ファイルを利用せずに暗号化・復号のロジックを実装しました。
しかし、暗号化されたテキストを復号しようとしても、うまく復号ができず、
空の実行結果が返されてしまいました。
まずは解決策
とにかく早く解決策を知りたい方のために、まず解決策を書きます。
GCPのCloud KMSでは、
- 暗号化の際は、平文をエンコードしたバイナリデータを渡す
-
復号されたデータは、デコードする
ということが必要です。
もっと詳しく
今後のサービス開発で利用する予定であるCloud KMSとCloud Firestoreのキャッチアップのため、
以下のような構成で処理を実装しました。
環境構成と処理の流れ
暗号化の処理の流れ
実際の暗号化やデータベースへの保存の処理は、CloudFunctions上で行っています。
① CloudFunctionsはAPIリクエストで起動し、その際にデータベース保存用のIDと暗号化したいテキストをクエリとして渡します
② 渡されたテキストをCloud KMSを利用して暗号化します
③ 暗号化されたテキストをFirestoreに保存します
復号の処理の流れ
① CloudFunctionsをAPIリクエストで起動、その際に取得したいデータのIDをクエリとして渡します
② 渡されたIDをもとに、Firestoreからデータを取得します
③ 取得した暗号化されたテキストを復号します
環境の準備と処理の実行まで
Cloud Key Management Serviceで暗号鍵の作成
Cloud KMSには、暗号鍵(キー)と、暗号鍵を束ねるキーリングというものがあります。
暗号鍵作成するためには、まずキーリングを作成し、そのあとで暗号鍵を作成します。
-
GCPコンソール上部の検索ボックスに、「暗号鍵」と入力し、KMSのページへ行く
-
画面上部の「キーリングを作成」ボタンをクリックする
-
下記の情報を入力し、キーリングを作成する
- キーリングの名前: 任意の名前を設定
- キーリングのロケーション: とりあえず「global」を選択
-
続けて、暗号鍵を作成する
- 生成する鍵の種類: 「生成した鍵」を選択
- 鍵名: 任意の名前を設定
- 保護レベル: とりあえず「ソフトウェア」を選択
- 目的: 「対称暗号化 / 復号化」を選択
- ローテーション期間: キャッチアップのための利用なので、「実行しない」を選択
Cloud Firestoreでコレクションを作成
Firestoreのコレクションとは、ざっくりいうとRDBでいうところのテーブルのようなものです。
正確な理解をしたい方は、ドキュメントを参照ください。
-
GCPコンソール上部の検索ボックスに、「Firestore」と入力し、Firestoreのページへ行く
-
画面左側の「コレクションを開始」ボタンをクリックする
-
下記の情報を入力し、保存ボタンをクリックし、コレクションを作成する
- コレクションID: 任意の名前を設定
処理ロジックの実装
今回は、Node.jsで実装しています。
大事なポイントには、コメントをしています。
const kms = require('@google-cloud/kms');
const admin = require('firebase-admin');
const functions = require('firebase-functions');
const client = new kms.KeyManagementServiceClient();
// projectName, keyringName, keyNameには、実際のプロジェクト名, キーリング名, キー名を設定する
const name = client.cryptoKeyPath('projectName', 'global', 'keyringName', 'keyName');
admin.initializeApp(functions.config().firebase);
const db = admin.firestore();
// collectionNameには、Firestoreのコレクション名を設定する
const COLLECTION_NAME = 'collectionName';
exports.handler = async (req, res) => {
// APIリクエストのクエリ isEncryption によって、暗号化を行うか復号を行うか決定する
const isEncryption = req.query.isEncryption === '1' ? true : false;
if(isEncryption) {
// 暗号化の場合の処理
console.log('encryption mode');
// APIリクエストのクエリから、idとmessageを取得する
// - id: Firestoreドキュメントのキーとして利用
// - message: 暗号化したい文字列
const { id, message } = req.query;
console.log(`id: ${id}`);
console.log(`message: ${message}`);
const encryptionResult = await encrypt(message);
storeCiphertext(id, encryptionResult.ciphertext);
res.status(200).send('storing encrypted message succeeded.');
return;
} else {
// 復号する場合の処理
console.log('decryption mode');
// APIリクエストのクエリから、復号したいデータのキーを取得
const { id } = req.query;
console.log(`id: ${id}`);
const ciphertext = await getCiphertext(id);
const message = await decrypt(ciphertext);
// レスポンスとして、復号した文字列を返還する
res.status(200).send(`decryption succeeded. (message: ${message})`);
return;
}
}
function storeCiphertext(id, ciphertext) {
// Firestoreのコレクションに、idをキー、暗号化された文字列を値として格納する
db.collection(COLLECTION_NAME).doc(id).set({ ciphertext });
}
function getCiphertext(id) {
// Firestoreのコレクションを、idで検索し、データを取得する
return new Promise((resolve, reject) => {
db.collection(COLLECTION_NAME).doc(id).get()
.then( doc => {
if (!doc.exists) {
console.log('no such document.');
resolve();
} else {
console.log(`document data: ${JSON.stringify(doc.data())}`);
resolve(doc.data()['ciphertext']);
}
}) .catch ( err => {
console.log('error', err);
reject(0);
});
})
}
async function encrypt(message) {
// 暗号化したい文字列をバイナリデータにエンコードする
const plaintext = Buffer.from(message, 'utf-8').toString('base64');
const [result] = await client.encrypt({ name, plaintext });
console.log(`encrypt result: ${JSON.stringify(result)}`);
return result;
}
async function decrypt(ciphertext){
const [result] = await client.decrypt({ name, ciphertext });
console.log(`decrypt result: ${JSON.stringify(result)}`);
// 復号した文字列をデコードする
return Buffer.from(result.plaintext, 'base64').toString('utf-8');
}
Cloud Functionsへのデプロイ
Node.jsで記述されたソースをCloud Functiosへデプロイするためには、package.jsonが必要です。
{
"name": "kms-test",
"version": "1.0.0",
"description": "",
"main": "kms-test.js",
"scripts": {},
"author": "",
"license": "ISC",
"dependencies": {
"@google-cloud/kms": "^1.6.3",
"firebase-admin": "^8.10.0",
"firebase-functions": "^3.5.0",
}
}
続いて、下記コマンドを実行し、Cloud Functionsへのデプロイを行います。
gcloud functions deploy kms-test --region=asia-northeast1 --runtime=nodejs10 --trigger-http --entry-point handler
ここでは、kms-testという関数を、asia-northeast1というリージョンに作成し、そこにソースコードをデプロイしています。
--entry-point handler
では、kms-test.js内のhandlerというfunctionがエントリーポイントとして実行されるように指定をしています。
このオプションを指定しないと、Cloud Functionsでは、関数名と同名のfunctionをエントリーポイントとして設定しようとしますが、それが無いので、デプロイ時にエラーとなってしまいます。
デプロイに成功すると、下記のようなURLが発行されます。
https://asia-northeast1-project-name.cloudfunctions.net/kms-test
実際に処理を行う際に必要となるので、控えておいてください。
暗号化の実行
ここでは、curlコマンドを利用して、暗号化を行います。
curl https://asia-northeast1-project-name.cloudfunctions.net/kms-test?isEncryption=1\&id=0001\&message=HelloWorld
暗号化を行いたいので、isEncryption=1として、任意のidとmessageを渡します。
ちなみに、Macの標準シェルがzshになったことで、curlコマンドを実行すると
zsh: no matches found: https://~~~
というエラーが起こる場合があります。
事前に、 setopt nonomatch
を実行してあげると、無事curlコマンドが実行できるようになります。
curlコマンドの実行結果として、下記が表示されれば、実行は成功です。
storing encrypted message succeeded.
kms-testというコレクションのなかに、idで渡した0001をキーとするドキュメントが作成されています。
また、ドキュメントの値には、なにやら暗号化されたっぽい文字列が格納されています。
ひとまず、暗号化成功です。
復号の実行
続いて、先ほど暗号化した文字列を復号してみます。
以下のcurlコマンドを実行します。
curl https://asia-northeast1-project-name.cloudfunctions.net/kms-test?isEncryption=0\&id=0001
復号を行いたいので、isEncryption=0として、idには復号したいデータのidを指定します。
すると、下記結果が得られます。
decryption succeeded. (message: HelloWorld)
暗号化の際に与えた HelloWorld という文字列を取得できていることが確認できました。
復号も成功です。
まとめ
Cloud KMSを利用してデータの暗号化・復号をする場合は、
- 暗号化の際は、平文をエンコードしたバイナリデータを渡す
-
復号されたデータは、デコードする
ということが必要です。
長くなってしまいましたが、同じところでつまずいてる人の役に立てると嬉しいです。