AWS KMSを使った暗号化

こんにちは テコテック CISOの木村です。

AWS Key Management Service

暗号化というのは様々な方法がありますがAWSのKMSを利用したサンプルを実装したので紹介します。

暗号化って別にAWS使わなくてもできるのになぜ使うのか?

どのような暗号化を行うかというのも大事ですがセキュアにデータを暗号化した後、難しい事のひとつが鍵の管理です。

データを暗号化する際には、文字列など「デジタルデータ」を鍵として使うことになるので、コピーや送信等も簡単に行えますし、それを検知するのは可能でも検知できる環境を用意するのは大変です。
例えば重要なデータを入れた暗号化zipを作成しパスワードを「ZZZZIP」にした時、忘れないように自分のパソコンに保存したりすると思いますが、そのデータが

  • どこかに送られてないか?
  • コピーされていないか?
  • 他の人に覗き見られていないか?

というのは簡単には分かりません。

そこでAWSを使う事により鍵の管理を任せて安全に行おうという訳です。
AWS以外にも類似サービスはありますが今回はAWS KMS(以後KMS)とPHPを利用しました。
もちろん自分の信用できるサービスを使う必要がありますし、無い場合は自ら実装する必要があります。

事前準備

  • AWSのアカウント
  • php, composerでAWS SDKを利用できるようにする
{
    "require": {
        "aws/aws-sdk-php": "3.*"
    }
}
  • KMSを利用できるIAMをコンソールで作成し.aws/credentialsの配置
[profile-name]
aws_access_key_id = ********************
aws_secret_access_key = ****************************************
  • KMSでCMKを作成する
    CMK とは Customer Master Key の略です。
    今回は暗号化を行う度に暗号化を行う為の鍵(データキー)を生成します。その元になる鍵がCMKです。
    プログラム上ではKeyId などのエイリアスでCMKを指定して使う為、管理や鍵の交換などAWSに任せることができます。

データの入力から暗号化して保存するまでのイメージ図

f:id:teco_kimura:20200128112805p:plain
AWS KMS暗号フローイメージ図



Userからの入力

まずUserがAppへ個人情報などの重要情報を入力します。 f:id:teco_kimura:20200121125106p:plain Appはスマホアプリなどを想定してます。

暗号化する為のデータキーの作成

AppはKMSを利用して受け取ったデータを暗号化する為の鍵(データキー)をCMKを元に作成します。 ここでAppはAWSのcredentialsというファイルで権限を事前に与えているのでKMSを利用して鍵となるデータキーを作成することができます。

f:id:teco_kimura:20200121125219p:plain

$client = new Aws\Kms\KmsClient([
    'profile' => 'profile-name', // 準備の.aws/credentialsの[]の中の文字列
    'version' => '2014-11-01',
    'region'  =>'ap-northeast-1',
]);

// データキーの作成
$res = $client->generateDataKey([
    'KeyId'  => 'arn:aws:kms:********...',
    'KeySpec'=> 'AES_256',
]);

// $res->get('CiphertextBlob');
// $res->get('KeyId');
// $res->get('Plaintext');

generateDataKeyのレスポンス、最後の3行について

  • CiphertextBlob 暗号化されたデータキーです。
  • KeyId 使用されたキーIDです。
  • Plaintext 暗号化されていないデータキーです。(取り扱い注意!!)

次に作成したデータキーを使用して暗号化したデータを作成します。 f:id:teco_kimura:20200121125334p:plain

$personalData = '暗号化したい文字列などのデータ';

// iv の決定(ランダムに決める)
$ivLength = openssl_cipher_iv_length(ENCRYPT_METHOD);
$iv = openssl_random_pseudo_bytes($ivLength);

// 暗号化
$encryptedPersonalData = openssl_encrypt(
    $personalData,
    'aes-128-cbc',
    $res->get('Plaintext'),
    OPENSSL_RAW_DATA,
    $iv
);

// 暗号化が終わったらメモリからデータキーを消す
unset($resJson->Plaintext);

暗号化完了

これで$encryptedPersonalDataに暗号化されたデータが入りました。

暗号化済みデータの送信、保存

いろいろな方法がありますが今回はDBサーバへデータをPOSTで送信し保存してもらいます。DB サーバ側の処理は省略しますがDBサーバは鍵を持たないのでデータを復号することはできません。




保存したデータを復号して利用するまでのイメージ図

f:id:teco_kimura:20200128113325p:plain
AWS KMS復号フローイメージ図

ユーザのリクエストなど正常な処理がデータを取得要求をしデータを受け取ります。

(省略しますが、サーバ間で認証を行っている前提です。) f:id:teco_kimura:20200128114723p:plain

データを受け取っただけでは正当なサーバも復号することはできず情報を利用することはできません。

このサーバはAWSへの権限(credentials)を持っているのでAWSにCMKを使い暗号化されたデータキーを復号してもらいます。

f:id:teco_kimura:20200121184153p:plain

// 暗号化されたデータキー。図のencryptedDataKey
$ciphertextblob = '*******************************'

// 上記のデータキーの作成と同じ方法
$client = new Aws\Kms\KmsClient([
    'profile' =>'.aws/credentialsのプロフィール名',
    'version' => '2014-11-01',
    'region'  =>'ap-northeast-1',
]);

$res= $client->decrypt(
    ['CiphertextBlob' => $ciphertextblob]
);

// 復号されたデータキー
$res['Plaintext'];

復号されたデータキーを利用し、暗号化されたデータを復号する。

f:id:teco_kimura:20200128121707p:plain

$decrypted = openssl_decrypt(
    $encryptedPersonalData,
    'aes-128-cbc',
    $res['Plaintext'],
    OPENSSL_RAW_DATA,
    $iv
);

復号完了

これでデータが元に戻り無事に暗号化復号が完了です。

改めて全体の処理イメージ図と大まかな流れがこちら

f:id:teco_kimura:20200128121434p:plain
AWS KMS全体フローイメージ図

ソースコード

<?php
require_once './vendor/autoload.php';

use Aws\Kms\KmsClient;
const CMK_KEYID = 'arn:aws:kms:******...';
const ENCRYPT_METHOD = 'aes-128-cbc';

main();
exit;

/**
 * データ復号処理
 */
function main() {

    // 暗号化したいデータ
    $personalData = 'tecotec Kimura';
    print('$personalData='.$personalData.PHP_EOL);

    $kmsClient = new Aws\Kms\KmsClient([
        'profile'=>'*********',
        'version' => '2014-11-01',
        'region'=>'**************',
    ]);

    // データキーの作成
    $res = $kmsClient->generateDataKey([
        'KeyId'  => CMK_KEYID,
        'KeySpec'=> 'AES_256',
    ]);

    // $res->get('CiphertextBlob');
    // $res->get('KeyId');
    // $res->get('Plaintext');

    // iv の決定
    $ivLength = openssl_cipher_iv_length(ENCRYPT_METHOD);
    $iv = openssl_random_pseudo_bytes($ivLength);

    // 暗号化
    $encrypted = openssl_encrypt(
        $personalData,
        ENCRYPT_METHOD,
        $res->get('Plaintext'),
        OPENSSL_RAW_DATA,
        $iv
    );
    print('base64_encode($encrypted)='.base64_encode($encrypted).PHP_EOL);

    // 暗号化が終わったらメモリからも消す
    $ciphertextblob = $res->get('CiphertextBlob');
    unset($res);
    
//-----------
// イメージ図ではここでDBServerへデータの送信、データの受信を行ってます。
// 今回はサンプルソースなので暗号化したデータを送受信せずそのまま復号します。
//-----------

    // AWSを使用してdata keyを復号
    // CiphertextBlobはjson化する際にencodeしているのでdecodeする
    $d = $kmsClient->decrypt(
        ['CiphertextBlob' => $ciphertextblob]
    );

    $plaintext = $d['Plaintext'];

    // 復号したデータキーを使って渡されたデータを復号する
    $decrypted = openssl_decrypt(
        $encrypted,
        ENCRYPT_METHOD,
        $plaintext,
        OPENSSL_RAW_DATA,
        $iv
    );

    // この値が最初に渡した $personalData と一致します。
    print('$decrypted='.$decrypted.PHP_EOL);
}

AWSを使うメリット

  • マスターとなる鍵(CMK)はAWSコンソールで作成した後は保持や管理をする必要がありません。
  • DB Server、Batch Serverのいずれかが物理的に盗まれても復号することはできません。
  • AWSのcredentialだけが盗まれた場合にはAWSコンソールから無効にし再発行することができます。

これですべてが解決するわけではないですが、リスクを分散して安全に鍵を管理できます。 AWSの事前準備ができれば実装はSDKもあり難しくないので簡単な文字列を暗号化復号してみる等、是非一度やってみてください。

tecotec.co.jp