Jim Pingle wrote in #note-2:
Dynamic tuning sounds like more trouble than it's worth, IMO. We'd have to test and cache the value or test each time, maybe periodically re-test (at boot? Some other time?). Depending on the load during the test the resulting value could vary as well.
Understood. I was in the same boat until I hit upon the solution that I linked to. It's actually much simpler than I thought. The value doesn't have to be stored anywhere, and the rounds value is dynamically calculated every time a password is changed. And the load variability is taken into account - in fact, that variability, and dynamically tuning the value every time a password is changed, is actually a feature in this context!
Allowing the user to manually set the work factor (bcrypt) or rounds (sha512) may be something we could consider doing
Rather than setting the work factor directly, setting how long the work should take might be more intuitive for the user, and might indeed be a useful future configuration item. In the code I linked to, I make the argument that .5 seconds is a threshold that multiple UX studies have shown that users can tolerate. But I don't think it's necessary to wait on that in order to get the much more cracking-resistant hashes that it would produce.
but I don't think trying to setup and maintain an automation system for this would be worth the technical debt it would incur right now.
The dynamic calculation of the rounds value is less than 20 lines of actual code (including variables and verbose feedback for the demo only), and the algorithm is very easy to understand:
function get_target_sha512crypt_rounds($target_seconds) {
// Purpose: Tune sha512crypt rounds to a target runtime.
// Note that we do *not* set a rounds value once globally, nor do we
// normalize or round up or down here, by design. This is because
// having a variable number of rounds is a security feature, to resist
// correlation attacks (JtR's single mode or hashcat -a 9 mode).
// Some variability in runtime also provides rough protection
// against sha512crypt's "guess how long the password is" flaw
// (see https://pthree.org/2018/05/23/do-not-use-sha256crypt-sha512crypt-theyre-dangerous/)
// Set a test password to use for the tuning.
// sha512crypt speed roughly increases with password length, so we
// pick a test password that is larger than an average simple password,
// but smaller than a passphrase.
$test_password = 'pfsense89ABCDEF';
// To minimize testing time, pick a relatively small value relative to
// modern performance for common platforms, but large enough to offset
// some variability in runtime. Very old systems may take significantly
// longer, so the initial rounds_candidate value may need to be adjusted.
$rounds_candidate = 100000;
$time_elapsed_secs = 0;
$accuracy_margin_seconds = .1;
// Set a minimum number of rounds.
// If the platform is slow, attack can happen on a faster system,
// so this value should be as high as can be tolerated across the
// expected fleet of systems we can reasonably expect to support.
// PHP's current minimum is 1000, so this value should never be less.
// For attack resistance, it should be far more than 1000 or even 5000.
$minimum_rounds = 50000;
// Adjust rounds until hash time is roughly close to the target time.
// Since we use the results to calculate the next run, and we don't
// care if it's rough, this loop should only run a couple of times.
//
// Reference round counts (dmidecode -s processor-version):
// (Examples wanted - especially old and new pfSense/Netgate appliances)
//
// - AMD Geode LX800 500 MHz (alix2): rounds=11851
// - AMD GX-412TC SOC (apu2): rounds=157921
// - Intel(R) Celeron(R) CPU N3150 @ 1.60GHz: rounds=209662
// - Pentium(R) Dual-Core CPU E5: rounds=568985
// - 11th Gen Intel(R) Core(TM) i7-11700K @ 3.60GHz: rounds=1741092
//
// By contrast, a medium-sized pentest cracking rig (equivalent of 6 GTX
// 1080s) can do a little over 2 *billion* rounds in half a second against
// a single hash (scaling downward against multiple salted hashes). So the
// goal is to counter such attack speeds by as much as can be tolerated.
while (abs($target_seconds - $time_elapsed_secs) > $accuracy_margin_seconds) {
// Time the hash.
$start = microtime(true);
$test_hash = hash_sha512crypt($test_password, $rounds_candidate);
$time_elapsed_secs = microtime(true) - $start;
print "Elapsed time: " . $time_elapsed_secs . ", rounds: $rounds_candidate\n";
// Adjust the number of rounds based on the runtime.
$perf_ratio = $target_seconds / $time_elapsed_secs;
$rounds_candidate = intval($rounds_candidate * $perf_ratio);
}
print "Final autotuned rounds: $rounds_candidate\n";
// If rounds are below minimum, warn the user and use the minimum instead.
// Should only happen on very old hardware.
if ($rounds_candidate < $minimum_rounds) {
fwrite(STDERR, "Warning: detected rounds $rounds_candidate is less than minimum of $minimum_rounds - using minimum\n");
$rounds_candidate = $minimum_rounds;
}
return $rounds_candidate;
}