Repository: randombit/botan
Pull request #1990 (2019-06-01 00:57:12)

Add Bcrypt-PBKDF


Patch:
diff --git a/src/lib/block/blowfish/blowfish.cpp b/src/lib/block/blowfish/blowfish.cpp
index d219bc22a..ecb9f82e3 100644
--- a/src/lib/block/blowfish/blowfish.cpp
+++ b/src/lib/block/blowfish/blowfish.cpp
@@ -374,7 +374,7 @@ void Blowfish::key_expansion(const uint8_t key[],
 */
 void Blowfish::salted_set_key(const uint8_t key[], size_t length,
                               const uint8_t salt[], size_t salt_length,
-                              size_t workfactor)
+                              size_t workfactor, bool salt_first)
    {
    BOTAN_ARG_CHECK(salt_length > 0 && salt_length % 4 == 0,
                    "Invalid salt length for Blowfish salted key schedule");
@@ -398,8 +398,16 @@ void Blowfish::salted_set_key(const uint8_t key[], size_t length,
 
       for(size_t r = 0; r != rounds; ++r)
          {
-         key_expansion(key, length, nullptr, 0);
-         key_expansion(salt, salt_length, nullptr, 0);
+         if(salt_first)
+            {
+            key_expansion(salt, salt_length, nullptr, 0);
+            key_expansion(key, length, nullptr, 0);
+            }
+         else
+            {
+            key_expansion(key, length, nullptr, 0);
+            key_expansion(salt, salt_length, nullptr, 0);
+            }
          }
       }
    }
diff --git a/src/lib/block/blowfish/blowfish.h b/src/lib/block/blowfish/blowfish.h
index d5c318752..97a1b841c 100644
--- a/src/lib/block/blowfish/blowfish.h
+++ b/src/lib/block/blowfish/blowfish.h
@@ -26,7 +26,7 @@ class BOTAN_PUBLIC_API(2,0) Blowfish final : public Block_Cipher_Fixed_Params<8,
       */
       void salted_set_key(const uint8_t key[], size_t key_length,
                           const uint8_t salt[], size_t salt_length,
-                          size_t workfactor);
+                          const size_t workfactor, bool salt_first = false);
 
       BOTAN_DEPRECATED("Use Blowfish::salted_set_key taking salt length")
       void eks_key_schedule(const uint8_t key[], size_t key_length,
diff --git a/src/lib/pbkdf/bcrypt_pbkdf/bcrypt_pbkdf.cpp b/src/lib/pbkdf/bcrypt_pbkdf/bcrypt_pbkdf.cpp
new file mode 100644
index 000000000..ecdac689a
--- /dev/null
+++ b/src/lib/pbkdf/bcrypt_pbkdf/bcrypt_pbkdf.cpp
@@ -0,0 +1,182 @@
+/*
+* (C) 2018,2019 Jack Lloyd
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#include <botan/bcrypt_pbkdf.h>
+#include <botan/blowfish.h>
+#include <botan/hash.h>
+#include <botan/internal/timer.h>
+
+namespace Botan {
+
+void Bcrypt_PBKDF::derive_key(uint8_t output[], size_t output_len,
+                              const char* password, const size_t password_len,
+                              const uint8_t salt[], size_t salt_len) const
+   {
+   bcrypt_pbkdf(output, output_len,
+                password, password_len,
+                salt, salt_len,
+                m_iterations);
+   }
+
+std::string Bcrypt_PBKDF::to_string() const
+   {
+   return "Bcrypt-PBKDF(" + std::to_string(m_iterations) + ")";
+   }
+
+std::string Bcrypt_PBKDF_Family::name() const
+   {
+   return "Bcrypt-PBKDF";
+   }
+
+std::unique_ptr<PasswordHash> Bcrypt_PBKDF_Family::tune(size_t output_length,
+                                                        std::chrono::milliseconds msec,
+                                                        size_t /*max_memory*/) const
+   {
+   Timer timer("Bcrypt_PBKDF");
+   const auto tune_time = BOTAN_PBKDF_TUNING_TIME;
+
+   const size_t blocks = (output_length + 32 - 1) / 32;
+
+   if(blocks == 0)
+      return default_params();
+
+   const size_t starting_iter = 2;
+
+   timer.run_until_elapsed(tune_time, [&]() {
+      uint8_t output[32] = { 0 };
+      bcrypt_pbkdf(output, sizeof(output), "test", 4, nullptr, 0, starting_iter);
+      });
+
+   if(timer.events() < blocks || timer.value() == 0)
+      return default_params();
+
+   const uint64_t measured_time = timer.value() / (timer.events() / blocks);
+
+   const uint64_t target_nsec = msec.count() * static_cast<uint64_t>(1000000);
+
+   const size_t desired_increase = target_nsec / measured_time;
+
+   if(desired_increase == 0)
+      return this->from_iterations(starting_iter);
+
+   return this->from_iterations(desired_increase * starting_iter);
+   }
+
+std::unique_ptr<PasswordHash> Bcrypt_PBKDF_Family::default_params() const
+   {
+   return this->from_iterations(32); // About 100 ms on fast machine
+   }
+
+std::unique_ptr<PasswordHash> Bcrypt_PBKDF_Family::from_iterations(size_t iter) const
+   {
+   return std::unique_ptr<PasswordHash>(new Bcrypt_PBKDF(iter));
+   }
+
+std::unique_ptr<PasswordHash> Bcrypt_PBKDF_Family::from_params(size_t iter, size_t /*t*/, size_t /*p*/) const
+   {
+   return this->from_iterations(iter);
+   }
+
+namespace {
+
+void bcrypt_round(Blowfish& blowfish,
+                  const secure_vector<uint8_t>& pass_hash,
+                  const secure_vector<uint8_t>& salt_hash,
+                  secure_vector<uint8_t>& out,
+                  secure_vector<uint8_t>& tmp)
+   {
+   const size_t BCRYPT_PBKDF_OUTPUT = 32;
+
+   // "OxychromaticBlowfishSwatDynamite"
+   static const uint8_t BCRYPT_PBKDF_MAGIC[BCRYPT_PBKDF_OUTPUT] = {
+      0x4F, 0x78, 0x79, 0x63, 0x68, 0x72, 0x6F, 0x6D,
+      0x61, 0x74, 0x69, 0x63, 0x42, 0x6C, 0x6F, 0x77,
+      0x66, 0x69, 0x73, 0x68, 0x53, 0x77, 0x61, 0x74,
+      0x44, 0x79, 0x6E, 0x61, 0x6D, 0x69, 0x74, 0x65
+   };
+
+   const size_t BCRYPT_PBKDF_WORKFACTOR = 6;
+   const size_t BCRYPT_PBKDF_ROUNDS = 64;
+
+   blowfish.salted_set_key(pass_hash.data(), pass_hash.size(),
+                           salt_hash.data(), salt_hash.size(),
+                           BCRYPT_PBKDF_WORKFACTOR, true);
+
+   copy_mem(tmp.data(), BCRYPT_PBKDF_MAGIC, BCRYPT_PBKDF_OUTPUT);
+   for(size_t i = 0; i != BCRYPT_PBKDF_ROUNDS; ++i)
+      blowfish.encrypt(tmp);
+
+   /*
+   Bcrypt PBKDF loads the Blowfish output as big endian for no reason
+   in particular. We can't just swap everything once at the end
+   because the (big-endian) values are fed into SHA-512 to generate
+   the salt for the next round
+   */
+   for(size_t i = 0; i != 32/4; ++i)
+      {
+      const uint32_t w = load_le<uint32_t>(tmp.data(), i);
+      store_be(w, &tmp[sizeof(uint32_t)*i]);
+      }
+
+   xor_buf(out.data(), tmp.data(), BCRYPT_PBKDF_OUTPUT);
+   }
+
+}
+
+void bcrypt_pbkdf(uint8_t output[], size_t output_len,
+                  const char* pass, size_t pass_len,
+                  const uint8_t salt[], size_t salt_len,
+                  size_t rounds)
+   {
+   BOTAN_ARG_CHECK(rounds >= 1, "Invalid rounds for Bcrypt PBKDF");
+
+   // No output desired, so we are all done already...
+   if(output_len == 0)
+      return;
+
+   BOTAN_ARG_CHECK(output_len <= 10*1024*1024, "Too much output for Bcrypt PBKDF");
+
+   const size_t BCRYPT_BLOCK_SIZE = 32;
+   const size_t blocks = (output_len + BCRYPT_BLOCK_SIZE - 1) / BCRYPT_BLOCK_SIZE;
+
+   std::unique_ptr<HashFunction> sha512 = HashFunction::create_or_throw("SHA-512");
+   const secure_vector<uint8_t> pass_hash = sha512->process(reinterpret_cast<const uint8_t*>(pass), pass_len);
+
+   secure_vector<uint8_t> salt_hash(sha512->output_length());
+
+   Blowfish blowfish;
+   secure_vector<uint8_t> out(BCRYPT_BLOCK_SIZE);
+   secure_vector<uint8_t> tmp(BCRYPT_BLOCK_SIZE);
+
+   for(size_t block = 0; block != blocks; ++block)
+      {
+      clear_mem(out.data(), out.size());
+
+      sha512->update(salt, salt_len);
+      sha512->update_be(static_cast<uint32_t>(block + 1));
+      sha512->final(salt_hash.data());
+
+      bcrypt_round(blowfish, pass_hash, salt_hash, out, tmp);
+
+      for(size_t r = 1; r != rounds; ++r)
+         {
+         // Next salt is H(prev_output)
+         sha512->update(tmp);
+         sha512->final(salt_hash.data());
+
+         bcrypt_round(blowfish, pass_hash, salt_hash, out, tmp);
+         }
+
+      for(size_t i = 0; i != BCRYPT_BLOCK_SIZE; ++i)
+         {
+         const size_t dest = i * blocks + block;
+         if(dest < output_len)
+            output[dest] = out[i];
+         }
+      }
+   }
+
+}
diff --git a/src/lib/pbkdf/bcrypt_pbkdf/bcrypt_pbkdf.h b/src/lib/pbkdf/bcrypt_pbkdf/bcrypt_pbkdf.h
new file mode 100644
index 000000000..8984e6497
--- /dev/null
+++ b/src/lib/pbkdf/bcrypt_pbkdf/bcrypt_pbkdf.h
@@ -0,0 +1,75 @@
+/*
+* (C) 2018,2019 Jack Lloyd
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#ifndef BOTAN_PBKDF_BCRYPT_H_
+#define BOTAN_PBKDF_BCRYPT_H_
+
+#include <botan/pwdhash.h>
+
+namespace Botan {
+
+/**
+* Bcrypt-PBKDF key derivation function
+*/
+class BOTAN_PUBLIC_API(2,11) Bcrypt_PBKDF final : public PasswordHash
+   {
+   public:
+      Bcrypt_PBKDF(size_t iterations) : m_iterations(iterations) {}
+
+      Bcrypt_PBKDF(const Bcrypt_PBKDF& other) = default;
+      Bcrypt_PBKDF& operator=(const Bcrypt_PBKDF&) = default;
+
+      /**
+      * Derive a new key under the current Bcrypt-PBKDF parameter set
+      */
+      void derive_key(uint8_t out[], size_t out_len,
+                      const char* password, const size_t password_len,
+                      const uint8_t salt[], size_t salt_len) const override;
+
+      std::string to_string() const override;
+
+      size_t iterations() const override { return m_iterations; }
+
+      size_t parallelism() const override { return 0; }
+
+      size_t memory_param() const override { return 0; }
+
+      size_t total_memory_usage() const override { return 4096; }
+
+   private:
+      size_t m_iterations;
+   };
+
+class BOTAN_PUBLIC_API(2,11) Bcrypt_PBKDF_Family final : public PasswordHashFamily
+   {
+   public:
+      Bcrypt_PBKDF_Family() {}
+
+      std::string name() const override;
+
+      std::unique_ptr<PasswordHash> tune(size_t output_length,
+                                         std::chrono::milliseconds msec,
+                                         size_t max_memory) const override;
+
+      std::unique_ptr<PasswordHash> default_params() const override;
+
+      std::unique_ptr<PasswordHash> from_iterations(size_t iter) const override;
+
+      std::unique_ptr<PasswordHash> from_params(
+         size_t i, size_t, size_t) const override;
+   };
+
+/**
+* Bcrypt PBKDF compatible with OpenBSD bcrypt_pbkdf
+*/
+void BOTAN_UNSTABLE_API bcrypt_pbkdf(uint8_t output[], size_t output_len,
+                                     const char* pass, size_t pass_len,
+                                     const uint8_t salt[], size_t salt_len,
+                                     size_t rounds);
+
+}
+
+#endif
diff --git a/src/lib/pbkdf/bcrypt_pbkdf/info.txt b/src/lib/pbkdf/bcrypt_pbkdf/info.txt
new file mode 100644
index 000000000..0e2b10178
--- /dev/null
+++ b/src/lib/pbkdf/bcrypt_pbkdf/info.txt
@@ -0,0 +1,8 @@
+<defines>
+PBKDF_BCRYPT -> 20190531
+</defines>
+
+<requires>
+blowfish
+sha2_64
+</requires>
diff --git a/src/lib/pbkdf/pwdhash.cpp b/src/lib/pbkdf/pwdhash.cpp
index f5ee26c6a..718024a22 100644
--- a/src/lib/pbkdf/pwdhash.cpp
+++ b/src/lib/pbkdf/pwdhash.cpp
@@ -24,6 +24,10 @@
    #include <botan/argon2.h>
 #endif
 
+#if defined(BOTAN_HAS_PBKDF_BCRYPT)
+   #include <botan/bcrypt_pbkdf.h>
+#endif
+
 namespace Botan {
 
 std::unique_ptr<PasswordHashFamily> PasswordHashFamily::create(const std::string& algo_spec,
@@ -71,6 +75,13 @@ std::unique_ptr<PasswordHashFamily> PasswordHashFamily::create(const std::string
       }
 #endif
 
+#if defined(BOTAN_HAS_PBKDF_BCRYPT)
+   if(req.algo_name() == "Bcrypt-PBKDF")
+      {
+      return std::unique_ptr<PasswordHashFamily>(new Bcrypt_PBKDF_Family);
+      }
+#endif
+
 #if defined(BOTAN_HAS_PGP_S2K)
    if(req.algo_name() == "OpenPGP-S2K" && req.arg_count() == 1)
       {


Test patch:
diff --git a/src/tests/data/bcrypt_pbkdf.vec b/src/tests/data/bcrypt_pbkdf.vec
new file mode 100644
index 000000000..e56745265
--- /dev/null
+++ b/src/tests/data/bcrypt_pbkdf.vec
@@ -0,0 +1,184 @@
+
+# From https://github.com/dchest/bcrypt_pbkdf/blob/master/bcrypt_pbkdf_test.go
+Passphrase = password
+Salt = 73616C74
+Iterations = 12
+Output = 1AE42C05D487BC02F64921A4EBE4EA93BCACFE135FDA99974C06B7B01FAE149A
+
+# Generated by OpenBSD bcrypt_pbkdf
+Passphrase = A
+Salt = 0001020304050607
+Iterations = 10
+Output = 3C705DA7E10C61C2523B
+
+Passphrase = A
+Salt = 0001020304050607
+Iterations = 10
+Output = 3CF670095D5FA753E1430C6D6177C299521B3B614D9E53FA89C7F893F33289F677DDD72EE1AA29CBF5574A948DE4623C71996FFD16C8806FDD7ACBE02992DEAC
+
+# Generated by https://github.com/dchest/bcrypt_pbkdf/
+Passphrase = password
+Salt = 73616C74
+Iterations = 3
+Output = CCB508027535A962F591D4A7EEA08FE15EE0B68044B1A7B209605FDFBFB41228
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 4
+Output = 5BBF0CC293587F1C3635555C27796598
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 4
+Output = 5BBF0CC293587F1C3635555C27796598D47E579071BF427E9D8FBE842ABA34D9
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 4
+Output = 5BA4BFC60C7AC272931458407F4C1C4936EA356C55125C5A279B791D65BF9842D49D7E1B572A9052715EBFA9421E7E94
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 4
+Output = 5BA4BFC60C7AC272931458407F4C1C4936EA356C55125C5A279B791D65BF9842D49D7E1B572A9052715EBFA9421E7E949D8F8F19BE3284732AF1BA28341DD9BF
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 5
+Output = 4137FF05DC28382925690E6FD53B8EA1
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 5
+Output = 4137FF05DC28382925690E6FD53B8EA112DA02D8F79B787E48ADBE77B1CE5B30
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 5
+Output = 41CF37FCFF41055CDC5C280D389829D625DD69490E3B6F6BD5EB3B4C8E51A19E12A2DAFB02E8D82EF7F69B4A78717E51
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 5
+Output = 41CF37FCFF41055CDC5C280D389829D625DD69490E3B6F6BD5EB3B4C8E51A19E12A2DAFB02E8D82EF7F69B4A78717E514812AD4CBE5877D3B1D0CEB35BA3308E
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 6
+Output = 1E6CF2445415961A1FC9FCEDA40DF381
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 6
+Output = 1E6CF2445415961A1FC9FCEDA40DF381A86F5B327864A3CEA627EA8CFB081969
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 6
+Output = 1E906CE9F245442B5418153196301AFE1F5DC9FAFC06ED55A4160DBBF3BC81FFA8A16FD95B15326C78B3644CA356CE28
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 6
+Output = 1E906CE9F245442B5418153196301AFE1F5DC9FAFC06ED55A4160DBBF3BC81FFA8A16FD95B15326C78B3644CA356CE28A6162741EAE68C2AFB5E08A81921697C
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 7
+Output = 2BF6125E9A0532A025B1E777FD5F9540
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 7
+Output = 2BF6125E9A0532A025B1E777FD5F9540C95408393ADEA4241A5AACFEC17A74FC
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 7
+Output = 2BC7F6A012985EF39A6B05343202A0972507B1DFE7F97703FD9D5FF7951F40E3C92854FE087739603ADDDE00A4722471
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 7
+Output = 2BC7F6A012985EF39A6B05343202A0972507B1DFE7F97703FD9D5FF7951F40E3C92854FE087739603ADDDE00A47224711A8F5A40ACBFFE3DC1A87A8174FAFC9E
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 8
+Output = E17E1533ACC14423155493C99B9C3BBE
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 8
+Output = E17E1533ACC14423155493C99B9C3BBE62EA0884207A7802E7BA72EFF94D085E
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 8
+Output = E1367EC5151A33FAAC4CC1C144CD23FA15D5548493ECC99B9B5D9C0D3B27BEC76227EA66088B849B20AB7AA478010246
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 8
+Output = E1367EC5151A33FAAC4CC1C144CD23FA15D5548493ECC99B9B5D9C0D3B27BEC76227EA66088B849B20AB7AA478010246E74BBA51723FEFA9F9474D6508845E8D
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 9
+Output = 5871A1FE827AC417E5B17BC90E0CF8EB
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 9
+Output = 5871A1FE827AC417E5B17BC90E0CF8EBC59384735EF44F0A4A79FFC70629D39B
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 9
+Output = 586571F0A1F9FE2982337A42C478174BE58EB17C7B90C9B40E800CBAF82DEB7BC54D936784A973FC5E01F4484F120AD3
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 9
+Output = 586571F0A1F9FE2982337A42C478174BE58EB17C7B90C9B40E800CBAF82DEB7BC54D936784A973FC5E01F4484F120AD34A7179E4FF1EC7D406922935D3809B86
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 10
+Output = ABC7DCFB41032D844AF67A1D1EF8A5E6
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 10
+Output = ABC7DCFB41032D844AF67A1D1EF8A5E6A04AA3073D4F2777BFA5FE07A54CFF9D
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 10
+Output = ABFBC7DBDCB6FBC9414703BF2D0584994A9DF60A7AA71DC51EC7F8E1A55BE673A01D4AB9A3BA070E3DD74F92270177F9
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 10
+Output = ABFBC7DBDCB6FBC9414703BF2D0584994A9DF60A7AA71DC51EC7F8E1A55BE673A01D4AB9A3BA070E3DD74F92270177F9BFCFA5A9FEA107DCA59F4C5EFFC09D96
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 11
+Output = 07B2C9848D4535A9E96EDB0AB4783841
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 11
+Output = 07B2C9848D4535A9E96EDB0AB4783841A2A4571BE59CB3D14742218AA72A89D5
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 11
+Output = 0726B275C9CD84B18DD245DB35F4A944E9A46E7FDBB50A14B4C4789E38AF41C8A228A4FC57601B32E5409C5DB3C6D194
+
+Passphrase = password
+Salt = 73616C74
+Iterations = 11
+Output = 0726B275C9CD84B18DD245DB35F4A944E9A46E7FDBB50A14B4C4789E38AF41C8A228A4FC57601B32E5409C5DB3C6D194475D424421568AEBA7732ABF89C6D56D
+
diff --git a/src/tests/test_pbkdf.cpp b/src/tests/test_pbkdf.cpp
index 0e780437b..f10b721e2 100644
--- a/src/tests/test_pbkdf.cpp
+++ b/src/tests/test_pbkdf.cpp
@@ -1,5 +1,5 @@
 /*
-* (C) 2014,2015 Jack Lloyd
+* (C) 2014,2015,2019 Jack Lloyd
 * (C) 2018 Ribose Inc
 *
 * Botan is released under the Simplified BSD License (see license.txt)
@@ -24,6 +24,10 @@
    #include <botan/argon2.h>
 #endif
 
+#if defined(BOTAN_HAS_PBKDF_BCRYPT)
+   #include <botan/bcrypt_pbkdf.h>
+#endif
+
 namespace Botan_Tests {
 
 namespace {
@@ -88,7 +92,17 @@ class Pwdhash_Tests : public Test
          {
          std::vector<Test::Result> results;
 
-         for(std::string pwdhash : { "Scrypt", "PBKDF2(SHA-256)", "OpenPGP-S2K(SHA-384)", "Argon2d", "Argon2i", "Argon2id" })
+         const std::vector<std::string> all_pwdhash = {
+            "Scrypt",
+            "PBKDF2(SHA-256)",
+            "OpenPGP-S2K(SHA-384)",
+            "Argon2d",
+            "Argon2i",
+            "Argon2id",
+            "Bcrypt-PBKDF"
+         };
+
+         for(std::string pwdhash : all_pwdhash)
             {
             Test::Result result("Pwdhash " + pwdhash);
             auto pwdhash_fam = Botan::PasswordHashFamily::create(pwdhash);
@@ -150,6 +164,38 @@ BOTAN_REGISTER_TEST("pwdhash", Pwdhash_Tests);
 
 #endif
 
+#if defined(BOTAN_HAS_PBKDF_BCRYPT)
+
+class Bcrypt_PBKDF_KAT_Tests final : public Text_Based_Test
+   {
+   public:
+      Bcrypt_PBKDF_KAT_Tests() : Text_Based_Test("bcrypt_pbkdf.vec", "Passphrase,Salt,Iterations,Output") {}
+
+      Test::Result run_one_test(const std::string&, const VarMap& vars) override
+         {
+         const size_t rounds = vars.get_req_sz("Iterations");
+         const std::vector<uint8_t> salt = vars.get_req_bin("Salt");
+         const std::string passphrase = vars.get_req_str("Passphrase");
+         const std::vector<uint8_t> expected = vars.get_req_bin("Output");
+
+         Test::Result result("bcrypt PBKDF");
+
+         std::vector<uint8_t> output(expected.size());
+         Botan::bcrypt_pbkdf(output.data(), output.size(),
+                             passphrase.data(), passphrase.size(),
+                             salt.data(), salt.size(),
+                             rounds);
+
+         result.test_eq("derived key", output, expected);
+
+         return result;
+         }
+   };
+
+BOTAN_REGISTER_TEST("bcrypt_pbkdf", Bcrypt_PBKDF_KAT_Tests);
+
+#endif
+
 #if defined(BOTAN_HAS_SCRYPT)
 
 class Scrypt_KAT_Tests final : public Text_Based_Test