暗号化処理をときほぐす: パスワードの格納に Base64 を使ってはいけない

パラゴン・イニシアチブ・エンタープライズのスタッフ(P.I.E. Staff)
Paragon Initiative Enterprises is an Orlando-based company that provides software consulting, application development, code auditing, and security engineering services.
(翻訳: Takayuki Kawamoto)

Original document was appeared as “You Wouldn't Base64 a Password - Cryptography Decoded” on 2015-08-07 at their blog in Paragon Initiative Enterprises, LLC.
1st appeared at www.markupdancing.net: 2016-03-10 14:35:40.
This usage of original document should follow the terms of Attribution-ShareAlike 4.0 International (CC BY-SA 4.0).

ネットには、プログラミングやセキュリティに関するバカげたアドバイスがたくさんある。バカさ加減にも色々あって、その一つは書いている人間が無知だったりするからだが、それだけでなく明確に語るよりも過度に正確さへ固執して多くの人を煙に巻いてしまうからでもある。

君は、暗号化の話は不気味で、ややこしくて、いくぶん威圧的な話題だと感じていて、(何も問題がなければ)気にしなくてもいいようなことだと思っているかもしれないけれど、君がこの記事を読み終えた時には、暗号論の話題が出てきたときにセキュリティ関係者が話していることをしっかり理解できるようになっている筈だ。

注意:この記事で使っているコードのサンプルは、説明の材料として使うために書いたものだ。君のプログラムにこれを使ってはいけない。もし現実の開発で使えるコードを知りたいなら、StackOverflow で当社の開発担当役員が投稿した回答を確認してほしい。

冒頭に戻る

開発者のための暗号化処理の基礎概念

では、こういう質問から始めよう。「暗号化処理とは、正確に言って何だろうか?」いちばん簡単に答えを要約するなら、こうなる。「暗号化処理とは、アプリケーションを安全にするために数学を使うということだ。 (“Cryptographic features use math to secure an application.”)」

この答えをもう少しだけ掘り下げてみよう。暗号化で扱うアルゴリズムにはたくさんの種類があり、それらは二つの規準に従って大別できる。

  1. 開発者がどれくらいの情報を与えるのか。
  2. 何をゴールとして意図しているのか。

暗号化処理の鉄則:自力で暗号化処理を実装するな

暗号化処理の開発は専門家に委ねるのがベストだ。どういうやりかたにせよ、コードを弄り回すのは勝手にすればいいが、そうしたコードを公開するシステムに自分で実装したり、公開するシステムに実装してしまうかもしれない人とコードを共有しないことだ。

そうする代わりに、専門家が慎重に検証してきている暗号化処理の高度なライブラリを使ってもらいたい。PHP の暗号化処理ライブラリとしてお勧めできるものを集めてある

鍵なしの暗号化処理

ここで扱う暗号化処理のうち、最も簡単なものは暗号論的ハッシュ関数だ。これは、一個の入力に対して固定長の決まった出力を返す。

hash("sha256", "");
// e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

hash("sha256", "The quick brown fox jumps over the lazy dog");
// d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592

しっかり設計された暗号論的ハッシュ関数、例えば BLAKE2 や SHA256 を使えば、入力するメッセージを少し変えただけで出力のハッシュ値は劇的に変ってしまうだろう。

hash("sha256", "The quick brown fox jumps over the lazy cog");
// e4c4d8f3bf76b692de791a173e05321150f7a345b46484fe427f6acc7ecc81be

単純なハッシュ関数は高速で決定論的だ。君がどんなメッセージを選んでも、その出力であるハッシュ値を計算できる。ハッシュ関数を使うと、エラーチェックとか、他の暗号化処理の仕組みの一部として、たいていはうまく使える。そして、そういう仕組みは開発者が自力でプログラミングする必要なんてない。

暗号論的ハッシュ関数は、一方向のデータ変換だ。好きなメッセージから簡単にハッシュ値(しばしば「メッセージ・ダイジェスト」とも呼ばれる)を計算できるが、その逆にハッシュ値から元のメッセージを計算するのは簡単ではない。

(MD5 のような)幾つかのハッシュ関数は、セキュリティ上の安全性が低く、出力されるハッシュ値の桁も少ない。その結果、別々のメッセージから同一のハッシュ値が出力されてしまうということが、いまやありふれた話題になっている

秘密鍵による暗号化処理

大多数の暗号アルゴリズムは、ハッシュ関数ほど単純ではない。その結果、それら大多数の暗号アルゴリズムはハッシュ関数よりも有効で、セキュリティ上の安全性も更に高いので、「よし。この出力は、この入力から計算した文字列だな(“Yes, this output can be reproduced from this input.”)」などということにはならない。

そのためには、たいていの場合に二つの入力を必要とする。入力する元のメッセージと、秘密鍵(secret key)だ。秘密鍵はランダムな長さのユニークな文字列であり、暗号化した情報を送る側と受け取る側だけがお互いにその文字列を知っており、そして他の誰も知らないような文字列である。

秘密鍵を使うハッシュ関数

秘密鍵を使うハッシュ関数、例えば HMAC のような関数は、メッセージと秘密鍵を入力として許容する特別な実装であり、メッセージ認証符号(“Message Authentication Code (MAC)”)を出力する。

hash_hmac("sha256", "The quick brown fox jumps over the lazy dog", "secret key");
// 4a513ac60b4f0253d95c2687fa104691c77c9ed77e884453c6a822b7b010d36f

hash_hmac("sha256", "The quick brown fox jumps over the lazy cog", "secret key");
// ed6b9bc9d64e4c923b2cc19c15fff329f343f30884935c10e617e0fe067adef1

hash_hmac("sha256", "The quick brown fox jumps over the lazy dog", "secret kez");
// 291579f3123c3126df04a57f78768b6290df93f979b86af25fecd98a9811da5a

hash_hmac("sha256", "The quick brown fox jumps over the lazy cog", "secret kez");
// 298bb0244ebc987810de3892384bb4663742a540db2b3a875f66b09d068d1f64

秘密鍵を使うハッシュ関数は、ハッシュ関数よりも更に有効だ。秘密鍵をもつ人物だけが、元のメッセージから MAC を計算できる。したがって、もし君が或るメッセージを伝達するときに、そのメッセージの MAC を一緒に伝達して秘密鍵は伝達しない場合、そのメッセージは本物だと合理的に確信できる。[訳注:自分で自分に伝達して内容が本物だという話は奇妙ですが、もちろん受信したのが別人で、その人も同じ秘密鍵を知っている場合には、その相手もそのメッセージが改竄されていないことを確信できます。]

秘密鍵による暗号化

注意:メッセージの真正性を伴わない暗号化は、選択暗号文攻撃(CCA: chosen ciphertext attacks)に対して脆弱だ。これについては、PHP でデータを安全に暗号化するための報告書を参照してもらいたい。

形式上は、暗号化というものは可逆のプロセスであり、或るメッセージ(「平文(ひらぶん, “plaintext”)」と呼ばれる)と秘密鍵とを使って、見た目はランダムな文字列(「暗号文(“ciphertext”)」と呼ばれる)へ変換することである。つまり、$message$key が与えられると、一意のランダムな文字列として encrypt( $message, $key ) が返らなくてはならない。

しかし残念ながら、(ECB モードとも呼ばれる)秘密鍵を使った単純な暗号化は安全ではない。もし ECB モードでメッセージに含まれる同じブロック(よく普及している暗号アルゴリズムの AES では 16 バイト長のブロック)を同一の秘密鍵で暗号化した場合、その出力は同じ文字列となってしまうからだ。

そこで、近年の秘密鍵を使った暗号化では、実際に入力として二つ以上の情報を要求している。plaintext であるメッセージと秘密鍵だけでなく、(CBC モードでは “IV” と呼ばれる)一意の初期化ベクトル、あるいは(CTR モードでは一度だけ使われるという意味で)ノンス(“nonce”)を要求する。初期化ベクトルとノンスには、厳密には違いがある

このページに掲載しているコードは安全ではない。暗号化の鍵も安全ではない。

/**
 * This code is for example purposes only. DO NOT USE IT.
 * Use https://github.com/defuse/php-encryption instead
 * 
 * Demo: http://3v4l.org/ih8om
 */
bin2hex(
    openssl_encrypt(
      /* Message: */
        "The quick brown fox jumps over the lazy dog",
      /* Cipher algorithm and block mode: */
        'aes-128-ctr',
      /* Encryption key: (don't use weak keys like this ever, it's just an example!): */
        "\x01\x02\x03\x04" . "\x05\x06\x07\x08" . "\x09\x0a\x0b\x0c" . "\x0d\x0e\x0f\x10",
      /* Constant that means "don't encode": */
        OPENSSL_RAW_DATA,
      /* Initialization Vector or Nonce -- don't ever actually use all NULL bytes: */
        str_repeat("\0", 16) // This is a really bad way to generate a nonce or IV. 
    )
);
// 8f99e1315fcc7875325149dda085c504fc157e39c0b7f31c6c0b333136a7a8877c4971a5ce5688f94ae650

/**
 * This code is for example purposes only. DO NOT USE IT.
 * Use https://github.com/defuse/php-encryption instead
 * 
 * Demo: http://3v4l.org/ZgW38
 */
openssl_decrypt(
  /* Message: */
    hex2bin(
        "8f99e1315fcc7875325149dda085c504fc157e39c0b7f31c6c0b333136a7a8877c4971a5ce5688f94ae650"
    ),
  /* Cipher algorithm and block mode: */
    'aes-128-ctr',
  /* Encryption key: (don't use weak keys like this ever, it's just an example!): */
    "\x01\x02\x03\x04" . "\x05\x06\x07\x08" . "\x09\x0a\x0b\x0c" . "\x0d\x0e\x0f\x10",
  /* Constant that means "don't encode": */
    OPENSSL_RAW_DATA,
  /* Initialization Vector or Nonce -- don't ever actually use all NULL bytes: */
    str_repeat("\0", 16) // This is a really bad way to generate a nonce or IV.
);
// The quick brown fox jumps over the lazy dog

暗号化処理の実例としては説明不足だが、更に詳しい(初期化ベクトルを正しく出力している)例は、こちらを参照してもらいたい

OpenSSL と対称鍵暗号を使って更に詳しい検証を行った結果については、我々の報告書を参照してもらいたい

データの復号は、同じ初期化ベクトルやノンス、そして同じ秘密鍵を使った場合にのみ成功する。とは言うものの、実際には秘密鍵だけが秘匿されていて、初期化ベクトルやノンスは暗号化されたメッセージと一緒に公開されている。

認証された秘密鍵による暗号化(認証付き暗号化)

我々が既に公開した “Using Encryption and Authentication Correctly (for PHP developers)” という記事を読んだ方はご承知のとおり、秘密鍵による暗号化は認証と組み合わせない限り改竄には脆弱だ。

これまでに証明されている中で安全とされている対策は、AEAD(Authenticated Encryption with Associated Data)モードを使うか、あるいは MAC を使って、メッセージを暗号化した後に暗号化したデータを必ず認証することである。

もし君が、暗号化してから MAC で認証するような仕組みに従うのであれば、二つの秘密鍵を必要とする。一つはメッセ―ジの暗号化に使い、もう一つは MAC に使う。言い換えると、これまで見てきた二つの節で紹介した手法を以下のように組み合わせるとよい。

/**
 * This code is for example purposes only. DO NOT USE IT.
 * Use https://github.com/defuse/php-encryption instead
 */
$nonce = random_bytes(16);
$ciphertext = openssl_encrypt(
  /* Message: */
    "The quick brown fox jumps over the lazy dog",
  /* Cipher algorithm and block mode: */
    'aes-128-ctr',
  /* Encryption key: (don't use weak keys like this ever, it's just an example!)
   *    Instead, you want to generate 16, 24, or 32 random bytes (i.e. random_bytes(16))
   *    on your own. It's generally a bad idea to copy and paste security code.
   */
    "\x01\x02\x03\x04" . "\x05\x06\x07\x08" . "\x09\x0a\x0b\x0c" . "\x0d\x0e\x0f\x10",
  /* Constants that mean "don't encode" and "we have no padding" to the OpenSSL API: */
    OPENSSL_RAW_DATA + OPENSSL_ZERO_PADDING,
  /* Initialization Vector or Nonce: */
    $nonce
);
// You should choose a better HMAC key than we did for this article:
$mac = hash_hmac("sha256", $nonce.$ciphertext, "\xff\xfe\xfd\xfc" . "\xfb\xfa\xf9\xf8" . "\xf7\xf6\xf5\xf4" . "\xf3\xf2\xf1\xf0", true);
echo bin2hex($nonce.$ciphertext.$mac);
/*
   71b5546f 6cb857cd 0d8f8be3 f9312c74 <- Nonce (randomly chosen)
   356146df 274552c2 e98d3008 b1dfa35c <- Ciphertext
   60d6130d 9c9ca525 6c2f2f25 0b321176
   06563174 c3b073a0 5ab263
   4d1c7416 b086a316 a0474a05 84e3793c <- MAC
   a32fde09 0d82a5ef 213cb329 da3b5b06 
 */

暗号化処理を組み合わせて使う場合には、よくよく注意する必要がある。上記で示した基本的な手順は、冗長化を施していないからだ。

言うまでもないことだが、君が認証付きの暗号化を必要とする場合に暗号化処理や認証の処理を重複して加えるのは、端的に言って馬鹿げている。

公開鍵による暗号化

公開鍵による暗号化は、技術を心得ていない人たちには理解し辛いものだ。それどころか技術を心得ている人たちにとってすら、重要なポイントを外すことなく、あるいは不用意に数学を持ち込んで素人を困惑させることなしに上手く説明するのが難しい。下手に説明すると、たいていは酷い混乱を引き起こして、場合によっては間違った理解を与えてしまう。(言うなれば、嘘の納得 (fauxreka moment) だ。)

private key vs. public key

ここで、まず君が知っておくべきことを言おう。秘密鍵を使った暗号化は、情報の送り手と受け手の双方が一つの同じ秘密鍵を持っているのに対して、公開鍵を使った暗号化は送り手と受け手がそれぞれ二つの鍵を持つ。

残念なことに、公開鍵の暗号化が発見されたとき、秘密鍵の暗号化で使われていた「鍵」という用語が普及してしまっていたし、これから進めてゆく説明と直感的によく似た物理的な仕組みがさほどなかった。そこで、或る人たちは公開鍵による暗号化を色で説明したり数学を使って詳しく説明してきた。もし公開鍵の暗号化と密接に関わる詳しい説明が知りたいなら、これら二つの説明をお勧めする。

特段の詳しい説明が不要だという他の方には、次の前提を理解してもらえれば後の説明は簡単に理解できる。

お分かりだろうか。ではこれらをもとに、説明していこう。

共通秘密鍵の共有

秘密鍵による暗号化を使って、或る男性が彼女とインターネット経由で何かを話したいとする(この方法は、公開鍵による暗号化よりも高速に行える)。しかし、その会話を他の誰にも読まれたくないとしよう。その男性と彼女は、まだ秘密鍵を共有していない。さて、どうしようか。[訳注:ここでは読者が男性だと仮定されているので、適宜 “you” は「或る男性」に置き換えてあります。]

細かい点を無視すれば(先ほど紹介した、色を使った解説ビデオは上手く説明している)、この男性がやるべきことは次のようになる。

  1. 男性が自分の公開鍵(先に挙げた図で薄い青色の鍵)を彼女に送る。
  2. 彼女が自信の公開鍵(薄い茶色の鍵)を男性に送る。
  3. 男性が自分の秘密鍵(濃い青色)と彼女の公開鍵(薄い茶色 [訳注:ちなみに原文は色を間違えています])を組み合わせて、共有秘密鍵を作る。
  4. 彼女もまた、自身の秘密鍵(濃い茶色)と男性の公開鍵(薄い青色)とを組み合わせて、男性がもつ共有秘密鍵と全く同じ鍵を作る。

相手の秘密鍵を知らないのに、そんなことがどうやってできるのか。これは、合同算術(モジュラー計算)(古典的なディフィー・ヘルマン鍵共有の場合)、あるいは有限体上で定義された楕円曲線におけるスカラー倍算(最近の楕円関数を使ったディフィー・ヘルマン鍵共有の場合)という手法を使うとできるのだ。

電子署名

電子署名のアルゴリズム、例えば EdDSA に代表されるアルゴリズムは、公開鍵による暗号化の手法から導かれた最も有用な発明の一つだ。

電子署名は、メッセージ秘密鍵から計算される。(ECDSA のような)初期のアルゴリズムでも、君は個々のメッセージについて一意のランダムなノンスを生成する必要があったが、これは現実にはエラーを起こしやすい実装だと証明されている。

君の公開鍵のコピーを持っている人なら誰でも、或るメッセージが君の秘密鍵で署名されているかどうかを確かめられる。鍵を使うハッシュ関数とは違って、君の秘密鍵が何であろうと、この確認作業は可能なのである。

冒頭に戻る

よくある誤解と危険

パスワードの格納

手短に言うなら、あれこれ考えず bcrypt を使え [訳注:後で編集したのか、未来の記事にリンクしています]。PHP の開発者なら、crypt() ではなく、password_hash()passowrd_verify() を使うということだ。

多くの開発者たちは、パスワードは暗号化されなくてはならないと考えているが、これは誤りである。そうではなく、パスワードはハッシュ化されなくてはならない。更に言うと、パスワードのハッシュ化アルゴリズムと単純な暗号論的ハッシュ関数を混同してはいけない。それらは同じことをやっているわけではないのだ。

暗号論的ハッシュ化 パスワードのハッシュ化
  • 高速である。
  • メッセージという一つの入力だけを必要とする。
  • 意図的に遅くしてある。
  • 少なくとも三つの入力を必要とする。
    1. パスワード文字列
    2. ユーザごとのソルト
    3. 計算コストの要因(計算にかかる時間の調整に使う)

暗号論的なハッシュ関数とは違って、パスワードのハッシュ化においては一つ以上の入力パラメータを要求する。しかし、暗号化のアルゴリズムとも違って、パスワードのハッシュ化は一方向の決定論的で落し戸のある (trap door) 計算を行う。また、秘密鍵を使った暗号化とも違って、ソルトを秘密にしておく必要はない。ここでソルトは、ユーザごとに一意の結果を生成するためだけに使われるにすぎないからだ。そうすることによって、事前計算(pre-computation)を妨害し、ハッシュのリストからパスワードを推定するブルートフォースの計算コストを増やすのである。

(bcrypt で)ハッシュ化したパスワードを暗号化できるか

できる。もし君がウェブアプリケーションを運用していて、データベースは別のハードウェアのサーバにあるなら、このやりかたは強力な防御になる。そして、我々が password_lock というライブラリを開発した背景にあるのと同じ考えだ。

ファイルの検証

電子署名は真正性を証明できるが、暗号論的ハッシュ関数には真正性を証明できない。

決して無視できない数の技術者が、ウェブサイトから実行可能ファイルをダウンロードするときに、ファイルの MD5 あるいは SHA1 のハッシュ値を計算して、ファイルをダウンロードしたウェブページに掲載されている値 [訳注:「チェックサム」と表記されることが多い] と比較している。自分で計算した値と掲載されている値が同じであれば、彼らはファイルを実行する。そのファイルは本物だと完全に信頼しているのだ。

ファイルとハッシュ値が同じサーバに保管されていたら、そのような検証は全くの時間の浪費である。なぜなら、あなたを騙して別のファイルをダウンロードさせられるような攻撃者は、ウェブページに掲載されているハッシュ値も都合のいい値に変更できるからだ。(もしそのファイルとハッシュ値がそれぞれ別のサーバにあるなら、攻撃者にとって状況はやや悪くなる。しかし、もっと良い解決策を退けられるほど劇的に有効な改善策というわけではない。)

結局、既に述べたように、MD5 や SHA1 のようなハッシュ関数は入力に対して決定論的で固定長の出力を生成しているにすぎない。ここには、何の秘密もないのだ。或る解決策がセキュリティを向上させずに、多くの人を安全だと誤解させてしまうなら、それは寧ろ解決策というよりもセキュリティーの劇場 (security theater) でしかない。

暗号論的ハッシュ関数を使うことは、この状況においてはセキュリティー劇場である。君は、その代わりに電子署名を使うべきだ。

セキュリティを向上させるには、MD5 や SHA1 のハッシュをウェブページへ掲載するのではなく、ソフトウェアベンダーはパッケージを EdDSA 秘密鍵で署名し、EdDSA 公開鍵を広く周知した方が良い。そうすると、君がファイルをダウンロードするときに、君は電子署名も一緒にダウンロードすることとなり、公開鍵でファイルを検証してファイルが本物であることを確かめられる。

一つの事例として、Minisign を紹介しよう。

さて、鍵を使うハッシュ関数は、ここでは役に立たない。それを使うなら、君は署名を検証しようとする誰に対しても秘密鍵をバラまかなければいけなくなるからだ。そして、誰でも秘密鍵を知り得るなら、誰であろうと自分で署名を偽造して、悪意をもってメッセージを改竄できる(ここでは実行可能ファイルを改竄できてしまう)。

電子署名は、何かをダウンロードさせる場合にファイルの真正性を保証する最善の方法だ。MD5 や SHA1 のハッシュ値は、いずれにせよ殆ど無意味である。

エンコーディングや圧縮処理は暗号化ではない

初心者によくある間違いの一つは、base64_encode() のようなエンコーディング関数を使って、情報を隠蔽しようとすることだ。以下のコードをご覧いただきたい。これは LinkedIn において、PHP のウェブアプリケーションでパスワードをどうやって正しく格納すればいいかという議論で出てきたものだ。

The worst password 'hashing' function of all time.

それから、これは史上最悪のパスワード格納関数だろう。

実に多くの開発者たちが、情報をエンコードしたり圧縮しつつ、それらの手法が現実の暗号化処理と同じていどのセキュリティになる解決策だと思い込んでいる。なぜなら、エンコードや圧縮によっても出力は人が判読できなくなるからだ。しかし、そうではない。

エンコーディングや圧縮のアルゴリズムは、どちらも、可逆的であり、鍵を使わない情報の変換である。エンコーディングは、情報を人の判読できるテキストに表現する方法を指定する。圧縮は、入力を可能な限り少ないスペースに減らそうとする。これらは有用だが、暗号化処理ではない。

まとめ

覚え書き

この記事が暗号化処理の概念のよい導入になっていればと思う。我々のチームは、暗号論、アプリケーション・セキュリティ、そして PHP によるウェブ開発について、(たいていは金曜日に)ひと月あたり 2 回から 5 回ていど記事を公表している。また、我々はコードの検証サービス技術的なコンサルティング・サービスも提供している。

冒頭に戻る


※ 以下の SNS 共有ボタンは JavaScript を使っておらず、ボタンを押すまでは SNS サイトと全く通信しません。

Twitter Facebook