パスワードのハッシュが遅い。 なんで?

こんにちは、habraparanoik! 今日は、セキュリティを強化する少し変わった方法、つまり
パスワードハッシュを遅くする方法について説明
します 。 周りの誰もが最適化しようとしているときに、なぜ何かを遅くするように思えますか?
少なくともそのとき、最も超大型の保護されたシステムであっても、最も弱いリンクは人のままです。 つまり、彼のパスワード。
暗号化されたrarアーカイブをクラックしようとしたことはありますか? また、1秒間にいくつのパスワードが通過しましたか? 50-100-200? 優れたGPUでも、悪名高いcRARkを使用すると、検索速度は約2400オプション/秒になります。 これは、zip / md5 / SHA1の1秒あたり数千(数億)のパスワードと比較されます。
カットの下で、このプロセスの私の自由な解釈。
アクション全体の意味は次のとおりです。
- 暗号化キーはパスワードではなく、そこからの(遅い)ハッシュが重要です
- パスワードがキャッシュされている間、ユーザーが1秒半待つ必要はありません。
- しかし、ホモ・ブルートフォーサスは忍耐強くなければなりません
はい、
砂糖と 塩を使用するシステムでも非常にうまく生成される、みんなのお気に入りの
レインボーテーブルについてはほとんど忘れていました。 彼らの世代はまた、役に立たないとしても、非常に労働集約的になります。
私は塩のシステムが悪いと言っているのではなく、単に強化すること
ができ
ます 。
そして、塩があり、パスワードがあります。 次は?
そして、すべてが非常に簡単です:
- ソルトとパスワードを連結する
- ハッシュ、結果を覚えている
- (多くの場合){
- 丸い塩を生成する
- 連結してハッシュする
- キャッシュ、結果を覚える}
Winrarでは(ちなみに、インスピレーションをくれた彼に感謝します)、私の記憶が私に役立つなら、丸い塩は反復数です。 私は
もう少し進んだ。
だから、コード。
BouncyCastleライブラリを使用してすべてがJavaで記述されていますが、思慮深いhabraparanoikがこれをTuringで完全なプログラミング言語に転送する(そしておそらく独自のものを追加する)ことは難しくありません。 さらに、C#用のBouncyCastleがあります。
1. 2つのハッシュアルゴリズム(SHA-256およびSHA-512)を使用しているため、初心者にはこのインターフェイス全体を多かれ少なかれ汎用化するのに役立つ小さなインターフェイスがあります。
パブリック インターフェイス IDigest
{
パブリック バイト []プロセス( バイト []データ);
public int getSize();
}
2. SHA-256アルゴリズム用のこのインターフェースの実装例(
BouncyCastleライブラリの
SHA256Digestクラスを使用):
パブリック クラス SHA256はIDigestを実装します
{
プライベート SHA256Digest m_SHA256 = 新しい SHA256Digest();
@Override
パブリック バイト []プロセス( バイト []データ)
{
m_SHA256.reset();
m_SHA256.update(data、0、data.length);
バイト []結果= 新しい バイト [m_SHA256.getDigestSize()];
m_SHA256.doFinal(結果、0);
結果を返す ;
}
@Override
public int getSize()
{
return m_SHA256.getDigestSize();
}
}
3.最もおいしい。 詳細に説明します。
パブリック クラス SlowHasher
{
private final static int BITS_IN_BYTE = 8;
private static final int [] s_primeIndices = new int [] {7、11、17、23、31、41、47、53、61};
/ **
*このメソッドはパスワードを0x50000回ハッシュし、各ラウンドにラウンドソルトを追加します
*
* パラメーターダイジェスト
* パラメータパスワード
* 戻る
* /
パブリック バイト [] CalculateSlowHash(IDigestダイジェスト、 文字列パスワード、 バイト []ソルト)
{
int roundSaltSize = digest.getSize()/ BITS_IN_BYTE;
バイト [] bPasswd = password.getBytes();
バイト [] toHash = 新しい バイト [bPasswd.length + salt.length];
/ *
*ハッシュされるバイトの配列を構成する
* /
System.arraycopy(salt、0、toHash、0、salt.length);
System.arraycopy(bPasswd、0、toHash、salt.length、bPasswd.length);
バイト [] res = digest.process(toHash);
byte [] temp = 新しい バイト [res.length + roundSaltSize];
for ( int i = 0; i <0x50000; i ++)
{
System.arraycopy(res、0、temp、0、res.length);
/ ***
*塩の計算
* /
for ( int j = 0; j <roundSaltSize; j ++)
{
int btmp = res [s_primeIndices [j]]&0xFF;
for ( int k = 1; k <BITS_IN_BYTE; k ++)
{
btmp = ror((btmp +(res [ror(btmp、k)%res.length]&0xFF))%256、BITS_IN_BYTE-k);
}
temp [res.length + j] =( byte )btmp;
}
res = digest.process(temp);
}
解像度を返す ;
}
/ **
*入力をバイトとして扱い、ビットを右に回転
*パラメーター値0 <=値<= 255
* param nシフトするビット数
* 戻る
* /
public static int ror( int value 、 int n)
{
return (( 値 >>(n%BITS_IN_BYTE))|(( 値 <<(8-(n%BITS_IN_BYTE)))&0xFF));
}
}
javaに慣れていない人は、多数の挿入& 0xFF
怖がってはいけないとすぐに言うでしょう。 これは、intに変換し、そのようなビットマスクを適用することにより、符号付きバイトを符号なしバイトに変換するだけです。 そしてすべての理由は、Javaには符号なしの型がないからです! 絶対に! さて、これは致命的ではありません。
ここでのすべての美しさは丸い塩の形成であるため、主な注意が払われます。
変数について少し:
- resは、SHA-256では32バイト、SHA-512では64バイトの配列です。 ハッシュ結果を保存します
- roundSaltSize-丸い塩のサイズ。 SHA-256の場合は4バイト、SHA-512の場合は8バイト
- temp -res配列サイズ要素とラウンドソルトサイズの配列roundSaltSize
- s_primeIndices -res配列内の要素のインデックスの配列。この配列から開始して、ラウンドソルトの対応するバイトを計算します。 つまり、res [7]でSHA-256のラウンドソルトの最初のバイト、res [11]で2番目のバイトの計算を開始します。 SHA-512の場合、すべてのインデックスが関係します
- btemp-一連のトリッキーな変換の後、ラウンドソルトの所定の位置に収まるバイト
ラウンドソルトアルゴリズムの形成の説明に移る前の別の発言。 アルゴリズム全体は、科学的な研究とテストの結果ではありません。 結果のハッシュのいくつかのバイトに依存する値を形成し、それを遅くして複雑にするのを助けるために作成されました。 さて、今の説明:
- 最初に、メインのソルトとパスワードはtoHash配列に収集されます
- toHash配列はハッシュされ、結果はres配列に保存されます。 基本的なソルトとパスワードに関するすべてが忘れられました
- 0x50000の繰り返しで長いサイクルを開始します
- 次に、res配列をコピーするtemp配列を使用します。 temp配列の最後に、salt用の4または8バイトがまだあります。 それらを記入する必要があります
- ラウンドソルトバイトごとに:
- 現在のバイト数(j)に応じて、インデックス(7、11、17 ...)のいずれかにある要素をbtmpに格納します
- 次に、次の7(k)回実行します。
- btmpの分割から残りを取得します。btmpのビットは、res配列の長さだけ、k位置だけ右にシフトされます
- 前の番号をres配列のインデックスとして使用し、このインデックスで番号を引き出します
- btmpはこの番号を割り当て、256を法とするbtmpに追加し、8 kビットずつ右にスクロールします
- temp配列のハッシュの後にbtmpを置きます
- ハッシュ温度
このメソッドの呼び出しは次のとおりです。
バイト []ソルト= 新しい バイト [16];
new SecureRandom()。nextBytes(salt); //ランダムな16バイトのソルトを生成します
byte [] hashedPassword = new SlowHasher()。computeSlowHash( new SHA256()、password、salt);
結果はハッシュ(ハッシュ(ハッシュ+ラヌドソルト)+ラウンドソルト)...です。これは、たとえばAES-256の重要な情報の256ビット暗号化キーとして使用できます。
私のマシン(C2D 2.6)では、1つのハッシュを生成するのに約0.25秒かかります。 彼らのプロジェクトで。 ラウンド数を増やすと、それに応じて時間が長くなります。
尊敬される一般の人々にとって興味深いものであれば、対称/非対称暗号化、証明書生成など、BouncyCastleライブラリの操作に適用される他の側面について説明できます。
UPD:
コメントはドックへのリンクを示しており、この種のスキームはさらに広い範囲を取ります
Source: https://habr.com/ru/post/J100138/
All Articles