ハッシュと暗号化アルゴリズムの間には多くの混乱が見られますが、次の点について専門家のアドバイスを聞きたいです。
ハッシュと暗号化の使い分け
ハッシュや暗号化アルゴリズムの違いは何ですか(理論的/数学的レベルから)、つまり、ハッシュが不可逆である理由は何ですか(レインボーツリーの助けがなければ)
以下は、私が探していたほど詳細には触れられていない、同様のSO の質問です。
ベストアンサー1
まあ、調べてみればウィキペディア...しかし、説明をご希望なので、ここで最善を尽くします。
ハッシュ関数
これらは、任意の長さの入力と (通常は) 固定長 (またはそれより短い) の出力との間のマッピングを提供します。単純な crc32 から、MD5 や SHA1/2/256/512 などの本格的な暗号化ハッシュ関数まで、何でもかまいません。ポイントは、一方向のマッピングが行われていることです。すべての関数は入力できるよりも小さい出力を生成するため、常に多対 1 のマッピング (つまり、常に衝突が発生する) になります (1 MB のファイルをすべて MD5 に入力すると、大量の衝突が発生します)。
これらを逆順にするのが難しい (または実際には不可能) 理由は、内部でどのように動作するかによるものです。ほとんどの暗号ハッシュ関数は、出力を生成するために入力セットを何度も繰り返します。したがって、入力の固定長の各チャンク (アルゴリズムによって異なります) を見ると、ハッシュ関数はそれを現在の状態と呼びます。次に、状態を繰り返して新しい状態に変更し、それをフィードバックとして使用します (MD5 は、512 ビットのデータ チャンクごとにこれを 64 回実行します)。次に、これらのすべての繰り返しから得られた結果の状態を何らかの方法で結合して、結果のハッシュを形成します。
さて、ハッシュをデコードしたい場合、まず与えられたハッシュをその反復状態に分割する方法を考え出す必要があります (データ チャンクのサイズより小さい入力の場合は 1 つの可能性、大きい入力の場合は多数の可能性)。次に、各状態について反復を逆にする必要があります。これが非常に難しい理由を説明するために、次の式からa
とを推測しようとすることを想像してください: 。機能するとの正の組み合わせは 10 通りあります。次に、これを何度もループします: 。64 回の反復では、10^64 を超える可能性を試すことができます。これは、反復ごとに一部の状態が保持される単純な加算です。実際のハッシュ関数は、1 つの操作よりもはるかに多くの操作を実行します (MD5 は 4 つの状態変数に対して約 15 の操作を実行します)。また、次の反復は前の反復の状態に依存し、前の反復は現在の状態を作成する際に破棄されるため、特定の出力状態につながった入力状態を特定することはほぼ不可能です (反復ごとに)。これに、関係する多数の可能性を組み合わせると、MD5 のデコードでさえ、ほぼ無限の (ただし、無限ではない) リソースが必要になります。リソースが非常に多いため、入力のサイズがわかっている場合は (入力が小さい場合)、ハッシュをブルート フォースで解読する方が、ハッシュをデコードするよりも大幅にコストが安くなります。b
10 = a + b
a
b
tmp = a + b; a = b; b = tmp
暗号化機能
これらは、任意の長さの入力と出力を 1:1 でマッピングします。また、常に可逆です。重要な点は、何らかの方法を使用して可逆であることです。また、特定のキーに対しては常に 1:1 です。現在、同じ出力を生成する可能性のある入力:キーのペアが複数あります (実際には、暗号化関数によって異なりますが、通常は複数あります)。適切に暗号化されたデータは、ランダム ノイズと区別がつきません。これは、常に一貫した形式である適切なハッシュ出力とは異なります。
ユースケース
ハッシュ関数は、値を比較したいが、プレーンな表現を保存できない場合 (さまざまな理由により) に使用します。パスワードは、セキュリティ上の理由からプレーンテキストで保存したくない (保存すべきではない) ため、このユースケースに非常に適しています。しかし、海賊版の音楽ファイルがファイルシステムにないか確認したい場合はどうでしょうか。音楽ファイルごとに 3 MB を保存するのは非現実的です。そのため、代わりにファイルのハッシュを取得して保存します (md5 では 3 MB ではなく 16 バイトを保存します)。この方法では、各ファイルをハッシュして、保存されているハッシュのデータベースと比較するだけです (再エンコード、ファイル ヘッダーの変更などにより、実際にはうまく機能しませんが、これはユースケースの例です)。
ハッシュ関数は、入力データの有効性をチェックするときに使用します。ハッシュ関数はそのために設計されています。2 つの入力があり、それらが同じかどうかを確認したい場合は、両方をハッシュ関数に通します。入力サイズが小さい場合、衝突の可能性は天文学的に低くなります (ハッシュ関数が優れていると仮定した場合)。これが、ハッシュ関数がパスワードに推奨される理由です。32 文字までのパスワードの場合、md5 の出力スペースは 4 倍です。SHA1 の出力スペースは 6 倍です (おおよそ)。SHA512 の出力スペースは約 16 倍です。パスワードが何であったかは実際には気にしません。パスワードが保存されたものと同じであるかどうかを気にします。これが、パスワードにハッシュを使用する理由です。
入力データを取り出す必要がある場合は、必ず暗号化を使用してください。「必要」という言葉に注意してください。クレジットカード番号を保存している場合、いつか取り出す必要がありますが、プレーンテキストで保存したくはありません。そのため、代わりに暗号化バージョンを保存し、キーを可能な限り安全に保管してください。
ハッシュ関数は、データの署名にも最適です。たとえば、HMAC を使用している場合、既知の送信されていない値 (秘密の値) と連結されたデータのハッシュを取得して、データに署名します。つまり、プレーンテキストと HMAC ハッシュを送信します。次に、受信側は送信されたデータを既知の値でハッシュし、送信された HMAC と一致するかどうかを確認します。同じであれば、秘密の値を持たない第三者によって改ざんされていないことがわかります。これは、HTTP フレームワークによる安全な Cookie システムや、データの整合性を保証したい HTTP 経由のメッセージ送信でよく使用されます。
パスワードのハッシュに関する注意:
暗号ハッシュ関数の主な特徴は、作成が非常に高速で、逆ハッシュが非常に困難/低速であることです (事実上不可能なほど)。これはパスワードで問題を引き起こします。 を保存するとsha512(password)
、レインボー テーブルやブルート フォース攻撃に対する防御策を講じていないことになります。ハッシュ関数は速度を重視して設計されていることを忘れないでください。そのため、攻撃者がハッシュ関数に辞書を実行して各結果をテストするのは簡単です。
ソルトを追加すると、ハッシュに未知のデータが少し追加されるため、状況は改善されます。したがって、一致するものを探す代わりにmd5(foo)
、既知のソルトに追加されると生成されるものを探す必要がありますmd5(foo.salt)
(これは非常に困難です)。ただし、ソルトがわかっていれば、辞書を実行するだけなので、速度の問題はまだ解決されません。
そこで、これに対処する方法があります。1つの一般的な方法は、キー強化(またはキー ストレッチ)。基本的に、ハッシュを何度も繰り返します (通常は数千回)。これにより、2 つのことが起こります。まず、ハッシュ アルゴリズムの実行時間が大幅に遅くなります。次に、正しく実装すると (各反復で入力とソルトを渡す)、実際に出力のエントロピー (使用可能なスペース) が増加し、衝突の可能性が減ります。簡単な実装は次のとおりです。
var hash = password + salt;
for (var i = 0; i < 5000; i++) {
hash = sha512(hash + password + salt);
}
他にも、より標準的な実装として、PBKDF2、BCryptただし、この手法は、多くのセキュリティ関連システム (PGP、WPA、Apache、OpenSSL など) で使用されています。
結局のところ、hash(password)
十分ではありません。hash(password + salt)
よりはましですが、まだ十分ではありません... パスワード ハッシュを生成するには、伸張ハッシュ メカニズムを使用します...
些細なストレッチに関するもう一つの注意点
いかなる状況でも、ハッシュの出力をハッシュ関数に直接戻さないでください。
hash = sha512(password + salt);
for (i = 0; i < 1000; i++) {
hash = sha512(hash); // <-- Do NOT do this!
}
この理由は衝突と関係があります。すべてのハッシュ関数には衝突があることに注意してください。これは、可能な出力スペース (可能な出力の数) が入力スペースよりも小さいためです。その理由を理解するために、何が起こるかを見てみましょう。その前に、衝突の可能性が 0.001% であると仮定しましょうsha1()
(実際にはもっと低いですが、デモ目的です)。
hash1 = sha1(password + salt);
これで、hash1
の衝突確率は 0.001% になりました。しかし、次の を実行するとhash2 = sha1(hash1);
、のすべての衝突はhash1
自動的に の衝突になりますhash2
。したがって、hash1 のレートは 0.001% になり、2 回目のsha1()
呼び出しでこれに追加されます。したがって、hash2
の衝突確率は 0.002% になります。つまり、確率が 2 倍になったということです。反復ごと0.001%
に、結果に衝突の確率が 1 つ追加されます。したがって、1000 回の反復で、衝突の確率は 0.001% から 1% に跳ね上がりました。これで、劣化は線形になり、実際の確率ははるかに小さくなりましたが、効果は同じです ( との単一の衝突の確率の推定値はmd5
約 1/(2 128 ) または 1/(3x10 38 ) です)。これは小さいように思えますが、誕生日攻撃実際は見た目ほど小さくはありません。
代わりに、毎回ソルトとパスワードを再追加することで、ハッシュ関数にデータを再導入します。したがって、特定のラウンドの衝突は、次のラウンドの衝突ではなくなります。つまり、
hash = sha512(password + salt);
for (i = 0; i < 1000; i++) {
hash = sha512(hash + password + salt);
}
ネイティブsha512
関数と同じ衝突の可能性があります。これが必要なことです。代わりにそれを使用してください。