wallet: use unsigned KDF iteration count #35859

pull benthecarman wants to merge 2 commits into bitcoin:master from benthecarman:fix-wallet-kdf-rounds-cap changing 5 files +49 −30
  1. benthecarman commented at 5:04 AM on August 1, 2026: contributor

    CMasterKey::nDeriveIterations values are deserialized from wallet files as unsigned 32-bit integers, but key derivation narrowed the count to a signed int. A count above INT_MAX became negative in the conversion, and the derivation loop counter then overflowed, which is undefined behavior.

    Keep the count unsigned through the derivation path to match the serialized type, and add tests for zero and normal counts.

    Also check key-derivation calibration failures and validate calculated iteration counts before conversion. Keep the output master key unchanged until derivation and encryption succeed, and mark fallible crypter methods as [[nodiscard]].

  2. DrahtBot added the label Wallet on Aug 1, 2026
  3. DrahtBot commented at 5:04 AM on August 1, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/35859.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    ACK l0rinc, achow101
    Stale ACK 151henry151

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  4. benthecarman force-pushed on Aug 1, 2026
  5. DrahtBot added the label CI failed on Aug 1, 2026
  6. DrahtBot commented at 5:31 AM on August 1, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task ASan + LSan + UBSan + integer: https://github.com/bitcoin/bitcoin/actions/runs/30685144257/job/91329306668</sub> <sub>LLM reason (✨ experimental): CI failed due to an UndefinedBehaviorSanitizer error (implicit integer sign change) in wallet/crypter.cpp triggered by wallet_crypto_tests (passphrase_rounds_limit).</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  7. DrahtBot removed the label CI failed on Aug 1, 2026
  8. 151henry151 commented at 5:54 PM on August 1, 2026: contributor

    ACK fa833d949b786e58c598b997054ec15de682c825

    Reviewed the diff and tested (wallet_crypto_tests; SetKeyFromPassphrase with UINT_MAX hangs past 45s on master and returns immediately here). count is -1 there, so the loop is i != -2 with i counting up and can't exit on its own. Rejecting rounds > INT_MAX before the signed conversion is the right shape β€” widening count to unsigned just makes UINT_MAX a well-defined multi-hour hang, and switching the loop to i < count - 1 alone would treat count == -1 as a successful 1-iteration derive.

    One gap that I think belongs in this PR: EncryptMasterKey ignores the return value of its two calibration SetKeyFromPassphrase calls. A near-zero first sample (coarse clock, VM glitch) can already push nDeriveIterations above INT_MAX; with this change the second call then returns in microseconds, so its elapsed is ~0 as well and the average is another garbage count (the double→unsigned conversion is also UB once the value exceeds UINT_MAX). The unchecked returns and that cast predate the PR — what's new is the second call failing silently instead of hanging. Check the return value and don't update nDeriveIterations on failure.

    Then, a few more things that could be addressed as follow-ups: INT_MAX is still accepted (~600ns/round at -O2 on an i7-7700HQ, so ~21 minutes; only reachable via the wallet file, so local DoS), and a MAX_DERIVE_ITERATIONS should be chosen against calibrations on fast hardware with a plan for wallets that already exceed it. Rejection also looks like a wrong passphrase with no log β€” a WalletLogPrintf of the rejected count would help.

  9. in src/wallet/crypter.cpp:23 in fa833d949b
      19 | @@ -19,7 +20,7 @@ int CCrypter::BytesToKeySHA512AES(const std::span<const unsigned char> salt, con
      20 |      // greater than the aes256 block size (16b) + aes256 key size (32b),
      21 |      // there's no need to process more than once (D_0).
      22 |  
      23 | -    if(!count || !key || !iv)
      24 | +    if(count < 1 || !key || !iv)
    


    l0rinc commented at 7:18 PM on August 1, 2026:

    fa833d9 Wallet: Reject oversized KDF iteration counts:

    nit: since this private KDF has one caller (which now validates the signed range before conversion) a negative count is unreachable. We could minimize the diff by leaving this line unchanged.

  10. in src/wallet/crypter.cpp:45 in fa833d949b
      40 | @@ -40,7 +41,8 @@ int CCrypter::BytesToKeySHA512AES(const std::span<const unsigned char> salt, con
      41 |  
      42 |  bool CCrypter::SetKeyFromPassphrase(const SecureString& key_data, const std::span<const unsigned char> salt, const unsigned int rounds, const unsigned int derivation_method)
      43 |  {
      44 | -    if (rounds < 1 || salt.size() != WALLET_CRYPTO_SALT_SIZE) {
      45 | +    // Reject counts that cannot be represented by the KDF's signed count parameter.
      46 | +    if (rounds < 1 || rounds > static_cast<unsigned int>(std::numeric_limits<int>::max()) || salt.size() != WALLET_CRYPTO_SALT_SIZE) {
    


    l0rinc commented at 7:19 PM on August 1, 2026:

    fa833d9 Wallet: Reject oversized KDF iteration counts:

    We could simplify this slightly and also be more in line with BytesToKeySHA512AES:

        if (!rounds || !std::in_range<int>(rounds) || salt.size() != WALLET_CRYPTO_SALT_SIZE) {
    
  11. in src/wallet/test/wallet_crypto_tests.cpp:101 in fa833d949b
      96 | @@ -96,6 +97,19 @@ BOOST_AUTO_TEST_CASE(passphrase) {
      97 |      TestCrypter::TestPassphrase(vchSalt, SecureString(hash.begin(), hash.end()), rounds);
      98 |  }
      99 |  
     100 | +BOOST_AUTO_TEST_CASE(passphrase_rounds_limit) {
     101 | +    constexpr std::array<uint8_t, WALLET_CRYPTO_SALT_SIZE> salt{"0000deadbeef0000"_hex_u8};
    


    l0rinc commented at 7:20 PM on August 1, 2026:

    fa833d9 Wallet: Reject oversized KDF iteration counts:

    nit:

        constexpr auto salt{"0000deadbeef0000"_hex_u8};
    
  12. in src/wallet/test/wallet_crypto_tests.cpp:108 in fa833d949b
     103 | +    // Iteration counts above INT_MAX (e.g. from a crafted or corrupted wallet file) narrow to a
     104 | +    // negative count in the KDF and must be rejected instead of making key derivation loop for an
     105 | +    // unbounded amount of time.
     106 | +    BOOST_CHECK(!crypt.SetKeyFromPassphrase("passphrase", salt, 0, 0));
     107 | +    BOOST_CHECK(!crypt.SetKeyFromPassphrase("passphrase", salt, static_cast<unsigned int>(std::numeric_limits<int>::max()) + 1, 0));
     108 | +    BOOST_CHECK(!crypt.SetKeyFromPassphrase("passphrase", salt, std::numeric_limits<unsigned int>::max(), 0));
    


    l0rinc commented at 7:23 PM on August 1, 2026:

    fa833d9 Wallet: Reject oversized KDF iteration counts:

    nit:

        for (unsigned int rounds : {0u, std::numeric_limits<int>::max() + 1u}) {
            BOOST_CHECK(!crypt.SetKeyFromPassphrase("passphrase", salt, rounds, /*derivation_method=*/0));
        }
    
  13. in src/wallet/crypter.cpp:42 in fa833d949b outdated
      40 | @@ -40,7 +41,8 @@ int CCrypter::BytesToKeySHA512AES(const std::span<const unsigned char> salt, con
      41 |  
      42 |  bool CCrypter::SetKeyFromPassphrase(const SecureString& key_data, const std::span<const unsigned char> salt, const unsigned int rounds, const unsigned int derivation_method)
    


    l0rinc commented at 9:40 PM on August 1, 2026:

    fa833d9 Wallet: Reject oversized KDF iteration counts:

    Can we make the methods in CCrypter all [[nodiscard]] and make sure the call sites are not ignored?


    benthecarman commented at 4:22 AM on August 2, 2026:

    Added in a second commit

  14. l0rinc approved
  15. l0rinc commented at 9:48 PM on August 1, 2026: contributor

    Concept ACK fa833d949b786e58c598b997054ec15de682c825, thanks a lot for checking!

    I left a few non-blocking suggestions. Could you also mark the fallible CCrypter methods [[nodiscard]] and handle their results at every call site - or at least for SetKeyFromPassphrase()?

  16. benthecarman force-pushed on Aug 2, 2026
  17. benthecarman requested review from l0rinc on Aug 2, 2026
  18. 151henry151 commented at 2:00 PM on August 2, 2026: contributor

    re-ACK 7b18a0c88c

    Reviewed the range-diff since fa833d9. Failing closed in EncryptMasterKey removes the garbage-average path from a near-zero calibration sample β€” checked that it aborts rather than averaging a second bogus count. MAX_DERIVE_ITERATIONS and logging the rejected count remain follow-ups, not blockers.

    Tested with wallet_crypto_tests,wallet_tests,walletload_tests and a full build with -DSANITIZERS=address,float-divide-by-zero,integer,undefined β€” passed.

  19. in src/wallet/crypter.cpp:50 in ec97238f45 outdated
      47 |      }
      48 |  
      49 |      int i = 0;
      50 |      if (derivation_method == 0) {
      51 | -        i = BytesToKeySHA512AES(salt, key_data, rounds, vchKey.data(), vchIV.data());
      52 | +        i = BytesToKeySHA512AES(salt, key_data, static_cast<int>(rounds), vchKey.data(), vchIV.data());
    


    l0rinc commented at 3:25 AM on August 3, 2026:

    ec97238 Wallet: Reject oversized KDF iteration counts:

    Unless there's a good reason for adding this, given that the assumptions only become stricter after this change, I'd revert this line.


    benthecarman commented at 10:50 PM on August 6, 2026:

    I kept it as i have it. The conversion is safe because we already verified it and the cast also makes it explicit


    achow101 commented at 8:46 PM on August 10, 2026:

    In a7a9e8506547f26d752cb1091164a097976b7268 "Wallet: Reject oversized KDF iteration counts"

    Everything else is handling the value as an unsigned int. I think it's preferable to change BytesToKeySHA512AES to take count as unsigned int rather than a narrowing conversion here.


    benthecarman commented at 11:03 PM on August 10, 2026:

    done

  20. in src/wallet/wallet.cpp:576 in 7b18a0c88c outdated
     569 | @@ -570,8 +570,11 @@ static bool EncryptMasterKey(const SecureString& wallet_passphrase, const CKeyin
     570 |      // Get the weighted average of iterations we can do in 100ms over 2 runs.
     571 |      for (int i = 0; i < 2; i++){
     572 |          auto start_time{NodeClock::now()};
     573 | -        crypter.SetKeyFromPassphrase(wallet_passphrase, master_key.vchSalt, master_key.nDeriveIterations, master_key.nDerivationMethod);
     574 | +        const bool key_set{crypter.SetKeyFromPassphrase(wallet_passphrase, master_key.vchSalt, master_key.nDeriveIterations, master_key.nDerivationMethod)};
     575 |          auto elapsed_time{NodeClock::now() - start_time};
     576 | +        if (!key_set) {
     577 | +            return false;
    


    l0rinc commented at 3:40 AM on August 3, 2026:

    7b18a0c Wallet: Check crypter return values:

    Now that we return early on failure, we shouldn't modify the output parameter: a failed calibration can leave nDeriveIterations changed while vchCryptedKey still contains the old ciphertext.

    Could we copy master_key and only move it to the output after encryption succeeds?

    <details><summary>preserve master key on failure</summary>

    diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp
    index bbb930864d..b76c5da070 100644
    --- a/src/wallet/wallet.cpp
    +++ b/src/wallet/wallet.cpp
    @@ -566,11 +566,12 @@ static bool EncryptMasterKey(const SecureString& wallet_passphrase, const CKeyin
     {
         constexpr MillisecondsDouble target_time{100};
         CCrypter crypter;
    +    CMasterKey updated_master_key{master_key};
     
         // Get the weighted average of iterations we can do in 100ms over 2 runs.
         for (int i = 0; i < 2; i++){
             auto start_time{NodeClock::now()};
    -        const bool key_set{crypter.SetKeyFromPassphrase(wallet_passphrase, master_key.vchSalt, master_key.nDeriveIterations, master_key.nDerivationMethod)};
    +        const bool key_set{crypter.SetKeyFromPassphrase(wallet_passphrase, updated_master_key.vchSalt, updated_master_key.nDeriveIterations, updated_master_key.nDerivationMethod)};
             auto elapsed_time{NodeClock::now() - start_time};
             if (!key_set) {
                 return false;
    @@ -578,27 +579,28 @@ static bool EncryptMasterKey(const SecureString& wallet_passphrase, const CKeyin
     
             if (elapsed_time <= 0s) {
                 // We are probably in a test with a mocked clock.
    -            master_key.nDeriveIterations = CMasterKey::DEFAULT_DERIVE_ITERATIONS;
    +            updated_master_key.nDeriveIterations = CMasterKey::DEFAULT_DERIVE_ITERATIONS;
                 break;
             }
     
             // target_iterations : elapsed_iterations :: target_time : elapsed_time
    -        unsigned int target_iterations = master_key.nDeriveIterations * target_time / elapsed_time;
    +        unsigned int target_iterations = updated_master_key.nDeriveIterations * target_time / elapsed_time;
             // Get the weighted average with previous runs.
    -        master_key.nDeriveIterations = (i * master_key.nDeriveIterations + target_iterations) / (i + 1);
    +        updated_master_key.nDeriveIterations = (i * updated_master_key.nDeriveIterations + target_iterations) / (i + 1);
         }
     
    -    if (master_key.nDeriveIterations < CMasterKey::DEFAULT_DERIVE_ITERATIONS) {
    -        master_key.nDeriveIterations = CMasterKey::DEFAULT_DERIVE_ITERATIONS;
    +    if (updated_master_key.nDeriveIterations < CMasterKey::DEFAULT_DERIVE_ITERATIONS) {
    +        updated_master_key.nDeriveIterations = CMasterKey::DEFAULT_DERIVE_ITERATIONS;
         }
     
    -    if (!crypter.SetKeyFromPassphrase(wallet_passphrase, master_key.vchSalt, master_key.nDeriveIterations, master_key.nDerivationMethod)) {
    +    if (!crypter.SetKeyFromPassphrase(wallet_passphrase, updated_master_key.vchSalt, updated_master_key.nDeriveIterations, updated_master_key.nDerivationMethod)) {
             return false;
         }
    -    if (!crypter.Encrypt(plain_master_key, master_key.vchCryptedKey)) {
    +    if (!crypter.Encrypt(plain_master_key, updated_master_key.vchCryptedKey)) {
             return false;
         }
     
    +    master_key = std::move(updated_master_key);
         return true;
     }
    

    </details>


    benthecarman commented at 10:47 PM on August 6, 2026:

    Done

  21. in src/wallet/test/wallet_crypto_tests.cpp:110 in ec97238f45
     105 | +    // unbounded amount of time.
     106 | +    for (unsigned int rounds : {0u, std::numeric_limits<int>::max() + 1u}) {
     107 | +        BOOST_CHECK(!crypt.SetKeyFromPassphrase("passphrase", salt, rounds, /*derivation_method=*/0));
     108 | +    }
     109 | +    // Sane values still work.
     110 | +    BOOST_CHECK(crypt.SetKeyFromPassphrase("passphrase", salt, CMasterKey::DEFAULT_DERIVE_ITERATIONS, /*derivation_method=*/0));
    


    l0rinc commented at 3:44 AM on August 3, 2026:

    ec97238 Wallet: Reject oversized KDF iteration counts:

    Do we really need 25,000 rounds to check a "sane" value?

    <details><summary>trim KDF rounds test</summary>

    diff --git a/src/wallet/test/wallet_crypto_tests.cpp b/src/wallet/test/wallet_crypto_tests.cpp
    index 39c0626697..cc6bdedc21 100644
    --- a/src/wallet/test/wallet_crypto_tests.cpp
    +++ b/src/wallet/test/wallet_crypto_tests.cpp
    @@ -107,8 +107,8 @@ BOOST_AUTO_TEST_CASE(passphrase_rounds_limit) {
         for (unsigned int rounds : {0u, std::numeric_limits<int>::max() + 1u}) {
             BOOST_CHECK(!crypt.SetKeyFromPassphrase("passphrase", salt, rounds, /*derivation_method=*/0));
         }
    -    // Sane values still work.
    -    BOOST_CHECK(crypt.SetKeyFromPassphrase("passphrase", salt, CMasterKey::DEFAULT_DERIVE_ITERATIONS, /*derivation_method=*/0));
    +    // In-range values still work.
    +    BOOST_CHECK(crypt.SetKeyFromPassphrase("passphrase", salt, /*rounds=*/1, /*derivation_method=*/0));
     }
     
     BOOST_AUTO_TEST_CASE(encrypt) {
    

    </details>


    benthecarman commented at 10:47 PM on August 6, 2026:

    changed to 1

  22. in src/wallet/crypter.h:109 in 7b18a0c88c


    l0rinc commented at 3:59 AM on August 3, 2026:

    7b18a0c Wallet: Check crypter return values:

    Could you please also make these [[nodiscard]] (no call-site adjustments needed as far as I can see)?

    [[nodiscard]] bool EncryptSecret(const CKeyingMaterial& vMasterKey, const CKeyingMaterial& vchPlaintext, const uint256& nIV, std::vector<unsigned char>& vchCiphertext);
    [[nodiscard]] bool DecryptSecret(const CKeyingMaterial& master_key, std::span<const unsigned char> ciphertext, const uint256& iv, CKeyingMaterial& plaintext);
    [[nodiscard]] bool DecryptKey(const CKeyingMaterial& master_key, std::span<const unsigned char> crypted_secret, const CPubKey& pub_key, CKey& key);
    

    benthecarman commented at 10:48 PM on August 6, 2026:

    done

  23. in src/wallet/wallet.cpp:575 in 7b18a0c88c outdated
     569 | @@ -570,8 +570,11 @@ static bool EncryptMasterKey(const SecureString& wallet_passphrase, const CKeyin
     570 |      // Get the weighted average of iterations we can do in 100ms over 2 runs.
     571 |      for (int i = 0; i < 2; i++){
     572 |          auto start_time{NodeClock::now()};
     573 | -        crypter.SetKeyFromPassphrase(wallet_passphrase, master_key.vchSalt, master_key.nDeriveIterations, master_key.nDerivationMethod);
     574 | +        const bool key_set{crypter.SetKeyFromPassphrase(wallet_passphrase, master_key.vchSalt, master_key.nDeriveIterations, master_key.nDerivationMethod)};
     575 |          auto elapsed_time{NodeClock::now() - start_time};
     576 | +        if (!key_set) {
    


    l0rinc commented at 6:26 PM on August 3, 2026:

    7b18a0c Wallet: Check crypter return values:

    Optional hardening since this predates the PR: target_iterations is converted from double to unsigned int before any range check, so a sufficiently small positive elapsed_time can make the conversion undefined. Would it make sense to reject values outside [1, INT_MAX] before converting, i.e. store the double calculation first, check whether we can safely cast it, and only do so when safe? (the preceding elapsed_time <= 0s should eliminate NaN already)

    <details><summary>validate calibrated KDF rounds</summary>

    diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp
    index b76c5da070..aa383e0893 100644
    --- a/src/wallet/wallet.cpp
    +++ b/src/wallet/wallet.cpp
    @@ -76,6 +76,7 @@
     #include <cassert>
     #include <condition_variable>
     #include <exception>
    +#include <limits>
     #include <optional>
     #include <stdexcept>
     #include <thread>
    @@ -584,9 +585,10 @@ static bool EncryptMasterKey(const SecureString& wallet_passphrase, const CKeyin
             }
    
             // target_iterations : elapsed_iterations :: target_time : elapsed_time
    -        unsigned int target_iterations = updated_master_key.nDeriveIterations * target_time / elapsed_time;
    +        double target_iterations{updated_master_key.nDeriveIterations * target_time / elapsed_time};
    +        if (target_iterations < 1 || target_iterations > std::numeric_limits<int>::max()) return false;
             // Get the weighted average with previous runs.
    -        updated_master_key.nDeriveIterations = (i * updated_master_key.nDeriveIterations + target_iterations) / (i + 1);
    +        updated_master_key.nDeriveIterations = (i * updated_master_key.nDeriveIterations + static_cast<unsigned int>(target_iterations)) / (i + 1);
         }
    
         if (updated_master_key.nDeriveIterations < CMasterKey::DEFAULT_DERIVE_ITERATIONS) {
    

    </details>


    benthecarman commented at 10:48 PM on August 6, 2026:

    done

  24. l0rinc approved
  25. l0rinc commented at 6:39 PM on August 3, 2026: contributor

    I left a few comments.

    Found and created with kimi k3.

    AI attribution is unnecessary - authors and reviewers fully bear responsibility for contributions and reviews.

  26. DrahtBot requested review from l0rinc on Aug 3, 2026
  27. benthecarman force-pushed on Aug 6, 2026
  28. l0rinc commented at 10:53 PM on August 6, 2026: contributor

    ACK f1b36ca273f57815ba9ed9ddc046968a9f7283dc

  29. 151henry151 commented at 10:04 PM on August 10, 2026: contributor

    re-ACK f1b36ca273f57815ba9ed9ddc046968a9f7283dc

    Reviewed the changes since 7b18a0c88c. EncryptMasterKey now updates a copy and only moves into master_key on success, so a failed calibration or encrypt no longer leaves a half-updated master key. The check that target_iterations is in [1, INT_MAX] before casting looks right, as do the extra [[nodiscard]]s and the lighter success-path rounds check in the test.

    MAX_DERIVE_ITERATIONS and logging the rejected count remain follow-ups.

    Tested with: ./build/bin/test_bitcoin --run_test=wallet_crypto_tests,wallet_tests,walletload_tests β€” passed

  30. benthecarman force-pushed on Aug 10, 2026
  31. 151henry151 commented at 11:34 PM on August 10, 2026: contributor

    re-ACK 27f007181bb0a4231b08bdf0f4a64a9218b0fafa

    Only change since my prior ACK is making BytesToKeySHA512AES take unsigned int count (and using that in the loop) so SetKeyFromPassphrase no longer narrows rounds at the call site. std::in_range<int>(rounds) still bounds what reaches the KDF.

    Tested with: ./build/bin/test_bitcoin --run_test=wallet_crypto_tests,wallet_tests,walletload_tests β€” passed

  32. l0rinc commented at 12:38 AM on August 11, 2026: contributor

    ACK 27f007181bb0a4231b08bdf0f4a64a9218b0fafa

    Now that we've removed the explicit narrowing at the call site, could we update the PR title, description, and test comment accordingly (and, if you push again, the first commit message)?

  33. benthecarman renamed this:
    wallet: reject KDF iteration counts that overflow the int count
    wallet: reject oversized KDF iteration counts
    on Aug 11, 2026
  34. benthecarman force-pushed on Aug 11, 2026
  35. l0rinc commented at 12:56 AM on August 11, 2026: contributor

    ACK a258145095e4ae21837c0a7c83e3f68db5686cc2

  36. Wallet: Use unsigned KDF iteration count
    CMasterKey::nDeriveIterations values are deserialized from wallet
    files as unsigned 32-bit integers, but key derivation narrowed the
    count to a signed int. A count above INT_MAX became negative in the
    conversion, and the derivation loop counter then overflowed, which
    is undefined behavior.
    
    Keep the count unsigned through the derivation path to match the
    serialized type. Add a unit test for zero and normal counts.
    b76afff274
  37. in src/wallet/crypter.cpp:44 in 002272bb3a
      40 | @@ -40,7 +41,7 @@ int CCrypter::BytesToKeySHA512AES(const std::span<const unsigned char> salt, con
      41 |  
      42 |  bool CCrypter::SetKeyFromPassphrase(const SecureString& key_data, const std::span<const unsigned char> salt, const unsigned int rounds, const unsigned int derivation_method)
      43 |  {
      44 | -    if (rounds < 1 || salt.size() != WALLET_CRYPTO_SALT_SIZE) {
      45 | +    if (!rounds || !std::in_range<int>(rounds) || salt.size() != WALLET_CRYPTO_SALT_SIZE) {
    


    achow101 commented at 5:44 PM on August 11, 2026:

    In 002272bb3a1aa2b414104fc8594c892a0a68ef94 "Wallet: Reject oversized KDF iteration counts"

    Since we got rid of the narrowing, I don't think this in_range check makes sense.

    In general, I'm hesitant to put bounds on the iteration count since the a sufficiently fast computer could legitimately use such high iteration counts.


    benthecarman commented at 6:24 PM on August 14, 2026:

    done

  38. benthecarman force-pushed on Aug 14, 2026
  39. benthecarman renamed this:
    wallet: reject oversized KDF iteration counts
    wallet: use unsigned KDF iteration count
    on Aug 14, 2026
  40. benthecarman requested review from achow101 on Aug 14, 2026
  41. Wallet: Check crypter return values
    Mark CCrypter's fallible methods and crypto helpers as nodiscard, and
    handle key-derivation calibration failures.
    
    Keep the output master key unchanged until encryption succeeds. Validate
    calibrated iteration counts before integer conversion. This prevents a
    failed calibration from leaving mismatched parameters and ciphertext or
    triggering undefined conversion behavior.
    cf36df070b
  42. in src/wallet/wallet.cpp:590 in 290404e4e2
     588 |          }
     589 |  
     590 |          // target_iterations : elapsed_iterations :: target_time : elapsed_time
     591 | -        unsigned int target_iterations = master_key.nDeriveIterations * target_time / elapsed_time;
     592 | +        const double target_iterations{updated_master_key.nDeriveIterations * target_time / elapsed_time};
     593 | +        if (target_iterations < 1 || target_iterations > std::numeric_limits<int>::max()) {
    


    l0rinc commented at 2:26 AM on August 15, 2026:

    target_iterations is cast to unsigned int below, so shouldn't we validate it against std::numeric_limits<unsigned int>::max() instead?


    benthecarman commented at 6:28 AM on August 18, 2026:

    done, thanks

  43. benthecarman force-pushed on Aug 18, 2026
  44. l0rinc commented at 6:32 AM on August 18, 2026: contributor

    code review ACK cf36df070b4dfa954df78bb59c687de54b277a5a

  45. achow101 commented at 5:37 PM on August 19, 2026: member

    ACK cf36df070b4dfa954df78bb59c687de54b277a5a

  46. achow101 merged this on Aug 19, 2026
  47. achow101 closed this on Aug 19, 2026

  48. benthecarman deleted the branch on Aug 19, 2026

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-08-25 09:51 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me