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

 BLF-CRYPTの生成方法は、サーバ管理用スクリプト作成時に海外のどこかのフォーラムだったかブログだったにあったpythonの例を元にしてperlで書き直したものになります。
 ただ、そのサイトを失念して思い出せないため参考文献にはありません。すいません。

 流れとしてはmakerandom_octet関数で16Byteの乱数を作って、入力された$passwordとそれをCrypt::Eksblowfishモジュールのbcrypt関数に突っ込んでシャドウパスワードを取得する流れになります。

 SHA512-CRYPTと異なりsaltやストレッチ(round)回数による差分はありませんが、ストレッチ回数は1桁の場合は07のように0をつけて必ず2桁で指定します。07の場合は2^7乗になります。

 BLF-CRYTはsaltもdigestもシャドウパスワードとして出力する際にBase64で符号化します。そのためsaltはバイナリ文字列でも問題ありません。
 ただ、このBase64はMIME::Base64ではなくen_base64というCyrpt::Eksblowfishに添付の専用の関数で出します。

 この専用の関数(en_base64/de_base64)は符号化の方法がMIME::Base64と異なるらしく、同じ平文を入れても同じ符号になりません。
※CPANのEksblowfishのドキュメントでは「Bcryptを使うのに便利な関数」という謎の説明がされています。)

 Table 1に例を示します。password3がそれです。見た感じシーザ暗号みたいに文字が2文字ほどずれているだけな気がしなくもないです。

Table 1. Base64の比較
 $password = "y17";
 $prefix = "{SHA512}";

 $shadow_password1 = $sha2->add("$password")->b64digest;
 $shadow_password2 = $sha2->add("$password")->digest;
 $shadow_password3 = $sha2->add("$password")->digest;
 
 $dovecot_password1 = sprintf("%s%s", $prefix, $shadow_password1);
 $dovecot_password2 = sprintf("%s%s", $prefix, encode_base64($shadow_password2, ""));
 $dovecot_password3 = sprintf("%s%s", $prefix, en_base64($shadow_password3));

print "$dovecot_password1\n";
print "$dovecot_password2\n";
print "$dovecot_password3\n";

※以下、出力結果
{SHA512}8gvHVYhwaek+BhOO9uo7HuAy1v+Y3/j6NZUXkgUynyOorSZ7GEF5RZ41sUMkALtgW1L3+82QjbHCBbq5dgLPuQ
{SHA512}8gvHVYhwaek+BhOO9uo7HuAy1v+Y3/j6NZUXkgUynyOorSZ7GEF5RZ41sUMkALtgW1L3+82QjbHCBbq5dgLPuQ==
{SHA512}6etFTWfuYci8/fMM7sm5Fs.wzt8W19h4LXSVieSwlwMmpQX5ECD3PX2zqSKi.JreUzJ1860OhZFA/Zo3beJNsO

password2がSHA512のdovecotのリファレンスです。
password3がen_base64です。
※特に書いてませんが、en_base64はEksblowfishのものを使っています。

 libcryptoが対応していれば、Eksblowfishを使わずともcrypt関数だけで作ることもできます。

●環境と用語

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

●ソースコード

 こちらからどうぞ。

●必要なモジュール

EksblowfishかBase64が必要です。Eksblowfishモジュールを使わず、crypt関数を使うならBase64が必要です。

いずれも、FreeBSDでバイナリパッケージが用意されており、pkgコマンドでインストール可能です。

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

●BLF-CRYPTのシャドウパスワードの生成例(Eksblowfish版)

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

 digestをEksblowfishのbcrypt関数で取得して、それにスキームとエンコード回数を組み込んで、saltとdigestのそれぞれをen_base64というBase64(もどき)でエンコードするだけです。

 en_base64はEksblowfishに組み込まれている関数です。Eksblowfishはこのen_base64関数を使わないとダメっぽいです。MIME:Base64の方のencode_base64ではうまくdoveadmを通りませんでした。

Table 2. BLF-CRYPT Eksblowfish モジュール版
a## Make encrypted password by BLF-CRYPT

## Set $passowrd as user input password.
$password = $pass;

## Set sheme
$scheme = '{BLF-CRYPT}';

## 01-31, Default:05
## 回数は2の$round乗になります。
$round = 11;

## Make salt/SALT
$salt = makerandom_octet(Length => 16);

## Get digest
## key_nulは付けなくてもデフォルト1です。この関数でcostをつけないとデフォルト8です。
## ※doveadmのデフォルトは5です。
$digest = bcrypt_hash({key_nul => 1, cost => $round, salt => $salt}, $password);

## Make shadow_password
## roundは2桁指定なので%02dにしています。
$shadow_password = sprintf("\$2a\$%02d\$%s%s", $round, en_base64($salt), en_base64($digest));

## Make dovecot_password
$dovecot_password = sprintf("%s%s", $scheme, $shadow_password);

print "${dovecot_password}\n";


●BLF-CRYPTのシャドウパスワード生成例(crypt関数版)

 ぶっちゃけていうとSHA512-CRYPT同様にcrypt関数でもBLF-CRYPTは生成可能です。ただ、Eksblowfishを入れたくないとかであれば特にメリットはありません(デメリットもありません)。

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

「PLAINTEXT」はここでいうと$password (ユーザが入力したパスワード)です。
「SALT」本コンテンツのsaltと異なり、saltとストレッチ(round)回数をまとめたマジックワードで、BLF-CRYPTの場合は「$2a$ストエレッチ回数(2の階乗で指定)$<Base64をエンコードしたsalt>」となっています。前述のようにSHA512-CRYPTと異なりストレッチ回数による差はありません。
ここではSALTとsaltは別物として扱います。

この2つを入れるとシャドウパスワードが返ってきます。これはどうもすでにBase64化されているようです(青文字部分)。

SALTの中のsaltはBase64でエンコード済みである必要があるみたいです。 

Table 3. BLF-CRYPTのシャドウパスワード生成例(crypt関数版)
## Make encrypted password by BLF-CRYPT

## Set $passowrd as user input password.
$password = $pass;

## Set sheme
$scheme = '{BLF-CRYPT}';

## 01-31, Default:05
$round = 11;

## Make salt/SALT
$salt = makerandom_octet(Length => 16);

## Make SALT
## ※SALTが$で終わってはいけないのはSHA512-CRYPTと同じです。
$SALT = sprintf("\$2a\$%02d\$%s", $round, encode_base64($salt, ""));

## Make shadow_password
$shadow_password = crypt($password, $SALT);

## Make dovecot_password
$dovecot_password = sprintf("%s%s", $scheme, $shadow_password);

print "${dovecot_password}\n";

●BLF-CRYPTの検証例(Eksblowfish版)

 Eksblowfishのモジュールを使うと検証が楽です。BLF-CRYPTは$2a$0x$<22文字>にストレッチ回数とsaltが全部入ってます。

 そのため正規表現でこの$2a$で始まる部分からsalt部分までを引き抜いてSALTとし、EksblowfishモジュールにはこのSALTと入力された$passowrdから再度digestを取得するbcrypt関数というものがあります。

 このbcrypt関数を使えば、saltやストレッチ回数を紐解かずとも一撃でシャドウパスワードを取得できます。まぁ、cryptと同じですね。

 取得後はスキームつけてdovecot_passwordとstored_passwordと比較するか、stored_passwordからスキーム剥がしてシャドウパスワード同士で比較するかのどちらかです。簡単です。

 ストレッチ回数やsaltを紐解いてまたbcrypt_hashで作り直してもいいかもしれませんが、時間の無駄だと思います。ちなみに紐解けるように(それ用かは存じませんが)ちゃんとde_base64という関数が用意されています。

Table 4. BLF-CRYPT検証の例(Eksblowfish版)
## Get password and extract salt from stored_password

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

## Scheme
$scheme = "{BLF-CRYPT}";

## Get SALT
$stored_password =~ m|(\$2a\$[01][0-9]\$[A-Za-z0-9./]{22})|;
$SALT = $1;

## Make digest
$digest = bcrypt($password, $SALT);

## Concatinate scheme
$dovecot_password = sprintf("%s%s", $scheme, $digest);

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";
}

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

●BLF-CRYPTの検証例(crypt版)

 crypt版の検証はさっきと1文字しか変わるところがありません。面白くも何ともありません。## Get digestでシャドウパスワードを求める部分がbcryptからcryptに変わっただけです。まぁ上でやってることがcryptと同じですからね。

 どちらでやっても構いませんが、生成のところでcrypt関数でパスワード生成できなかったのであればこちらも使えません。

Table 5. BLF-CRYPTの検証例(crypt版)
## Get password and extract salt from stored_password

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

## Sheme
$scheme = "{BLF-CRYPT}";

## Get SALT
$stored_password =~ m|(\$2a\$[01][0-9]\$[A-Za-z0-9./]{22})|;
$SALT = $1;

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

## Concatinate sheme
$dovecot_password = sprintf("%s%s", $scheme, $shadow_password);

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";
}


●あとがき

 今回4つのスキームを紹介しました。実際に私がサーバ上で実装したスキームはSHA512、SSHA512とBLF-CRYPTの3つで、事実上BLF-CRYPT以外は使っていません。

 本当はArgon2とかも作って紹介したかったのですが、検証環境を用意するのが面倒なので今回はここまでです。

●参考文献