6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

GCP Cloud KMSで復号がうまくできなかった話

Last updated at Posted at 2020-04-18

この記事について

GCPを利用して、下記のようなアプリケーションの開発を目指しています。

  • 機密情報を暗号化する
  • 暗号化した機密情報をデータストアに保存する
  • 保存された機密情報を復号し、処理に利用する

そこで、簡単な処理を実装し、暗号化・復号に用いるCloud KMSや、
データの保存に用いるFirestoreのキャッチアップを行っていました。

しかし、Cloud KMSを用いた復号でつまずいたので、その内容と解決策についての記事です。

どこでつまずいたか

Cloud KMSを用いたデータの暗号化と復号の方法に関しては、公式のドキュメントに記載されています。

このドキュメントでは、ファイルを経由したデータの暗号化・復号の方法が解説されています。
具体的には、

  1. ファイルに平文を記述
  2. ファイルに記述された平文を読み込み、暗号化
  3. 暗号化されたテキストをファイルに記述
  4. ファイルに記述された暗号化テキストを読み込み、復号
  5. 復号して取得した平文をファイルに記述
    という方法です。

自分の場合は、Cloud KMSが正しく動作することを手っ取り早く確認したかったことと、
実際の利用時には暗号化されたテキストを、ファイルではなくデータベースに保存しようと考えていたため、
ファイルを利用せずに暗号化・復号のロジックを実装しました。

しかし、暗号化されたテキストを復号しようとしても、うまく復号ができず、
空の実行結果が返されてしまいました。

まずは解決策

とにかく早く解決策を知りたい方のために、まず解決策を書きます。
GCPのCloud KMSでは、

  • 暗号化の際は、平文をエンコードしたバイナリデータを渡す
  • 復号されたデータは、デコードする
    ということが必要です。

もっと詳しく

今後のサービス開発で利用する予定であるCloud KMSとCloud Firestoreのキャッチアップのため、
以下のような構成で処理を実装しました。

環境構成と処理の流れ

暗号化の処理の流れ

encryption.jpg

実際の暗号化やデータベースへの保存の処理は、CloudFunctions上で行っています。
① CloudFunctionsはAPIリクエストで起動し、その際にデータベース保存用のIDと暗号化したいテキストをクエリとして渡します
② 渡されたテキストをCloud KMSを利用して暗号化します
③ 暗号化されたテキストをFirestoreに保存します

復号の処理の流れ

decryption.jpg

① CloudFunctionsをAPIリクエストで起動、その際に取得したいデータのIDをクエリとして渡します
② 渡されたIDをもとに、Firestoreからデータを取得します
③ 取得した暗号化されたテキストを復号します

環境の準備と処理の実行まで

Cloud Key Management Serviceで暗号鍵の作成

Cloud KMSには、暗号鍵(キー)と、暗号鍵を束ねるキーリングというものがあります。
暗号鍵作成するためには、まずキーリングを作成し、そのあとで暗号鍵を作成します。

  1. GCPコンソール上部の検索ボックスに、「暗号鍵」と入力し、KMSのページへ行く

  2. 画面上部の「キーリングを作成」ボタンをクリックする

  3. 下記の情報を入力し、キーリングを作成する

    • キーリングの名前: 任意の名前を設定
    • キーリングのロケーション: とりあえず「global」を選択
  4. 続けて、暗号鍵を作成する

    • 生成する鍵の種類: 「生成した鍵」を選択
    • 鍵名: 任意の名前を設定
    • 保護レベル: とりあえず「ソフトウェア」を選択
    • 目的: 「対称暗号化 / 復号化」を選択
    • ローテーション期間: キャッチアップのための利用なので、「実行しない」を選択

Cloud Firestoreでコレクションを作成

Firestoreのコレクションとは、ざっくりいうとRDBでいうところのテーブルのようなものです。
正確な理解をしたい方は、ドキュメントを参照ください。

  1. GCPコンソール上部の検索ボックスに、「Firestore」と入力し、Firestoreのページへ行く

  2. 画面左側の「コレクションを開始」ボタンをクリックする

  3. 下記の情報を入力し、保存ボタンをクリックし、コレクションを作成する

    • コレクションID: 任意の名前を設定

処理ロジックの実装

今回は、Node.jsで実装しています。
大事なポイントには、コメントをしています。

kms-test.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が必要です。

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.

Firestoreも確認してみます。
firestore.png

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を利用してデータの暗号化・復号をする場合は、

  • 暗号化の際は、平文をエンコードしたバイナリデータを渡す
  • 復号されたデータは、デコードする
    ということが必要です。

長くなってしまいましたが、同じところでつまずいてる人の役に立てると嬉しいです。

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?