ソースコードのセキュリティ判定機関はないのんかという話

Blogだけじゃなくて知恵袋にも出してみようかなぁとは思っているんだけど、ソースコードのセキュリティを判定してくれる機関って無いもんかな。
誰か偉い人のコメントをもらいたいけどこのBlogじゃなぁ・・(汗)
タカギセンセイにTwitterでメンション飛ばしてみたりするのがいいんじゃろか。


具体的に評価したいのはこのコード。

<?php
function get_daily_secret() {
	$m = @filemtime(DATA.'secret');
	if ($m === FALSE || date('d') != date('d', $m))
		update_daily_secret();

	return file_get_contents(DATA.'secret');
}
function update_daily_secret() {
	return file_put_contents(DATA.'secret', mt_rand(0,1000000));
}
function generate_id($host=NULL) {
	if (! $host) {
		$host = @$_SERVER['REMOTE_ADDR'];
		if (empty($host))
			$host = microtime(true);
	}

	$tmp = explode('.', $host);
	$plus = (date('d')+$tmp[0]-$tmp[count($tmp)-1]) % strlen($host);
	$host = substr($host, $plus+1).substr($host, $plus, 1).substr($host, 0, $plus);
	$pos = strpos($host, '.');
	if ($pos !== FALSE)
		$host = substr($host, 0, $pos).substr($host, $pos+1);

	return hash_visualization(md5(
		BULLET_SECRET_KEY
		.get_daily_secret()
		.$host
	, true));
}
function hash_visualization($ary, $plus_salt=0, $len=8) {
	$cnt = 16;//count($ary);
	$seed = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_';
	$seed_len = 64;//strlen($seed);
	$ret = array();
	$bytes = unpack('C*', $ary);
	$plus = ($bytes[1]+$plus_salt) % $cnt;
	for ($i=0; $i<$len; $i++) {
		$ret[] = substr($seed, $bytes[($i * $len + $i + $plus) % $cnt+1] % $seed_len, 1);
	}
	return implode('', $ret);
}

テーマ指向ミニブログで、利用者のIDを生成するために使おうかなと思って書いたコード。
専門家じゃないからどの程度攻撃に耐性があるのか、はっきりしたことがいえない。
目的としては、IPが推測できず、かつそれなりに一意性があり、かつ可読性がそれなりにあればいい、という形。


俺の専門外の頭では、俺の理解が及ばずクラックされたという最悪のケースで、秘密鍵が漏れてかつ全IPのハッシュ値を網羅したDBを用意するくらいに攻撃態勢整えても、192.168.1.1の内の半分くらいしか推測出来ないと思うけど、半分推測されたらちょっと嫌なので、そこんとこどうなのかなと。
最良の場合は解読不可能になるはず。

解説

解説してみる。

日別ソルト

一応、日別の暗号化キーを生成するコード。

<?php
function get_daily_secret() {
	$m = @filemtime(DATA.'secret');
	if ($m === FALSE || date('d') != date('d', $m))
		update_daily_secret();

	return file_get_contents(DATA.'secret');
}
function update_daily_secret() {
	return file_put_contents(DATA.'secret', mt_rand(0,1000000));
}


単純に毎日0〜100万のランダムな値をキーとして保存しているだけ。
乱数であるがゆえ100万分の1の確率で、二日続けて同一IDが生成される可能性はある。

IPの取得
<?php
function generate_id($host=NULL) {
	if (! $host) {
		$host = @$_SERVER['REMOTE_ADDR'];
		if (empty($host))
			$host = microtime(true);
	}

最初にIPをとる。IPがとれなければ一意じゃないけどミリ秒をシードにする。
IPとれないケースなんて無いはずだけど。

IP並べ替え

とったIPは適当に並べ替える。

<?php
	$tmp = explode('.', $host);
	$plus = (date('d')+$tmp[0]-$tmp[count($tmp)-1]) % strlen($host);
	$host = substr($host, $plus+1).substr($host, $plus, 1).substr($host, 0, $plus);
	$pos = strpos($host, '.');
	if ($pos !== FALSE)
		$host = substr($host, 0, $pos).substr($host, $pos+1);

192.168.1.10なら、192.168.1.10の内192と10と今日の日にち部分を計算。今日だと18 + 192 - 10でちょうど200。
この200を192.168.1.10の文字列長(12)でmodをとって、8。
この8の部分を基に、.101192.168.にする。


このパーツ順に結合してるだけ。
パーツA: 192.168.1.10 -> .10
パーツB: 192.168.1.10 -> 1
パーツC: 192.168.1.10 -> 192.168.


ちなみに127.0.0.1だと27.0.0.11になり、明日19日だと7.0.0.121になる。


日付を入れてる理由は、毎回IDかぶりしている相手が同じだと気持ち悪そうなので。

"."の削除

次に最初に見つかった"."を削除。

<?php
	$pos = strpos($host, '.');
	if ($pos !== FALSE)
		$host = substr($host, 0, $pos).substr($host, $pos+1);

つまり192.168.1.10はこの時点で101192.168.になる。
これはだぶらせるため。

ハッシュ取得と処理委譲

最後に秘密鍵と先述の日別ソルトを混ぜて、101192.168.をmd5でハッシュ化する。

<?php
	return hash_visualization(md5(
		BULLET_SECRET_KEY
		.get_daily_secret()
		.$host
	, true));
}


弱いmd5にしている理由は速度が速いためと、8文字の出力結果に対して16バイトが相性がいいため。


秘密鍵がaで、日別ソルトが0の場合、955db075c39cd52bec5141b5358b8051のmd5値が取れる。

可視化

<?php
function hash_visualization($ary, $len=8) {
	$cnt = 16;//count($ary);
	$seed = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_';
	$seed_len = 64;//strlen($seed);
	$ret = array();
	$bytes = unpack('C*', $ary);
	$plus = ($bytes[1]+$plus_salt) % $cnt;
	for ($i=0; $i<$len; $i++) {
		$ret[] = substr($seed, $bytes[($i * $len + $i + $plus) % $cnt+1] % $seed_len, 1);
	}
	return implode('', $ret);

この関数は最初に出力用の種を用意。この種は16,32,64,128個あればなんでもよくて、今回はa-zA-Z0-9-_の64パターンを採用。
次にハッシュ関数の最初の1バイトを16でmodした値を変数plusに保存。最終的には0〜15の値。


次に8文字分の出力を生成する。この際、md5の出力結果16バイトの内、8バイトしか利用しない。
計算式はあまり重要でなくて、次のバイト順でindexが割り出される。

1,10,3,12,5,14,7,16

前述のplusによる変動があるので、例えば1バイト目が77ならplusが13となって、こう変化する。

14,7,16,9,2,11,4,13


最終的にこのindex値からそれぞれのバイトにある値を見て、seed値から値を8文字持ってきて人間が見れる形の不可逆ハッシュを作っている。


192.168.1.10のplus値は5であり、6,15,8,1,10,3,12,5の順番になる。
このインデックス順にとっていくとバイト値は156,128,43,149,81,176,181,195になり、ここを基にシードから文字列をとっていくと、最終的な出力結果はCaRvrW1dになる。

ポイント

まず1方向ハッシュ関数としては脆弱なMD5だけど、MD5の出力から8バイトを切っており、またそれぞれのバイトも精度が4分の1になっており、aの文字列が出る可能性は0,64,128,196の4値に分散される。
どのバイトが切られているかは、失われている可能性のある1バイト目の出力結果によって決定されるため、失われているバイトの推測はおそらく難しいんじゃないかなぁと思うんだけどどーだろーか・・。


また、192.168.1.0〜192.168.3.255までのIPで生成して検証した結果、それなりにだぶる。
18日、日別ソルト0、秘密鍵aの場合、192.3.1.45と192.3.14.5は同じqE1trvJ-でだぶっている。

想定している攻撃

ぶっちゃけmd5をどうやって破るのかがよくわかってないんで、欠けたハッシュ関数出力の方からどうやって元の値に近づけるつもりなのかは想像ついてない。
欠損しまくってるから、多分でーじょぶなんじゃないのと思うんだが・・。


もう一つは秘密鍵も日別ソルトもばれて、IDが野ざらしになっている場合、攻撃者がこのアルゴリズムを元に43億分のDBを作った場合。
これやられると、ハッシュを基にある程度IPが限定されてくる。例えばqE1trvJ-だと192.168.3.1.45か192.3.14.5のどっちかだよねと。


これはまあ、しゃあねぇかなと。
つまり二段階の秘密鍵破られても限定まではされなくて、これかこれかこれのどれかでしょ、というレベルなら許容範囲だろと思ってる。

質問したい点

DB作って攻撃、の方はキーが漏れる想定であればある程度想像付く。
問題は、欠けたハッシュ関数出力から元の値をたどれるのかという一点。


ここを、聞きたい。
まあ、そんな技術のコアなところをただで答えてくれる奇特な人はあんまりいないかもしれんけどなぁ。


あと、「しょうがねぇかな」って俺が思ってるレベルが許容範囲かどうかも聞きたいんだけどね。
こういうのって実際アタックしてみないとなんともなんじゃねぇのとは思うけど。


最近これについてはオープンソースにしようかなぁと思うんだけど、これはその理由の一つでもある。