CGI 暗号 2016年08月22日 12:22   編集
パスワードを保存する必要のあるCGIでは、生のパスワードのまま保存するより、ハッシュ化したほうが安心ということでこれまですべてcrypt関数でハッシュ化した上で保存していました。
このcrypt関数は手軽に使えて便利なのですが、有効なパスワードの文字数が8文字までという制限があります。

たとえば、「password123」というパスワードをcryptでハッシュ化して保存すると、9文字以降は無視されるため、「password」でも、「password012」でも正しいパスワードと認識されてしまいます。

Digest::MD5モジュールを使う

これは、やはりマズイので、何とかできないか調べてみました。 perlのモジュールでDigest::MD5というのを使う方法があるようです。
これを使うと8文字以上のパスワードでも使えるし、強度も高いようです。
perlのバージョン5.8以降は標準で入っているということですが、入っていない場合は処理を分ける必要があります。
Digest::MD5が入っているか、あるいはperlのバージョンが5.8以降であるかどうをユーザーに調べてもらって、cryptかMD5どちらを使うか選択してもらうというのは、わかりにくいと思うので、CGIで自動的に選択する方法を考えてみました。
まず、モジュールがインストールされているかどうか調べる方法ですが、モジュール関係は@INCという配列にライブラリのパスが保存されているので、このなかに「MD5.pm」というファイルが存在するかどうかを調べればよさそうです。しかし、@INCに保存されているディレクトリ直下にあるとは限らないので、サブディレクトリもすべて調べなければならないようです。
use File::Find;
find(\&search, @INC);
sub search {
if ($_ eq 'MD5.pm') {
$md5ok = 1;
last;
}
}
のような処理で'MD5.pm'ファイルがあるかどうか調べられますが、結構時間がかかります。
perlのバージョンで分岐する方式だと、
if ($] >= 5.8) {
$md5ok = 1;
}
と簡単で、あっという間に終わります。
しかし、このperlのバージョンが今ひとつよくわかりません。
たとえば、ウチの環境では、CGIテスト用にローカルにインストールしているXAMPPのperlのバージョンは5.16.3ということになっていますが、
perlの$]では5.016003ということになります。そして、MD5モジュールは入っています。

ということは、分岐の条件は
$] >= 5.008
にすべきなのかとかよくわからないので、結局'MD5.pm'があるかどうかで決めることにしました。
※(その後の調査で、Perlバージョン5.8以降かどうかの分岐は$] >= 5.008 でいいことがわかりました。
以下モジュール内を検索する方法を説明していますが、Perlのバージョンで分岐した方がかなり簡単です。)
といっても、処理に時間がかかるので、パスワード生成や照合のたびに(特に照合)調べるのは避けたいところです。
設定ファイルinit.cgiでハッシュ化方法を$codeに保存することにして、$codeが未設定の場合だけ
モジュール内を検索してその結果をinit.cgiに書き込むことにしました。
MD5を使ったハッシュ化と照合処理についてはKENT-WEBの暗号化入門を参考にさせてもらいました。

crypt流用

MD5が使えない環境では今まで通り8文字までというのも、気になるのでついでにcrypt使って8文字以上使える方法も考えてみました。
手順としては、パスワード保存する場合
まず、通常通りcrypt処理してハッシュ化した文字列を$cypted1とします。
これは、入力されたパスワードの8文字目分しか見ていません。
入力したパスワードが8文字以上だった場合、9文字目以降を取り出して再度crypt処理してこれを$cypted2とします。
$cypted1、$cypted2それぞれ保存してもいいのですが、cryptでハッシュ化された文字列は13文字と決まっているようなので、保存、読み込みが一回で済むように

$cypted = $cypted1 . $cypted2;

としてまとめて保存することにします。
照合処理は、
入力されたパスワードが8文字以上の場合は、通常の照合作業に加えて、
9文字目以降分の照合処理を追加します。その照合に使用するハッシュ化文字列は$cyptedに保存された文字列の14文字以降ということになります。
環境や入力したパスワードの長さによってハッシュ化パスワード処理が違い、当然照合処理も違うことになりますが、照合処理の分岐はハッシュ化文字列の長さによってのみ振り分けるのが良さそうです。
MD5でハッシュ化するよう設定したとしても、crypt方式でハッシュ化した登録はその方式で照合しなければならないからです。

ハッシュ化文字列が13文字の場合は従来のcrypt照合
ハッシュ化文字列が26文字の場合は従来のcrypt照合2回
ハッシュ化文字列が40文字の場合はMD5照合。

実装

ハッシュ作成処理と照合処理は以下のようなものにしました。

ハッシュ化
sub encrypt {
my($inpw) = $_[0];
if ($inpw =~ /([$ban])/) {
&error("パスワードエラー","「$1」はパスワードに含めないでください。");
}
my $encrypt;
if ($code == 2) {
use Digest::MD5 qw/md5_hex/;
my @str = ('a' .. 'f', 0 .. 9);
my $salt;
for (1 .. 8) {
$salt .= $str[int(rand(@str))];
}
$encrypt = $salt . md5_hex($salt . $inpw);
} else {
my(@SALT, $salt);
@SALT = ('a'..'z', 'A'..'Z', '0'..'9');
srand(int(rand(100000)));
$salt = $SALT[int(rand(@SALT))] . $SALT[int(rand(@SALT))];
$encrypt = crypt($inpw, $salt) || crypt ($inpw, '$1$' . $salt);
if (length($inpw) > 8) {
my $inpw2 = substr($inpw,8,8);
srand(int(rand(100000)));
my $salt2 = $SALT[int(rand(@SALT))] . $SALT[int(rand(@SALT))];
my $encrypt2 = crypt($inpw2, $salt2) || crypt ($inpw2, '$1$' . $salt2);
$encrypt .= $encrypt2;
}
}
$encrypt;
}

照合
sub decrypt {
my($inpw, $logpw) = @_;
my($salt, $check);
if (length($logpw) == 40) {
# saltは先頭の8文字を抜き出す
my $salt = substr($logpw, 0, 8);
$check = "no";
# 照合
if ($logpw eq ($salt . md5_hex($salt . $inpw))) {
$check = "yes";
}
} else {
my $logpw1 = substr($logpw,0,13);
my $inpw1 = substr($logpw,0,8);
$salt = $logpw1 =~ /^\$1\$(.*)\$/ && $1 || substr($logpw1, 0, 2);
$check = "no";
if (crypt($inpw, $salt) eq $logpw1 || crypt($inpw1, '$1$' . $salt) eq $logpw1) {
$check = "yes";
}
if ($check eq "yes" && length($logpw) == 26) {
my $logpw2 = substr($logpw,13,13);
my $inpw2 = substr($inpw,8,8);
my $salt2 = $logpw2 =~ /^\$1\$(.*)\$/ && $1 || substr($logpw2, 0, 2);
if (crypt($inpw2, $salt2) eq $logpw2 || crypt($inpw2, '$1$' . $salt2) eq $logpw2) {
$check = "yes";
} else {
$check = "no";
}
}
}
$check;
}
スクリプトに実装するには、このsub encryptとsub decryptを古いものと入れ換えることになりますが、その前提として、スクリプトで使用するハッシュ化方式を決めておく必要があります。
sub encryptで使用されている$codeです。
MD5方式でハッシュ化する場合は
$code = 2;
cryptを使用する場合は
$code = 1;
としておきますが、これは前述のように、インストールされているモジュールを検索してMD5.pmが存在すれば、initファイルなり、スクリプトファイル自身なりに自動的に書き込みます。
この処理はスクリプトによって多少違いますが、multiupload.cgiの場合は

if (! $code) {
# ハッシュ化方法が未定の場合のみスクリプトファイルを書き換え
use File::Find;
# MD5モジュールがインストールされているかどうか調べる
my $md5ok;
find(\&mdsearch, @INC);
sub mdsearch {
if ($_ eq 'MD5.pm') {
$md5ok = 1;
return $md5ok;
}
}
&lock;
open(SCR,$script) || &error("$scriptが開けません。");
my @scr = <SCR>;
close(SCR);

my $code_str;
if ($md5ok) {
$code_str = 'my $code = 2;' . "\t" x 9 ."# ハッシュ化方法(1:crypt 2:MD5)\n";
} else {
$code_str = 'my $code = 1;' . "\t" x 9 ."# ハッシュ化方法(1:crypt 2:MD5)\n";
}
open(SCR,">$script");
my $flag;
foreach (@scr) {
if (/^\s*my\s+\$code\s*=/) {
print SCR $code_str;
$flag = 1;
} else {
if (! $flag && /^\s*my\s+\$adminpass\s*=/) {
print SCR $code_str;
}
print SCR $_;
}
}
close(SCR);
&unlock;
}

のような感じです。
これでほぼOKですが、さらに、9文字以上のパスワードを使用する管理者やユーザーがログインした際、旧方式でハッシュ化されたパスワードしか保存されていない場合(つまりハッシュ化パスワードの文字数が13文字の場合)9文字以降の文字も有効にするために、再度パスワードを登録するよう促す文章を表示するようにしました。
counter:8,170