●SHA512/SSHA512でシャドウパスワードを作成する。

 このスキームは脆弱ですので新規に導入するには値しないと思ってますが、最初の実証コードを書いてみる場合には一番簡単なスキームですし、ハッシュさえしておけば大丈夫という思想を持っている方も一定数いて、しぶしぶ保守している方もいると思いますので、一応掲載します。

 SHA512の方は簡単で、文字列をハッシュ関数に通すだけです。検証はdoveoct_passwordとその通して得たシャドウパスワードをstored_passwordと比較するだけです。

●環境と用語

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

●ソースコード

 こちらからどうぞ。

●必要なモジュール

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

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

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

 SHA512の例を掲載します。

 「## Get User password」から「### Make shadow password with SHA512 hash.」までは、コンソールからユーザのパスワード入力を求める部分です。

 モジュール関係を除けばここはスキームが変わっても同じですので次回以降は掲載しません。

Table 2. SHA512生成の例
#!/usr/bin/env perl

## Load perl Modules
use Digest::SHA qw(sha512);
use Crypt::Random qw(makerandom_octet);
use MIME::Base64;

## Get User Password
print STDERR "Please enter new password > ";
system("stty -echo");
$pass1 = <STDIN>;
system("stty echo");
chomp($pass1);
print "\n";

## ※system("stty -echo")はパスワードを入力しても表示されないように、コンソールのエコーバックを無効するためのシステム関数です。やりっぱなしで放置すると入力した文字がわからなくなるので、パスワード受付後はsystem("stty echo")で戻します。

print STDERR "Please re-enter password > ";
system("stty -echo");
$pass2= <STDIN>;
system("stty echo");
chomp($pass2);

## Compare and verify 1st and 2nd time inputs are same.
if($pass1 eq $pass2){
        print STDERR "\n";
        print STDERR "ok\n";
        $pass = $pass1;
}else{
        print STDERR "\n";
        print STDERR "Password is not match\n";
        exit;
}


### ここから上は次回以降削除
## Make shadow password with SHA512 hash.

$password = $pass;
$scheme = '{SHA512}';
$round = "";
$salt = "";

## Create new sha2 instance
$sha2 = Digest::SHA->new(512);

$prefix = "$scheme";

## Get Digest
$digest = $sha2->add("$password")->digest;

## Encode digest to output
$shadow_password =
encode_base64($digest, "")
## ※第2引数は改行抑制です。

## Make dovecot password (concatinate scheme prefix and shadow password)
$dovecot_password = sprintf("%s%s", $prefix, $shadow_password);

print "$dovecot_password\n";

 $sha2 = Digest::SHA->new(512)で、SHA2クラスのインスタンスを作成します。この時に512に変えて256を指定するとSHA256になります。

 あとはその生成したクラスに$sha2->add("平文パスワード")でハッシュしたい文字列を入れて、$sha2->digest関数を呼んで$digestを得ます。

 この関数で得た$digestはバイナリ文字列になっていますので、Base64でエンコードするとシャドウパスワードになります。doveadmで得られるシャドウパスワードもBase64でエンコードされた状態です。

 Base64エンコードを行うencode_base64()関数は勝手に改行を入れるため、その改行文字を指定する第2引数に空白を入れて改行表示を抑制しています。

 最後にBase64エンコードをせず、$digestは$sha2->add("$password")->b64digestで取得して、これをそのまま$shadow_password = $digestとする方法もあります。これでも問題なく認証できます。

 違いはシャドウパスワード末尾の「==」があるかないだけです。ただ、doveadmでは「==」の文字がないとverifiedになりません。

Table 3. b64で取得する方法と、Base64であとからエンコード
※$digestをBase64でエンコード(doveadmと同じ出力結果)
{SHA512}8gvHVYhwaek+BhOO9uo7HuAy1v+Y3/j6NZUXkgUynyOorSZ7GEF5RZ41sUMkALtgW1L3+82QjbHCBbq5dgLPuQ==


※認証自体はこれでも大丈夫っぽいです。(b64digestで取得)
{SHA512}8gvHVYhwaek+BhOO9uo7HuAy1v+Y3/j6NZUXkgUynyOorSZ7GEF5RZ41sUMkALtgW1L3+82QjbHCBbq5dgLPuQ

※(参考)Eksblowfishのen_base64はSHA512用には使えません(逆も無理だと思いますが)。
{SHA512}6etFTWfuYci8/fMM7sm5Fs.wzt8W19h4LXSVieSwlwMmpQX5ECD3PX2zqSKi.JreUzJ1860OhZFA/Zo3beJNsO

●SHA512の検証例

これは説明不要ですね。できた$dovecot_passwordと$stored_passwordを比較して一致していればパスワード認証成功です。

●SSHA512のシャドウパスワード生成例

 SSHA512の例を掲載します。「 ## Make SSHA512 Password」より上の部分はSHA512と同じなので省略します。
 SSHA512は($password.$salt)のSHA512ダイジェストに、$saltを連結して、Base64でエンコードした構造になっています。$saltは4バイトです。

 やり方ですが、まず、makerandom_octet(Length => 4)関数で4バイトの乱数を作ります。

 makerandom_octet関数は指定されたLength Byte分の暗号学的に安全(Cryptographically Secure)なランダムなバイナリ文字列を生成する関数らしいです。4文字の乱数が欲しいならこれが一番簡単で安全だと思います。

 簡単ですが、この関数は/dev/randomによるランダムプールを使うらしく、短時間に使いすぎるとブロッキングして出てこなくなるらしいので一度に大量生成する時には不向きです。

 あとは$passwordと$saltを連結してSHA512のダイジェスト($digest)を取得します。

 digestを取得したら$digestと$saltをこの順で連結して、その後その連結したものをBase64でエンコードして、スキームを頭につけて終わりです。

Table 3. SSHA512生成の例
## Make SSHA512 Password

$scheme = '{SSHA512}';

## Get 4 byte random characters
$salt = makerandom_octet(Length => 4);

## Get new sha2 instance
$sha2 = Digest::SHA->new(512);
$prefix = "$scheme";

## Add pass + salt to get digest.
$sha2->add(${pass}.${salt});

## Get Digest
$digest = $sha2->digest;

## Make shadow password (Base64 encode conbinated strings=digest+salt)
$shadow_password = encode_base64(${digest}.${salt}, "");

$dovecot_password = sprintf("%s%s", ${prefix}, ${shadow_password});

print "$dovecot_password\n";

Perlの文字列連結は.(ピリオド)で連結すると遅いとどこかのサイトで見たことがありますが、ここでは見やすさを考えてピリオドで連結しています。

 興味ないと思いますが、コード中512と書いてある部分をすべて256にするとそのままSSHA256を生成できます。

●SSHA512の検証例

 SSHA512ではstored_passowrdからsaltを抽出し、そのsaltとユーザが入力したパスワードからもう一度SSHAのシャドウパスワードを生成することで、パスワードが正しいかを検証します。

 ですので、コードの流れとしては、stored_passwordをBase64デコードして、末尾の4バイトからsaltを抽出し、そのsaltとユーザが検証用に入れてきたパスワードからSSHA512を再生成します。
 その再生成したシャドウパスワード、stored_passwordが一致すればOKです。

 コードの$stored_shadow_passwordは$stored_passwordからスキーム文字列である{SSHA512}を外したものです。
 $stored_passwordは最後に比較用に使用するのと、Perlは置き換えに自分自身を参照するので、いったん$stored_shadow_passwordに$stored_passwordを代入してコピーします。

 $stored_shadow_password_decoded は $stored_shadow_passwordをBase64でデコードしたものです。

 saltはデコードされた$stored_shadow_password_decodedの末尾4byteにくっ付いてますので、substrで切り出します。substrは第2引数にマイナスを指定すると右から指定された文字数分の文字を切り出します。
※この方法ならSSHA256でもコードを変えずに対応できます。

 あとは上記で得られたsaltと入力されたパスワードでもう一回SSHA512を作って、最初に入力されたstored (dovecot) passwordと比較一致してパスワードが正当なものかを確認します。

 サンプルコードは最初にスキーム付きのSSHA512形式の$dovecot_passwordの入力を求め、次にセットしたパスワードを求めてきます。

最初のプロンプトには{SCHME}で始まる文字列を、2番目のプロンプトには設定しているはずのパスワードを入力してください。
最初に入れた文字列のsaltと2番目のパスワードから無事同じパスワードが生成できるとverfifiedと表示します。

Table 4. SSHA512検証例
#!/usr/bin/env perl

### Test SHA512-CRYPT

## Load perl Modules
use Digest::SHA qw(sha512);
use MIME::Base64;
use Crypt::Random qw(makerandom_octet);

## Get stored dovecot password
print STDERR "Please enter stored dovecot password > ";
$stored_password = <STDIN>;
chomp($stored_password);
print "\n";

print STDERR "Please enter password > ";
system("stty -echo");
$pass1 = <STDIN>;
system("stty echo");
chomp($pass1);

## Get password and extract salt from stored_password

## $pass1 is same as user set password in stored_password
$password = $pass1;

$scheme = '{SSHA512}';

## Stored password uses last of code.
## But before get decode by base64, remove schem strings.
$stored_shadow_password = $stored_password;

## Remove Scheme strings
$stored_shadow_password =~ s/$scheme//eg;

## Decode $stored_password by Base64
$stored_shadow_password_decoded = decode_base64($stored_shadow_password);

## Extract last 4 bytes from shadow password in decoded stored password.
$salt = substr($stored_shadow_password_decoded, -4);

## Make SSHA512 from decoded salt and input password.
$sha2 = Digest::SHA->new(512);
$sha2->add(${password}.${salt});

$digest = $sha2->digest;

$shadow_password = encode_base64(${digest}.${salt}, "");

$dovecot_password = sprintf("%s%s", ${scheme}, ${shadow_password});

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

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

●参考文献