From 44831d1d0577b28baa6af0c37b0b3ea6e24d76b0 Mon Sep 17 00:00:00 2001 From: Aster Seker Date: Fri, 5 Sep 2025 19:08:16 +0300 Subject: [PATCH] docs(readme): document PBKDF2 key derivation --- README-RU.md | 18 +++++++++ README.md | 18 +++++++++ include/hmac_cpp/hmac_utils.hpp | 40 +++++++++++++++++++ src/hmac_utils.cpp | 68 +++++++++++++++++++++++++++++++++ test_all.cpp | 16 ++++++++ 5 files changed, 160 insertions(+) diff --git a/README-RU.md b/README-RU.md index 46ab90e..03b77ce 100644 --- a/README-RU.md +++ b/README-RU.md @@ -10,6 +10,7 @@ - Совместимость с **C++11** - Поддержка `HMAC` на основе `SHA256`, `SHA512`, `SHA1` - Прямая работа с бинарным или hex-форматом +- Поддержка **PBKDF2** (RFC 8018) - Поддержка **временных токенов**: - **HOTP (RFC 4226)** — счётчики - **TOTP (RFC 6238)** — временные токены @@ -130,6 +131,23 @@ std::vector get_hmac( Возвращает: Бинарный HMAC в виде `std::vector` +### PBKDF2 (RFC 8018) + +```cpp +#include + +std::string password = "password"; +std::string salt = "salt"; +std::vector dk = hmac::pbkdf2(password, salt, 1000, 32, hmac::TypeHash::SHA256); +``` + +Параметры: + +- `password`, `salt` — строки с паролем и солью +- `iterations` — число итераций +- `dk_len` — длина ключа в байтах +- `hash_type` — хеш-функция (`SHA1`, `SHA256`, `SHA512`) + ### 🕓 HOTP и TOTP токены Библиотека поддерживает генерацию одноразовых паролей по RFC 4226 и RFC 6238. diff --git a/README.md b/README.md index 1254ec8..f7e6480 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ A lightweight `C++11` library for computing `HMAC` (hash-based message authentic - Compatible with **C++11** - Supports `HMAC` using `SHA256`, `SHA512`, `SHA1` - Outputs in binary or hex format +- Provides **PBKDF2 key derivation** (RFC 8018) - Support for **time-based tokens**: - **HOTP (RFC 4226)** — counter-based one-time passwords - **TOTP (RFC 6238)** — time-based one-time passwords @@ -154,6 +155,23 @@ Parameters: Returns: Binary digest as `std::vector` +### PBKDF2 Key Derivation + +```cpp +#include + +std::string password = "password"; +std::string salt = "salt"; +std::vector dk = hmac::pbkdf2(password, salt, 1000, 32, hmac::TypeHash::SHA256); +``` + +Parameters: + +- `password`, `salt` — Raw byte strings +- `iterations` — Number of iterations +- `dk_len` — Desired key length in bytes +- `hash_type` — Hash function (`SHA1`, `SHA256`, `SHA512`) + ### 🕓 HOTP and TOTP Tokens The library supports generating one-time passwords based on RFC 4226 and RFC 6238. diff --git a/include/hmac_cpp/hmac_utils.hpp b/include/hmac_cpp/hmac_utils.hpp index e3ea892..72ffdd8 100644 --- a/include/hmac_cpp/hmac_utils.hpp +++ b/include/hmac_cpp/hmac_utils.hpp @@ -13,6 +13,46 @@ namespace hmac_cpp { /// \return true if both strings are equal bool constant_time_equals(const std::string &a, const std::string &b); + /// \brief Derives a key from a password using PBKDF2 (RFC 8018) + /// \param password_ptr Pointer to the password buffer + /// \param password_len Length of the password in bytes + /// \param salt_ptr Pointer to the salt buffer + /// \param salt_len Length of the salt in bytes + /// \param iterations Number of iterations, must be positive + /// \param dk_len Desired length of the derived key in bytes, must be positive + /// \param hash_type Hash function to use (SHA1, SHA256, SHA512) + /// \return Derived key as a vector of bytes + 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); + + /// \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) { + 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); + } + + /// \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) { + return pbkdf2(password.data(), password.size(), + salt.data(), salt.size(), + iterations, dk_len, hash_type); + } + /// \brief Generates a time-based HMAC-SHA256 token /// \param key Secret key used for HMAC /// \param interval_sec Interval in seconds that defines token rotation. Must be positive. Default is 60 seconds diff --git a/src/hmac_utils.cpp b/src/hmac_utils.cpp index b70cb99..5b58b04 100644 --- a/src/hmac_utils.cpp +++ b/src/hmac_utils.cpp @@ -17,6 +17,74 @@ namespace hmac_cpp { return diff == 0; } + 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) { + 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 (dk_len == 0) + throw std::invalid_argument("PBKDF2: dk_len must be positive"); + + size_t hlen = 0; + switch (hash_type) { + case TypeHash::SHA1: + hlen = hmac_hash::SHA1::DIGEST_SIZE; + break; + case TypeHash::SHA256: + hlen = hmac_hash::SHA256::DIGEST_SIZE; + break; + case TypeHash::SHA512: + hlen = hmac_hash::SHA512::DIGEST_SIZE; + break; + default: + throw std::invalid_argument("Unsupported hash type"); + } + + size_t l = (dk_len + hlen - 1) / hlen; + size_t r = dk_len - (l - 1) * hlen; + + std::vector derived; + derived.reserve(dk_len); + + std::vector salt_block; + salt_block.reserve(salt_len + 4); + salt_block.insert(salt_block.end(), + reinterpret_cast(salt_ptr), + reinterpret_cast(salt_ptr) + salt_len); + salt_block.resize(salt_len + 4); + + for (size_t i = 1; i <= l; ++i) { + salt_block[salt_len ] = static_cast((i >> 24) & 0xFF); + salt_block[salt_len + 1] = static_cast((i >> 16) & 0xFF); + salt_block[salt_len + 2] = static_cast((i >> 8) & 0xFF); + salt_block[salt_len + 3] = static_cast(i & 0xFF); + + std::vector u = get_hmac(password_ptr, password_len, + salt_block.data(), salt_block.size(), + hash_type); + std::vector t = u; + for (int 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) { + t[k] ^= u[k]; + } + } + if (i == l) { + derived.insert(derived.end(), t.begin(), t.begin() + r); + } else { + derived.insert(derived.end(), t.begin(), t.end()); + } + } + + return derived; + } + 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 ca8343b..2c02dcd 100644 --- a/test_all.cpp +++ b/test_all.cpp @@ -200,6 +200,22 @@ 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(PBKDF2Test, SHA256) { + const std::string password = "password"; + const std::string salt = "salt"; + std::vector dk = hmac::pbkdf2(password, salt, 2, 32, hmac::TypeHash::SHA256); + std::string hex = hmac::to_hex(std::string(dk.begin(), dk.end())); + EXPECT_EQ(hex, "ae4d0c95af6b46d32d0adff928f06dd02a303f8ef3c251dfd6e2d85a95474c43"); +} + TEST(TimeErrorTest, MinusOneNoErrno) { const std::string key = "12345"; mock_time_value = static_cast(-1);