補題:32bit符号なし整数変数間の演算をブール関数で表す
初期seedから日時を求める無謀な挑戦 - Plus Le Toolのための補題。
使う記号の定義は[id:plusletool:20130427:p2]参照。
SHA−1アルゴリズムは32bit符号なし整数(uint型)間の演算で記述されているが、あえてそれを1bit整数(bool型)間の演算に書き換えることで、手がかりを得ようという試み。
最初に断っておくと、メモリ使用量的にも計算時間的にも明らかに無謀。でもやる。
uint型整数変数をbool型整数変数で記述する方法を考える。例えばuint型整数の変数を とする。
の第 bitを とする、つまり、 を二進表示すると次のようになるとする。
(*1)
見ての通り、 の第1bit(最下位bit)は であり、第32bit(最上位bit)は である。
また、この は 個のbool型変数 の関数であるとする。
(*2)
これらの関数をシャノン展開してみよう。
【補足】シャノン展開
例えばブール関数 について、次の式を考える。のときは、 である。
のときは、 である。
つまり は の値によらず に一致するので、関数 は次のように書き換えられる。次に、上式に含まれる および は に依存しないので、 の関数である。
上と同様に考えることで、これらの関数はそれぞれ次のように書き換えられる。同様に、 は に依存しないので の関数であり、これらはそれぞれ次のように書き換えられる。
この式中に現れる , , , , , , , は に依存しない。
(もし に別の変数 が含まれていたとしたらこれら( など)は の関数になるが、 以外の変数が含まれないならこれら( など)は定数である。)上述の式変形を順に行い整理すると、関数 は次のように変形できる。
このような変形をシャノン展開という。
上の例は3変数の場合だが、一般には次のように書ける。
※ はブール代数で使用するすべての元(簡単に言うと整数のこと)の集合、つまり である。
※右辺に出てくる係数 は に依存しない(≒定数)。><
※ より、 は「 のとき 、 のとき 」を意味する。
なお この式(上記の一般の場合の式)の右辺は、「 の肯定リテラルまたは否定リテラルのいずれか一方を1個ずつすべて論理積した項」の組み合わせをすべて含むので、項数は である。
また、この式の右辺は積和標準形になっている。
(*3)
ところで、積和標準形は「式中の論理和を排他的論理和に置き換えても等式が成り立つ」という性質がある。
また 排他的論理和は、法2の下での算術和の値に等しい。
これらより(*3)は次のように変形できる(第 bitの場合を示す)。
(*4)
これを使って(*3)を行列で表すと次のようになる。
(*5)
左辺の列ベクトルは32次、右辺の係数行列は 行列、右辺の列ベクトルは 次である。
ただし、 とおいた。
(*5)で、係数行列を とする。
(*6)
この行列の各成分は に依存しない値である( について定数)。
そこで の 成分を とおく。
(*7)
ただし、
紛らわしいので念のため確認しておくが、
- は の関数で、 の第 bitを表す。
- は について定数で、 を積和標準形にした時の第 項の係数を表す。
また、(*5)の右辺の列ベクトルはこれ以降で変形されないので、これを とおく。
(*8)
(*7)(*8)を使って(*9)を書き直して、次式を得る。
(*9)
(*9)の左辺の列ベクトルは、あるuint型変数 の各bitを並べたものなので、 と は一対一に対応している。
よってuint型変数(この例では )を調べたいときは、その係数行列(この例では )を調べればいい。
さて、ここからが本題。
SHA−1アルゴリズムで出てくる演算は、論理積,論理和,排他的論理和,論理否定,算術和,循環シフト,および代入の7種類。
これらの演算が1bit単位ではどのように表されるのかを、簡単なものから順に見ていこうと思う。
最初に1番簡単な代入演算から。
32bit整数の代入演算は、各bitを個別に代入したと考えても同じである。
よって(*10)の32bit整数の代入演算を1bit単位で記述し直すと、(*11)のようになる。
(*10)代入(uint型)
(*11)
(*11)において、 は の関数である。
2つの関数が等価ということは、その積和標準形の各係数がすべて等しいことを意味する。
よってそれぞれの係数行列 を比較することで、次のようになる。
(*12)代入(bool型)
同じように考えていこう。
次は論理積,論理和,排他的論理和,論理否定をまとめて考えてみる。
(*13)論理積(uint型)
(*14)論理和(uint型)
(*15)排他的論理和(uint型)
(*16)論理否定(uint型)
複数bitの論理演算は各bitごとに演算するよう定義されているので、代入演算と同様に考えることができる。
よって(*13)〜(*16)を1bit単位で記述し直すと、次のようになる。
(*17)
論理積
論理否定
ところで、 は および を係数とする積和標準形で表されるのだった。
(*18)
積和標準形どうしの論理積は、リテラル( のこと)の組み合わせが同じである項の係数どうしの論理積を新たな係数とする積和標準形で表される。論理和と排他的論理和も同様。
……言葉で書くと分かりにくいので式を見たほうが早いだろうか。
(*19)
よってそれぞれの係数行列を比較して、次のようになる。
(*20)論理積(bool型)
(*21)論理和(bool型)
(*22)排他的論理和(bool型)
(*23)論理否定(bool型)
続いて、循環シフト演算を見てみる。
32bit整数の循環シフトなので、32bitシフトすると元の値に戻るため、32bit以上のシフト幅は考えなくてよい。
よってシフト幅 を に限定し、次の左循環シフトを考える。
(*24)左循環シフト(uint型)
式(*24)を1bit単位で記述し直すと、次のようになる。
(*25)
(※一般項は である。)
代入演算と同様に係数行列を比較して、次のようになる。
(*26)左循環シフト(bool型)
同様に、右循環シフトは次のようになる。
(*27)右循環シフト(uint型)
(*28)
(※一般項は である。)
(*29)右循環シフト(bool型)
最後に、算術和について見てみる。
32bitの算術和演算をすると結果は最大33bit(2変数の場合)になりオーバーフローする可能性があるが、そのオーバーフロー分は無視することになっている。
よって32bit整数の算術和演算は次のように書ける。
(*30)算術和(uint型)
(*30)をbit単位で計算する場合、(*31)のように書ける。ただし は繰り上がり項で、(*32)のように漸化式で表される。
(証明はしないけど「全加算器」あたりで検索すればたくさん出るはず。)
(*31)
(*32)
(*32)の漸化式を解くと は次のようになる。
(*33)
(*33)を(*31)に代入することで次式を得る。
(*34)
この式中に出てくる演算子は論理積と排他的論理和だけなので、(*20)(*22)を繰り返し適用出来る。
そしてその係数行列を比較することで、次のように書ける。
(*35)算術和(bool型)
無駄に複雑になるので展開まではしてないが、 成分は(*35)のようになる。
以上の結果をまとめよう。
32bit符号なし整数 の論理積,論理和,排他的論理和,論理否定,算術和,循環シフト,および代入の各演算を、 の積和標準形の係数 で表すと次のようになる。
(*A)代入
(*B)左循環シフト
(*C)右循環シフト
(*D)論理積
(*E)論理和
(*F)排他的論理和
(*G)論理否定
(*H)算術和
これらの演算によって得たこの係数行列 の各成分は、右辺の係数行列(この場合は )の成分(つまり )の関数になっている(※ の関数ではないので注意)。
この や が各種演算によって得たのなら、 の成分は、それを求めた時の右辺( とする)の係数行列の成分の関数である。
その の係数行列 の成分も、それを求めた時の右辺( とする)の係数行列の成分の関数である。
このように繰り返し遡っていけば、 の各成分は最終的に、SHA−1の入力値の係数行列の成分の関数になる。
いや言い方が逆か。
SHA−1の入力値に対して、SHA−1の計算手順に従って(*A)〜(*H)を繰り返し適用していくことで、SHA−1の出力値の係数行列の成分をSHA−1の入力値の係数行列の成分の関数で記述できる。
よってそれらの式を連立方程式として解けばいいということだ。
……(現実的な時間で)解ければの話だが。
【余談】
なぜこんなめんどうなことをするかというと、簡単にいえばコンピュータに「式」を記憶させるためだ。
例えば という式があったとして、 が既知の値なら には を計算した値を代入すればいい。
しかし例えば が未知の値だった場合、 に という式を「式として」記憶させることはできない。
いや確かに、強引な方法を使えばできないことはない。例えば"ax+b"のように文字列で記憶させるとか。
でもその方法だとその文字列を構文木解析して数式に戻さないといけない。これ結構面倒なんだよね。
あるいは式を文字列ではなく最初から構文木の形で記憶させる方法もある。
具体的には、「式インターフェース」を継承した「論理積クラス」「論理和クラス」「定数クラス」「未知変数クラス」などを作り、各インスタンスに「式インターフェース」を継承したクラスのインスタンスを0〜2個持たせることで実装できる。
……結局構文木作ってるわけだからこれも面倒なんだけどね。(面倒の一言で済ませてるけど、ただの面倒じゃなくてかなり真剣に面倒なんですよ。)
こういう場合、式を「具体的な定数値」(= )と「計算方法」(=未知数 の定数 倍に定数 を加算する)に分けて考えるのがよい。(たぶん。)
「計算方法」を決めておけば、「具体的な定数値」を記憶させるだけで済むからだ。
この記事の方法では、 が「具体的な定数値」に当たり、 が「計算方法」に当たる。
特に、未知変数を全部 の方にまとめてしまえば係数行列 の成分はすべて定数になるので、コンピュータに記憶させることができる。
その上、(*A)〜(*H)は係数行列どうしの計算で完結してるので、その計算結果も定数であり、やはりコンピュータに記憶させることができる。
これは重要なことだ。……たぶん。
と、今回こんなめんどうなことをしたのは、こういう事情があるから。
他にもっと簡単な方法があるならその方法を使うんだけどね。
【追記】
ここまで書いといて後から気づいたけど、 のように行列を使って を32bitずつで表すんじゃなくて、 と定義して のようにベクトルの内積を使って を1bitずつ表すだけで充分だった。というかそうするべきだった気がする。
……今さら全部書き換えるのめんどうだなぁ。