• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

randombit / botan / 20698655531

04 Jan 2026 08:25PM UTC coverage: 90.416% (-0.009%) from 90.425%
20698655531

Pull #5124

github

web-flow
Merge 627aed356 into f283c5d29
Pull Request #5124: Cooperative Cancellation in PasswordHash

101700 of 112480 relevant lines covered (90.42%)

13010053.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.98
/src/tests/test_pbkdf.cpp
1
/*
2
* (C) 2014,2015,2019 Jack Lloyd
3
* (C) 2018 Ribose Inc
4
*
5
* Botan is released under the Simplified BSD License (see license.txt)
6
*/
7

8
#include "tests.h"
9

10
#if defined(BOTAN_HAS_PBKDF)
11
   #include <botan/pbkdf.h>
12
   #include <botan/pwdhash.h>
13
#endif
14

15
#if defined(BOTAN_HAS_RFC4880)
16
   #include <botan/rfc4880.h>
17
#endif
18

19
#if defined(BOTAN_HAS_THREAD_UTILS)
20
   #include <botan/internal/thread_pool.h>
21
   #include <stop_token>
22
#endif
23

24
namespace Botan_Tests {
25

26
namespace {
27

28
#if defined(BOTAN_HAS_PBKDF)
29
class PBKDF_KAT_Tests final : public Text_Based_Test {
×
30
   public:
31
      PBKDF_KAT_Tests() : Text_Based_Test("pbkdf", "Iterations,Salt,Passphrase,Output") {}
2✔
32

33
      Test::Result run_one_test(const std::string& pbkdf_name, const VarMap& vars) override {
28✔
34
         const size_t iterations = vars.get_req_sz("Iterations");
28✔
35
         const std::vector<uint8_t> salt = vars.get_req_bin("Salt");
28✔
36
         const std::string passphrase = vars.get_req_str("Passphrase");
28✔
37
         const std::vector<uint8_t> expected = vars.get_req_bin("Output");
28✔
38
         const size_t outlen = expected.size();
28✔
39

40
         Test::Result result(pbkdf_name);
56✔
41
         auto pbkdf = Botan::PBKDF::create(pbkdf_name);
28✔
42

43
         if(!pbkdf) {
28✔
44
            result.note_missing(pbkdf_name);
×
45
            return result;
46
         }
47

48
         result.test_eq("Expected name", pbkdf->name(), pbkdf_name);
56✔
49

50
         const Botan::secure_vector<uint8_t> derived =
28✔
51
            pbkdf->derive_key(outlen, passphrase, salt.data(), salt.size(), iterations).bits_of();
28✔
52
         result.test_eq("derived key", derived, expected);
56✔
53

54
         auto pwdhash_fam = Botan::PasswordHashFamily::create(pbkdf_name);
28✔
55

56
         if(!pwdhash_fam) {
28✔
57
            result.note_missing("No PasswordHashFamily for " + pbkdf_name);
×
58
            return result;
×
59
         }
60

61
         auto pwdhash = pwdhash_fam->from_params(iterations);
28✔
62

63
         std::vector<uint8_t> pwdhash_derived(outlen);
28✔
64
         pwdhash->hash(pwdhash_derived, passphrase, salt);
56✔
65

66
         result.test_eq("pwdhash derived key", pwdhash_derived, expected);
56✔
67

68
         return result;
28✔
69
      }
196✔
70
};
71

72
BOTAN_REGISTER_SMOKE_TEST("pbkdf", "pbkdf_kat", PBKDF_KAT_Tests);
73

74
class Pwdhash_Tests : public Test {
1✔
75
   public:
76
      std::vector<Test::Result> run() override {
1✔
77
         std::vector<Test::Result> results;
1✔
78

79
         const std::vector<std::string> all_pwdhash = {
1✔
80
            "Scrypt", "PBKDF2(SHA-256)", "OpenPGP-S2K(SHA-384)", "Argon2d", "Argon2i", "Argon2id", "Bcrypt-PBKDF"};
1✔
81

82
         const auto run_time = std::chrono::milliseconds(3);
1✔
83
         const auto tune_time = std::chrono::milliseconds(1);
1✔
84
         const size_t max_mem = 32;
1✔
85

86
         for(const std::string& pwdhash : all_pwdhash) {
8✔
87
            Test::Result result("Pwdhash " + pwdhash);
7✔
88
            auto pwdhash_fam = Botan::PasswordHashFamily::create(pwdhash);
7✔
89

90
            if(pwdhash_fam) {
7✔
91
               result.start_timer();
7✔
92

93
               const std::vector<uint8_t> salt(8);
7✔
94
               const std::string password = "test";
7✔
95

96
               auto tuned_pwhash = pwdhash_fam->tune(32, run_time, max_mem, tune_time);
7✔
97

98
               std::vector<uint8_t> output1(32);
7✔
99
               tuned_pwhash->hash(output1, password, salt);
14✔
100

101
               std::unique_ptr<Botan::PasswordHash> pwhash;
7✔
102

103
               if(pwdhash_fam->name() == "Scrypt" || pwdhash_fam->name().starts_with("Argon2")) {
7✔
104
                  pwhash = pwdhash_fam->from_params(
4✔
105
                     tuned_pwhash->memory_param(), tuned_pwhash->iterations(), tuned_pwhash->parallelism());
8✔
106
               } else {
107
                  pwhash = pwdhash_fam->from_params(tuned_pwhash->iterations());
3✔
108
               }
109

110
               std::vector<uint8_t> output2(32);
7✔
111
               pwhash->hash(output2, password, salt);
14✔
112

113
               result.test_eq("PasswordHash produced same output when run with same params", output1, output2);
14✔
114

115
               auto default_pwhash = pwdhash_fam->default_params();
7✔
116
               std::vector<uint8_t> output3(32);
7✔
117
               default_pwhash->hash(output3, password, salt);
14✔
118

119
               result.end_timer();
7✔
120
            } else {
42✔
121
               result.test_note("No such algo " + pwdhash);
×
122
            }
123

124
            results.push_back(result);
7✔
125
         }
7✔
126

127
         return results;
1✔
128
      }
1✔
129
};
130

131
BOTAN_REGISTER_TEST("pbkdf", "pwdhash", Pwdhash_Tests);
132

133
   #if defined(BOTAN_HAS_THREAD_UTILS)
134
class Pwdhash_StopToken_Test final : public Test {
1✔
135
   public:
136
      std::vector<Test::Result> run() override {
1✔
137
         std::vector<Test::Result> results;
1✔
138

139
         const std::vector<std::string> all_pwdhash = {
1✔
140
            "Scrypt", "PBKDF2(SHA-256)", "Argon2d", "Argon2i", "Argon2id", "Bcrypt-PBKDF"};
1✔
141
         // Private thread pool to guarantee thread availability for cancellation.
142
         // We need just 1 thread as the cancellation tests are executed serially.
143
         Botan::Thread_Pool thread_pool(1);
1✔
144

145
         for(const std::string& algo_spec : all_pwdhash) {
7✔
146
            Test::Result result("Cooperative cancellation " + algo_spec);
6✔
147

148
            auto fam = Botan::PasswordHashFamily::create(algo_spec);
6✔
149
            if(!fam) {
6✔
150
               result.test_note(algo_spec + " family unavailable");
×
151
               results.push_back(result);
×
152
               continue;
×
153
            }
154

155
            result.start_timer();
6✔
156

157
            // Test will be cancelled after 500ms below. If cancellation fails, test runs for 10s.
158
            const auto run_time = std::chrono::milliseconds(10000);
6✔
159
            const auto tune_time = std::chrono::milliseconds(100);
6✔
160
            const size_t max_mem = 32;
6✔
161
            auto pwdhash = fam->tune(32, run_time, max_mem, tune_time);
6✔
162

163
            const std::string password = "cancel-me";
12✔
164
            const std::vector<uint8_t> salt(16, 0xAA);
6✔
165
            std::vector<uint8_t> out(32);
6✔
166

167
            std::stop_source src;
6✔
168

169
            // Helper thread that will request cancellation
170
            auto future = thread_pool.run([&src]() {
18✔
171
               const auto stop_time = std::chrono::milliseconds(500);
6✔
172
               std::this_thread::sleep_for(stop_time);
6✔
173
               src.request_stop();  // fire the cancellation
6✔
174
            });
6✔
175

176
            // Run the derivation on the main thread and evaluate the result
177
            try {
6✔
178
               const uint64_t start = timestamp();
6✔
179
               pwdhash->derive_key(
18✔
180
                  out.data(), out.size(), password.c_str(), password.size(), salt.data(), salt.size(), src.get_token());
6✔
181
               // If we reach this line, the stop token was ignored
182
               const uint64_t ms_taken = (timestamp() - start) / 1000000;
×
183
               if(ms_taken < static_cast<uint64_t>(run_time.count()) / 2) {
×
184
                  result.test_note("Derivation completed in " + std::to_string(ms_taken) +
×
185
                                   "ms. Ignoring mistuned password hash.");
186
               } else {
187
                  result.test_failure("Derivation completed without observing stop token");
×
188
               }
189
            } catch(const Botan::Operation_Canceled& e) {
6✔
190
               // Expected – password hash saw the stop token and threw
191
               result.test_success("Cancellation raised Botan::Operation_Canceled: " + std::string(e.what()));
18✔
192
            } catch(const std::exception& e) {
6✔
193
               result.test_failure("Unexpected std::exception", e.what());
×
194
            } catch(...) {
×
195
               result.test_failure("Non-standard exception thrown on cancellation");
×
196
            }
×
197

198
            future.get();  // ensure the canceller thread finished
6✔
199

200
            result.end_timer();
6✔
201
            results.push_back(result);
6✔
202
         }
30✔
203
         return results;
1✔
204
      }
1✔
205
};
206

207
BOTAN_REGISTER_TEST("pbkdf", "pwdhash_stop_token", Pwdhash_StopToken_Test);
208
   #endif
209

210
#endif
211

212
#if defined(BOTAN_HAS_PBKDF_BCRYPT)
213

214
class Bcrypt_PBKDF_KAT_Tests final : public Text_Based_Test {
×
215
   public:
216
      Bcrypt_PBKDF_KAT_Tests() : Text_Based_Test("bcrypt_pbkdf.vec", "Passphrase,Salt,Iterations,Output") {}
2✔
217

218
      Test::Result run_one_test(const std::string& /*header*/, const VarMap& vars) override {
36✔
219
         const size_t rounds = vars.get_req_sz("Iterations");
36✔
220
         const std::vector<uint8_t> salt = vars.get_req_bin("Salt");
36✔
221
         const std::string passphrase = vars.get_req_str("Passphrase");
36✔
222
         const std::vector<uint8_t> expected = vars.get_req_bin("Output");
36✔
223

224
         Test::Result result("bcrypt PBKDF");
36✔
225

226
         auto pwdhash_fam = Botan::PasswordHashFamily::create("Bcrypt-PBKDF");
36✔
227

228
         if(!pwdhash_fam) {
36✔
229
            result.test_failure("Bcrypt-PBKDF is missing PasswordHashFamily");
×
230
            return result;
×
231
         }
232

233
         auto pwdhash = pwdhash_fam->from_iterations(rounds);
36✔
234

235
         std::vector<uint8_t> derived(expected.size());
36✔
236
         pwdhash->hash(derived, passphrase, salt);
72✔
237

238
         result.test_eq("derived key", derived, expected);
72✔
239

240
         return result;
36✔
241
      }
180✔
242
};
243

244
BOTAN_REGISTER_TEST("pbkdf", "bcrypt_pbkdf", Bcrypt_PBKDF_KAT_Tests);
245

246
#endif
247

248
#if defined(BOTAN_HAS_SCRYPT)
249

250
class Scrypt_KAT_Tests final : public Text_Based_Test {
×
251
   public:
252
      Scrypt_KAT_Tests() : Text_Based_Test("scrypt.vec", "Passphrase,Salt,N,R,P,Output") {}
2✔
253

254
      Test::Result run_one_test(const std::string& /*header*/, const VarMap& vars) override {
15✔
255
         const size_t N = vars.get_req_sz("N");
15✔
256
         const size_t R = vars.get_req_sz("R");
15✔
257
         const size_t P = vars.get_req_sz("P");
15✔
258
         const std::vector<uint8_t> salt = vars.get_req_bin("Salt");
15✔
259
         const std::string passphrase = vars.get_req_str("Passphrase");
15✔
260
         const std::vector<uint8_t> expected = vars.get_req_bin("Output");
15✔
261

262
         Test::Result result("scrypt");
15✔
263

264
         if(N >= 1048576 && Test::run_long_tests() == false) {
15✔
265
            return result;
266
         }
267

268
         auto pwdhash_fam = Botan::PasswordHashFamily::create("Scrypt");
15✔
269

270
         if(!pwdhash_fam) {
15✔
271
            result.test_failure("Scrypt is missing PasswordHashFamily");
×
272
            return result;
×
273
         }
274

275
         auto pwdhash = pwdhash_fam->from_params(N, R, P);
15✔
276

277
         std::vector<uint8_t> pwdhash_derived(expected.size());
15✔
278
         pwdhash->hash(pwdhash_derived, passphrase, salt);
30✔
279

280
         result.test_eq("pwdhash derived key", pwdhash_derived, expected);
30✔
281

282
         return result;
15✔
283
      }
75✔
284
};
285

286
BOTAN_REGISTER_TEST("pbkdf", "scrypt", Scrypt_KAT_Tests);
287

288
#endif
289

290
#if defined(BOTAN_HAS_ARGON2)
291

292
class Argon2_KAT_Tests final : public Text_Based_Test {
×
293
   public:
294
      Argon2_KAT_Tests() : Text_Based_Test("argon2.vec", "Passphrase,Salt,P,M,T,Output", "Secret,AD") {}
2✔
295

296
      Test::Result run_one_test(const std::string& mode, const VarMap& vars) override {
1,071✔
297
         const size_t P = vars.get_req_sz("P");
1,071✔
298
         const size_t M = vars.get_req_sz("M");
1,071✔
299
         const size_t T = vars.get_req_sz("T");
1,071✔
300
         const std::vector<uint8_t> key = vars.get_opt_bin("Secret");
1,071✔
301
         const std::vector<uint8_t> ad = vars.get_opt_bin("AD");
1,071✔
302
         const std::vector<uint8_t> salt = vars.get_req_bin("Salt");
1,071✔
303
         const std::vector<uint8_t> passphrase = vars.get_req_bin("Passphrase");
1,071✔
304
         const std::vector<uint8_t> expected = vars.get_req_bin("Output");
1,071✔
305

306
         Test::Result result(mode);
2,142✔
307

308
         auto pwdhash_fam = Botan::PasswordHashFamily::create(mode);
1,071✔
309

310
         if(!pwdhash_fam) {
1,071✔
311
            result.test_failure("Argon2 is missing PasswordHashFamily");
×
312
            return result;
×
313
         }
314

315
         auto pwdhash = pwdhash_fam->from_params(M, T, P);
1,071✔
316

317
         const std::string passphrase_str(passphrase.begin(), passphrase.end());
2,142✔
318

319
         std::vector<uint8_t> pwdhash_derived(expected.size());
1,071✔
320
         pwdhash->hash(pwdhash_derived, passphrase_str, salt, ad, key);
2,142✔
321

322
         result.test_eq("pwdhash derived key", pwdhash_derived, expected);
2,142✔
323

324
         return result;
1,071✔
325
      }
6,654✔
326
};
327

328
BOTAN_REGISTER_SERIALIZED_TEST("pbkdf", "argon2", Argon2_KAT_Tests);
329

330
#endif
331

332
#if defined(BOTAN_HAS_PGP_S2K)
333

334
class PGP_S2K_Iter_Test final : public Test {
1✔
335
   public:
336
      std::vector<Test::Result> run() override {
1✔
337
         Test::Result result("PGP_S2K iteration encoding");
1✔
338

339
         // The maximum representable iteration count
340
         const size_t max_iter = 65011712;
1✔
341

342
         result.test_eq("Encoding of large value accepted", Botan::RFC4880_encode_count(max_iter * 2), size_t(255));
1✔
343
         result.test_eq("Encoding of small value accepted", Botan::RFC4880_encode_count(0), size_t(0));
1✔
344

345
         for(size_t c = 0; c != 256; ++c) {
257✔
346
            const size_t dec = Botan::RFC4880_decode_count(static_cast<uint8_t>(c));
256✔
347
            const size_t comp_dec = (16 + (c & 0x0F)) << ((c >> 4) + 6);
256✔
348
            result.test_eq("Decoded value matches PGP formula", dec, comp_dec);
256✔
349

350
            const size_t enc = Botan::RFC4880_encode_count(comp_dec);
256✔
351
            result.test_eq("Encoded value matches PGP formula", enc, c);
512✔
352
         }
353

354
         uint8_t last_enc = 0;
355

356
         for(size_t i = 0; i <= max_iter; i += 64) {
1,015,810✔
357
            const uint8_t enc = Botan::RFC4880_encode_count(i);
1,015,809✔
358
            result.test_lte("Encoded value non-decreasing", last_enc, enc);
1,015,809✔
359

360
            /*
361
            The iteration count as encoded may not be exactly the
362
            value requested, but should never be less
363
            */
364
            const size_t dec = Botan::RFC4880_decode_count(enc);
1,015,809✔
365
            result.test_gte("Decoded value is >= requested", dec, i);
1,015,809✔
366

367
            last_enc = enc;
1,015,809✔
368
         }
369

370
         return std::vector<Test::Result>{result};
3✔
371
      }
2✔
372
};
373

374
BOTAN_REGISTER_TEST("pbkdf", "pgp_s2k_iter", PGP_S2K_Iter_Test);
375

376
#endif
377

378
}  // namespace
379

380
}  // namespace Botan_Tests
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc