●SHA512-CRYPTでシャドウパスワードを作成

 HA512-CRYPTは一撃で作成可能なモジュールがない(私には見つけられなかった)ため、Perlのcrypt関数で作ります。これはlibcrypt (だかlibcrypto)に依存しているみたいで、このライブラリがSHA512でcrypt出来ないとperlでも作れないと思います。

crypt関数は下記のようになっています。
$shadow_password = crypt(<PLAINTEXT>, <SALT>);

「PLAINTEXT」はここでいうと$password (ユーザが入力したパスワード)です。
「SALT」本コンテンツのsaltと異なり、saltとround回数をまとめたマジックワードで「$6$<16文字のsalt>」または「$6$rounds=<ストレッチ回数>$<16文字のsalt>」となっています。どちらの形式をとるかはストレッチ回数(5,000回とそれ以外)によります。
ここではSALTとsaltは別物として扱います。

この2つを入れるとバイナリ文字列のシャドウパスワードが返ってきますので、これをBase64でエンコードして使用します。

crypt関数はシャドウパスワードを返します。digestを返すわけではないためdigestだけ欲しい場合は切り出す必要があります。

SHA512-CRYPTはsaltの作り方でやり方が2通りあります。2通りとはsaltをバイナリ文字列で作る場合と、可読文字列で作った場合です。

 saltをバイナリ文字列で作った場合はsaltが化けるのでシャドウパスワード全体をBase64でエンコードします。
 saltを可読文字列で作った場合はsaltは文字化けしませんので、シャドウパスワード全体をBase64でエンコードする必要はありません。
※可読文字列のsaltで作ったシャドウパスワード全体をBase64でエンコードしても特に問題はありません。

 バイナリ文字列で作った方がなんとなくセキュリティが硬そうな感じはしますですが、saltはシャドウパスワードに平文で書かれてますので、パスワードリスト漏洩時のセキュリティ強度はどちらも理論的には同じです。

 あと、ストレッチ回数が5,000回とそれ以外でも処理が分かれます。

●環境と用語

前ページをご覧ください。FreeBSD 12.0Rです。

●ソースコード

 こちらからどうぞ。

●必要なモジュール

 いずれも、FreeBSDでバイナリパッケージが用意されており、pkgコマンドでインストール可能です。
 意外かもしれませんが、SHAモジュールは使用しません。

【凡例】
1段目:Perlモジュールとしての名前
2段目:FreeBSDのpkg名(2019年06月現在)
3段目:コード内での宣言です。

●SHA512-CRYPTのシャドウパスワードの生成例

 パスワードを受け付ける部分などは## Make encrypted password by SHA512-CRYPTより上にありますので、前ページを参照ください。

●ストレッチ回数部/SALT_head

 まず、ストレッチ回数でSALTの頭の部分の生成をわけます。「5,000回」と「5,000回以外」です。

 つまり生成される文字列は「$6$」か「$6$rounds=xxxxxx$」のいずれかです。ストレッチ回数25,000回は特に根拠はないので適当にチューニングしてください。

 SALT_headは最終的にはsaltと連結し、「$6$<16Byteのsalt>」か「$6$rounds=xxxxxx$<16Byteのsalt>」のいずれかになります。これをSALTとしてcrypt関数に放り込まれます。

●salt生成

 「$salt_is_binary」変数はsaltをバイナリ文字列16文字にする場合は1に、可読文字のみにする場合は0を入力してください。可読文字は[a-z][A-Z][0-9][./]です(doveadm pw -s SHA512-CRYPTに準拠します)。

 スキームは「{SHA512-CRYPT}」です。バイナリ文字列にした場合は、スキームが「{SHA512-CRYPT.B64}」になり、シャドウパスワードがBase64でエンコードされた形で出力されます。

 saltを可読文字列のみにする場合は、makerandom_octet(Length => 12);で12Byteのランダム文字列を作成し、これをBase64でエンコードします。
 これでdoveadmに準拠した文字種で16文字の可読文字になります。(※12Byte=96bit、Base64は6ビット毎に印字可能文字→96÷6=16の16ByteのBase64文字列を得られます。)

 saltをバイナリ文字列にする場合はmakerandom_octet(Length => 16);で16Byteのランダム文字列を作成します。

 makerandom_octet関数は指定されたLength Byte分の暗号学的に安全(Cryptographically Secure)なランダムなバイナリ文字列を生成する関数です。

 別にこれ以外の方法でsaltを作ってもも構いませんが、エントロピープールが枯渇する勢いで作らないのであればモジュールは必要ですが楽です。ただ、FreeBSDのエントロピープールは某早明浦ダムばりに枯渇しやすいですが。

 ●シャドウパスワード生成

 ここまで終わったら、SALTにsaltを連結して、crypt関数にSALTと$passwordを放り込みます。このSALTは$で終わってはいけません。
 つまりSALTは必ず「$6$<16文字のsalt>」や「$6$rounds=xxxxxx$<16文字のsalt>」です。「$6$<16文字のsalt>$」や「$6$rounds=xxxxxx$<16文字のsalt>$」ように$で終わってはいけません。saltとdigestの間の$マークはcrypt関数がつけてくれます。

 まぁ厳密にいうとcrypt関数は引数で入れたSALTの後ろに、$で始まるdigstをつけてシャドウパスワードを返してきます。そのため、末尾が$で終わるSALTを入れるとsaltとdigestの間が$$になってしまうのでシャドウパスワードがおかしくなります。。

 cryptにSALTとパスワード放り込むと$shadow_password_tempにシャドウパスワードが返ります。(返り値はdigestではないです→$6$で始まる文字列が返ります)。

 $saltが可読文字の場合はそのままそれをシャドウパスワードに、バイナリ文字列の後はこれをBase64でエンコードしてシャドウパスワードにします。

 ここまで終わったらスキームを連結して$dovecot_passwordにします。

Table 1. SHA512-CRYPT生成の例
## Make encrypted password by SHA512-CRYPT

$password = $pass;

## Set 1 if you allow including unprintable characaters in salt.
$salt_is_binary = 0;

## 1,000 - 999,999,999(Default:5000)
$round = 25000;

if($round == 5000){
        $SALT_head = sprintf("\$6\$");
}else{
        $SALT_head = sprintf("\$6\$rounds=%s\$", $round);
}

if($salt_is_binary){
        ## Set sheme
        $scheme = '{SHA512-CRYPT.B64}';

        ## Make salt
        $salt = makerandom_octet(Length => 16);
        $SALT = sprintf("%s%s", $SALT_head, $salt);

        ## Get shadow_password
        $shadow_password_temp = crypt($pass, $SALT);
        $shadow_password = encode_base64($shadow_password_temp, "");

        ## Format as Dovecot password
        $dovecot_password = sprintf("%s%s", ${scheme}, ${shadow_password});

} else {
        ## Set scheme
        $scheme = '{SHA512-CRYPT}';

        ## Make salt
        $salt = makerandom_octet(Length => 12);
        $salt = encode_base64($salt);
        $SALT = sprintf("%s%s", $SALT_head, $salt);


        ## Get shadow_password
        $shadow_password_temp = crypt($password, $SALT);
        $shadow_password = $shadow_password_temp;

        ## Format as Dovecot password
        $dovecot_password = sprintf("%s%s", ${scheme}, ${shadow_password});
}

print "${dovecot_password}\n";


●SHA512-CRYPTの検証例

 面倒臭いのが検証です。やり方はSSHA512と同じで、$stored_passwordからsaltとストレッチ回数を抽出して、それらとユーザが検証用に入れたパスワードからシャドウパスワード→$dovecot_passwordを作って一致すればOKです。

 注意するところはsalt抜き出しとストレッチ回数(round)の抜き出しですかね。正しく抜かないとパスワードが正しくても一致しません。そのため正しく抜けたかのデバッグ出力を入れています。それ以外はただひたすら面倒臭いの一言です。 

Table 3. SHA512-CRYPT検証の例
## Get password and extract salt from stored_password

## $pass1 is same as stored_password
$password = $pass1;

## Recognize scheme and decode when it was decoded.
if ($stored_password =~ /{SHA512-CRYPT}/){
        $stored_shadow_password = $stored_password;
        $stored_shadow_password =~ s/$1//eg;
        $salt_is_binary = 0;
}elsif($stored_password =~ /{SHA512-CRYPT\.B64}/){
        $stored_shadow_password = $stored_password;
        $stored_shadow_password_b64encoded =~ s/$1//eg;
        $stored_shadow_password = decode_base64($stored_shadow_password_b64encoded);
        $salt_is_binary = 1;
}

## Recognize Round times
if ($stored_shadow_password =~ m|(^\$6\$rounds=([0-9]{4,9})\$(.+)\$)|){
        $round = $2;
        $salt = $3;
}else{
        $stored_shadow_password =~ m|(^\$6\$(.+)\$(.+)$)|;
        $round = 5000;
        $salt = $2;
}

## ※この3行はデバッグ用出力です。
print "Rounds: $round\n";
print "Salt: $salt\n";
print "Password: $password\n";

if($round == 5000){
        $SALT_head = sprintf("\$6\$");
}else{
        $SALT_head = sprintf("\$6\$rounds=%s\$", $round);
}
if($salt_is_binary){
        ## Set sheme
        $scheme = '{SHA512-CRYPT.B64}';

        ## Make salt
        $SALT = sprintf("%s%s", $SALT_head, $salt);

        ## Get shadow_password
        $shadow_password_temp = crypt($password, $SALT);
        $shadow_password = encode_base64($shadow_password_temp, "");

        ## Format as Dovecot password
        $dovecot_password = sprintf("%s%s", ${scheme}, ${shadow_password});

} else {
        ## Set scheme
        $scheme = '{SHA512-CRYPT}';

        ## Make salt
        $SALT = sprintf("%s%s", $SALT_head, $salt);

        ## Get shadow_password
        $shadow_password_temp = crypt($password, $SALT);
        $shadow_password = $shadow_password_temp;

        ## Format as Dovecot password
        $dovecot_password = sprintf("%s%s", ${scheme}, ${shadow_password});
}

## この2行もデバッグ用出力です。
print "Stored Password:  ${stored_password}\n";
print "Dovecot Password: ${dovecot_password}\n";

## If $dovecot_password and $stored_password are same, print verified.
if($dovecot_password eq $stored_password){
        print "Verified!\n";
}else{
        print "Not Verified\n";
}

 このスキームに限ったことではありませんが、エラーチェックはありません。そのため例えばストレッチ回数の下限は、本当は1,000ですが0050とかの文字列でも通ってしまいます。
 また$stored_passwordは必ず正しいフォーマットのSHA512-CRYPTがくると決め打ちて書いています。

●次のセクションでやること。

●あとがき

 今回4つのスキームを紹介していますが、実は、SHA512-CRYPTだけはサイトを作るためだけにわざわざ検証したもので、私がdovecotメールサーバ内のスクリプトで使用しているスキームに、SHA512-CRYPTはありませんでした(というか今でも未採用)。

 本当は私が自分用に実装したSHA512、SSHA512とBLF-CRYPTだけを紹介する予定でした。

 ただ、この組み合わせは列車種別に例えると各停(普通)、区間快速、特別急行(特急)みたいな感じで、区間快速と特別急行の間が無くバランスが悪かったので、バランスを取るために入れたのがこのSHA512-CRYPTです。

●参考文献