Repository: randombit/botan
Pull request #3420 (2023-03-24 21:47:50)

Documentation and example improvements
Plus a bit of `std::span`

codecov-commenter: ## [Codecov](https://codecov.io/gh/randombit/botan/pull/3420?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd) Report
Patch coverage: **`100.00`**% and project coverage change: **`-0.01`** :warning:
> Comparison is base [(`55aaa51`)](https://codecov.io/gh/randombit/botan/commit/55aaa5149c5d4cda52f14093db2c770dcc4da6d9?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd) 88.19% compared to head [(`56345dd`)](https://codecov.io/gh/randombit/botan/pull/3420?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd) 88.18%.

> :exclamation: Current head 56345dd differs from pull request most recent head b00cbfd. Consider uploading reports for the commit b00cbfd to get more accurate results

:mega: This organization is not using Codecov’s [GitHub App Integration](https://github.com/apps/codecov). We recommend you install it so Codecov can continue to function properly for your repositories. [Learn more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd)

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #3420      +/-   ##
==========================================
- Coverage   88.19%   88.18%   -0.01%     
==========================================
  Files         619      619              
  Lines       70291    70286       -5     
  Branches     6995     6993       -2     
==========================================
- Hits        61995    61985      -10     
- Misses       5371     5374       +3     
- Partials     2925     2927       +2     
```


| [Impacted Files](https://codecov.io/gh/randombit/botan/pull/3420?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd) | Coverage Δ | |
|---|---|---|
| [src/tests/test\_pbkdf.cpp](https://codecov.io/gh/randombit/botan/pull/3420?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd#diff-c3JjL3Rlc3RzL3Rlc3RfcGJrZGYuY3Bw) | `82.73% <100.00%> (ø)` | |

... and [12 files with indirect coverage changes](https://codecov.io/gh/randombit/botan/pull/3420/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd)

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd)

</details>

[:umbrella: View full report in Codecov by Sentry](https://codecov.io/gh/randombit/botan/pull/3420?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jack+Lloyd).


Patch:
diff --git a/doc/api_ref/kdf.rst b/doc/api_ref/kdf.rst
index 01892ddca..71cb49760 100644
--- a/doc/api_ref/kdf.rst
+++ b/doc/api_ref/kdf.rst
@@ -4,13 +4,46 @@
 Key Derivation Functions (KDF)
 ========================================
 
-Key derivation functions are used to turn some amount of shared secret
-material into uniform random keys suitable for use with symmetric
-algorithms. An example of an input which is useful for a KDF is a
-shared secret created using Diffie-Hellman key agreement.
+Key derivation functions are used to turn some amount of shared secret material
+into uniform random keys suitable for use with symmetric algorithms. An example
+of an input which is useful for a KDF is a shared secret created using
+Diffie-Hellman key agreement.
+
+Typically a KDF is also used with a *salt* and a *label*. The *salt* should be
+some random information which is available to all of the parties that would need
+to use the KDF; this could be performed by setting the salt to some kind of
+session identifier, or by having one of the parties generate a random salt and
+including it in a message.
+
+The *label* is used to bind the KDF output to some specific context. For
+instance if you were using the KDF to derive a specific key referred to as the
+"message key" in the protocol description, you might use a label of "FooProtocol
+v2 MessageKey". This labeling ensures that if you accidentally use the same
+input key and salt in some other context, you still use different keys in the
+two contexts.
 
 .. cpp:class:: KDF
 
+  .. cpp:function:: std::unique_ptr<KDF> KDF::create(const std::string& algo)
+
+      Create a new KDF object. Returns nullptr if the named key derivation
+      function was not available
+
+  .. cpp:function:: std::unique_ptr<KDF> KDF::create_or_throw(const std::string& algo)
+
+      Create a new KDF object. Throws an exception if the named key derivation
+      function was not available
+
+  .. cpp:function:: template<concepts::resizable_byte_buffer T = secure_vector<uint8_t>> \
+      T derive_key(size_t key_len, \
+                   std::span<const uint8_t> secret, \
+                   std::span<const uint8_t> salt, \
+                   std::span<const uint8_t> label) const
+
+      This version is parameterized to the output buffer type, so it can be used
+      to return a ``std::vector``, a ``secure_vector``, or anything else
+      satisfying the ``resizable_byte_buffer`` concept.
+
   .. cpp:function:: secure_vector<uint8_t> derive_key( \
                                     const uint8_t secret[], \
                                     size_t secret_len, \
@@ -33,16 +66,16 @@ shared secret created using Diffie-Hellman key agreement.
      const std::string& salt) const
 
    All variations on the same theme. Deterministically creates a
-   uniform random value from *secret*, *salt*, and *label*.
+   uniform random value from *secret*, *salt*, and *label*, whose
+   meaning is described above.
 
-   Typically *label* is a fixed used to identity a protocol context,
-   for example "FooProtocol v1.0 key derivation", and *salt* is chosen
-   randomly or is some string associated with the current protocol
-   instance, for example a session identifier.
+Code Example
+------------
 
-You can create a :cpp:class:`KDF` using the static function
+An example demonstrating using the API to hash a secret using HKDF
 
-.. cpp:function:: std::unique_ptr<KDF> KDF::create(const std::string& algo)
+.. literalinclude:: /../src/examples/kdf.cpp
+   :language: cpp
 
 
 Available KDFs
diff --git a/doc/api_ref/pbkdf.rst b/doc/api_ref/pbkdf.rst
index 5e3b5f4bd..ae545b962 100644
--- a/doc/api_ref/pbkdf.rst
+++ b/doc/api_ref/pbkdf.rst
@@ -22,11 +22,27 @@ specified with all parameters (say "Scrypt" with ``N`` = 8192, ``r`` = 64, and
 
 .. cpp:class:: PasswordHash
 
+   .. cpp:function:: void hash(std::span<uint8_t> out, \
+                               std::string_view password, \
+                               std::span<uint8> salt)
+
+      Derive a key from the specified *password* and *salt*, placing it into *out*.
+
+   .. cpp:function:: void hash(std::span<uint8_t> out, \
+                               std::string_view password, \
+                               std::span<const uint8> salt, \
+                               std::span<const uint8> ad, \
+                               std::span<const uint8> key)
+
+      Derive a key from the specified *password*, *salt*, associated data (*ad*), and
+      secret *key*, placing it into *out*. The *ad* and *key* are both allowed
+      to be empty. Currently non-empty AD/key is only supported with Argon2.
+
    .. cpp:function:: 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
 
-      Derive a key from the specified password and salt, placing it into output.
+      Same functionality as the 3 argument variant of :cpp:func:`PasswordHash::hash`.
 
    .. cpp:function:: void derive_key(uint8_t out[], size_t out_len, \
                      const char* password, const size_t password_len, \
@@ -34,9 +50,7 @@ specified with all parameters (say "Scrypt" with ``N`` = 8192, ``r`` = 64, and
                      const uint8_t ad[], size_t ad_len, \
                      const uint8_t key[], size_t key_len) const
 
-      Derive a key from the specified password, salt, associated data, and
-      secret key, placing it into output. The ad and key are both allowed
-      to be empty. Currently non-empty AD/key is only supported with Argon2.
+       Same functionality as the 5 argument variant of :cpp:func:`PasswordHash::hash`.
 
    .. cpp:function:: std::string to_string() const
 
@@ -63,6 +77,16 @@ specified with all parameters (say "Scrypt" with ``N`` = 8192, ``r`` = 64, and
       and uses an effictively fixed amount of memory when running, this function
       returns 0.
 
+   .. cpp:function:: bool supports_keyed_operation() const
+
+      Returns true if this password hash supports supplying a secret key
+      to :cpp:func:`PasswordHash::hash`.
+
+   .. cpp:function:: bool supports_associated_data() const
+
+      Returns true if this password hash supports supplying associated data
+      to :cpp:func:`PasswordHash::hash`.
+
 The ``PasswordHashFamily`` creates specific instances of ``PasswordHash``:
 
 .. cpp:class:: PasswordHashFamily
@@ -77,12 +101,24 @@ The ``PasswordHashFamily`` creates specific instances of ``PasswordHash``:
       Create a default instance of the password hashing algorithm. Be warned the
       value returned here may change from release to release.
 
-   .. cpp:function:: std::unique_ptr<PasswordHash> tune(size_t output_len, std::chrono::milliseconds msec) const
+   .. cpp:function:: std::unique_ptr<PasswordHash> tune( \
+                     size_t output_len, \
+                     std::chrono::milliseconds msec, \
+                     size_t max_memory_usage_mb = 0, \
+                     std::chrono::milliseconds tuning_msec = std::chrono::milliseconds(10)) const
 
       Return a password hash instance tuned to run for approximately ``msec``
       milliseconds when producing an output of length ``output_len``. (Accuracy
       may vary, use the command line utility ``botan pbkdf_tune`` to check.)
 
+      The parameters will be selected to use at most *max_memory_usage_mb* megabytes
+      of memory, or if left as zero any size is allowed.
+
+      This function works by runing a short tuning loop to estimate the
+      performance of the algorithm, then scaling the parameters appropriately to
+      hit the target size. The length of time the tuning loop runs can be
+      controlled using the *tuning_msec* parameter.
+
    .. cpp:function:: std::unique_ptr<PasswordHash> from_params( \
          size_t i1, size_t i2 = 0, size_t i3 = 0) const
 
@@ -94,6 +130,14 @@ The ``PasswordHashFamily`` creates specific instances of ``PasswordHash``:
 
       All unneeded parameters should be set to 0 or left blank.
 
+Code Example
+------------
+
+An example demonstrating using the API to hash a password using Argon2i:
+
+.. literalinclude:: /../src/examples/pwdhash.cpp
+   :language: cpp
+
 Available Schemes
 ----------------------
 
@@ -102,7 +146,7 @@ General Recommendations
 
 If you need wide interoperability use PBKDF2 with HMAC-SHA256 and at least 50K
 iterations. If you don't, use Argon2id with p=1, t=3 and M as large as you
-can reasonable set (say 1 gigabyte).
+can reasonably set (say 1 gigabyte).
 
 You can test how long a particular PBKDF takes to execute using the cli tool
 ``pbkdf_tune``::
diff --git a/doc/api_ref/pubkey.rst b/doc/api_ref/pubkey.rst
index 924f6c9d5..b7975c8b2 100644
--- a/doc/api_ref/pubkey.rst
+++ b/doc/api_ref/pubkey.rst
@@ -557,7 +557,7 @@ Botan implements the following encryption algorithms and padding schemes:
 Code Example
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 The following Code sample reads a PKCS #8 keypair from the passed location and
-subsequently encrypts a fixed plaintext with the included public key, using EME1
+subsequently encrypts a fixed plaintext with the included public key, using OAEP
 with SHA-256. For the sake of completeness, the ciphertext is then decrypted using
 the private key.
 
@@ -916,21 +916,28 @@ Scheme (XMSS) using Winternitz One Time Signatures+ (WOTS+). The implementation
 is based on `RFC 8391 "XMSS: eXtended Merkle Signature Scheme"
 <https://tools.ietf.org/html/rfc8391>`_.
 
+.. warning::
+
+   XMSS is stateful, meaning the private key must be updated after
+   each signature. If the same private key is ever used to generate
+   two different signatures, then the scheme becomes insecure. For
+   this reason it can be challening to use XMSS securely.
+
 XMSS uses the Botan interfaces for public key cryptography.
 The following algorithms are implemented:
 
 1. XMSS-SHA2_10_256
-# XMSS-SHA2_16_256
-# XMSS-SHA2_20_256
-# XMSS-SHA2_10_512
-# XMSS-SHA2_16_512
-# XMSS-SHA2_20_512
-# XMSS-SHAKE_10_256
-# XMSS-SHAKE_16_256
-# XMSS-SHAKE_20_256
-# XMSS-SHAKE_10_512
-# XMSS-SHAKE_16_512
-# XMSS-SHAKE_20_512
+#. XMSS-SHA2_16_256
+#. XMSS-SHA2_20_256
+#. XMSS-SHA2_10_512
+#. XMSS-SHA2_16_512
+#. XMSS-SHA2_20_512
+#. XMSS-SHAKE_10_256
+#. XMSS-SHAKE_16_256
+#. XMSS-SHAKE_20_256
+#. XMSS-SHAKE_10_512
+#. XMSS-SHAKE_16_512
+#. XMSS-SHAKE_20_512
 
 The algorithm name contains the hash function name, tree height and digest
 width defined by the corresponding parameter set. Choosing `XMSS-SHA2_10_256`
diff --git a/src/examples/Makefile b/src/examples/Makefile
index 40c9ef9e9..195c1b15f 100644
--- a/src/examples/Makefile
+++ b/src/examples/Makefile
@@ -47,7 +47,7 @@ clean:
 
 PKCS11_EXAMPLES = pkcs11_low_level pkcs11_module pkcs11_slot pkcs11_session pkcs11_objects pkcs11_rsa pkcs11_ecdsa pkcs11_ecdh pkcs11_rng pkcs11_token_management pkcs11_x509
 
-EXAMPLES = aes aes_cbc chacha check_key cmac dl_group ecdh ecdsa encrypt_with_pkcs8_key gmac hash hmac tls_client tls_custom_curves_client tls_proxy tls_stream_client xmss $(PKCS11_EXAMPLES)
+EXAMPLES = aes aes_cbc chacha check_key cmac dl_group ecdh ecdsa encrypt_with_pkcs8_key gmac hash hmac kdf kyber pwdhash tls_client tls_custom_curves_client tls_proxy tls_stream_client xmss $(PKCS11_EXAMPLES)
 
 examples: $(addprefix build/, $(EXAMPLES))
 
diff --git a/src/examples/ecdh.cpp b/src/examples/ecdh.cpp
index 7591d892e..75f662574 100644
--- a/src/examples/ecdh.cpp
+++ b/src/examples/ecdh.cpp
@@ -8,20 +8,27 @@
 
 int main() {
   Botan::AutoSeeded_RNG rng;
-  // ec domain and
+
+  // ec domain and KDF
   Botan::EC_Group domain("secp521r1");
-  std::string kdf = "KDF2(SHA-256)";
-  // generate ECDH keys
-  Botan::ECDH_PrivateKey keyA(rng, domain);
-  Botan::ECDH_PrivateKey keyB(rng, domain);
-  // Construct key agreements
-  Botan::PK_Key_Agreement ecdhA(keyA, rng, kdf);
-  Botan::PK_Key_Agreement ecdhB(keyB, rng, kdf);
-  // Agree on shared secret and derive symmetric key of 256 bit length
-  Botan::secure_vector<uint8_t> sA = ecdhA.derive_key(32, keyB.public_value()).bits_of();
-  Botan::secure_vector<uint8_t> sB = ecdhB.derive_key(32, keyA.public_value()).bits_of();
-
-  if (sA != sB)
+  const std::string kdf = "KDF2(SHA-256)";
+
+  // the two parties generate ECDH keys
+  Botan::ECDH_PrivateKey key_a(rng, domain);
+  Botan::ECDH_PrivateKey key_b(rng, domain);
+
+  // now they exchange their public values
+  const auto key_apub = key_a.public_value();
+  const auto key_bpub = key_b.public_value();
+
+  // Construct key agreements and agree on a shared secret
+  Botan::PK_Key_Agreement ka_a(key_a, rng, kdf);
+  const auto sA = ka_a.derive_key(32, key_bpub).bits_of();
+
+  Botan::PK_Key_Agreement ka_b(key_b, rng, kdf);
+  const auto sB = ka_b.derive_key(32, key_apub).bits_of();
+
+  if(sA != sB)
     return 1;
 
   std::cout << "agreed key: " << std::endl << Botan::hex_encode(sA);
diff --git a/src/examples/ecdsa.cpp b/src/examples/ecdsa.cpp
index e26500395..cfd477489 100644
--- a/src/examples/ecdsa.cpp
+++ b/src/examples/ecdsa.cpp
@@ -11,16 +11,17 @@ int main() {
   // Generate ECDSA keypair
   Botan::ECDSA_PrivateKey key(rng, Botan::EC_Group("secp521r1"));
 
-  std::string text("This is a tasty burger!");
-  std::vector<uint8_t> data(text.data(), text.data() + text.length());
+  const std::string message("This is a tasty burger!");
+
   // sign data
   Botan::PK_Signer signer(key, rng, "SHA-256");
-  signer.update(data);
+  signer.update(message);
   std::vector<uint8_t> signature = signer.signature(rng);
   std::cout << "Signature:" << std::endl << Botan::hex_encode(signature);
-  // verify signature
+
+  // now verify the signature
   Botan::PK_Verifier verifier(key, "SHA-256");
-  verifier.update(data);
+  verifier.update(message);
   std::cout << std::endl << "is " << (verifier.check_signature(signature) ? "valid" : "invalid");
   return 0;
 }
diff --git a/src/examples/encrypt_with_pkcs8_key.cpp b/src/examples/encrypt_with_pkcs8_key.cpp
index 8dfbd632b..d2dd76970 100644
--- a/src/examples/encrypt_with_pkcs8_key.cpp
+++ b/src/examples/encrypt_with_pkcs8_key.cpp
@@ -20,11 +20,11 @@ int main(int argc, char *argv[]) {
   auto kp = Botan::PKCS8::load_key(in);
 
   // encrypt with pk
-  Botan::PK_Encryptor_EME enc(*kp, rng, "EME1(SHA-256)");
+  Botan::PK_Encryptor_EME enc(*kp, rng, "OAEP(SHA-256)");
   std::vector<uint8_t> ct = enc.encrypt(pt, rng);
 
   // decrypt with sk
-  Botan::PK_Decryptor_EME dec(*kp, rng, "EME1(SHA-256)");
+  Botan::PK_Decryptor_EME dec(*kp, rng, "OAEP(SHA-256)");
   Botan::secure_vector<uint8_t> pt2 = dec.decrypt(ct);
 
   std::cout << std::endl
diff --git a/src/examples/kdf.cpp b/src/examples/kdf.cpp
new file mode 100644
index 000000000..134f802d6
--- /dev/null
+++ b/src/examples/kdf.cpp
@@ -0,0 +1,19 @@
+#include <botan/kdf.h>
+#include <botan/hex.h>
+#include <iostream>
+
+int main()
+   {
+   // Replicate a test from RFC 5869
+   // https://www.rfc-editor.org/rfc/rfc5869#appendix-A.1
+   const std::vector<uint8_t> input_secret(22, 0x0b);
+   const std::vector<uint8_t> salt = Botan::hex_decode("000102030405060708090a0b0c");
+   const std::vector<uint8_t> label = Botan::hex_decode("f0f1f2f3f4f5f6f7f8f9");
+   const size_t derived_key_len = 42;
+
+   auto kdf = Botan::KDF::create_or_throw("HKDF(SHA-256)");
+   auto derived_key = kdf->derive_key(derived_key_len, input_secret, salt, label);
+
+   // OKM = 0x3cb25f25faacd57a90434f64d0362f2a...
+   std::cout << Botan::hex_encode(derived_key) << "\n";
+   }
diff --git a/src/examples/pwdhash.cpp b/src/examples/pwdhash.cpp
new file mode 100644
index 000000000..6f3357078
--- /dev/null
+++ b/src/examples/pwdhash.cpp
@@ -0,0 +1,32 @@
+#include <botan/pwdhash.h>
+#include <botan/system_rng.h>
+#include <botan/hex.h>
+#include <iostream>
+#include <array>
+
+int main()
+   {
+   // You can change this to "PBKDF2(SHA-512)" or "Scrypt" or "Argon2id" or ...
+   const std::string pbkdf_algo = "Argon2i";
+   auto pbkdf_runtime = std::chrono::milliseconds(300);
+   const size_t output_hash = 32;
+   const size_t max_pbkdf_mb = 128;
+
+   auto pwd_fam = Botan::PasswordHashFamily::create_or_throw(pbkdf_algo);
+
+   auto pwdhash = pwd_fam->tune(output_hash, pbkdf_runtime, max_pbkdf_mb);
+
+   std::cout << "Using params " << pwdhash->to_string() << "\n";
+
+   std::array<uint8_t, 32> salt;
+   Botan::system_rng().randomize(salt);
+
+   const std::string password = "tell no one";
+
+   std::array<uint8_t, output_hash> key;
+   pwdhash->hash(key, password, salt);
+
+   std::cout << Botan::hex_encode(key) << "\n";
+
+   return 0;
+   }
diff --git a/src/lib/pbkdf/argon2/argon2.h b/src/lib/pbkdf/argon2/argon2.h
index 4e125dd7d..106263fe4 100644
--- a/src/lib/pbkdf/argon2/argon2.h
+++ b/src/lib/pbkdf/argon2/argon2.h
@@ -50,6 +50,10 @@ class BOTAN_PUBLIC_API(2,11) Argon2 final : public PasswordHash
       size_t t() const { return m_t; }
       size_t p() const { return m_p; }
 
+      bool supports_keyed_operation() const override { return true; }
+
+      bool supports_associated_data() const override { return true; }
+
       size_t iterations() const override { return t(); }
 
       size_t parallelism() const override { return p(); }
diff --git a/src/lib/pbkdf/pwdhash.h b/src/lib/pbkdf/pwdhash.h
index 34a0d212f..4861c2444 100644
--- a/src/lib/pbkdf/pwdhash.h
+++ b/src/lib/pbkdf/pwdhash.h
@@ -12,6 +12,7 @@
 #include <memory>
 #include <vector>
 #include <chrono>
+#include <span>
 
 namespace Botan {
 
@@ -58,6 +59,60 @@ class BOTAN_PUBLIC_API(2,8) PasswordHash
       */
       virtual size_t total_memory_usage() const { return 0; }
 
+      /**
+      * Returns true if this password hash supports supplying a key
+      */
+      virtual bool supports_keyed_operation() const { return false; }
+
+      /**
+      * Returns true if this password hash supports supplying associated data
+      */
+      virtual bool supports_associated_data() const { return false; }
+
+      /**
+      * Hash a password into a bitstring
+      *
+      * @param out a span where the derived key will be placed
+      * @param password the password to derive the key from
+      * @param salt a randomly chosen salt
+      *
+      * This function is const, but is not thread safe. Different threads should
+      * either use unique objects, or serialize all access.
+      */
+      void hash(std::span<uint8_t> out,
+                std::string_view password,
+                std::span<const uint8_t> salt)
+         {
+         this->derive_key(out.data(), out.size(),
+                          password.data(), password.size(),
+                          salt.data(), salt.size());
+         }
+
+      /**
+      * Hash a password into a bitstring
+      *
+      * @param out a span where the derived key will be placed
+      * @param password the password to derive the key from
+      * @param salt a randomly chosen salt
+      * @param associated_data some additional data
+      * @param key a secret key
+      *
+      * This function is const, but is not thread safe. Different threads should
+      * either use unique objects, or serialize all access.
+      */
+      void hash(std::span<uint8_t> out,
+                std::string_view password,
+                std::span<const uint8_t> salt,
+                std::span<const uint8_t> associated_data,
+                std::span<const uint8_t> key)
+         {
+         this->derive_key(out.data(), out.size(),
+                          password.data(), password.size(),
+                          salt.data(), salt.size(),
+                          associated_data.data(), associated_data.size(),
+                          key.data(), key.size());
+         }
+
       /**
       * Derive a key from a password
       *


Test patch:
diff --git a/src/tests/test_pbkdf.cpp b/src/tests/test_pbkdf.cpp
index 990d45489..34fde1656 100644
--- a/src/tests/test_pbkdf.cpp
+++ b/src/tests/test_pbkdf.cpp
@@ -60,9 +60,7 @@ class PBKDF_KAT_Tests final : public Text_Based_Test
          auto pwdhash = pwdhash_fam->from_params(iterations);
 
          std::vector<uint8_t> pwdhash_derived(outlen);
-         pwdhash->derive_key(pwdhash_derived.data(), pwdhash_derived.size(),
-                             passphrase.c_str(), passphrase.size(),
-                             salt.data(), salt.size());
+         pwdhash->hash(pwdhash_derived, passphrase, salt);
 
          result.test_eq("pwdhash derived key", pwdhash_derived, expected);
 
@@ -109,9 +107,7 @@ class Pwdhash_Tests : public Test
                auto tuned_pwhash = pwdhash_fam->tune(32, run_time, max_mem, tune_time);
 
                std::vector<uint8_t> output1(32);
-               tuned_pwhash->derive_key(output1.data(), output1.size(),
-                                        password.c_str(), password.size(),
-                                        salt.data(), salt.size());
+               tuned_pwhash->hash(output1, password, salt);
 
                std::unique_ptr<Botan::PasswordHash> pwhash;
 
@@ -127,18 +123,14 @@ class Pwdhash_Tests : public Test
                   }
 
                std::vector<uint8_t> output2(32);
-               pwhash->derive_key(output2.data(), output2.size(),
-                                  password.c_str(), password.size(),
-                                  salt.data(), salt.size());
+               pwhash->hash(output2, password, salt);
 
                result.test_eq("PasswordHash produced same output when run with same params",
                               output1, output2);
 
                auto default_pwhash = pwdhash_fam->default_params();
                std::vector<uint8_t> output3(32);
-               default_pwhash->derive_key(output3.data(), output3.size(),
-                                          password.c_str(), password.size(),
-                                          salt.data(), salt.size());
+               default_pwhash->hash(output3, password, salt);
 
                result.end_timer();
                }
@@ -183,10 +175,7 @@ class Bcrypt_PBKDF_KAT_Tests final : public Text_Based_Test
          auto pwdhash = pwdhash_fam->from_iterations(rounds);
 
          std::vector<uint8_t> derived(expected.size());
-         pwdhash->derive_key(derived.data(), derived.size(),
-                             reinterpret_cast<const char*>(passphrase.data()),
-                             passphrase.size(),
-                             salt.data(), salt.size());
+         pwdhash->hash(derived, passphrase, salt);
 
          result.test_eq("derived key", derived, expected);
 
@@ -230,9 +219,7 @@ class Scrypt_KAT_Tests final : public Text_Based_Test
          auto pwdhash = pwdhash_fam->from_params(N, R, P);
 
          std::vector<uint8_t> pwdhash_derived(expected.size());
-         pwdhash->derive_key(pwdhash_derived.data(), pwdhash_derived.size(),
-                             passphrase.c_str(), passphrase.size(),
-                             salt.data(), salt.size());
+         pwdhash->hash(pwdhash_derived, passphrase, salt);
 
          result.test_eq("pwdhash derived key", pwdhash_derived, expected);
 
@@ -275,13 +262,10 @@ class Argon2_KAT_Tests final : public Text_Based_Test
 
          auto pwdhash = pwdhash_fam->from_params(M, T, P);
 
+         const std::string passphrase_str(passphrase.begin(), passphrase.end());
+
          std::vector<uint8_t> pwdhash_derived(expected.size());
-         pwdhash->derive_key(pwdhash_derived.data(), pwdhash_derived.size(),
-                             reinterpret_cast<const char*>(passphrase.data()),
-                             passphrase.size(),
-                             salt.data(), salt.size(),
-                             ad.data(), ad.size(),
-                             key.data(), key.size());
+         pwdhash->hash(pwdhash_derived, passphrase_str, salt, ad, key);
 
          result.test_eq("pwdhash derived key", pwdhash_derived, expected);