暗号によるデータ保存
Cryptographic Storage Cheat Sheet
- 原典
- Cryptographic Storage Cheat Sheet(OWASP Cheat Sheet Series)
- 原文
- GitHub 上の Markdown
- 底本
bac04fb5(2026-07-29 時点)- ライセンス
- CC BY-SA 4.0(原典と同一。訳文も同ライセンスで再配布できます)
はじめに
この記事は、保存されたデータを保護する仕組みを実装するときに従うべき、単純なモデルを示す。
パスワードは可逆な暗号化で保存すべきではなく、安全なパスワードハッシュアルゴリズムを使う。 パスワードの保存についてさらに詳しくはパスワードの保存を参照する。
アーキテクチャの設計
どのアプリケーションでも設計の最初の一歩は、システム全体のアーキテクチャを検討することである。 これは技術的な実装に大きく影響する。
この検討は、アプリケーションの脅威モデル、すなわち誰からそのデータを守ろうとしているのかを考えることから始めるべきである。
シークレットや鍵の専用管理システムを使うと、保護の層が一つ増え、シークレットの管理も著しく容易になる。 ただし複雑さと運用の手間が増えるので、すべてのアプリケーションで現実的とはいえない。 多くのクラウド環境がこうしたサービスを提供しているので、可能な場合はそれを活用するべきである。 この話題についてさらに詳しくはシークレット管理を参照する。
どこで暗号化を行うか
暗号化はアプリケーションのスタックの複数の層で行える。
- アプリケーションの層
- データベースの層(SQL Server TDE など)
- ファイルシステムの層(BitLocker、LUKS など)
- ハードウェアの層(暗号化に対応した RAID カードや SSD など)
どの層が最も適切かは脅威モデルによって決まる。 たとえばハードウェアの層での暗号化はサーバーの物理的な盗難に対しては有効であるが、攻撃者がサーバーをリモートから侵害できる場合には何の保護も与えない。
機微な情報の保存を最小限にする
機微な情報を守る最善の方法は、そもそも保存しないことである。 これはあらゆる種類の情報にあてはまるが、最もよくあてはまるのはクレジットカードの情報である。 攻撃者にとって非常に望ましい対象であり、PCI DSS が保存方法について厳しい要件を課しているからである。 可能なかぎり、機微な情報の保存は避けるべきである。
アルゴリズム
共通鍵暗号では、128 ビット以上(理想的には 256 ビット)の鍵と安全な暗号利用モードを用いた AES を、優先すべきアルゴリズムとする。
公開鍵暗号では、Curve25519 のような安全な曲線を用いた楕円曲線暗号(ECC)を優先すべきアルゴリズムとする。 ECC が使えず RSA を使わなければならない場合は、鍵の長さを 2048 ビット以上にする。
他にも多くの共通鍵アルゴリズムと公開鍵アルゴリズムがあり、それぞれ長所と短所を持つ。 特定の用途では AES や Curve25519 より優れていることも劣っていることもある。 それらを検討するときは、次のような要素を考慮に入れるべきである。
- 鍵の長さ
- そのアルゴリズムに対する既知の攻撃と弱点
- そのアルゴリズムの成熟度
- NIST の暗号アルゴリズム検証プログラムのような第三者による承認
- 性能(暗号化と復号の両方)
- 利用できるライブラリの品質
- そのアルゴリズムの可搬性(どれだけ広く対応されているか)
FIPS 140-2 や PCI DSS のように、使えるアルゴリズムを制限する規制上の要件がある場合もある。
独自のアルゴリズム
作らないこと。
暗号利用モード
AES のようなブロック暗号がストリーム暗号と同じように任意の量のデータを暗号化できるようにするためのモードが各種存在する。 これらのモードは安全性と性能の特性が異なり、その全面的な議論はこのチートシートの範囲を超える。 一部のモードは安全な初期化ベクトル(IV)などの属性を生成する要件を持つが、これらはライブラリが自動的に扱うべきものである。
利用できる場合は、常に認証付きのモードを使うべきである。 これらは機密性に加えて、データの完全性と真正性の保証を与える。 最もよく使われる認証付きのモードは GCM と CCM であり、これを第一に選ぶ。
GCM も CCM も使えない場合は、CTR モードまたは CBC モードを使う。 これらはデータの真正性について何の保証も与えないので、Encrypt-then-MAC のような別途の認証を実装する必要がある。 この方法を可変長のメッセージに用いるときは注意が必要である。
ECB は、ごく限られた状況を除いて使うべきではない。
ランダムなパディング
RSA では、ランダムなパディングを有効にすることが不可欠である。 ランダムなパディングは OAEP(Optimal Asymmetric Encryption Padding)とも呼ばれる。 この種の防御は、ペイロードの先頭にランダム性を加えることで既知平文攻撃を防ぐ。
この場合、通常は PKCS#1 のパディング方式が使われる。
安全な乱数生成
乱数(や乱数文字列)は、暗号鍵、IV、セッション ID、CSRF トークン、パスワード再設定トークンの生成など、安全性に関わるさまざまな機能で必要になる。 そのため、これらが安全に生成され、攻撃者が推測や予測できないことが重要である。
一般に、コンピュータが(専用のハードウェアなしに)真の乱数を生成することはできない。 そこでほとんどのシステムと言語は、二種類のランダム性を提供する。
疑似乱数生成器(PRNG)は品質の低いランダム性を提供するがはるかに高速であり、安全性に関わらない機能(ページ上の結果の並び替え、UI 要素のランダム化など)に使える。 しかし安全性に関わるものには使ってはならない。 攻撃者がその出力を推測または予測できることがしばしばあるからである。
暗号論的に安全な疑似乱数生成器(CSPRNG)は、はるかに高品質なランダム性(より厳密には、より多くのエントロピー)を生み出すよう設計されており、安全性に関わる機能に使っても安全である。 ただし低速で CPU を多く使い、大量の乱数データを要求すると場合によっては処理が待たされることになる。 そのため安全性に関わらないランダム性を大量に必要とする場合には、適切でないこともある。
以下の表は、言語ごとの推奨アルゴリズムと、使うべきでない安全でない関数を示す。
| 言語 | 安全でない関数 | 暗号論的に安全な関数 |
|---|---|---|
| C | random(), rand() | getrandom(2) |
| Java | Math.random(), StrictMath.random(), java.util.Random, java.util.SplittableRandom, java.util.concurrent.ThreadLocalRandom | java.security.SecureRandom, java.util.UUID.randomUUID() |
| PHP | array_rand(), lcg_value(), mt_rand(), rand(), uniqid() | random_bytes()、PHP 8 では Random\Engine\Secure、PHP 7 では random_int()、PHP 5 では openssl_random_pseudo_bytes() |
| .NET/C# | Random() | RandomNumberGenerator |
| Objective-C | arc4random()/arc4random_uniform()(RC4 暗号を使う)、GKRandomSource のサブクラス、rand()、random() | SecRandomCopyBytes |
| Python | random() | secrets |
| Ruby | rand(), Random | SecureRandom |
| Go | math/rand パッケージの rand | crypto/rand パッケージ |
| Rust | rand::prng::XorShiftRng | rand::prng::chacha::ChaChaRng および Rust のライブラリの他の CSPRNG |
| Node.js | Math.random() | crypto.randomBytes(), crypto.randomInt(), crypto.randomUUID() |
UUID と GUID
汎用一意識別子(UUID、GUID)は、ランダムな文字列を手早く生成する手段として使われることがある。 妥当なランダム性の源になりうるが、それは生成される UUID の種別やバージョンによって決まる。
とくにバージョン 1 の UUID は高精度のタイムスタンプと生成したシステムの MAC アドレスから構成されるため、ランダムではない(ただしタイムスタンプが 100ns 単位であるため推測は難しいかもしれない)。 バージョン 4 の UUID はランダムに生成されるが、それが CSPRNG を用いて行われるかは実装によって決まる。 使用している言語やフレームワークにおいてそれが安全だとわかっていないかぎり、UUID のランダム性に依存すべきではない。
多層防御
アプリケーションは、暗号による対策が破れた場合でも安全であるように設計するべきである。 暗号化された形で保存されている情報は、追加の安全対策の層によっても保護されているべきである。 また、暗号化された URL パラメータの安全性に依存せず、情報への不正なアクセスを防ぐために強固なアクセス制御を強制するべきである。
鍵管理
手続き
鍵管理のあらゆる側面を網羅する、正式な手続きを実装し、試験しておくべきである。 次のものを含む。
- 新しい鍵の生成と保存
- 必要な関係者への鍵の配布
- アプリケーションサーバーへの鍵の展開
- 古い鍵のローテーションと廃止
鍵の生成
鍵は、安全な乱数生成の節で述べたような暗号論的に安全な関数を用いてランダムに生成するべきである。 鍵は、よく使われる単語や語句に基づくもの、あるいはキーボードを適当に叩いて作った「ランダムな」文字に基づくものであってはならない。
複数の鍵を使う場合(データ暗号化鍵と鍵暗号化鍵を分けるなど)、それらは互いに完全に独立であるべきである。
鍵の寿命とローテーション
暗号鍵は、次のような複数の基準に基づいて変更(ローテーション)するべきである。
- 以前の鍵が漏洩したとわかった、または疑われる場合
- 鍵にアクセスできた人が組織を離れたことによって生じる場合もある。
- 定められた期間(暗号期間、cryptoperiod)が経過した場合
- 適切な暗号期間に影響する要素は多く、鍵の長さ、データの機微さ、システムの脅威モデルなどがある。さらに詳しくは NIST SP 800-57 の 5.3 節を参照する。
- その鍵で特定の量のデータを暗号化したあと
- 通常、64 ビット鍵では
2^35バイト(約 34GB)、128 ビットのブロックサイズでは2^68バイト(約 295 エクサバイト)となる。
- 通常、64 ビット鍵では
- そのアルゴリズムが提供する安全性に大きな変化があった場合(新しい攻撃が公表されたなど)
これらの基準のいずれかが満たされたら、新しい鍵を生成し、以後の新しいデータの暗号化にはそれを使う。 古い鍵で暗号化された既存のデータの扱いには、主に二つの方法がある。
- 復号して新しい鍵で再暗号化する。
- 各項目に暗号化に使った鍵の ID を付け、古いデータを復号できるように複数の鍵を保存する。
一般には一つめを選ぶべきである。 アプリケーションのコードと鍵管理の手続きの両方が大幅に単純になるからである。 ただし常に実現可能とは限らない。 なお、データの複製の古いバックアップを復号する必要が生じる場合に備えて、廃止した鍵も一定期間は保存しておくのが一般的である。
鍵のローテーションに必要なコードと手続きは、それが必要になる前に用意しておくことが重要である。 そうすれば漏洩時に迅速にローテーションできる。 加えて、アルゴリズムや実装に新しい脆弱性が見つかった場合に備え、暗号アルゴリズムやライブラリを変更できるようにする手続きも実装しておくべきである。
鍵の保存
暗号鍵を安全に保存することは最も解決の難しい問題の一つである。 データを復号するために、アプリケーションは常に何らかの形で鍵にアクセスできる必要があるからである。 アプリケーションを完全に侵害した攻撃者から鍵を完全に守ることはできないかもしれないが、鍵の入手を難しくするための手立てはいくつもある。
利用できる場合は、OS、フレームワーク、クラウド事業者が提供する安全な保存機構を使うべきである。 次のようなものがある。
- 物理的なハードウェアセキュリティモジュール(HSM)
- 仮想 HSM
- Amazon KMS や Azure Key Vault のような鍵の保管庫
- Conjur や HashiCorp Vault のような外部のシークレット管理サービス
- .NET Framework の ProtectedData クラスが提供する安全な保存 API
鍵を単に設定ファイルに置くのに比べ、この種の安全な保存を使う利点は多い。 詳細は採用する仕組みによって異なるが、次のものが含まれる。
- 鍵の集中管理。とくにコンテナ化された環境で有効である。
- 鍵のローテーションと交換が容易になる。
- 鍵の安全な生成。
- FIPS 140 や PCI DSS のような規制上の基準への準拠が単純になる。
- 攻撃者が鍵を持ち出したり盗んだりすることが難しくなる。
共用ホスティング環境のように、これらのいずれも利用できない場合もある。 その場合、暗号鍵に対して高い水準の保護を得ることはできない。 それでも次の基本的な規則は守れる。
- 鍵をアプリケーションのソースコードに直接埋め込まない。
- 鍵をバージョン管理システムに入れない。
- 鍵を含む設定ファイルを、制限の厳しい権限で保護する。
- 鍵を環境変数に保存することを避ける。phpinfo() のような関数や
/proc/self/environファイルを通じて偶発的に露出しうるからである。
シークレットの安全な保存についてさらに詳しくはシークレット管理を参照する。
鍵とデータの分離
可能であれば、暗号鍵は暗号化されたデータとは別の場所に保存するべきである。 たとえばデータがデータベースに保存されているなら、鍵はファイルシステムに保存する。 そうすれば、攻撃者が(ディレクトリトラバーサルや SQL インジェクションなどによって)どちらか一方にしかアクセスできない場合、鍵とデータの両方を得ることはできない。
環境のアーキテクチャによっては、鍵とデータを別のシステムに保存できることもあり、その場合はより高い分離が得られる。
保存する鍵の暗号化
可能であれば、暗号鍵そのものも暗号化された形で保存するべきである。 そのためには少なくとも二つの鍵が必要になる。
- データ暗号化鍵(DEK)はデータの暗号化に使う。
- 鍵暗号化鍵(KEK)は DEK の暗号化に使う。
これが有効に働くには、KEK を DEK とは別に保存しなければならない。 暗号化された DEK はデータと一緒に保存してもよいが、別のシステムに保存された KEK も攻撃者が入手できなければ、それは使えない。
KEK は少なくとも DEK と同じ強度を持つべきである。 DEK と KEK の管理方法についてさらに詳しくは Google のエンベロープ暗号化の解説を参照する。
共用ホスティング環境のように、KEK と DEK を別々に保存できない単純なアプリケーション構成では、この方法の価値は限られる。 攻撃者が両方の鍵を同時に入手できる可能性が高いからである。 ただし、熟練していない攻撃者に対しては追加の障壁になりうる。
鍵導出関数(KDF)を用いて、利用者が入力した値(パスフレーズなど)から KEK を生成し、それでランダムに生成した DEK を暗号化することもできる。 これにより、(利用者がパスフレーズを変えたときに)データを再暗号化することなく KEK を容易に変更できる。 DEK は同じままだからである。