私は最近、Marcus BointonがPHP7.2で暗号に関する素晴らしい講演をしたLARACON EU2018に出席しました。 私は、暗号化がどれほど非常に複雑であるかだけでなく、PhpがSodiumの導入により暗号化をよりアクセスしやすくする方法についても、はるかに感謝して PHPでのデータ暗号化は、私がSpinupWPの作業の一環として取り組んできたものなので、いくつかの洞察を共有する時だと思いました。 これはでこぼこに乗ることができるので、アップバックル!
暗号化の種類
今日使用されているさまざまな暗号化方法があり、最も一般的なものはハッシュ、秘密鍵暗号化、公開鍵暗号化です。 さらに、各暗号化方法には、複数のアルゴリズムまたは暗号があります(それぞれ独自の長所と短所があります)。 この記事では、ハッシュと秘密鍵の暗号化の実装に焦点を当てます。
ハッシュ
ハッシュアルゴリズムは入力値を受け取り、それをメッセージダイジェストに変換します。 一言で言えば、平文の値は固定長のハッシュに変換され、元の値をハッシュアルゴリズムに渡すことによってのみ検証できます。 これにより、ハッシュはユーザーパスワードを格納するのに最適です。
ハッシュは防弾ソリューションではなく、すべてのハッシュアルゴリズムが等しいわけではないことに注目する価値があります。 MD5とSHA1は高速で効率的であり、チェックサムやファイル検証に最適です。 しかし、その速度は、ユーザーのパスワードをハッシュするためにそれらを不適当にします。 現代のGpuの今日の計算能力では、パスワードは、元の平文のパスワードを明らかにし、ほんの数分でブルートフォースによってクラックすることができます。 代わりに、bcryptやArgon2などの意図的に遅いハッシュアルゴリズムを使用する必要があります。
任意のアルゴリズムによって生成されたハッシュ化されたパスワードは、確かに元のデータを不明瞭にし、任意の自称攻撃者を遅くしますが、開発者は利用可能な最強のアルゴリズムを使用するよう努めなければなりません。 幸いなことに、PHPはpassword_hash()
のおかげでこれを簡単にします。
$hash = password_hash($password, PASSWORD_DEFAULT);
password_hash()
関数は安全な一方向ハッシュアルゴリズムを使用するだけでなく、saltを自動的に処理し、時間ベースのサイドチャネル攻撃を防止します。 PHP5.5では、bcryptを使用してハッシュを生成しますが、これは将来、より新しく安全なハッシュアルゴリズムがPHPに追加されるにつれて変更されます。 Argon2は次のデフォルトのハッシュアルゴリズムになる可能性が高く、PASSWORD_DEFAULT
の代わりにPASSWORD_ARGON2I
フラグを渡すことで今日(PHP7.2で)使用できます。
ユーザーのパスワードを確認することもpassword_verify()
機能のおかげで簡単なプロセスです。 ユーザーが提供した平文のパスワードを渡し、格納されたハッシュと比較するだけです:
if (password_verify($password, $hash)) { echo "Let me in, I'm genuine!";}
PHPでパスワードの検証がどのように実行されるかに注意してください。 ユーザーの資格情報をデータベースに保存している場合は、ログイン時に入力したパスワードをハッシュしてから、次のようにデータベースクエリを実行する:
SELECT * FROM usersWHERE username = 'Ashley'AND password = 'password_hash'LIMIT 1;
このアプローチは、サイドチャネル攻撃の影響を受けやすいため、避ける必要があります。 代わりに、ユーザーを返し、PHPでパスワードハッシュを確認します。
SELECT username, password FROM usersWHERE username = 'Ashley'LIMIT 1;
ハッシュはユーザーのパスワードを保存するのに最適ですが、アプリケーションがユーザーの介入なしにアクセスする必要がある任意のデータには機能しません。 ユーザーのクレジットカード情報を暗号化する課金アプリケーションを考えてみましょう。 毎月、私たちのアプリケーションは、前月の使用量のためにユーザーに請求する必要があります。 それは私たちのアプリケーションは、それが平文でそれを取得するための元のデータを知っている必要があるため、クレジットカードのデータをハ
秘密鍵暗号化を救出へ!
秘密鍵暗号化
秘密鍵暗号化(または対称暗号化とも呼ばれます)は、データの暗号化と復号化の両方に単一の鍵を使用します。 PHP7.2で導入されたSodiumを使用してこのようなメカニズムをどのように実装するかを見てみましょう。 古いバージョンのPHPを実行している場合は、PECL経由でsodiumをインストールできます。
まず、random_bytes()
関数を使用して生成できる暗号化キーが必要です。 通常、これは一度だけ行い、環境変数として保存します。 このキーは、すべてのコストで秘密にする必要があることを覚えておいてください。 キーが侵害されると、暗号化されたデータも侵害されます。
$key = random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
値を暗号化するために、$key
と$nonce
を使用してsodium_crypto_secretbox()
に渡します。 ノンスはrandom_bytes()
を使用して生成されますが、これは同じノンスを再利用することはできないためです。
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);$ciphertext = sodium_crypto_secretbox('This is a secret!', $nonce, $key);
これは、後で値を復号化するためにnonceが必要なため、問題が発生します。 幸いなことに、noncesを秘密にする必要はないので、データベースに保存する前に値を$ciphertext
、次にbase64_encode()
に追加することができます。
$encoded = base64_encode($nonce . $ciphertext);var_dump($encoded);// string 'v6KhzRACVfUCyJKCGQF4VNoPXYfeFY+/pyRZcixz4x/0jLJOo+RbeGBTiZudMLEO7aRvg44HRecC' (length=76)
値の復号化に関しては、逆のことを行います。
$decoded = base64_decode($encoded);
ノンスの長さがわかっているので、値を復号化する前にmb_substr()
を使用して抽出できます。
$nonce = mb_substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit');$ciphertext = mb_substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit');$plaintext = sodium_crypto_secretbox_open($ciphertext, $nonce, $key);var_dump($plaintext);// string 'This is a secret!' (length=17)
それは、ナトリウムのおかげで、PHPで秘密鍵の暗号化にあるすべてです!
エンベロープ暗号化
上記で概説したアプローチは確かに正しい方向への一歩ですが、秘密鍵が侵害された場合でも、データは脆弱です。 アプリケーションをホストするサーバーにアクセスする悪意のあるユーザーを考えてみましょう。 このシナリオでは、攻撃者がデータを暗号化するために使用した秘密鍵を発見できる可能性があります。 これにより、データは完全に公開されます。
簡単な解決策は、暗号化されたデータと同じ場所に秘密鍵を保存しないことですが、これは問題を提示します。 オンデマンドでどのように暗号化と復号化を行うのですか? Google Cloud Key Management Service(Cloud KMS)に入ります。
Cloud KMSは、暗号化キーを安全にホストするためにGoogleが提供するサービスです。 これは、自動キー回転と遅延キー破壊を含むキーストレージの周りに便利な機能の様々なを提供します。 ただし、この例では、主に秘密鍵をデータから離れて保存することに関心があります。
物事をより安全にするために、我々はエンベロープ暗号化として知られている技術を使用するつもりです。 基本的に、エンベロープ暗号化には、キーを別のキーで暗号化することが含まれます。 私たちは二つの理由でこれを行います:
- Cloud KMSには、暗号化および復号化できるデータに64KiBのサイズ制限があります。 したがって、すべてのデータを一挙に送信することはできない場合があります。
- さらに重要なのは、機密性の高い平文データを第三者に送信したくないことです。
平文データをCloud KMSに送信する代わりに、機密データをデータベースに書き込むたびに一意の暗号化キーを生成します。 このキーは、データの暗号化に使用されるデータ暗号化キー(DEK)として知られています。 次に、DEKは暗号化されるためにCloud KMSに送信され、暗号化キー(KEKと呼ばれます)が返されます。 最後に、KEKは暗号化されたデータの横に並んでデータベースに格納され、DEKは破棄されます。 プロセスは次のようになります:
- 一意の暗号化キー(DEK)の生成
- 秘密キー暗号化を使用してデータを暗号化
- 一意の暗号化キー(DEK)をCloud KMSに送信して暗号化し、KEKを返します
- 暗号化されたデータと暗号化されたキー(KEK)をサイドバイサイド
- 生成されたキー(DEK)を破棄します(DEK)。)
データを復号化すると、プロセスは逆になります:
- データベースから暗号化されたデータと暗号化されたキー(KEK)を取得します
- 復号化のためにKekをCloud KMSに送信し、DEKを返します
- DEKを使用して暗号化されたデータを復号化します
- DEKを破棄します
これを念頭に置いて、私は非常に単純なヘルパークラスを作成しましたエンベロープ暗号化を実行するため。 クイックスタートと認証ガイドでは、開始するために必要なすべての概要が説明されているため、Google Cloud consoleで必要な手順については説明しません。 簡潔にするために、エラー処理などはありません。 この例では。
<?phpuse Google_Service_CloudKMS as Kms;use Google_Service_CloudKMS_DecryptRequest as DecryptRequest;use Google_Service_CloudKMS_EncryptRequest as EncryptRequest;class KeyManager{ private $kms; private $encryptRequest; private $decryptRequest; private $projectId; private $locationId; private $keyRingId; private $cryptoKeyId; public function __construct(Kms $kms, EncryptRequest $encryptRequest, DecryptRequest $decryptRequest, $projectId, $locationId, $keyRingId, $cryptoKeyId) { $this->kms = $kms; $this->encryptRequest = $encryptRequest; $this->decryptRequest = $decryptRequest; $this->projectId = $projectId; $this->locationId = $locationId; $this->keyRingId = $keyRingId; $this->cryptoKeyId = $cryptoKeyId; } public function encrypt($data) { $key = random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES); $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); $ciphertext = sodium_crypto_secretbox($data, $nonce, $key); return ; } public function decrypt($secret, $data) { $decoded = base64_decode($data); $key = $this->decryptSecret($secret); $nonce = mb_substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit'); $ciphertext = mb_substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit'); return sodium_crypto_secretbox_open($ciphertext, $nonce, $key); } private function encryptKey($key) { $this->encryptRequest->setPlaintext(base64_encode($key)); $response = $this->kms->projects_locations_keyRings_cryptoKeys->encrypt( $this->getResourceName(), $this->encryptRequest ); return $response; } private function decryptSecret($secret) { $this->decryptRequest->setCiphertext($secret); $response = $this->kms->projects_locations_keyRings_cryptoKeys->decrypt( $this->getResourceName(), $this->decryptRequest ); return base64_decode($response); } private function getResourceName() { return sprintf( 'projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s', $this->projectId, $this->locationId, $this->keyRingId, $this->cryptoKeyId ); }}
実際の暗号化と復号化の方法は、上記で紹介した秘密鍵の実装とほぼ同じであることがわかります。 ただし、違いは、複数の暗号化キーを使用していることです。 ヘルパークラスの動作を見てみましょう。 あなたの提供する必要があります$projectId
, $locationId
, $keyRingId
そして$cryptoKeyId
はGoogle Cloud consoleから入手できます。
<?phpuse Google_Service_CloudKMS as Kms;use Google_Service_CloudKMS_DecryptRequest as DecryptRequest;use Google_Service_CloudKMS_EncryptRequest as EncryptRequest;$client = new Google_Client();$client->setAuthConfig(getenv('GOOGLE_CREDENTIALS_FILE'));$client->addScope('https://www.googleapis.com/auth/cloud-platform');$keyManager = new KeyManager( new Kms($client), new EncryptRequest(), new DecryptRequest(), $projectId, $locationId, $keyRingId, $cryptoKeyId);$encrypted = $keyManager->encrypt('This is a secret!');var_dump($encrypted);// array (size=2)// 'data' => string 'uKjmEU7e1JEU+2vL3hBK2wBk6afCSgb+Y4GQtu/mmLuffgHlnqxnqOMPOI6WGkM18vAGGvFVDTvd' (length=76)// 'secret' => string 'CiQAdA0emUW2nhlU3RijX/5GnUsTnPPrQdLZNxdHWXWYugx49a4SSQBHyYr0T/PEbKwyFhIkaZl28oKkJRkXqNcqOL4Z+OTQFLpGvS6zCDt2mFn/nUQ/bi4znD4DORk9ZDTqiIBK3UNFUZcrXvoExds=' (length=152)$decrypted = $keyManager->decrypt($encrypted, $encrypted);var_dump($decrypted);// string 'This is a secret!' (length=17)
攻撃者がシステムを侵害した場合、Cloud KMSのAPI認証情報も取得できますか? 使用される認証方法に応じて、はい、それは可能性があります。 そのような場合は、エンベロープ暗号化が通常の秘密鍵暗号化よりもどのように安全であるか疑問に思うかもしれませんか? 主な違い(意図されている)は、APIアクセスを取り消すことができるため、機密データで行われた攻撃者を阻止することです。 誰かがあなたの家の鍵を盗んだ場合、それはあなたのロックを変更するのと同じです。 単一のローカルキーが侵害される通常の秘密キー暗号化では、その贅沢はありません。 攻撃者は、機密データを復号化するために、世界中のすべての時間を持っています。
ラップアップ
データのセキュリティと暗号化は広大な主題であり、私はPHPを使用して機密データを保護する方法のほんの一握りをカバーしてきました。 (私たちは以前にあなたのローカル環境でこの種のデータを保護することについて書きました)