HC-128 (pdf)-欧州プロジェクト
eSTREAMの ファイナリスト、内部状態がかなり大きいストリーム暗号
(512個の32ビットワードの2つの独立した配列)。 大きなストリームを暗号化するのは非常に賢明ですが、これらの配列の初期化にはかなりの時間がかかるため、バッチモードではあまり効率的ではありません。 右側には、その中で使用される6つの主要な機能があります。 定数の恐ろしい長い配列でオーバーロードされておらず、その実装(カットの下)は、他と比較して、シンプルで、多かれ少なかれはっきりしています。 それはすべて、2つの関数
f1と
f2が私を引っ掛けたという事実から始まりました
BouncyCastleライブラリから取得した標準の実装を次に示します
HC-128package org.bouncycastle.crypto.engines; import org.bouncycastle.crypto.CipherParameters; import org.bouncycastle.crypto.DataLengthException; import org.bouncycastle.crypto.StreamCipher; import org.bouncycastle.crypto.params.KeyParameter; import org.bouncycastle.crypto.params.ParametersWithIV; public class HC128Engine implements StreamCipher { private int[] p = new int[512]; private int[] q = new int[512]; private int cnt = 0; private static int f1(int x) { return rotateRight(x, 7) ^ rotateRight(x, 18) ^ (x >>> 3); } private static int f2(int x) { return rotateRight(x, 17) ^ rotateRight(x, 19) ^ (x >>> 10); } private int g1(int x, int y, int z) { return (rotateRight(x, 10) ^ rotateRight(z, 23)) + rotateRight(y, 8); } private int g2(int x, int y, int z) { return (rotateLeft(x, 10) ^ rotateLeft(z, 23)) + rotateLeft(y, 8); } private static int rotateLeft( int x, int bits) { return (x << bits) | (x >>> -bits); } private static int rotateRight( int x, int bits) { return (x >>> bits) | (x << -bits); } private int h1(int x) { return q[x & 0xFF] + q[((x >> 16) & 0xFF) + 256]; } private int h2(int x) { return p[x & 0xFF] + p[((x >> 16) & 0xFF) + 256]; } private static int mod1024(int x) { return x & 0x3FF; } private static int mod512(int x) { return x & 0x1FF; } private static int dim(int x, int y) { return mod512(x - y); } private int step() { int j = mod512(cnt); int ret; if (cnt < 512) { p[j] += g1(p[dim(j, 3)], p[dim(j, 10)], p[dim(j, 511)]); ret = h1(p[dim(j, 12)]) ^ p[j]; } else { q[j] += g2(q[dim(j, 3)], q[dim(j, 10)], q[dim(j, 511)]); ret = h2(q[dim(j, 12)]) ^ q[j]; } cnt = mod1024(cnt + 1); return ret; } private byte[] key, iv; private boolean initialised; private void init() { if (key.length != 16) { throw new java.lang.IllegalArgumentException( "The key must be 128 bits long"); } cnt = 0; int[] w = new int[1280]; for (int i = 0; i < 16; i++) { w[i >> 2] |= (key[i] & 0xff) << (8 * (i & 0x3)); } System.arraycopy(w, 0, w, 4, 4); for (int i = 0; i < iv.length && i < 16; i++) { w[(i >> 2) + 8] |= (iv[i] & 0xff) << (8 * (i & 0x3)); } System.arraycopy(w, 8, w, 12, 4); for (int i = 16; i < 1280; i++) { w[i] = f2(w[i - 2]) + w[i - 7] + f1(w[i - 15]) + w[i - 16] + i; } System.arraycopy(w, 256, p, 0, 512); System.arraycopy(w, 768, q, 0, 512); for (int i = 0; i < 512; i++) { p[i] = step(); } for (int i = 0; i < 512; i++) { q[i] = step(); } cnt = 0; } public String getAlgorithmName() { return "HC-128"; } public void init(boolean forEncryption, CipherParameters params) throws IllegalArgumentException { CipherParameters keyParam = params; if (params instanceof ParametersWithIV) { iv = ((ParametersWithIV)params).getIV(); keyParam = ((ParametersWithIV)params).getParameters(); } else { iv = new byte[0]; } if (keyParam instanceof KeyParameter) { key = ((KeyParameter)keyParam).getKey(); init(); } else { throw new IllegalArgumentException( "Invalid parameter passed to HC128 init - " + params.getClass().getName()); } initialised = true; } private byte[] buf = new byte[4]; private int idx = 0; private byte getByte() { if (idx == 0) { int step = step(); buf[0] = (byte)(step & 0xFF); step >>= 8; buf[1] = (byte)(step & 0xFF); step >>= 8; buf[2] = (byte)(step & 0xFF); step >>= 8; buf[3] = (byte)(step & 0xFF); } byte ret = buf[idx]; idx = idx + 1 & 0x3; return ret; } public void processBytes(byte[] in, int inOff, int len, byte[] out, int outOff) throws DataLengthException { if (!initialised) { throw new IllegalStateException(getAlgorithmName() + " not initialised"); } if ((inOff + len) > in.length) { throw new DataLengthException("input buffer too short"); } if ((outOff + len) > out.length) { throw new DataLengthException("output buffer too short"); } for (int i = 0; i < len; i++) { out[outOff + i] = (byte)(in[inOff + i] ^ getByte()); } } public void reset() { idx = 0; init(); } public byte returnByte(byte in) { return (byte)(in ^ getByte()); } }
先ほど
書いた関数
f1と
f2について。 要するに、それらはSHA-2(hello、NSA!)から取得され、1つの32ビット数値から別の数値への明確な変換を表します。
私は、これらの関数の定数、それらがどこから来て、どのように計算されるかに興味がありました。 それに関する情報は
0を少し超えていることが判明しました。すべての情報は上記のリンクに記載されています。 これら2つの関数の期間を計算しましたが、それはすべての最大値ではなく、最大期間に対応する定数を選択しました。 3つの定数すべてを並べ替える単純なサイクルを使用して、期間が最大になるトリプルを検索しました。 さて、期間の計算中に2つの異なるトリプルの値が交差しないように見えました。 ここでは、たとえば、そのようなトリプル(そのようなペアがたくさんあります。私は好きなものを選びました)。
元の{7、18、3}および{17、19、10}の代わりに
{22、13、3}および{18、4、9}これらの機能の開発でNSAがどのようにガイドされたかはわかりません。ブックマークがあるかどうかはわかりません。 しかし、私の定数は彼らの定数より悪くない可能性が非常に高いです。 また、明確な対応(すべての値0-(2
32 -1)をチェック)を提供し、標準のサイクルよりも長いサイクルを持っています。 だから、私とそれらを交換してお気軽に
private static int f1(int x) { return rotateRight(x, 22) ^ rotateRight(x, 13) ^ (x >>> 3); } private static int f2(int x) { return rotateRight(x, 18) ^ rotateRight(x, 4) ^ (x >>> 9); }
Cはそれを理解しました。
もう1つ。 HC-128のコンビナトリアル分析
に関するドキュメントに出会いました。 10億のマタナがありますが、さらに興味深いことに、関数
h1 、
h2および
g1 、
g2を改善するための提案があります(セクション4)
h1(x)および
h2(x)関数は、それらが提供する32のうち16ビットのみを使用します。これらの16ビットは、状態配列
Pおよび
Qの 2つのインデックス(2x8)として使用されます
。 ただし、32個すべてを使用する必要があります。そうしないと、特定の条件下で内部状態を復元できます。 したがって、これらの関数(2
32を法とする2つの要素の合計)に最初にあると考えられるのは、xと
x自体です。 次のようになります(上記のオプションと比較してください):
private int h1(int x) { return (q[x & 0xFF] + q[((x >> 16) & 0xFF) + 256]) ^ x; } private int h2(int x) { return (p[x & 0xFF] + p[((x >> 16) & 0xFF) + 256]) ^ x; }
次に、関数
g1(x)および
g2(x)について説明します。 状態
Pおよび
Qの配列は、互いに独立して存在します。 したがって、悪条件(これらの配列の1つと生成されたバイトのストリームの一部の認識)の下では、これは悪い結果につながる可能性があります。
したがって、関数
g1および
g2を変更して、
Pの各要素が
Qのランダム要素に依存し、その逆も成り立つようにします。 そして、要素
yを循環シフトする代わりに、配列
Qおよび
Pからの要素のインデックスとして、そこから数ビットを取得します
。 private int g1(int x, int y, int z) { return (rotateRight(x, 10) ^ rotateRight(z, 23)) + Q[(y >> 7) & 0x1FF]; } private int g2(int x, int y, int z) { return (rotateLeft(x, 10) ^ rotateLeft(z, 23)) + P[(y >> 7) & 0x1FF]; }
これですべて、元のインラインHC-128に劣らない(理論的には優れた)新しいアルゴリズムができました。
発言
もちろん、これは
概してゲーム全体であり
、実際のアプリケーションではそのようなものを使用しないようにアドバイスする必要があります 。 しかし、頭脳、「自分のための」アプリケーション、および標準と特許の意図的な回避(HC-128が特許化されていた場合)の責任として、そのような研究と
正確な修正には生命権があります。 たとえば、パスワードハッシュを遅くする手順でこのオプションを使用します(パスワード15からハッシュ(パスワード)+ハッシュ(ハッシュ(パスワード))+(ハッシュ(パスワード)+ハッシュ(ハッシュ(パスワード))の15キロバイト配列を生成します... )、この配列から変更されたHC-128ハッシュを初期化し、サイクル内でこの配列のさまざまな小さな部分を変更されたHC-128で数百万回暗号化して、最後に配列からハッシュを計算します。これが遅いパスワードハッシュになります。
ちなみに、これは
スポンジ機能に
似ています。 しかし、私はすでにドーパーの後。
このサイクルの後、もう一度配列からのハッシュを検討します。これは遅いパスワードハッシュになります。 そのような恐怖に対する効果的なブルートフォースは、書くのが非現実的です。
これを行うクラスを次に示します。 ObfuscatorEngineは修正されたHC-128です。
エントロピー -ハッシュから収集され、ランダムな場所の断片で暗号化される配列。
反復回数は素数です。
オフセットが計算される場所を見るのも面白いです。
スローヘッシャー package com.paranoim.crypto.utils; import java.util.Arrays; import org.bouncycastle.crypto.util.Pack; import com.paranoim.crypto.Consts; import com.paranoim.crypto.digest.SHA3_256; import com.paranoim.crypto.digest.SHA3_512; import com.paranoim.crypto.utils.obfuscator.ObfuscatorEngine; import fr.cryptohash.DigestEngine; import fr.cryptohash.Keccak256; import fr.cryptohash.Keccak512; public class SlowHasher { private static final int ITERATIONS_COUNT = 0x133ECD; private static final int CHUNK_SIZE = 71; public static byte[] calculateSlowHash(final String password, final byte[] salt) { if (salt.length != Consts.BIG_SALT_SIZE) { throw new IllegalArgumentException("Salt size must be " + Consts.BIG_SALT_SIZE + " bytes!"); } final byte[] saltedPassword = ByteUtils.concat(salt, password.getBytes()); final byte[] hashOfSaltedPassword = SHA3_512.process(saltedPassword);
これは金曜日のスケッチです。