Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 25 additions & 15 deletions hmac_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,29 @@ namespace hmac {
return false;
}

namespace detail {
int hotp_from_digest(const std::vector<uint8_t>& hmac_result, int digits) {
if (hmac_result.empty()) {
throw std::runtime_error("HOTP: HMAC result too short");
}
int offset = hmac_result.back() & 0x0F;
if (hmac_result.size() < static_cast<size_t>(offset) + 4) {
throw std::runtime_error("HOTP: HMAC result too short");
}
uint32_t bin_code =
((hmac_result[offset] & 0x7F) << 24) |
((hmac_result[offset + 1] & 0xFF) << 16) |
((hmac_result[offset + 2] & 0xFF) << 8) |
((hmac_result[offset + 3] & 0xFF));
static const uint64_t divisor[] = {
10UL, 100UL, 1000UL, 10000UL,
100000UL, 1000000UL, 10000000UL,
100000000UL, 1000000000UL
};
return bin_code % divisor[digits - 1];
}
}

int get_hotp_code(const void* key_ptr, size_t key_len, uint64_t counter, int digits, TypeHash hash_type) {
if (digits < 1 || digits > 9) throw std::invalid_argument("HOTP: digits must be in range [1, 9]");

Expand All @@ -101,21 +124,8 @@ namespace hmac {
// Step 2: Compute HMAC
std::vector<uint8_t> hmac_result = hmac::get_hmac(key_ptr, key_len, counter_bytes, 8, hash_type);

// Step 3: Dynamic truncation
int offset = hmac_result.back() & 0x0F;
uint32_t bin_code =
((hmac_result[offset] & 0x7F) << 24) |
((hmac_result[offset + 1] & 0xFF) << 16) |
((hmac_result[offset + 2] & 0xFF) << 8) |
((hmac_result[offset + 3] & 0xFF));

// Step 4: Modulo to get N-digit code
static const uint64_t divisor[] = {
10UL, 100UL, 1000UL, 10000UL,
100000UL, 1000000UL, 10000000UL,
100000000UL, 1000000000UL
};
return bin_code % divisor[digits - 1];
// Step 3: Dynamic truncation and modulo
return detail::hotp_from_digest(hmac_result, digits);
}

int get_totp_code_at(
Expand Down
9 changes: 9 additions & 0 deletions hmac_utils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,15 @@ namespace hmac {
inline int get_hotp_code(const std::string& key, uint64_t counter, int digits = 6, TypeHash hash_type = TypeHash::SHA1) {
return get_hotp_code(key.data(), key.size(), counter, digits, hash_type);
}

namespace detail {
/// \brief Computes HOTP code from a precomputed HMAC digest
/// \param hmac_result HMAC digest bytes
/// \param digits Desired number of digits in the OTP (1-9)
/// \return One-Time Password (OTP) as an integer
/// \throws std::runtime_error if the digest is too short for dynamic truncation
int hotp_from_digest(const std::vector<uint8_t>& hmac_result, int digits);
}

/// \brief Computes TOTP (Time-Based One-Time Password) code for a specific timestamp
/// Implements RFC 6238
Expand Down
5 changes: 5 additions & 0 deletions test_all.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ TEST(HMACTest, InvalidTypeThrowsString) {
EXPECT_THROW(hmac::get_hmac(key, msg, invalid), std::invalid_argument);
}

TEST(HOTPTest, ShortDigestThrows) {
std::vector<uint8_t> short_digest = {0x00, 0x01, 0x02};
EXPECT_THROW(hmac::detail::hotp_from_digest(short_digest, 6), std::runtime_error);
}

TEST(TOTPTest, AtTime) {
const std::string totp_key = "12345678901234567890";
uint64_t test_time = 1234567890;
Expand Down
Loading