はじめに
処方箋データ基盤チームでエンジニアをしている岩佐 (孝浩) です。
カケハシには「岩佐」さんが複数名在籍しており、社内では「わささん」と呼ばれています。
私が所属する処方箋データ基盤チームは、日本全国の薬局から送信される処方箋データを S3 に保存しています。
処方箋データは TLS 暗号で送信されますが、送信中のデータに対する盗聴リスクや改ざんリスクをさらに低減するために、AWS KMS RSA 非対称鍵を利用したハイブリッド暗号化を検証しました。
この投稿では、AWS KMS RSA 非対称鍵を利用したハイブリッド暗号方式での暗号化について、OpenSSL で暗号化して TypeScript で復号化する方法を紹介します。
ハイブリッド暗号化について
ハイブリッド暗号化は、対称鍵暗号と非対称鍵暗号を組み合わせた暗号化方式です。
それぞれの暗号方式の強みを活かして、大量データを効率的に暗号化できます。
ハイブリッド暗号化のフロー
- データを暗号化するための対称鍵 (Data Encryption Key: DEK) を生成
- データを DEK で暗号化
- DEK を非対称鍵 (Key Encryption Key: KEK) で暗号化
- 暗号化されたデータと鍵をセットで保管
- 暗号化された鍵を秘密鍵で復号化し DEK を抽出
- 暗号化されたデータを DEK で復号化
AWS KMS を利用した場合のイメージ:
AWS KMS を利用することで、秘密鍵の管理を AWS に任せることができるので、鍵の漏洩リスクを低減できます。
また、アクセス制御を IAM ポリシーで厳密に設定することで、安全に鍵を管理できます。
RSA 非対称鍵で暗号化できるデータサイズ
RSA 非対称鍵で暗号化できるデータサイズは、下記のとおり非常に小さいサイズになります。公開鍵暗号の仕組み上、暗号化処理が複雑で計算コストが高いためです。
鍵長 (bit) | パディング | パディングサイズ (byte) | データサイズ (byte) |
---|---|---|---|
2048 | PKCS#1 v1.5 | 11 | 2048 / 8 - 11 = 245 |
2048 | OAEP SHA-256 | 2 * 32 + 2 = 66 | 2048 / 8 -66 = 190 |
4096 | PKCS#1 v1.5 | 11 | 4096 / 8 - 11 = 501 |
4096 | OAEP SHA-256 | 2 * 32 + 2 = 66 | 4096 / 8 -66 = 446 |
ほとんどのユースケースでは上記のデータサイズを超過するため、対称鍵暗号方式かハイブリッド暗号方式のいずれかを採用することになります。
KMS RSA 非対称鍵を利用したハイブリッド暗号化
1. データを暗号化するための対称鍵 (Data Encryption Key: DEK) を生成
以下のコマンドを実行して、データを暗号化するための Data Encryption Key (DEK) を生成します。
openssl rand -base64 32 > data-encryption.key
2. データを DEK で暗号化
以下のコマンドを実行して、カケハシのテックブログのトップページをダウンロードします。
これを暗号化のサンプルデータとして利用します。
curl https://kakehashi-dev.hatenablog.com/ > data.txt
DEK を利用して、サンプルデータを暗号化します。
openssl enc \ -aes-256-cbc \ -salt \ -pbkdf2 \ -in data.txt \ -out data.txt.enc \ -pass file:./data-encryption.key
コマンドのオプション:
オプション | 説明 |
---|---|
-aes-256-cbc | 暗号アルゴリズム (AES 256 CBC) |
-salt | 暗号化時にランダムなソルトを生成 |
-pbkdf2 | パスワードから暗号化キーを導出する際に使用される鍵導出関数 |
-in | 暗号化したいファイル |
-out | 暗号化されたファイル |
-pass | 暗号化に利用するパスワード |
暗号化が完了したら、元のファイルを削除します。
rm data.txt
3. DEK を非対称鍵 (Key Encryption Key: KEK) で暗号化
KMS RSA 非対称鍵の作成
以下のコマンドを実行して、KMS に RSA 非対称鍵を作成します。
--key-spec
は、要件に応じて適切な鍵の長さを選択してください。
この投稿では 4096 ビットを選択しています。
aws kms create-key \ --key-spec RSA_4096 \ --key-usage ENCRYPT_DECRYPT
公開鍵のダウンロード
以下のコマンドを実行して、公開鍵をダウンロードします。
この公開鍵を Key Encryption Key (KEK) として利用します。
aws kms get-public-key \ --key-id <KMS_KEY_ID> \ | jq -r .PublicKey | base64 -d > kms-public-key-encryption.key
補足:
KMS に保管されている秘密鍵を取得することはできません。
このおかげでセキュアな運用が可能になりますが、セルフマネージドな秘密鍵を利用したい場合、CloudHSM 等のサービスをご検討ください。
KEK (公開鍵) で DEK を暗号化
以下のコマンドを実行して、KEK (公開鍵) で DEK を暗号化します。
パディングには OAEP SHA-256
を指定しています。
openssl pkeyutl \ -in data-encryption.key \ -out encrypted.key \ -inkey kms-public-key-encryption.key \ -pubin \ -encrypt \ -pkeyopt rsa_padding_mode:oaep \ -pkeyopt rsa_oaep_md:sha256
DEK はパスワードに相当するので、暗号化したらすぐに削除します。
rm data-encryption.key
KEK は公開鍵のため削除不要ですが、この投稿では利用することが無いため削除しておきます。
rm kms-public-key-encryption.key
4. 暗号化されたデータと鍵をセットで保管
この時点で以下のファイルが存在しているはずです。
. ├── data.txt.enc └── encrypted.key
復号化 TypeScript
要件
以下のコマンドを実行して、必要なライブラリをインストールしてください。
npm i @aws-sdk/client-kms
npm i -D @types/node tsx typescript
ライブラリ | 用途 |
---|---|
@aws-sdk/client-kms | AWS SDK for JavaScript KMS Client |
@types/node | Node.js 型定義 |
tsx | TypeScript をトランスパイルせずに直接実行するツール |
typescript | プログラミング言語 |
サンプルコード
先にサンプルコードの全体を掲載しておきます。
main.ts
というファイル名で保存してご利用ください。
import { Buffer } from 'node:buffer'; import * as crypto from 'node:crypto'; import * as fs from 'node:fs'; import * as path from 'node:path'; import { DecryptCommand, KMSClient } from '@aws-sdk/client-kms'; const deriveDataEncryptionKey = async ( encryptedKey: Buffer<ArrayBufferLike>, kmsKeyId: string, ): Promise<string> => { const command = new DecryptCommand({ KeyId: kmsKeyId, CiphertextBlob: encryptedKey, EncryptionAlgorithm: 'RSAES_OAEP_SHA_256', }); const text = (await new KMSClient().send(command)).Plaintext; if (!text) { return ''; } return new TextDecoder('utf-8').decode(text).replace('\n', '').trim(); }; const decryptFile = (filePath: string, password: string): Buffer => { const encryptedData = fs.readFileSync(filePath); const salt = encryptedData.subarray(8, 16); const derivedKey = crypto.pbkdf2Sync( password, salt, 10000, 32 + 16, // 32 bytes key + 16 bytes IV 'sha256', ); const aesKey = derivedKey.subarray(0, 32); const iv = derivedKey.subarray(32); const decipher = crypto.createDecipheriv('aes-256-cbc', aesKey, iv); return Buffer.concat([ decipher.update(encryptedData.subarray(16)), // ソルト以降のデータを復号 decipher.final(), // 最後の復号ブロック ]); }; const main = async ( keyEncryptionKeyPath: string, encryptedFilePath: string, kmsKeyId: string, ): Promise<void> => { // Read encrypted key const encryptedKey = fs.readFileSync(keyEncryptionKeyPath); // Derive data encryption key const dataEncryptionKey = await deriveDataEncryptionKey( encryptedKey, kmsKeyId, ); // Decrypt const decryptedData = decryptFile(encryptedFilePath, dataEncryptionKey); // Write decrypted data const filename = `decrypted-${path.basename(encryptedFilePath).replace(/\.enc$/, '')}`; fs.writeFileSync( path.join(path.dirname(encryptedFilePath), filename), decryptedData, ); }; main( process.argv[2], // encrypted.key process.argv[3], // data.txt.enc process.argv[4], // AWS KMS key ID );
5. 暗号化された鍵を秘密鍵で復号化し DEK を抽出
下記のコードは、暗号化された鍵を KMS 秘密鍵で復号化し DEK を抽出するための関数です。
const deriveDataEncryptionKey = async ( encryptedKey: Buffer<ArrayBufferLike>, kmsKeyId: string, ): Promise<string> => { const command = new DecryptCommand({ KeyId: kmsKeyId, CiphertextBlob: encryptedKey, EncryptionAlgorithm: 'RSAES_OAEP_SHA_256', }); const text = (await new KMSClient().send(command)).Plaintext; if (!text) { return ''; } return new TextDecoder('utf-8').decode(text).replace('\n', '').trim(); };
ポイント:
- AWS KMS の DecryptCommand API を使用
- 復号化された DEK を文字列として返却
6. 暗号化されたデータを DEK で復号化
下記のコードは、暗号化されたデータを DEK で復号化するための関数です。
const decryptFile = (filePath: string, password: string): Buffer => { const encryptedData = fs.readFileSync(filePath); const salt = encryptedData.subarray(8, 16); const derivedKey = crypto.pbkdf2Sync( password, salt, 10000, 32 + 16, // 32 bytes key + 16 bytes IV 'sha256', ); const aesKey = derivedKey.subarray(0, 32); const iv = derivedKey.subarray(32); const decipher = crypto.createDecipheriv('aes-256-cbc', aesKey, iv); return Buffer.concat([ decipher.update(encryptedData.subarray(16)), // ソルト以降のデータを復号 decipher.final(), // 最後の復号ブロック ]); };
ポイント:
- ソルトは先頭8バイト (
Salted__
) の次の8バイトに格納 - ストレッチングは 10,000 回 (OpenSSL デフォルト値)
- 導出する鍵は 32バイト (AES 256 bit) + 16バイト (初期化ベクトル)
- 復号化されたデータをバイナリ (Buffer) として返却
以下のコマンドを実行して、TypeScript を実行します。
npx tsx main.ts \ encrypted.key \ data.txt.enc \ <KMS_KEY_ID>
スクリプトを実行すると、復号化されたデータが decrypted-data.txt
に出力されます。
head -10 decrypted-data.txt
<!DOCTYPE html> <html lang="ja" data-admin-domain="//blog.hatena.ne.jp" data-admin-origin="https://blog.hatena.ne.jp" data-author="kakehashi_dev" data-avail-langs="ja en" data-blog="kakehashi-dev.hatenablog.com" data-blog-host="kakehashi-dev.hatenablog.com"
まとめ
AWS KMS RSA 非対称鍵を利用したハイブリッド暗号方式での暗号化について、OpenSSL で暗号化して TypeScript で復号化する方法を具体的な手順を通じて紹介しました。
KMS を利用すると、非対称鍵の管理と運用が大幅に簡素化されます。
主なポイントは以下のとおりです。
- 秘密鍵が KMS 内で安全に管理され、漏洩リスクが低減
- IAM ポリシーを利用した厳密な権限設定
- 高い可用性
紹介した TypeScript のサンプルコードは、AWS Lambda で実行することも可能です。
信頼性の低いネットワークを通じてデータを連携する際に、セキュリティを高めることができます。
以上、お役に立てれば幸いです。
補足:非対称鍵を利用したハイブリッド暗号化の検証理由
今回の検証と同様の暗号化方法として、AWS KMS ではエンベロープ暗号化の利用が一般的です。
今回の検証では以下が要件になっており、非対称鍵を利用したハイブリッド暗号化を試しました。
- 複数のクライアントに公開鍵を事前に共有
- クライアントに AWS CLI や AWS SDK のインストール不可
- クライアントとサーバー間の通信経路で Data Encryption Key が露出しないこと
If your use case requires encryption outside of AWS by users who cannot call AWS KMS, asymmetric KMS keys are a good choice.