From 57b3737cc3f9a600c36691aa80817b85d39e5937 Mon Sep 17 00:00:00 2001 From: Aster Seker Date: Fri, 5 Sep 2025 19:39:14 +0300 Subject: [PATCH] feat(kdf): add HKDF and PBKDF2 improvements --- CMakeLists.txt | 4 +- README-RU.md | 13 ++-- README.md | 28 +++++-- include/hmac_cpp/hmac_utils.hpp | 114 ++++++++++++++++++++++++---- include/hmac_cpp/secure_buffer.hpp | 44 +++++++++++ src/hmac_utils.cpp | 116 ++++++++++++++++++++++++++--- test_all.cpp | 99 +++++++++++++++++++++--- 7 files changed, 370 insertions(+), 48 deletions(-) create mode 100644 include/hmac_cpp/secure_buffer.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0651545..64bd9cd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,7 @@ set(HMAC_HEADERS include/hmac_cpp/sha1.hpp include/hmac_cpp/sha256.hpp include/hmac_cpp/sha512.hpp + include/hmac_cpp/secure_buffer.hpp ) add_library(hmac_cpp STATIC ${HMAC_SOURCES}) @@ -68,8 +69,9 @@ if(BUILD_TESTS) DOWNLOAD_EXTRACT_TIMESTAMP TRUE ) FetchContent_MakeAvailable(googletest) + find_package(OpenSSL REQUIRED) add_executable(test_all test_all.cpp) - target_link_libraries(test_all PRIVATE hmac_cpp gtest_main) + target_link_libraries(test_all PRIVATE hmac_cpp gtest_main OpenSSL::Crypto) target_include_directories(test_all PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include) add_test(NAME test_all COMMAND test_all) diff --git a/README-RU.md b/README-RU.md index 03b77ce..53bdeef 100644 --- a/README-RU.md +++ b/README-RU.md @@ -11,6 +11,7 @@ - Поддержка `HMAC` на основе `SHA256`, `SHA512`, `SHA1` - Прямая работа с бинарным или hex-форматом - Поддержка **PBKDF2** (RFC 8018) +- Поддержка **HKDF** (RFC 5869) для извлечения и расширения ключей - Поддержка **временных токенов**: - **HOTP (RFC 4226)** — счётчики - **TOTP (RFC 6238)** — временные токены @@ -137,16 +138,16 @@ std::vector get_hmac( #include std::string password = "password"; -std::string salt = "salt"; -std::vector dk = hmac::pbkdf2(password, salt, 1000, 32, hmac::TypeHash::SHA256); +std::vector salt(16, 0x01); +std::vector dk = hmac::pbkdf2(password, salt, 1000, 32); ``` Параметры: -- `password`, `salt` — строки с паролем и солью -- `iterations` — число итераций -- `dk_len` — длина ключа в байтах -- `hash_type` — хеш-функция (`SHA1`, `SHA256`, `SHA512`) +- `password`, `salt` — байтовые строки; `salt` должна быть уникальной и не короче 16 байт +- `iterations` — число итераций (≥1), подбирается по [рекомендациям OWASP](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html) +- `dk_len` — длина ключа в байтах, не более `(2^32−1) * hLen` +- `prf` — выбор хеша (`Sha1`, `Sha256` по умолчанию, `Sha512`) ### 🕓 HOTP и TOTP токены diff --git a/README.md b/README.md index f7e6480..a20c9d5 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ A lightweight `C++11` library for computing `HMAC` (hash-based message authentic - Supports `HMAC` using `SHA256`, `SHA512`, `SHA1` - Outputs in binary or hex format - Provides **PBKDF2 key derivation** (RFC 8018) +- Implements **HKDF (RFC 5869)** for key extraction/expansion - Support for **time-based tokens**: - **HOTP (RFC 4226)** — counter-based one-time passwords - **TOTP (RFC 6238)** — time-based one-time passwords @@ -161,16 +162,31 @@ Returns: Binary digest as `std::vector` #include std::string password = "password"; -std::string salt = "salt"; -std::vector dk = hmac::pbkdf2(password, salt, 1000, 32, hmac::TypeHash::SHA256); +std::vector salt(16, 0x01); // at least 16 bytes +std::vector dk = hmac::pbkdf2(password, salt, 1000, 32); ``` Parameters: -- `password`, `salt` — Raw byte strings -- `iterations` — Number of iterations -- `dk_len` — Desired key length in bytes -- `hash_type` — Hash function (`SHA1`, `SHA256`, `SHA512`) +- `password`, `salt` — Raw byte arrays. `salt` must be **>=16 bytes** and unique. +- `iterations` — Number of iterations (>=1). Tune according to the + [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html). +- `dk_len` — Desired key length in bytes, up to `(2^32-1) * hLen` (RFC 8018). +- `prf` — Optional hash (`Sha1`, `Sha256` default, `Sha512`). + +⚠️ Low iteration counts or short salts reduce security. + +For deployments with a server-side *pepper*, use `pbkdf2_with_pepper(password, salt, pepper, iters, dkLen)`. +The pepper is a secret key stored separately from the hashed password. + +### HKDF (RFC 5869) + +```cpp +std::vector ikm = {/* secret material */}; +std::vector salt(16, 0x00); +auto prk = hmac::hkdf_extract_sha256(ikm, salt); +auto okm = hmac::hkdf_expand_sha256(prk, {}, 32); // derive 32 bytes +``` ### 🕓 HOTP and TOTP Tokens diff --git a/include/hmac_cpp/hmac_utils.hpp b/include/hmac_cpp/hmac_utils.hpp index 72ffdd8..216667f 100644 --- a/include/hmac_cpp/hmac_utils.hpp +++ b/include/hmac_cpp/hmac_utils.hpp @@ -2,16 +2,33 @@ #define _HMAC_UTILS_HPP_INCLUDED #include "hmac.hpp" +#include #include #include namespace hmac_cpp { - /// \brief Compares two strings in constant time - /// \param a First string - /// \param b Second string - /// \return true if both strings are equal - bool constant_time_equals(const std::string &a, const std::string &b); + /// \brief Compares two byte arrays in constant time + /// \param a Pointer to first array + /// \param a_len Length of the first array + /// \param b Pointer to second array + /// \param b_len Length of the second array + /// \return true if both arrays are equal + bool constant_time_equals(const uint8_t* a, size_t a_len, + const uint8_t* b, size_t b_len); + + inline bool constant_time_equals(const std::vector& a, + const std::vector& b) { + return constant_time_equals(a.data(), a.size(), b.data(), b.size()); + } + + inline bool constant_time_equals(const std::string &a, const std::string &b) { + return constant_time_equals(reinterpret_cast(a.data()), a.size(), + reinterpret_cast(b.data()), b.size()); + } + + /// \brief Hash choices for PBKDF2 + enum class Pbkdf2Hash { Sha1, Sha256, Sha512 }; /// \brief Derives a key from a password using PBKDF2 (RFC 8018) /// \param password_ptr Pointer to the password buffer @@ -25,32 +42,103 @@ namespace hmac_cpp { std::vector pbkdf2( const void* password_ptr, size_t password_len, const void* salt_ptr, size_t salt_len, - int iterations, size_t dk_len, - TypeHash hash_type); + uint32_t iterations, size_t dk_len, + Pbkdf2Hash prf = Pbkdf2Hash::Sha256); /// \brief Derives a key using PBKDF2 from vector-based password and salt template inline std::vector pbkdf2( const std::vector& password, const std::vector& salt, - int iterations, size_t dk_len, - TypeHash hash_type) { + uint32_t iterations, size_t dk_len, + Pbkdf2Hash prf = Pbkdf2Hash::Sha256) { static_assert(std::is_same::value || std::is_same::value, "pbkdf2(vector) supports only char or uint8_t"); return pbkdf2(password.data(), password.size(), salt.data(), salt.size(), - iterations, dk_len, hash_type); + iterations, dk_len, prf); } /// \brief Derives a key using PBKDF2 from string-based password and salt inline std::vector pbkdf2( const std::string& password, const std::string& salt, - int iterations, size_t dk_len, - TypeHash hash_type) { + uint32_t iterations, size_t dk_len, + Pbkdf2Hash prf = Pbkdf2Hash::Sha256) { return pbkdf2(password.data(), password.size(), salt.data(), salt.size(), - iterations, dk_len, hash_type); + iterations, dk_len, prf); + } + + std::vector pbkdf2_with_pepper( + const void* password_ptr, size_t password_len, + const void* salt_ptr, size_t salt_len, + const void* pepper_ptr, size_t pepper_len, + uint32_t iterations, size_t dk_len, + Pbkdf2Hash prf = Pbkdf2Hash::Sha256); + + template + inline std::vector pbkdf2_with_pepper( + const std::vector& password, + const std::vector& salt, + const std::vector& pepper, + uint32_t iterations, size_t dk_len, + Pbkdf2Hash prf = Pbkdf2Hash::Sha256) { + static_assert(std::is_same::value || std::is_same::value, + "pbkdf2_with_pepper(vector) supports only char or uint8_t"); + return pbkdf2_with_pepper(password.data(), password.size(), + salt.data(), salt.size(), + pepper.data(), pepper.size(), + iterations, dk_len, prf); + } + + inline std::vector pbkdf2_with_pepper( + const std::string& password, + const std::string& salt, + const std::string& pepper, + uint32_t iterations, size_t dk_len, + Pbkdf2Hash prf = Pbkdf2Hash::Sha256) { + return pbkdf2_with_pepper(password.data(), password.size(), + salt.data(), salt.size(), + pepper.data(), pepper.size(), + iterations, dk_len, prf); + } + + std::vector hkdf_extract_sha256( + const void* ikm_ptr, size_t ikm_len, + const void* salt_ptr, size_t salt_len); + + inline std::vector hkdf_extract_sha256( + const std::vector& ikm, + const std::vector& salt) { + return hkdf_extract_sha256(ikm.data(), ikm.size(), salt.data(), salt.size()); + } + + std::vector hkdf_expand_sha256( + const void* prk_ptr, size_t prk_len, + const void* info_ptr, size_t info_len, + size_t L); + + inline std::vector hkdf_expand_sha256( + const std::vector& prk, + const std::vector& info, + size_t L) { + return hkdf_expand_sha256(prk.data(), prk.size(), info.data(), info.size(), L); + } + + struct KeyIv { + std::array key; + std::array iv; + }; + + KeyIv hkdf_key_iv_256(const void* ikm_ptr, size_t ikm_len, + const void* salt_ptr, size_t salt_len, + const std::string& context); + + inline KeyIv hkdf_key_iv_256(const std::vector& ikm, + const std::vector& salt, + const std::string& context) { + return hkdf_key_iv_256(ikm.data(), ikm.size(), salt.data(), salt.size(), context); } /// \brief Generates a time-based HMAC-SHA256 token diff --git a/include/hmac_cpp/secure_buffer.hpp b/include/hmac_cpp/secure_buffer.hpp new file mode 100644 index 0000000..6df07f7 --- /dev/null +++ b/include/hmac_cpp/secure_buffer.hpp @@ -0,0 +1,44 @@ +#ifndef HMAC_CPP_SECURE_BUFFER_HPP +#define HMAC_CPP_SECURE_BUFFER_HPP + +#include +#include +#include + +namespace hmac_cpp { + +inline void secure_zero(void* ptr, size_t len) { + volatile unsigned char* p = static_cast(ptr); + while (len--) { + *p++ = 0; + } +} + +template +struct secure_buffer { + static_assert(std::is_trivial::value, "secure_buffer requires trivial type"); + + secure_buffer() = default; + explicit secure_buffer(size_t n) : buf(n) {} + explicit secure_buffer(std::vector&& v) : buf(std::move(v)) {} + ~secure_buffer() { secure_zero(buf.data(), buf.size() * sizeof(T)); } + + T* data() { return buf.data(); } + const T* data() const { return buf.data(); } + size_t size() const { return buf.size(); } + + T& operator[](size_t i) { return buf[i]; } + const T& operator[](size_t i) const { return buf[i]; } + + typename std::vector::iterator begin() { return buf.begin(); } + typename std::vector::iterator end() { return buf.end(); } + typename std::vector::const_iterator begin() const { return buf.begin(); } + typename std::vector::const_iterator end() const { return buf.end(); } + +private: + std::vector buf; +}; + +} // namespace hmac_cpp + +#endif // HMAC_CPP_SECURE_BUFFER_HPP diff --git a/src/hmac_utils.cpp b/src/hmac_utils.cpp index 5b58b04..05568c9 100644 --- a/src/hmac_utils.cpp +++ b/src/hmac_utils.cpp @@ -1,36 +1,51 @@ #include "hmac_cpp/hmac_utils.hpp" +#include "hmac_cpp/secure_buffer.hpp" #include #include #include #include +#include namespace hmac_cpp { - bool constant_time_equals(const std::string &a, const std::string &b) { - size_t max_len = a.size() > b.size() ? a.size() : b.size(); - unsigned int diff = (a.size() != b.size()); + bool constant_time_equals(const uint8_t* a, size_t a_len, + const uint8_t* b, size_t b_len) { + size_t max_len = a_len > b_len ? a_len : b_len; + unsigned int diff = (a_len != b_len); for (size_t i = 0; i < max_len; ++i) { - unsigned char ac = i < a.size() ? static_cast(a[i]) : 0; - unsigned char bc = i < b.size() ? static_cast(b[i]) : 0; + unsigned char ac = i < a_len ? a[i] : 0; + unsigned char bc = i < b_len ? b[i] : 0; diff |= ac ^ bc; } return diff == 0; } + static TypeHash to_type_hash(Pbkdf2Hash prf) { + switch (prf) { + case Pbkdf2Hash::Sha1: return TypeHash::SHA1; + case Pbkdf2Hash::Sha256: return TypeHash::SHA256; + case Pbkdf2Hash::Sha512: return TypeHash::SHA512; + } + throw std::invalid_argument("Unsupported hash type"); + } + std::vector pbkdf2( const void* password_ptr, size_t password_len, const void* salt_ptr, size_t salt_len, - int iterations, size_t dk_len, - TypeHash hash_type) { + uint32_t iterations, size_t dk_len, + Pbkdf2Hash prf) { if ((password_len > 0 && password_ptr == nullptr) || (salt_len > 0 && salt_ptr == nullptr)) throw std::invalid_argument("Null pointer with non-zero length"); - if (iterations <= 0) - throw std::invalid_argument("PBKDF2: iterations must be positive"); + if (iterations < 1) + throw std::invalid_argument("PBKDF2: iterations must be >= 1"); if (dk_len == 0) throw std::invalid_argument("PBKDF2: dk_len must be positive"); + if (salt_len < 16) + throw std::invalid_argument("PBKDF2: salt must be at least 16 bytes"); size_t hlen = 0; + TypeHash hash_type = to_type_hash(prf); switch (hash_type) { case TypeHash::SHA1: hlen = hmac_hash::SHA1::DIGEST_SIZE; @@ -45,6 +60,11 @@ namespace hmac_cpp { throw std::invalid_argument("Unsupported hash type"); } + uint64_t max_dk = (static_cast(1) << 32) - 1; + max_dk *= hlen; + if (dk_len > max_dk) + throw std::invalid_argument("PBKDF2: dk_len too large"); + size_t l = (dk_len + hlen - 1) / hlen; size_t r = dk_len - (l - 1) * hlen; @@ -68,7 +88,7 @@ namespace hmac_cpp { salt_block.data(), salt_block.size(), hash_type); std::vector t = u; - for (int j = 1; j < iterations; ++j) { + for (uint32_t j = 1; j < iterations; ++j) { u = get_hmac(password_ptr, password_len, u.data(), u.size(), hash_type); for (size_t k = 0; k < t.size(); ++k) { @@ -80,11 +100,85 @@ namespace hmac_cpp { } else { derived.insert(derived.end(), t.begin(), t.end()); } + secure_zero(u.data(), u.size()); + secure_zero(t.data(), t.size()); } - + secure_zero(salt_block.data(), salt_block.size()); return derived; } + std::vector pbkdf2_with_pepper( + const void* password_ptr, size_t password_len, + const void* salt_ptr, size_t salt_len, + const void* pepper_ptr, size_t pepper_len, + uint32_t iterations, size_t dk_len, + Pbkdf2Hash prf) { + TypeHash hash_type = to_type_hash(prf); + auto pwd_prime = get_hmac(pepper_ptr, pepper_len, password_ptr, password_len, hash_type); + secure_buffer tmp(std::move(pwd_prime)); + auto dk = pbkdf2(tmp.data(), tmp.size(), salt_ptr, salt_len, iterations, dk_len, prf); + return dk; + } + + std::vector hkdf_extract_sha256( + const void* ikm_ptr, size_t ikm_len, + const void* salt_ptr, size_t salt_len) { + std::vector salt_buf; + if (salt_ptr == nullptr || salt_len == 0) { + salt_buf.assign(hmac_hash::SHA256::DIGEST_SIZE, 0); + salt_ptr = salt_buf.data(); + salt_len = salt_buf.size(); + } + auto prk = get_hmac(salt_ptr, salt_len, ikm_ptr, ikm_len, TypeHash::SHA256); + return prk; + } + + std::vector hkdf_expand_sha256( + const void* prk_ptr, size_t prk_len, + const void* info_ptr, size_t info_len, + size_t L) { + const size_t HashLen = hmac_hash::SHA256::DIGEST_SIZE; + if (prk_ptr == nullptr || prk_len != HashLen) + throw std::invalid_argument("HKDF: prk must be HashLen bytes"); + if (L > 255 * HashLen) + throw std::invalid_argument("HKDF: L too large"); + + std::vector okm; + okm.reserve(L); + std::vector previous; + size_t n = (L + HashLen - 1) / HashLen; + for (size_t i = 1; i <= n; ++i) { + std::vector input(previous.begin(), previous.end()); + if (info_ptr && info_len) + input.insert(input.end(), + reinterpret_cast(info_ptr), + reinterpret_cast(info_ptr) + info_len); + input.push_back(static_cast(i)); + auto t = get_hmac(prk_ptr, prk_len, input.data(), input.size(), TypeHash::SHA256); + size_t take = (i == n) ? (L - okm.size()) : t.size(); + okm.insert(okm.end(), t.begin(), t.begin() + take); + previous.assign(t.begin(), t.end()); + secure_zero(t.data(), t.size()); + secure_zero(input.data(), input.size()); + } + secure_zero(previous.data(), previous.size()); + return okm; + } + + KeyIv hkdf_key_iv_256(const void* ikm_ptr, size_t ikm_len, + const void* salt_ptr, size_t salt_len, + const std::string& context) { + auto prk = hkdf_extract_sha256(ikm_ptr, ikm_len, salt_ptr, salt_len); + auto okm = hkdf_expand_sha256(prk.data(), prk.size(), + context.data(), context.size(), 44); + KeyIv out{}; + std::copy(okm.begin(), okm.begin() + 32, out.key.begin()); + std::copy(okm.begin() + 32, okm.begin() + 44, out.iv.begin()); + secure_zero(prk.data(), prk.size()); + secure_zero(okm.data(), okm.size()); + return out; + } + std::string generate_time_token(const std::string &key, int interval_sec, TypeHash hash_type) { if (interval_sec <= 0) { throw std::invalid_argument("interval_sec must be positive"); diff --git a/test_all.cpp b/test_all.cpp index 2c02dcd..c61af8a 100644 --- a/test_all.cpp +++ b/test_all.cpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include "hmac_cpp/hmac.hpp" #include "hmac_cpp/hmac_utils.hpp" @@ -16,6 +18,16 @@ extern "C" std::time_t time(std::time_t* t) { return mock_time_value; } +static std::vector from_hex(const std::string& hex) { + std::vector out; + out.reserve(hex.size() / 2); + for (size_t i = 0; i < hex.size(); i += 2) { + std::string byte = hex.substr(i, 2); + out.push_back(static_cast(std::stoi(byte, nullptr, 16))); + } + return out; +} + TEST(HashTest, SHA1) { EXPECT_EQ(hmac_hash::sha1("grape"), "bc8a2f8cdedb005b5c787692853709b060db75ff"); @@ -80,6 +92,14 @@ TEST(UtilsTest, ConstantTimeEqualsLengthMultiples256) { EXPECT_FALSE(hmac::constant_time_equals(base, plus512)); } +TEST(UtilsTest, ConstantTimeEqualsVector) { + std::vector a = {1, 2, 3}; + std::vector b = {1, 2, 3}; + EXPECT_TRUE(hmac::constant_time_equals(a, b)); + b[2] = 4; + EXPECT_FALSE(hmac::constant_time_equals(a, b)); +} + TEST(HMACTest, SHA256) { const std::string key = "12345"; const std::string input = "grape"; @@ -200,20 +220,77 @@ TEST(TokenBoundaryFingerprintTest, MinTime) { EXPECT_TRUE(hmac::is_token_valid(token_next, key, fingerprint, interval)); } -TEST(PBKDF2Test, SHA1) { - const std::string password = "password"; - const std::string salt = "salt"; - std::vector dk = hmac::pbkdf2(password, salt, 2, 20, hmac::TypeHash::SHA1); - std::string hex = hmac::to_hex(std::string(dk.begin(), dk.end())); - EXPECT_EQ(hex, "ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957"); +TEST(PBKDF2Validation, ShortSaltThrows) { + EXPECT_THROW(hmac::pbkdf2("password", "salt", 2, 20, hmac::Pbkdf2Hash::Sha1), std::invalid_argument); } -TEST(PBKDF2Test, SHA256) { - const std::string password = "password"; - const std::string salt = "salt"; - std::vector dk = hmac::pbkdf2(password, salt, 2, 32, hmac::TypeHash::SHA256); +TEST(PBKDF2Test, SHA256WithValidSalt) { + auto salt = from_hex("000102030405060708090a0b0c0d0e0f"); + std::string salt_str(salt.begin(), salt.end()); + auto dk = hmac::pbkdf2(std::string("password"), salt_str, 2, 32, hmac::Pbkdf2Hash::Sha256); + std::vector ref(32); + ASSERT_TRUE(PKCS5_PBKDF2_HMAC("password", 8, salt.data(), salt.size(), 2, EVP_sha256(), ref.size(), ref.data())); + EXPECT_TRUE(hmac::constant_time_equals(dk, ref)); +} + +// SHA512 vector from BoringSSL pbkdf_test.cc +TEST(PBKDF2Test, BoringSSL_SHA512) { + auto dk = hmac::pbkdf2("passwordPASSWORDpassword", + "saltSALTsaltSALTsaltSALTsaltSALTsalt", + 4096, 64, hmac::Pbkdf2Hash::Sha512); std::string hex = hmac::to_hex(std::string(dk.begin(), dk.end())); - EXPECT_EQ(hex, "ae4d0c95af6b46d32d0adff928f06dd02a303f8ef3c251dfd6e2d85a95474c43"); + EXPECT_EQ(hex, + "8c0511f4c6e597c6ac6315d8f0362e225f3c501495ba23b868c005174dc4ee71115b59f9e60cd9532fa33e0f75aefe30225c583a186cd82bd4daea9724a3d3b8"); +} + +TEST(PBKDF2Test, OpenSSLRandom) { + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution dist(0, 255); + for (auto prf : {hmac::Pbkdf2Hash::Sha1, hmac::Pbkdf2Hash::Sha256, hmac::Pbkdf2Hash::Sha512}) { + std::vector pwd(16), salt(16); + for (int i = 0; i < 2; ++i) { + for (auto &x : pwd) x = static_cast(dist(gen)); + for (auto &x : salt) x = static_cast(dist(gen)); + size_t dk_len = (prf == hmac::Pbkdf2Hash::Sha1) ? 20 : 32; + auto ours = hmac::pbkdf2(pwd, salt, 1000, dk_len, prf); + std::vector ref(dk_len); + const EVP_MD* md = nullptr; + switch (prf) { + case hmac::Pbkdf2Hash::Sha1: md = EVP_sha1(); break; + case hmac::Pbkdf2Hash::Sha256: md = EVP_sha256(); break; + case hmac::Pbkdf2Hash::Sha512: md = EVP_sha512(); break; + } + ASSERT_TRUE(PKCS5_PBKDF2_HMAC(reinterpret_cast(pwd.data()), pwd.size(), + salt.data(), salt.size(), 1000, md, dk_len, ref.data())); + EXPECT_TRUE(hmac::constant_time_equals(ours, ref)); + } + } +} + +TEST(HKDFTest, RFC5869Case1) { + std::vector ikm(22, 0x0b); + auto salt = from_hex("000102030405060708090a0b0c"); + auto info = from_hex("f0f1f2f3f4f5f6f7f8f9"); + auto prk = hmac::hkdf_extract_sha256(ikm, salt); + std::string prk_hex = hmac::to_hex(std::string(prk.begin(), prk.end())); + EXPECT_EQ(prk_hex, "077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5"); + auto okm = hmac::hkdf_expand_sha256(prk, info, 42); + std::string okm_hex = hmac::to_hex(std::string(okm.begin(), okm.end())); + EXPECT_EQ(okm_hex, + "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865"); +} + +TEST(PBKDF2Test, WithPepper) { + std::string password = "secret"; + std::string pepper = "server"; + std::vector salt_vec(16, 0x03); + std::string salt(salt_vec.begin(), salt_vec.end()); + auto dk1 = hmac::pbkdf2_with_pepper(password, salt, pepper, 1000, 32); + auto inner = hmac::get_hmac(pepper, password, hmac::TypeHash::SHA256, false); + std::vector inner_vec(inner.begin(), inner.end()); + auto dk2 = hmac::pbkdf2(inner_vec, salt_vec, 1000, 32, hmac::Pbkdf2Hash::Sha256); + EXPECT_TRUE(hmac::constant_time_equals(dk1, dk2)); } TEST(TimeErrorTest, MinusOneNoErrno) {