Skip to content

Commit d8e6b37

Browse files
authored
fix(hotp): check digest length
Add length check before dynamic truncation and cover short digests with unit test.
1 parent d93f34b commit d8e6b37

3 files changed

Lines changed: 39 additions & 15 deletions

File tree

hmac_utils.cpp

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,29 @@ namespace hmac {
8888
return false;
8989
}
9090

91+
namespace detail {
92+
int hotp_from_digest(const std::vector<uint8_t>& hmac_result, int digits) {
93+
if (hmac_result.empty()) {
94+
throw std::runtime_error("HOTP: HMAC result too short");
95+
}
96+
int offset = hmac_result.back() & 0x0F;
97+
if (hmac_result.size() < static_cast<size_t>(offset) + 4) {
98+
throw std::runtime_error("HOTP: HMAC result too short");
99+
}
100+
uint32_t bin_code =
101+
((hmac_result[offset] & 0x7F) << 24) |
102+
((hmac_result[offset + 1] & 0xFF) << 16) |
103+
((hmac_result[offset + 2] & 0xFF) << 8) |
104+
((hmac_result[offset + 3] & 0xFF));
105+
static const uint64_t divisor[] = {
106+
10UL, 100UL, 1000UL, 10000UL,
107+
100000UL, 1000000UL, 10000000UL,
108+
100000000UL, 1000000000UL
109+
};
110+
return bin_code % divisor[digits - 1];
111+
}
112+
}
113+
91114
int get_hotp_code(const void* key_ptr, size_t key_len, uint64_t counter, int digits, TypeHash hash_type) {
92115
if (digits < 1 || digits > 9) throw std::invalid_argument("HOTP: digits must be in range [1, 9]");
93116

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

104-
// Step 3: Dynamic truncation
105-
int offset = hmac_result.back() & 0x0F;
106-
uint32_t bin_code =
107-
((hmac_result[offset] & 0x7F) << 24) |
108-
((hmac_result[offset + 1] & 0xFF) << 16) |
109-
((hmac_result[offset + 2] & 0xFF) << 8) |
110-
((hmac_result[offset + 3] & 0xFF));
111-
112-
// Step 4: Modulo to get N-digit code
113-
static const uint64_t divisor[] = {
114-
10UL, 100UL, 1000UL, 10000UL,
115-
100000UL, 1000000UL, 10000000UL,
116-
100000000UL, 1000000000UL
117-
};
118-
return bin_code % divisor[digits - 1];
127+
// Step 3: Dynamic truncation and modulo
128+
return detail::hotp_from_digest(hmac_result, digits);
119129
}
120130

121131
int get_totp_code_at(

hmac_utils.hpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,15 @@ namespace hmac {
8181
inline int get_hotp_code(const std::string& key, uint64_t counter, int digits = 6, TypeHash hash_type = TypeHash::SHA1) {
8282
return get_hotp_code(key.data(), key.size(), counter, digits, hash_type);
8383
}
84+
85+
namespace detail {
86+
/// \brief Computes HOTP code from a precomputed HMAC digest
87+
/// \param hmac_result HMAC digest bytes
88+
/// \param digits Desired number of digits in the OTP (1-9)
89+
/// \return One-Time Password (OTP) as an integer
90+
/// \throws std::runtime_error if the digest is too short for dynamic truncation
91+
int hotp_from_digest(const std::vector<uint8_t>& hmac_result, int digits);
92+
}
8493

8594
/// \brief Computes TOTP (Time-Based One-Time Password) code for a specific timestamp
8695
/// Implements RFC 6238

test_all.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,11 @@ TEST(HMACTest, InvalidTypeThrowsString) {
131131
EXPECT_THROW(hmac::get_hmac(key, msg, invalid), std::invalid_argument);
132132
}
133133

134+
TEST(HOTPTest, ShortDigestThrows) {
135+
std::vector<uint8_t> short_digest = {0x00, 0x01, 0x02};
136+
EXPECT_THROW(hmac::detail::hotp_from_digest(short_digest, 6), std::runtime_error);
137+
}
138+
134139
TEST(TOTPTest, AtTime) {
135140
const std::string totp_key = "12345678901234567890";
136141
uint64_t test_time = 1234567890;

0 commit comments

Comments
 (0)