HMAC-SHA-256 固定の PBKDF2 関数

HTTP Basic 認証のパスワード・ファイル用に、Pythonpasslib.hash.pbkdf2_sha256 を真似た PBKDF2 ライブラリを作ります。 もちろん、Basic 認証だけでなく、フォームによる認証にも流用するつもりです。 passlib に似てますが、クラスではなく、namespace の関数にしています。 HMAC-SHA-256 はdigest::SHA256 と digest::HMAC を使い、 crypt 用 BASE64Base 64 エンコーダとデコーダ を使います。

GitHub の digest::SHA256 と digest::HMAC のリポジトリへ pbkdf2-sha256 と mime-base64 を追加しました。

tociyuki/libdigest-hmac-sha256-cxx11

#include "pbkdf20-sha256.hpp"
#include "mime-base64.hpp"
#include "taptests.hpp"

int
main ()
{
    test::simple t (2);

    // salt を乱数から自動生成し、パスワードを encrypt します。
    t.diag (pbkdf2_sha256::encrypt ("password"));

    // encrypt は、salt と PBKDF2 キーを crypt 用 BASE64 でエンコードします。
    std::string expected = "$pbkdf2-sha256$6400$0ZrzXitFSGltTQnBWOsdAw$Y11AchqV4b0sUisdZd0Xr97KWoymNE0LNNrnEgY4H9M";

    // encrypt に指定する salt は BASE64 エンコードする前のオクテットです。
    std::string salt;
    mime::decode_base64crypt ("0ZrzXitFSGltTQnBWOsdAw", salt);

    std::string got = pbkdf2_sha256::encrypt ("password", salt);

    t.ok (got == expected, "encrypt salt");

    // パスワードを verify できたら真を返します。
    t.ok (pbkdf2_sha256::verify ("password", got), "verify password");

    return t.done_testing ();
}

encrypt 関数で、PBKDF2 の繰り返し回数を何回おこなうか、salt を自動生成させるかどうか、自動生成するときは何オクテット生成するか、これらを指定することもできますし、デフォルト値で生成することもできます。

//@<pbkdf2-sha256.hpp@>=
#ifndef PBKDF2_SHA256_HPP
#define PBKDF2_SHA256_HPP

#include <string>

namespace pbkdf2_sha256 {

std::string encrypt (std::string const& password);
std::string encrypt (std::string const& password, std::size_t const rounds);
std::string encrypt (std::string const& password, std::string const& salt);
std::string encrypt (std::string const& password, std::size_t const rounds, std::size_t const salt_size);
std::string encrypt (std::string const& password, std::size_t const rounds, std::string const& salt);

bool verify (std::string const& password, std::string const& pubkey);

void pbkdf2_sha256 (std::string const& secret, std::string const& salt, std::size_t const rounds, std::size_t keylen, std::string& dkout);

}//namespace pbkdf2_sha256

#endif

PBKDF2 の繰り返し回数のデフォルトは 6400 回。slat のデフォルト長は 16 オクテットです。PBKDF2 キーの長さは 32 オクテットに固定しています。

//@<pbkdf2-sha256.cpp@>=
#include <algorithm>
#include <string>
#include <cctype>
#include <random>
#include "pbkdf20-sha256.hpp"
#include "digest.hpp"
#include "mime-base64.hpp"

namespace pbkdf2_sha256 {

static const std::string IDENT = "$pbkdf2-sha256$";
static const std::size_t ROUNDS_DEFAULT = 6400U;
static const std::size_t SALT_SIZE_DEFAULT = 16U;
static const std::size_t KEYLEN_DEFAULT = 32U;

//@<encrypt のデフォルト値を渡す関数を定義します@>
//@<encrypt の salt を生成する関数を定義します@>
//@<encrypt の PBKDF2 をおこなう関数を定義します@>
//@<verify 関数を定義します@>
//@<pbkdf2_sha256 関数を定義します@>
}//namespace pbkdf2_sha256

繰り返し回数、ソルト長を省略しているときは、省略値を使います。

//@<encrypt のデフォルト値を渡す関数を定義します@>=
std::string
encrypt (std::string const& password)
{
    return encrypt (password, ROUNDS_DEFAULT, SALT_SIZE_DEFAULT);
}

std::string
encrypt (std::string const& password, std::size_t const rounds)
{
    return encrypt (password, rounds, SALT_SIZE_DEFAULT);
}

std::string
encrypt (std::string const& password, std::string const& salt)
{
    return encrypt (password, ROUNDS_DEFAULT, salt);
}

ソルトは、指定オクテット数の乱数を連結して作成します。

//@<encrypt の salt を生成する関数を定義します@>=
std::string
encrypt (std::string const& password, std::size_t const rounds, std::size_t const salt_size)
{
    std::string salt;
    std::random_device randev;
    std::mt19937 gen (randev ());
    std::uniform_int_distribution<uint8_t> dist (0, 255);
    for (std::size_t i = 0; i < salt_size; ++i)
        salt.push_back (dist (gen));
    return encrypt (password, rounds, salt);
}

識別子、繰り返し回数、ソルト、生成した PBKDF2 キーをドル記号で連結します。その際、ソルトと PBKDF2 キーはそれぞれを crypt 用 BASE64エンコードします。

//@<encrypt の PBKDF2 をおこなう関数を定義します@>=
std::string
encrypt (std::string const& password, std::size_t const rounds, std::string const& salt)
{
    std::string dk;
    pbkdf2_sha256 (password, salt, rounds, KEYLEN_DEFAULT, dk);
    return IDENT + std::to_string (rounds)
           + "$" + mime::encode_base64crypt (salt)
           + "$" + mime::encode_base64crypt (dk);
}

パスワードを、エンコードされた PBKDF2 キーで照合します。 そのために、ドル記号で分割して、繰り返し回数、ソルト、 PBKDF2 キーに切り分けます。続いて、ソルトを crypt 用 BASE64 デコードします。引数のパスワードを、繰り返し回数とソルトを使って encrypt して、引数のエンコードされた PBKDF2 キーと比較します。

//@<verify 関数を定義します@>=
bool
verify (std::string const& password, std::string const& pubkey)
{
    if (pubkey.compare (0, IDENT.size (), IDENT) != 0)
        return false;
    auto s = pubkey.cbegin () + IDENT.size ();
    auto const e = pubkey.cend ();
    std::size_t rounds = 0;
    if (s >= e || ! std::isdigit (*s))
        return false;
    while (s < e && std::isdigit (*s))
        rounds = rounds * 10 + *s++ - '0';
    if (s >= e || '$' != *s)
        return false;
    auto const s1 = ++s;
    s = std::find (s, e, '$');
    std::string salt64 (s1, s);
    std::string salt;
    mime::decode_base64crypt (salt64, salt);
    return pubkey == encrypt (password, rounds, salt);
}

PBKDF2 関数は RFC 2898 の定義通りです。

//@<pbkdf2_sha256 関数を定義します@>=
void
pbkdf2_sha256 (std::string const& secret, std::string const& salt, std::size_t const rounds, std::size_t keylen, std::string& dkout)
{
    digest::HMAC<digest::SHA256> prf (secret);
    std::string dk;
    uint32_t i = 0;
    while (keylen > 0) {
        ++i;
        std::string block_number; // network byte order (big endian)
        block_number.push_back ((i >> 24) & 0xff);
        block_number.push_back ((i >> 16) & 0xff);
        block_number.push_back ((i >>  8) & 0xff);
        block_number.push_back (i & 0xff);
        std::string u = prf.add (salt).add (block_number).digest ();
        std::string t = u;
        for (std::size_t j = 1; j < rounds; ++j) {
            u = prf.add (u).digest ();
            for (std::size_t k = 0; k < u.size (); ++k)
                t[k] ^= u[k];
        }
        std::size_t n = std::min (keylen, t.size ());
        dk.append (t.begin (), t.begin () + n);
        keylen -= n;
    }
    std::swap (dkout, dk);
}