From 6e78eba8864f36847482bfba51a7877a83ae02ef Mon Sep 17 00:00:00 2001 From: Richard Huveneers Date: Sun, 19 Oct 2025 19:30:10 +0200 Subject: [PATCH] Added Cooperative Cancellation to PasswordHash --- .github/workflows/ci.yml | 12 +-- doc/api_ref/pbkdf.rst | 26 ++++-- .../pbkdf_cooperative_cancellation.cpp | 39 +++++++++ src/lib/block/blowfish/blowfish.cpp | 12 ++- src/lib/block/blowfish/blowfish.h | 5 +- src/lib/ffi/ffi.cpp | 1 + src/lib/pbkdf/argon2/argon2.cpp | 23 ++++-- src/lib/pbkdf/argon2/argon2.h | 9 +- src/lib/pbkdf/argon2/argon2pwhash.cpp | 10 ++- src/lib/pbkdf/bcrypt_pbkdf/bcrypt_pbkdf.cpp | 19 +++-- src/lib/pbkdf/bcrypt_pbkdf/bcrypt_pbkdf.h | 3 +- src/lib/pbkdf/pbkdf2/pbkdf2.cpp | 15 ++-- src/lib/pbkdf/pbkdf2/pbkdf2.h | 6 +- src/lib/pbkdf/pgp_s2k/pgp_s2k.cpp | 14 +++- src/lib/pbkdf/pgp_s2k/pgp_s2k.h | 3 +- src/lib/pbkdf/pwdhash.cpp | 5 +- src/lib/pbkdf/pwdhash.h | 34 ++++++-- src/lib/pbkdf/scrypt/scrypt.cpp | 14 +++- src/lib/pbkdf/scrypt/scrypt.h | 3 +- src/lib/utils/exceptn.cpp | 4 + src/lib/utils/exceptn.h | 13 +++ src/scripts/ci_build.py | 2 +- src/tests/test_pbkdf.cpp | 82 +++++++++++++++++++ 23 files changed, 290 insertions(+), 64 deletions(-) create mode 100644 src/examples/pbkdf_cooperative_cancellation.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b5d10fcba4..730a0ce2e93 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -129,18 +129,10 @@ jobs: include: - target: shared compiler: xcode - host_os: macos-15-intel - - target: amalgamation - compiler: xcode - host_os: macos-15-intel - make_tool: ninja - - target: shared - compiler: xcode - host_os: macos-15 # uses Apple Silicon - make_tool: ninja + host_os: macos-26 - target: amalgamation compiler: xcode - host_os: macos-15 # uses Apple Silicon + host_os: macos-26 runs-on: ${{ matrix.host_os }} diff --git a/doc/api_ref/pbkdf.rst b/doc/api_ref/pbkdf.rst index 099306aceb6..35437587473 100644 --- a/doc/api_ref/pbkdf.rst +++ b/doc/api_ref/pbkdf.rst @@ -24,33 +24,39 @@ specified with all parameters (say "Scrypt" with ``N`` = 8192, ``r`` = 64, and .. cpp:function:: void hash(std::span out, \ std::string_view password, \ - std::span salt) + std::span salt, \ + const std::optional& stop_token = std::nullopt) Derive a key from the specified *password* and *salt*, placing it into *out*. + Optionally pass *stop_token* to enable cooperative cancellation (example below). .. cpp:function:: void hash(std::span out, \ std::string_view password, \ std::span salt, \ std::span ad, \ - std::span key) + std::span key, \ + const std::optional& stop_token = std::nullopt) 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. + Optionally pass *stop_token* to enable cooperative cancellation (example below). .. 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 + const uint8_t salt[], size_t salt_len, \ + const std::optional& stop_token = std::nullopt) const - Same functionality as the 3 argument variant of :cpp:func:`PasswordHash::hash`. + Same functionality as the 4 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, \ const uint8_t salt[], size_t salt_len, \ const uint8_t ad[], size_t ad_len, \ - const uint8_t key[], size_t key_len) const + const uint8_t key[], size_t key_len, \ + const std::optional& stop_token = std::nullopt) const - Same functionality as the 5 argument variant of :cpp:func:`PasswordHash::hash`. + Same functionality as the 6 argument variant of :cpp:func:`PasswordHash::hash`. .. cpp:function:: std::string to_string() const @@ -149,6 +155,14 @@ as associated data. See :ref:`aead` for more information. .. literalinclude:: /../src/examples/password_encryption.cpp :language: cpp +To enable cooperative cancellation in a multi-threaded context, provide a ``std::stop_token`` +obtained from a ``std::jthread`` or manually constructed ``std::stop_source``. +If cancellation is requested, the operation will terminate early by throwing an +``Operation_Canceled`` exception. + +.. literalinclude:: /../src/examples/pbkdf_cooperative_cancellation.cpp + :language: cpp + Available Schemes ---------------------- diff --git a/src/examples/pbkdf_cooperative_cancellation.cpp b/src/examples/pbkdf_cooperative_cancellation.cpp new file mode 100644 index 00000000000..cfb2c6d52e9 --- /dev/null +++ b/src/examples/pbkdf_cooperative_cancellation.cpp @@ -0,0 +1,39 @@ +#include +#include +#include +#include +#include + +int main() { + std::promise> result_promise; + std::future> result_future = result_promise.get_future(); + + std::jthread worker([&](std::stop_token st) { + try { + // Construct expensive password hash + auto pwd_fam = Botan::PasswordHashFamily::create_or_throw("PBKDF2(SHA-256)"); + auto pwdhash = pwd_fam->from_params(static_cast(1) << 31); + // Derive key + Botan::secure_vector out(32); + const auto salt = Botan::system_rng().random_array<32>(); + pwdhash->hash(out, "secret", salt, st); + // Not canceled + result_promise.set_value(out); + } catch(...) { + result_promise.set_exception(std::current_exception()); + } + }); + + // Simulate cancellation after 0.1s + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + worker.request_stop(); // asks the thread to stop + + try { + auto key = result_future.get(); + // Handle successful derivation + } catch(const Botan::Operation_Canceled&) { + // Handle cancellation + } + + // jthread joins automatically on destruction +} diff --git a/src/lib/block/blowfish/blowfish.cpp b/src/lib/block/blowfish/blowfish.cpp index e20e4bde3b4..e4a6098497e 100644 --- a/src/lib/block/blowfish/blowfish.cpp +++ b/src/lib/block/blowfish/blowfish.cpp @@ -336,8 +336,13 @@ void Blowfish::key_expansion(const uint8_t key[], size_t length, const uint8_t s /* * Modified key schedule used for bcrypt password hashing */ -void Blowfish::salted_set_key( - const uint8_t key[], size_t length, const uint8_t salt[], size_t salt_length, size_t workfactor, bool salt_first) { +void Blowfish::salted_set_key(const uint8_t key[], + size_t length, + const uint8_t salt[], + size_t salt_length, + size_t workfactor, + bool salt_first, + const std::optional& stop_token) { BOTAN_ARG_CHECK(salt_length > 0 && salt_length % 4 == 0, "Invalid salt length for Blowfish salted key schedule"); // Truncate longer passwords to the 72 char bcrypt limit @@ -354,6 +359,9 @@ void Blowfish::salted_set_key( const size_t rounds = static_cast(1) << workfactor; for(size_t r = 0; r != rounds; ++r) { + if(stop_token.has_value() && stop_token->stop_requested()) { + throw Botan::Operation_Canceled("blowfish_salted_set_key"); + } if(salt_first) { key_expansion(salt, salt_length, nullptr, 0); key_expansion(key, length, nullptr, 0); diff --git a/src/lib/block/blowfish/blowfish.h b/src/lib/block/blowfish/blowfish.h index 2befc8b2e8c..9eef6ffb1b1 100644 --- a/src/lib/block/blowfish/blowfish.h +++ b/src/lib/block/blowfish/blowfish.h @@ -10,6 +10,8 @@ #include #include +#include +#include namespace Botan { @@ -29,7 +31,8 @@ class BOTAN_TEST_API Blowfish final : public Block_Cipher_Fixed_Params<8, 1, 56> const uint8_t salt[], size_t salt_length, size_t workfactor, - bool salt_first = false); + bool salt_first = false, + const std::optional& stop_token = std::nullopt); void clear() override; diff --git a/src/lib/ffi/ffi.cpp b/src/lib/ffi/ffi.cpp index 07aa4c9edb2..6bd08e0da9d 100644 --- a/src/lib/ffi/ffi.cpp +++ b/src/lib/ffi/ffi.cpp @@ -50,6 +50,7 @@ int ffi_map_error_type(Botan::ErrorType err) { return BOTAN_FFI_ERROR_OUT_OF_MEMORY; case Botan::ErrorType::InternalError: return BOTAN_FFI_ERROR_INTERNAL_ERROR; + case Botan::ErrorType::OperationCanceled: case Botan::ErrorType::InvalidObjectState: return BOTAN_FFI_ERROR_INVALID_OBJECT_STATE; case Botan::ErrorType::KeyNotSet: diff --git a/src/lib/pbkdf/argon2/argon2.cpp b/src/lib/pbkdf/argon2/argon2.cpp index d14e02ee4c4..4a4f447ca5d 100644 --- a/src/lib/pbkdf/argon2/argon2.cpp +++ b/src/lib/pbkdf/argon2/argon2.cpp @@ -279,7 +279,8 @@ void process_block(secure_vector& B, size_t threads, uint8_t mode, size_t memory, - size_t time) { + size_t time, + const std::optional& stop_token) { uint64_t T[128]; size_t index = 0; if(n == 0 && slice == 0) { @@ -296,6 +297,10 @@ void process_block(secure_vector& B, } while(index < segments) { + if((index & 63) == 0 && stop_token.has_value() && stop_token->stop_requested()) { + throw Botan::Operation_Canceled("argon2"); + } + const size_t offset = lane * lanes + slice * segments + index; size_t prev = offset - 1; @@ -326,7 +331,12 @@ void process_block(secure_vector& B, } } -void process_blocks(secure_vector& B, size_t t, size_t memory, size_t threads, uint8_t mode) { +void process_blocks(secure_vector& B, + size_t t, + size_t memory, + size_t threads, + uint8_t mode, + const std::optional& stop_token) { const size_t lanes = memory / threads; const size_t segments = lanes / SYNC_POINTS; @@ -341,7 +351,7 @@ void process_blocks(secure_vector& B, size_t t, size_t memory, size_t for(size_t lane = 0; lane != threads; ++lane) { fut_results.push_back(thread_pool.run( - process_block, std::ref(B), n, slice, lane, lanes, segments, threads, mode, memory, t)); + process_block, std::ref(B), n, slice, lane, lanes, segments, threads, mode, memory, t, stop_token)); } for(auto& fut : fut_results) { @@ -357,7 +367,7 @@ void process_blocks(secure_vector& B, size_t t, size_t memory, size_t for(size_t n = 0; n != t; ++n) { for(size_t slice = 0; slice != SYNC_POINTS; ++slice) { for(size_t lane = 0; lane != threads; ++lane) { - process_block(B, n, slice, lane, lanes, segments, threads, mode, memory, t); + process_block(B, n, slice, lane, lanes, segments, threads, mode, memory, t, stop_token); } } } @@ -374,7 +384,8 @@ void Argon2::argon2(uint8_t output[], const uint8_t key[], size_t key_len, const uint8_t ad[], - size_t ad_len) const { + size_t ad_len, + const std::optional& stop_token) const { BOTAN_ARG_CHECK(output_len >= 4 && output_len <= std::numeric_limits::max(), "Invalid Argon2 output length"); BOTAN_ARG_CHECK(password_len <= std::numeric_limits::max(), "Invalid Argon2 password length"); @@ -406,7 +417,7 @@ void Argon2::argon2(uint8_t output[], secure_vector B(memory * 1024 / 8); init_blocks(B, *blake2, H0, memory, m_p); - process_blocks(B, m_t, memory, m_p, m_family); + process_blocks(B, m_t, memory, m_p, m_family, stop_token); clear_mem(output, output_len); extract_key(output, output_len, B, memory, m_p); diff --git a/src/lib/pbkdf/argon2/argon2.h b/src/lib/pbkdf/argon2/argon2.h index 9445f126c6a..e5759890455 100644 --- a/src/lib/pbkdf/argon2/argon2.h +++ b/src/lib/pbkdf/argon2/argon2.h @@ -35,7 +35,8 @@ class BOTAN_PUBLIC_API(2, 11) Argon2 final : public PasswordHash { const char* password, size_t password_len, const uint8_t salt[], - size_t salt_len) const override; + size_t salt_len, + const std::optional& stop_token) const override; void derive_key(uint8_t out[], size_t out_len, @@ -46,7 +47,8 @@ class BOTAN_PUBLIC_API(2, 11) Argon2 final : public PasswordHash { const uint8_t ad[], size_t ad_len, const uint8_t key[], - size_t key_len) const override; + size_t key_len, + const std::optional& stop_token) const override; std::string to_string() const override; @@ -91,7 +93,8 @@ class BOTAN_PUBLIC_API(2, 11) Argon2 final : public PasswordHash { const uint8_t key[], size_t key_len, const uint8_t ad[], - size_t ad_len) const; + size_t ad_len, + const std::optional& stop_token) const; uint8_t m_family; size_t m_M, m_t, m_p; diff --git a/src/lib/pbkdf/argon2/argon2pwhash.cpp b/src/lib/pbkdf/argon2/argon2pwhash.cpp index c31e859c977..6022ea2c0b5 100644 --- a/src/lib/pbkdf/argon2/argon2pwhash.cpp +++ b/src/lib/pbkdf/argon2/argon2pwhash.cpp @@ -26,8 +26,9 @@ void Argon2::derive_key(uint8_t output[], const char* password, size_t password_len, const uint8_t salt[], - size_t salt_len) const { - argon2(output, output_len, password, password_len, salt, salt_len, nullptr, 0, nullptr, 0); + size_t salt_len, + const std::optional& stop_token) const { + argon2(output, output_len, password, password_len, salt, salt_len, nullptr, 0, nullptr, 0, stop_token); } void Argon2::derive_key(uint8_t output[], @@ -39,8 +40,9 @@ void Argon2::derive_key(uint8_t output[], const uint8_t ad[], size_t ad_len, const uint8_t key[], - size_t key_len) const { - argon2(output, output_len, password, password_len, salt, salt_len, key, key_len, ad, ad_len); + size_t key_len, + const std::optional& stop_token) const { + argon2(output, output_len, password, password_len, salt, salt_len, key, key_len, ad, ad_len, stop_token); } namespace { diff --git a/src/lib/pbkdf/bcrypt_pbkdf/bcrypt_pbkdf.cpp b/src/lib/pbkdf/bcrypt_pbkdf/bcrypt_pbkdf.cpp index 973e0a6f544..49f440e3952 100644 --- a/src/lib/pbkdf/bcrypt_pbkdf/bcrypt_pbkdf.cpp +++ b/src/lib/pbkdf/bcrypt_pbkdf/bcrypt_pbkdf.cpp @@ -77,7 +77,8 @@ void bcrypt_round(Blowfish& blowfish, const secure_vector& pass_hash, const secure_vector& salt_hash, secure_vector& out, - secure_vector& tmp) { + secure_vector& tmp, + const std::optional& stop_token) { const size_t BCRYPT_PBKDF_OUTPUT = 32; // "OxychromaticBlowfishSwatDynamite" @@ -88,8 +89,13 @@ void bcrypt_round(Blowfish& blowfish, 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); + blowfish.salted_set_key(pass_hash.data(), + pass_hash.size(), + salt_hash.data(), + salt_hash.size(), + BCRYPT_PBKDF_WORKFACTOR, + true, + stop_token); copy_mem(tmp.data(), BCRYPT_PBKDF_MAGIC, BCRYPT_PBKDF_OUTPUT); for(size_t i = 0; i != BCRYPT_PBKDF_ROUNDS; ++i) { @@ -117,7 +123,8 @@ void Bcrypt_PBKDF::derive_key(uint8_t output[], const char* password, size_t password_len, const uint8_t salt[], - size_t salt_len) const { + size_t salt_len, + const std::optional& stop_token) const { // No output desired, so we are all done already... if(output_len == 0) { return; @@ -144,14 +151,14 @@ void Bcrypt_PBKDF::derive_key(uint8_t output[], sha512->update_be(static_cast(block + 1)); sha512->final(salt_hash.data()); - bcrypt_round(blowfish, pass_hash, salt_hash, out, tmp); + bcrypt_round(blowfish, pass_hash, salt_hash, out, tmp, stop_token); for(size_t r = 1; r < m_iterations; ++r) { // Next salt is H(prev_output) sha512->update(tmp); sha512->final(salt_hash.data()); - bcrypt_round(blowfish, pass_hash, salt_hash, out, tmp); + bcrypt_round(blowfish, pass_hash, salt_hash, out, tmp, stop_token); } for(size_t i = 0; i != BCRYPT_BLOCK_SIZE; ++i) { diff --git a/src/lib/pbkdf/bcrypt_pbkdf/bcrypt_pbkdf.h b/src/lib/pbkdf/bcrypt_pbkdf/bcrypt_pbkdf.h index 04d268c1aaf..4549c01898a 100644 --- a/src/lib/pbkdf/bcrypt_pbkdf/bcrypt_pbkdf.h +++ b/src/lib/pbkdf/bcrypt_pbkdf/bcrypt_pbkdf.h @@ -29,7 +29,8 @@ class BOTAN_PUBLIC_API(2, 11) Bcrypt_PBKDF final : public PasswordHash { const char* password, size_t password_len, const uint8_t salt[], - size_t salt_len) const override; + size_t salt_len, + const std::optional& stop_token) const override; std::string to_string() const override; diff --git a/src/lib/pbkdf/pbkdf2/pbkdf2.cpp b/src/lib/pbkdf/pbkdf2/pbkdf2.cpp index 9b1cbd69455..3e97603fc73 100644 --- a/src/lib/pbkdf/pbkdf2/pbkdf2.cpp +++ b/src/lib/pbkdf/pbkdf2/pbkdf2.cpp @@ -83,7 +83,7 @@ size_t pbkdf2(MessageAuthenticationCode& prf, const PBKDF2 pbkdf2(prf, iterations); - pbkdf2.derive_key(out, out_len, password.data(), password.size(), salt, salt_len); + pbkdf2.derive_key(out, out_len, password.data(), password.size(), salt, salt_len, std::nullopt); return iterations; } @@ -93,7 +93,8 @@ void pbkdf2(MessageAuthenticationCode& prf, size_t out_len, const uint8_t salt[], size_t salt_len, - size_t iterations) { + size_t iterations, + const std::optional& stop_token) { if(iterations == 0) { throw Invalid_Argument("PBKDF2: Invalid iteration count"); } @@ -120,6 +121,9 @@ void pbkdf2(MessageAuthenticationCode& prf, xor_buf(out, U.data(), prf_output); for(size_t i = 1; i != iterations; ++i) { + if((i & 4095) == 1 && stop_token.has_value() && stop_token->stop_requested()) { + throw Botan::Operation_Canceled("pbkdf2"); + } prf.update(U); prf.final(U.data()); xor_buf(out, U.data(), prf_output); @@ -144,7 +148,7 @@ size_t PKCS5_PBKDF2::pbkdf(uint8_t key[], const PBKDF2 pbkdf2(*m_mac, iterations); - pbkdf2.derive_key(key, key_len, password.data(), password.size(), salt, salt_len); + pbkdf2.derive_key(key, key_len, password.data(), password.size(), salt, salt_len, std::nullopt); return iterations; } @@ -171,9 +175,10 @@ void PBKDF2::derive_key(uint8_t out[], const char* password, const size_t password_len, const uint8_t salt[], - size_t salt_len) const { + size_t salt_len, + const std::optional& stop_token) const { pbkdf2_set_key(*m_prf, password, password_len); - pbkdf2(*m_prf, out, out_len, salt, salt_len, m_iterations); + pbkdf2(*m_prf, out, out_len, salt, salt_len, m_iterations, stop_token); } std::string PBKDF2_Family::name() const { diff --git a/src/lib/pbkdf/pbkdf2/pbkdf2.h b/src/lib/pbkdf/pbkdf2/pbkdf2.h index 1f9e555e7ad..fcf8418d98e 100644 --- a/src/lib/pbkdf/pbkdf2/pbkdf2.h +++ b/src/lib/pbkdf/pbkdf2/pbkdf2.h @@ -37,7 +37,8 @@ void pbkdf2(MessageAuthenticationCode& prf, size_t out_len, const uint8_t salt[], size_t salt_len, - size_t iterations); + size_t iterations, + const std::optional& stop_token = std::nullopt); /** * PBKDF2 @@ -58,7 +59,8 @@ class BOTAN_PUBLIC_API(2, 8) PBKDF2 final : public PasswordHash { const char* password, size_t password_len, const uint8_t salt[], - size_t salt_len) const override; + size_t salt_len, + const std::optional& stop_token) const override; private: std::unique_ptr m_prf; diff --git a/src/lib/pbkdf/pgp_s2k/pgp_s2k.cpp b/src/lib/pbkdf/pgp_s2k/pgp_s2k.cpp index db8ba7e24fd..3c9e32ea4f4 100644 --- a/src/lib/pbkdf/pgp_s2k/pgp_s2k.cpp +++ b/src/lib/pbkdf/pgp_s2k/pgp_s2k.cpp @@ -26,7 +26,8 @@ void pgp_s2k(HashFunction& hash, const size_t password_size, const uint8_t salt[], size_t salt_len, - size_t iterations) { + size_t iterations, + const std::optional& stop_token) { if(iterations > 1 && salt_len == 0) { throw Invalid_Argument("OpenPGP S2K requires a salt in iterated mode"); } @@ -54,7 +55,11 @@ void pgp_s2k(HashFunction& hash, // The input is always fully processed even if iterations is very small if(!input_buf.empty()) { size_t left = std::max(iterations, input_buf.size()); + size_t iter = 0; while(left > 0) { + if((iter++ & 255) == 0 && stop_token.has_value() && stop_token->stop_requested()) { + throw Botan::Operation_Canceled("pgp_s2k"); + } const size_t input_to_take = std::min(left, input_buf.size()); hash.update(input_buf.data(), input_to_take); left -= input_to_take; @@ -82,7 +87,7 @@ size_t OpenPGP_S2K::pbkdf(uint8_t output_buf[], iterations = s2k_params.tune(output_len, msec, 0, std::chrono::milliseconds(10))->iterations(); } - pgp_s2k(*m_hash, output_buf, output_len, password.data(), password.size(), salt, salt_len, iterations); + pgp_s2k(*m_hash, output_buf, output_len, password.data(), password.size(), salt, salt_len, iterations, std::nullopt); return iterations; } @@ -138,8 +143,9 @@ void RFC4880_S2K::derive_key(uint8_t out[], const char* password, const size_t password_len, const uint8_t salt[], - size_t salt_len) const { - pgp_s2k(*m_hash, out, out_len, password, password_len, salt, salt_len, m_iterations); + size_t salt_len, + const std::optional& stop_token) const { + pgp_s2k(*m_hash, out, out_len, password, password_len, salt, salt_len, m_iterations, stop_token); } } // namespace Botan diff --git a/src/lib/pbkdf/pgp_s2k/pgp_s2k.h b/src/lib/pbkdf/pgp_s2k/pgp_s2k.h index 0dd4728efa6..f39c99d475c 100644 --- a/src/lib/pbkdf/pgp_s2k/pgp_s2k.h +++ b/src/lib/pbkdf/pgp_s2k/pgp_s2k.h @@ -94,7 +94,8 @@ class BOTAN_PUBLIC_API(2, 8) RFC4880_S2K final : public PasswordHash { const char* password, size_t password_len, const uint8_t salt[], - size_t salt_len) const override; + size_t salt_len, + const std::optional& stop_token) const override; private: std::unique_ptr m_hash; diff --git a/src/lib/pbkdf/pwdhash.cpp b/src/lib/pbkdf/pwdhash.cpp index 0ec706d884d..3baffebd6f4 100644 --- a/src/lib/pbkdf/pwdhash.cpp +++ b/src/lib/pbkdf/pwdhash.cpp @@ -41,11 +41,12 @@ void PasswordHash::derive_key(uint8_t out[], const uint8_t ad[], size_t ad_len, const uint8_t key[], - size_t key_len) const { + size_t key_len, + const std::optional& stop_token) const { BOTAN_UNUSED(ad, key); if(ad_len == 0 && key_len == 0) { - return this->derive_key(out, out_len, password, password_len, salt, salt_len); + return this->derive_key(out, out_len, password, password_len, salt, salt_len, stop_token); } else { throw Not_Implemented("PasswordHash " + this->to_string() + " does not support AD or key"); } diff --git a/src/lib/pbkdf/pwdhash.h b/src/lib/pbkdf/pwdhash.h index f79055b57b6..a8d79991a5b 100644 --- a/src/lib/pbkdf/pwdhash.h +++ b/src/lib/pbkdf/pwdhash.h @@ -10,7 +10,9 @@ #include #include #include +#include #include +#include #include #include @@ -77,12 +79,19 @@ class BOTAN_PUBLIC_API(2, 8) PasswordHash /* NOLINT(*-special-member-functions) * @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 stop_token (optional) A cancellation token. If provided and cancellation is requested + * via @p stop_token.stop_requested(), the operation will terminate early by throwing + * an Operation_Canceled exception. * * This function is const, but is not thread safe. Different threads should * either use unique objects, or serialize all access. */ - void hash(std::span out, std::string_view password, std::span salt) const { - this->derive_key(out.data(), out.size(), password.data(), password.size(), salt.data(), salt.size()); + void hash(std::span out, + std::string_view password, + std::span salt, + const std::optional& stop_token = std::nullopt) const { + this->derive_key( + out.data(), out.size(), password.data(), password.size(), salt.data(), salt.size(), stop_token); } /** @@ -98,6 +107,9 @@ class BOTAN_PUBLIC_API(2, 8) PasswordHash /* NOLINT(*-special-member-functions) * @param salt a randomly chosen salt * @param associated_data some additional data * @param key a secret key + * @param stop_token (optional) A cancellation token. If provided and cancellation is requested + * via @p stop_token.stop_requested(), the operation will terminate early by throwing + * an Operation_Canceled exception. * * This function is const, but is not thread safe. Different threads should * either use unique objects, or serialize all access. @@ -106,7 +118,8 @@ class BOTAN_PUBLIC_API(2, 8) PasswordHash /* NOLINT(*-special-member-functions) std::string_view password, std::span salt, std::span associated_data, - std::span key) const { + std::span key, + const std::optional& stop_token = std::nullopt) const { this->derive_key(out.data(), out.size(), password.data(), @@ -116,7 +129,8 @@ class BOTAN_PUBLIC_API(2, 8) PasswordHash /* NOLINT(*-special-member-functions) associated_data.data(), associated_data.size(), key.data(), - key.size()); + key.size(), + stop_token); } /** @@ -128,6 +142,9 @@ class BOTAN_PUBLIC_API(2, 8) PasswordHash /* NOLINT(*-special-member-functions) * @param password_len the length of password in bytes * @param salt a randomly chosen salt * @param salt_len length of salt in bytes + * @param stop_token (optional) A cancellation token. If provided and cancellation is requested + * via @p stop_token.stop_requested(), the operation will terminate early by throwing + * an Operation_Canceled exception. * * This function is const, but is not thread safe. Different threads should * either use unique objects, or serialize all access. @@ -137,7 +154,8 @@ class BOTAN_PUBLIC_API(2, 8) PasswordHash /* NOLINT(*-special-member-functions) const char* password, size_t password_len, const uint8_t salt[], - size_t salt_len) const = 0; + size_t salt_len, + const std::optional& stop_token = std::nullopt) const = 0; /** * Derive a key from a password plus additional data and/or a secret key @@ -155,6 +173,9 @@ class BOTAN_PUBLIC_API(2, 8) PasswordHash /* NOLINT(*-special-member-functions) * @param ad_len length of ad in bytes * @param key a secret key * @param key_len length of key in bytes + * @param stop_token (optional) A cancellation token. If provided and cancellation is requested + * via @p stop_token.stop_requested(), the operation will terminate early by throwing + * an Operation_Canceled exception. * * This function is const, but is not thread safe. Different threads should * either use unique objects, or serialize all access. @@ -168,7 +189,8 @@ class BOTAN_PUBLIC_API(2, 8) PasswordHash /* NOLINT(*-special-member-functions) const uint8_t ad[], size_t ad_len, const uint8_t key[], - size_t key_len) const; + size_t key_len, + const std::optional& stop_token = std::nullopt) const; }; class BOTAN_PUBLIC_API(2, 8) PasswordHashFamily /* NOLINT(*-special-member-functions) */ { diff --git a/src/lib/pbkdf/scrypt/scrypt.cpp b/src/lib/pbkdf/scrypt/scrypt.cpp index 3b41a21331e..9a2f579701d 100644 --- a/src/lib/pbkdf/scrypt/scrypt.cpp +++ b/src/lib/pbkdf/scrypt/scrypt.cpp @@ -175,15 +175,22 @@ void scryptBlockMix(size_t r, uint8_t* B, uint8_t* Y) { } } -void scryptROMmix(size_t r, size_t N, uint8_t* B, secure_vector& V) { +void scryptROMmix( + size_t r, size_t N, uint8_t* B, secure_vector& V, const std::optional& stop_token) { const size_t S = 128 * r; for(size_t i = 0; i != N; ++i) { + if((i & 63) == 0 && stop_token.has_value() && stop_token->stop_requested()) { + throw Botan::Operation_Canceled("scrypt"); + } copy_mem(&V[S * i], B, S); scryptBlockMix(r, B, &V[N * S]); } for(size_t i = 0; i != N; ++i) { + if((i & 63) == 0 && stop_token.has_value() && stop_token->stop_requested()) { + throw Botan::Operation_Canceled("scrypt"); + } // compiler doesn't know here that N is power of 2 const size_t j = load_le(&B[(2 * r - 1) * 64], 0) & (N - 1); xor_buf(B, &V[j * S], S); @@ -198,7 +205,8 @@ void Scrypt::derive_key(uint8_t output[], const char* password, size_t password_len, const uint8_t salt[], - size_t salt_len) const { + size_t salt_len, + const std::optional& stop_token) const { const size_t N = memory_param(); const size_t p = parallelism(); const size_t r = iterations(); @@ -220,7 +228,7 @@ void Scrypt::derive_key(uint8_t output[], // these can be parallel for(size_t i = 0; i != p; ++i) { - scryptROMmix(r, N, &B[128 * r * i], V); + scryptROMmix(r, N, &B[128 * r * i], V, stop_token); } pbkdf2(*hmac_sha256, output, output_len, B.data(), B.size(), 1); diff --git a/src/lib/pbkdf/scrypt/scrypt.h b/src/lib/pbkdf/scrypt/scrypt.h index 03b013d7de3..1de215bfdb7 100644 --- a/src/lib/pbkdf/scrypt/scrypt.h +++ b/src/lib/pbkdf/scrypt/scrypt.h @@ -30,7 +30,8 @@ class BOTAN_PUBLIC_API(2, 8) Scrypt final : public PasswordHash { const char* password, size_t password_len, const uint8_t salt[], - size_t salt_len) const override; + size_t salt_len, + const std::optional& stop_token) const override; std::string to_string() const override; diff --git a/src/lib/utils/exceptn.cpp b/src/lib/utils/exceptn.cpp index 83a72964946..e066ac07a84 100644 --- a/src/lib/utils/exceptn.cpp +++ b/src/lib/utils/exceptn.cpp @@ -28,6 +28,8 @@ std::string to_string(ErrorType type) { return "InvalidObjectState"; case ErrorType::KeyNotSet: return "KeyNotSet"; + case ErrorType::OperationCanceled: + return "OperationCanceled"; case ErrorType::InvalidArgument: return "InvalidArgument"; case ErrorType::InvalidKeyLength: @@ -109,6 +111,8 @@ Invalid_IV_Length::Invalid_IV_Length(std::string_view mode, size_t bad_len) : Key_Not_Set::Key_Not_Set(std::string_view algo) : Invalid_State(fmt("Key not set in {}", algo)) {} +Operation_Canceled::Operation_Canceled(std::string_view oper) : Invalid_State(fmt("{} canceled", oper)) {} + PRNG_Unseeded::PRNG_Unseeded(std::string_view algo) : Invalid_State(fmt("PRNG {} not seeded", algo)) {} Algorithm_Not_Found::Algorithm_Not_Found(std::string_view name) : diff --git a/src/lib/utils/exceptn.h b/src/lib/utils/exceptn.h index 94aa75815da..8359860a6b9 100644 --- a/src/lib/utils/exceptn.h +++ b/src/lib/utils/exceptn.h @@ -56,6 +56,8 @@ enum class ErrorType : uint16_t { InvalidTag = 110, /** An error during Roughtime validation */ RoughtimeError = 111, + /** The operation was canceled */ + OperationCanceled = 112, /** An error when interacting with CommonCrypto API */ CommonCryptoError = 201, @@ -230,6 +232,17 @@ class BOTAN_PUBLIC_API(2, 4) Key_Not_Set : public Invalid_State { ErrorType error_type() const noexcept override { return ErrorType::KeyNotSet; } }; +/** +* The operation was canceled. Thrown when the caller aborts a long-running operation, +* for example a key derivation. +*/ +class BOTAN_PUBLIC_API(3, 10) Operation_Canceled : public Invalid_State { + public: + explicit Operation_Canceled(std::string_view operation); + + ErrorType error_type() const noexcept override { return ErrorType::OperationCanceled; } +}; + /** * A request was made for some kind of object which could not be located */ diff --git a/src/scripts/ci_build.py b/src/scripts/ci_build.py index 68cc94c1fd5..86b8dc0bdf4 100755 --- a/src/scripts/ci_build.py +++ b/src/scripts/ci_build.py @@ -426,7 +426,7 @@ def sanitize_kv(some_string): cc_bin = 'mips64-linux-gnuabi64-g++' test_prefix = ['qemu-mips64', '-L', '/usr/mips64-linux-gnuabi64/'] elif target in ['cross-arm32-baremetal']: - flags += ['--cpu=arm32', '--disable-neon', '--without-stack-protector', '--ldflags=-specs=nosys.specs'] + flags += ['--cpu=arm32', '--disable-neon', '--without-stack-protector', '--ldflags=-specs=nosys.specs', '--extra-cxxflags=-march=armv6'] cc_bin = 'arm-none-eabi-c++' test_cmd = None else: diff --git a/src/tests/test_pbkdf.cpp b/src/tests/test_pbkdf.cpp index 77811dbdc8c..428028f00fb 100644 --- a/src/tests/test_pbkdf.cpp +++ b/src/tests/test_pbkdf.cpp @@ -16,6 +16,11 @@ #include #endif +#if defined(BOTAN_HAS_THREAD_UTILS) + #include + #include +#endif + namespace Botan_Tests { namespace { @@ -125,6 +130,83 @@ class Pwdhash_Tests : public Test { BOTAN_REGISTER_TEST("pbkdf", "pwdhash", Pwdhash_Tests); + #if defined(BOTAN_HAS_THREAD_UTILS) +class Pwdhash_StopToken_Test final : public Test { + public: + std::vector run() override { + std::vector results; + + const std::vector all_pwdhash = { + "Scrypt", "PBKDF2(SHA-256)", "Argon2d", "Argon2i", "Argon2id", "Bcrypt-PBKDF"}; + // Private thread pool to guarantee thread availability for cancellation. + // We need just 1 thread as the cancellation tests are executed serially. + Botan::Thread_Pool thread_pool(1); + + for(const std::string& algo_spec : all_pwdhash) { + Test::Result result("Cooperative cancellation " + algo_spec); + + auto fam = Botan::PasswordHashFamily::create(algo_spec); + if(!fam) { + result.test_note(algo_spec + " family unavailable"); + results.push_back(result); + continue; + } + + result.start_timer(); + + // Test will be cancelled after 500ms below. If cancellation fails, test runs for 10s. + const auto run_time = std::chrono::milliseconds(10000); + const auto tune_time = std::chrono::milliseconds(100); + const size_t max_mem = 32; + auto pwdhash = fam->tune(32, run_time, max_mem, tune_time); + + const std::string password = "cancel-me"; + const std::vector salt(16, 0xAA); + std::vector out(32); + + std::stop_source src; + + // Helper thread that will request cancellation + auto future = thread_pool.run([&src]() { + const auto stop_time = std::chrono::milliseconds(500); + std::this_thread::sleep_for(stop_time); + src.request_stop(); // fire the cancellation + }); + + // Run the derivation on the main thread and evaluate the result + try { + const uint64_t start = timestamp(); + pwdhash->derive_key( + out.data(), out.size(), password.c_str(), password.size(), salt.data(), salt.size(), src.get_token()); + // If we reach this line, the stop token was ignored + const uint64_t ms_taken = (timestamp() - start) / 1000000; + if(ms_taken < static_cast(run_time.count()) / 2) { + result.test_note("Derivation completed in " + std::to_string(ms_taken) + + "ms. Ignoring mistuned password hash."); + } else { + result.test_failure("Derivation completed without observing stop token"); + } + } catch(const Botan::Operation_Canceled& e) { + // Expected – password hash saw the stop token and threw + result.test_success("Cancellation raised Botan::Operation_Canceled: " + std::string(e.what())); + } catch(const std::exception& e) { + result.test_failure("Unexpected std::exception", e.what()); + } catch(...) { + result.test_failure("Non-standard exception thrown on cancellation"); + } + + future.get(); // ensure the canceller thread finished + + result.end_timer(); + results.push_back(result); + } + return results; + } +}; + +BOTAN_REGISTER_TEST("pbkdf", "pwdhash_stop_token", Pwdhash_StopToken_Test); + #endif + #endif #if defined(BOTAN_HAS_PBKDF_BCRYPT)