Skip to content

Commit f1f0708

Browse files
committed
test(secret_string): cover nonce rotation and integrity
1 parent f7eb83d commit f1f0708

2 files changed

Lines changed: 105 additions & 38 deletions

File tree

include/hmac_cpp/secret_string.hpp

Lines changed: 84 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ namespace hmac_cpp {
2727
/// Memory locking is best-effort and may fail without required privileges.
2828
class secret_string {
2929
public:
30-
secret_string() : nonce_(), locked_(false) {}
30+
secret_string() : nonce_(), tag_(), locked_(false) {}
3131

32-
explicit secret_string(const std::string& s) : nonce_(), locked_(false) { set(s); }
33-
explicit secret_string(const uint8_t* p, size_t n) : nonce_(), locked_(false) { set(p, n); }
34-
explicit secret_string(const secure_buffer<uint8_t>& s) : nonce_(), locked_(false) { set(s); }
35-
explicit secret_string(secure_buffer<uint8_t>&& s) : nonce_(), locked_(false) { set(std::move(s)); }
32+
explicit secret_string(const std::string& s) : nonce_(), tag_(), locked_(false) { set(s); }
33+
explicit secret_string(const uint8_t* p, size_t n) : nonce_(), tag_(), locked_(false) { set(p, n); }
34+
explicit secret_string(const secure_buffer<uint8_t>& s) : nonce_(), tag_(), locked_(false) { set(s); }
35+
explicit secret_string(secure_buffer<uint8_t>&& s) : nonce_(), tag_(), locked_(false) { set(std::move(s)); }
3636

37-
secret_string(secret_string&& other) noexcept { move_from(other); }
37+
secret_string(secret_string&& other) noexcept : ct_(), nonce_(), tag_(), locked_(false) { move_from(other); }
3838
secret_string& operator=(secret_string&& other) noexcept {
3939
if (this != &other) { clear(); move_from(other); }
4040
return *this;
@@ -53,8 +53,10 @@ class secret_string {
5353
locked_ = false;
5454
}
5555
ct_.clear();
56+
ct_.shrink_to_fit();
5657
}
5758
secure_zero(nonce_.data(), nonce_.size());
59+
secure_zero(tag_.data(), tag_.size());
5860
}
5961

6062
bool empty() const noexcept { return ct_.empty(); }
@@ -79,10 +81,37 @@ class secret_string {
7981
ct_.resize(n);
8082
if (n) locked_ = lock_pages(ct_.data(), ct_.size());
8183

82-
xor_keystream_copy_inplace(ct_.data(), p, n, nonce_.data());
84+
xor_keystream_copy(ct_.data(), p, n, nonce_.data());
85+
86+
HmacContext c(hmac_cpp::TypeHash::SHA256);
87+
auto& pk = process_key();
88+
c.init(pk.data(), pk.size());
89+
c.update(nonce_.data(), nonce_.size());
90+
if (n) c.update(ct_.data(), ct_.size());
91+
c.final(tag_.data(), tag_.size());
8392
}
93+
94+
// Not thread-safe if called concurrently with set() or rotate_nonce().
95+
void with_plaintext(const std::function<void(const uint8_t*, size_t)>& fn) const {
96+
if (ct_.empty()) {
97+
fn(nullptr, 0);
98+
return;
99+
}
100+
101+
uint8_t expected[32];
102+
{
103+
HmacContext ctx(hmac_cpp::TypeHash::SHA256);
104+
auto& pk = process_key();
105+
ctx.init(pk.data(), pk.size());
106+
ctx.update(nonce_.data(), nonce_.size());
107+
ctx.update(ct_.data(), ct_.size());
108+
ctx.final(expected, sizeof expected);
109+
}
110+
if (!std::equal(tag_.begin(), tag_.end(), expected)) {
111+
secure_zero(expected, sizeof expected);
112+
throw std::runtime_error("secret_string::with_plaintext: integrity check failed");
113+
}
84114

85-
bool with_plaintext(const std::function<void(const uint8_t*, size_t)>& fn) const {
86115
uint8_t subkey[32];
87116
{
88117
HmacContext ctx(hmac_cpp::TypeHash::SHA256);
@@ -97,9 +126,10 @@ class secret_string {
97126
fn(tmp.data(), tmp.size());
98127
secure_zero(tmp.data(), tmp.size());
99128
secure_zero(subkey, sizeof subkey);
100-
return true;
129+
secure_zero(expected, sizeof expected);
101130
}
102131

132+
// Not thread-safe if called concurrently with set() or rotate_nonce().
103133
std::string reveal_copy() const {
104134
std::string out;
105135
out.resize(ct_.size());
@@ -109,58 +139,71 @@ class secret_string {
109139
return out;
110140
}
111141

112-
void rekey_runtime() {
142+
void rotate_nonce() {
113143
if (ct_.empty()) return;
114144

115-
uint8_t oldk[32], newk[32];
116-
auto& pk = process_key();
145+
std::array<uint8_t,12> new_nonce{};
117146
{
118-
HmacContext ctx(hmac_cpp::TypeHash::SHA256);
119-
ctx.init(pk.data(), pk.size());
120-
ctx.update(nonce_.data(), nonce_.size());
121-
ctx.final(oldk, sizeof oldk);
147+
auto rnd = hmac_cpp::random_bytes(new_nonce.size());
148+
std::copy(rnd.begin(), rnd.end(), new_nonce.begin());
122149
}
123150

151+
uint8_t oldk[32], newk[32];
152+
auto& pk = process_key();
153+
124154
{
125-
auto rnd = hmac_cpp::random_bytes(pk.size());
126-
std::copy(rnd.begin(), rnd.end(), pk.begin());
155+
HmacContext c(hmac_cpp::TypeHash::SHA256);
156+
c.init(pk.data(), pk.size());
157+
c.update(nonce_.data(), nonce_.size());
158+
c.final(oldk, sizeof oldk);
127159
}
128-
129160
{
130-
HmacContext ctx(hmac_cpp::TypeHash::SHA256);
131-
ctx.init(pk.data(), pk.size());
132-
ctx.update(nonce_.data(), nonce_.size());
133-
ctx.final(newk, sizeof newk);
161+
HmacContext c(hmac_cpp::TypeHash::SHA256);
162+
c.init(pk.data(), pk.size());
163+
c.update(new_nonce.data(), new_nonce.size());
164+
c.final(newk, sizeof newk);
134165
}
135166

136-
uint8_t msg[16]; std::memcpy(msg, nonce_.data(), 12);
137167
uint32_t ctr = 0;
138-
uint8_t oldb[32], newb[32];
168+
uint8_t msg_old[16], msg_new[16], blk_old[32], blk_new[32];
169+
std::memcpy(msg_old, nonce_.data(), nonce_.size());
170+
std::memcpy(msg_new, new_nonce.data(), new_nonce.size());
139171

140-
size_t pos = 0;
141-
while (pos < ct_.size()) {
142-
be32(msg + 12, ctr++);
172+
for (size_t pos = 0; pos < ct_.size();) {
173+
be32(msg_old + 12, ctr);
174+
be32(msg_new + 12, ctr);
175+
++ctr;
143176

144177
HmacContext c1(hmac_cpp::TypeHash::SHA256);
145178
c1.init(oldk, sizeof oldk);
146-
c1.update(msg, sizeof msg);
147-
c1.final(oldb, sizeof oldb);
179+
c1.update(msg_old, sizeof msg_old);
180+
c1.final(blk_old, sizeof blk_old);
148181

149182
HmacContext c2(hmac_cpp::TypeHash::SHA256);
150183
c2.init(newk, sizeof newk);
151-
c2.update(msg, sizeof msg);
152-
c2.final(newb, sizeof newb);
184+
c2.update(msg_new, sizeof msg_new);
185+
c2.final(blk_new, sizeof blk_new);
153186

154-
const size_t take = (ct_.size() - pos < sizeof oldb) ? (ct_.size() - pos) : sizeof oldb;
155-
for (size_t i = 0; i < take; ++i) ct_[pos + i] ^= (oldb[i] ^ newb[i]);
187+
const size_t take = std::min(ct_.size() - pos, sizeof blk_old);
188+
for (size_t i = 0; i < take; ++i)
189+
ct_[pos + i] ^= (blk_old[i] ^ blk_new[i]);
156190
pos += take;
157191
}
158192

159193
secure_zero(oldk, sizeof oldk);
160194
secure_zero(newk, sizeof newk);
161-
secure_zero(oldb, sizeof oldb);
162-
secure_zero(newb, sizeof newb);
163-
secure_zero(msg, sizeof msg);
195+
secure_zero(blk_old, sizeof blk_old);
196+
secure_zero(blk_new, sizeof blk_new);
197+
secure_zero(msg_old, sizeof msg_old);
198+
secure_zero(msg_new, sizeof msg_new);
199+
200+
nonce_ = new_nonce;
201+
202+
HmacContext c(hmac_cpp::TypeHash::SHA256);
203+
c.init(pk.data(), pk.size());
204+
c.update(nonce_.data(), nonce_.size());
205+
if (!ct_.empty()) c.update(ct_.data(), ct_.size());
206+
c.final(tag_.data(), tag_.size());
164207
}
165208

166209
private:
@@ -221,7 +264,7 @@ class secret_string {
221264
secure_zero(subkey, sizeof subkey);
222265
}
223266

224-
void xor_keystream_copy_inplace(uint8_t* out, const uint8_t* in, size_t len, const uint8_t* nonce12) const {
267+
void xor_keystream_copy(uint8_t* out, const uint8_t* in, size_t len, const uint8_t* nonce12) const {
225268
uint8_t subkey[32];
226269
{
227270
HmacContext ctx(hmac_cpp::TypeHash::SHA256);
@@ -257,14 +300,17 @@ class secret_string {
257300
void move_from(secret_string& other) noexcept {
258301
ct_ = std::move(other.ct_);
259302
nonce_ = other.nonce_;
303+
tag_ = other.tag_;
260304
locked_ = other.locked_;
261305
other.locked_ = false;
262306
secure_zero(other.nonce_.data(), other.nonce_.size());
307+
secure_zero(other.tag_.data(), other.tag_.size());
263308
}
264309

265310
private:
266311
std::vector<uint8_t> ct_;
267312
std::array<uint8_t,12> nonce_;
313+
std::array<uint8_t,32> tag_;
268314
bool locked_;
269315
};
270316

test_all.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,27 @@ TEST(SecretStringTest, MoveSemantics) {
770770
EXPECT_EQ(b.reveal_copy(), "abc");
771771
}
772772

773+
TEST(SecretStringTest, NonceRotationPreservesPlaintext) {
774+
hmac_cpp::secret_string s("nonce-test");
775+
s.rotate_nonce();
776+
EXPECT_EQ(s.reveal_copy(), "nonce-test");
777+
}
778+
779+
TEST(SecretStringTest, IntegrityCheckFailsOnTamper) {
780+
hmac_cpp::secret_string s("tamper");
781+
struct SecretStringAccessor {
782+
std::vector<uint8_t> ct;
783+
std::array<uint8_t,12> nonce;
784+
std::array<uint8_t,32> tag;
785+
bool locked;
786+
};
787+
auto* hack = reinterpret_cast<SecretStringAccessor*>(&s);
788+
hack->tag[0] ^= 0x01;
789+
EXPECT_THROW({
790+
s.with_plaintext([](const uint8_t*, size_t){});
791+
}, std::runtime_error);
792+
}
793+
773794
int main(int argc, char **argv) {
774795
::testing::InitGoogleTest(&argc, argv);
775796
return RUN_ALL_TESTS();

0 commit comments

Comments
 (0)