diff options
-rw-r--r-- | doc/manual/passhash.rst | 48 | ||||
-rw-r--r-- | doc/manual/pbkdf.rst | 8 | ||||
-rw-r--r-- | src/cli/argon2.cpp | 78 | ||||
-rw-r--r-- | src/cli/bcrypt.cpp | 4 | ||||
-rw-r--r-- | src/lib/pbkdf/argon2/argon2.h | 78 | ||||
-rw-r--r-- | src/lib/pbkdf/argon2/argon2fmt.cpp | 125 | ||||
-rw-r--r-- | src/lib/pbkdf/argon2/argon2pwhash.cpp | 151 | ||||
-rw-r--r-- | src/lib/pbkdf/pwdhash.cpp | 19 | ||||
-rw-r--r-- | src/tests/data/passhash/argon2.vec | 38 | ||||
-rw-r--r-- | src/tests/test_passhash.cpp | 53 |
10 files changed, 595 insertions, 7 deletions
diff --git a/doc/manual/passhash.rst b/doc/manual/passhash.rst index 02094a99a..4ef26f7be 100644 --- a/doc/manual/passhash.rst +++ b/doc/manual/passhash.rst @@ -65,8 +65,50 @@ designs, such as scrypt, explicitly attempt to provide this. The bcrypt approach requires over 4 KiB of RAM (for the Blowfish key schedule) and may also make some hardware attacks more expensive. -Botan provides two techniques for password hashing, bcrypt and -passhash9. +Botan provides three techniques for password hashing: Argon2, bcrypt, and +passhash9 (based on PBKDF2). + +Argon2 +---------------------------------------- + +.. versionadded:: 2.11.0 + +Argon2 is the winner of the PHC (Password Hashing Competition) and provides +a tunable memory hard password hash. It has a standard string encoding, which looks like:: + + "$argon2i$v=19$m=8192,t=10,p=3$YWFhYWFhYWE$itkWB9ODqTd85wUsoib7pfpVTNGMOu0ZJan1odl25V8" + +Argon2 has three tunable parameters: ``M``, ``p``, and ``t``. ``M`` gives the +total memory consumption of the algorithm in kilobytes. Increasing ``p`` +increases the available parallelism of the computation. The ``t`` parameter +gives the number of passes which are made over the data. + +.. note:: + Currently Botan does not make use of ``p`` > 1, so it is best to set it to 1 + to minimize any advantage to highly parallel cracking attempts. + +There are three variants of Argon2, namely Argon2d, Argon2i and Argon2id. +Argon2d uses data dependent table lookups with may leak information about the +password via side channel attacks, and is **not recommended** for password +hashing. Argon2i uses data independent table lookups and is immune to these +attacks, but at the cost of requiring higher ``t`` for security. Argon2id uses a +hybrid approach which is thought to be highly secure. The algorithm designers +recommend using Argon2id with ``t`` and ``p`` both equal to 1 and ``M`` set to +the largest amount of memory usable in your environment. + +.. cpp:function:: std::string argon2_generate_pwhash(const char* password, size_t password_len, \ + RandomNumberGenerator& rng, \ + size_t p, size_t M, size_t t, \ + size_t y = 2, size_t salt_len = 16, size_t output_len = 32) + + Generate an Argon2 hash of the specified password. The ``y`` parameter specifies + the variant: 0 for Argon2d, 1 for Argon2i, and 2 for Argon2id. + +.. cpp:function:: bool argon2_check_pwhash(const char* password, size_t password_len, \ + const std::string& hash) + + Verify an Argon2 password hash against the provided password. Returns false if + the input hash seems malformed or if the computed hash does not match. Bcrypt ---------------------------------------- @@ -153,7 +195,7 @@ Passhash9 hashes look like:: This function should be secure with the proper parameters, and will remain in the library for the foreseeable future, but it is specific to Botan rather than -being a widely used password hash. Prefer bcrypt. +being a widely used password hash. Prefer bcrypt or Argon2. .. warning:: diff --git a/doc/manual/pbkdf.rst b/doc/manual/pbkdf.rst index 6539c6f0f..92f59f278 100644 --- a/doc/manual/pbkdf.rst +++ b/doc/manual/pbkdf.rst @@ -150,6 +150,14 @@ with this function: As a general recommendation, use N=32768, r=8, p=1 +Argon2 +^^^^^^^^^^ + +.. versionadded:: 2.11.0 + +Argon2 is the winner of the PHC (Password Hashing Competition) and +provides a tunable memory hard PBKDF. + OpenPGP S2K ^^^^^^^^^^^^ diff --git a/src/cli/argon2.cpp b/src/cli/argon2.cpp new file mode 100644 index 000000000..2b07027c3 --- /dev/null +++ b/src/cli/argon2.cpp @@ -0,0 +1,78 @@ +/* +* (C) 2019 Jack Lloyd +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#include "cli.h" + +#if defined(BOTAN_HAS_ARGON2) + #include <botan/argon2.h> +#endif + +namespace Botan_CLI { + +#if defined(BOTAN_HAS_ARGON2) + +class Generate_Argon2 final : public Command + { + public: + Generate_Argon2() : Command("gen_argon2 --mem=65536 --p=1 --t=1 password") {} + + std::string group() const override + { + return "passhash"; + } + + std::string description() const override + { + return "Calculate Argon2 password hash"; + } + + void go() override + { + const std::string password = get_passphrase_arg("Passphrase to hash", "password"); + const size_t M = get_arg_sz("mem"); + const size_t p = get_arg_sz("p"); + const size_t t = get_arg_sz("t"); + + output() << Botan::argon2_generate_pwhash(password.data(), password.size(), rng(), p, M, t) << "\n"; + } + }; + +BOTAN_REGISTER_COMMAND("gen_argon2", Generate_Argon2); + +class Check_Argon2 final : public Command + { + public: + Check_Argon2() : Command("check_argon2 password hash") {} + + std::string group() const override + { + return "passhash"; + } + + std::string description() const override + { + return "Verify Argon2 password hash"; + } + + void go() override + { + const std::string password = get_passphrase_arg("Password to check", "password"); + const std::string hash = get_arg("hash"); + + const bool ok = Botan::argon2_check_pwhash(password.data(), password.size(), hash); + + output() << "Password is " << (ok ? "valid" : "NOT valid") << std::endl; + + if(ok == false) + set_return_code(1); + } + }; + +BOTAN_REGISTER_COMMAND("check_argon2", Check_Argon2); + +#endif // argon2 + +} diff --git a/src/cli/bcrypt.cpp b/src/cli/bcrypt.cpp index 6d7d7124c..68e77b8e6 100644 --- a/src/cli/bcrypt.cpp +++ b/src/cli/bcrypt.cpp @@ -26,7 +26,7 @@ class Generate_Bcrypt final : public Command std::string description() const override { - return "Calculate the bcrypt password digest of a given file"; + return "Calculate bcrypt password hash"; } void go() override @@ -60,7 +60,7 @@ class Check_Bcrypt final : public Command std::string description() const override { - return "Checks a given bcrypt hash against hash"; + return "Verify bcrypt password hash"; } void go() override diff --git a/src/lib/pbkdf/argon2/argon2.h b/src/lib/pbkdf/argon2/argon2.h index 27a6a3220..e0fe1d83d 100644 --- a/src/lib/pbkdf/argon2/argon2.h +++ b/src/lib/pbkdf/argon2/argon2.h @@ -1,5 +1,5 @@ /** -* (C) 2018 Jack Lloyd +* (C) 2018,2019 Jack Lloyd * * Botan is released under the Simplified BSD License (see license.txt) */ @@ -7,10 +7,70 @@ #ifndef BOTAN_ARGON2_H_ #define BOTAN_ARGON2_H_ -#include <botan/types.h> +#include <botan/pwdhash.h> namespace Botan { +class RandomNumberGenerator; + +/** +* Argon2 key derivation function +*/ +class BOTAN_PUBLIC_API(2,11) Argon2 final : public PasswordHash + { + public: + Argon2(uint8_t family, size_t M, size_t t, size_t p); + + Argon2(const Argon2& other) = default; + Argon2& operator=(const Argon2&) = default; + + /** + * Derive a new key under the current Argon2 parameter set + */ + void derive_key(uint8_t out[], size_t out_len, + const char* password, const size_t password_len, + const uint8_t salt[], size_t salt_len) const override; + + std::string to_string() const override; + + size_t M() const { return m_M; } + size_t t() const { return m_t; } + size_t p() const { return m_p; } + + size_t iterations() const override { return t(); } + + size_t parallelism() const override { return p(); } + + size_t memory_param() const override { return M(); } + + size_t total_memory_usage() const override { return M() * 1024; } + + private: + uint8_t m_family; + size_t m_M, m_t, m_p; + }; + +class BOTAN_PUBLIC_API(2,11) Argon2_Family final : public PasswordHashFamily + { + public: + Argon2_Family(uint8_t family); + + std::string name() const override; + + std::unique_ptr<PasswordHash> tune(size_t output_length, + std::chrono::milliseconds msec, + size_t max_memory) const override; + + std::unique_ptr<PasswordHash> default_params() const override; + + std::unique_ptr<PasswordHash> from_iterations(size_t iter) const override; + + std::unique_ptr<PasswordHash> from_params( + size_t M, size_t t, size_t p) const override; + private: + const uint8_t m_family; + }; + /** * Argon2 key derivation function * @@ -31,6 +91,20 @@ void BOTAN_PUBLIC_API(2,11) argon2(uint8_t output[], size_t output_len, const uint8_t ad[], size_t ad_len, size_t y, size_t p, size_t M, size_t t); +std::string BOTAN_PUBLIC_API(2,11) + argon2_generate_pwhash(const char* password, size_t password_len, + RandomNumberGenerator& rng, + size_t p, size_t M, size_t t, + size_t y = 2, size_t salt_len = 16, size_t output_len = 32); + +/** +* Check a previously created password hash +* @param password the password to check against +* @param hash the stored hash to check against +*/ +bool BOTAN_PUBLIC_API(2,11) argon2_check_pwhash(const char* password, size_t password_len, + const std::string& hash); + } #endif diff --git a/src/lib/pbkdf/argon2/argon2fmt.cpp b/src/lib/pbkdf/argon2/argon2fmt.cpp new file mode 100644 index 000000000..f64cd2bbd --- /dev/null +++ b/src/lib/pbkdf/argon2/argon2fmt.cpp @@ -0,0 +1,125 @@ +/** +* (C) 2019 Jack Lloyd +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#include <botan/argon2.h> +#include <botan/rng.h> +#include <botan/base64.h> +#include <botan/parsing.h> +#include <sstream> + +namespace Botan { + +namespace { + +std::string strip_padding(std::string s) + { + while(s.size() > 0 && s[s.size()-1] == '=') + s.resize(s.size() - 1); + return s; + } + +} + +std::string argon2_generate_pwhash(const char* password, size_t password_len, + RandomNumberGenerator& rng, + size_t p, size_t M, size_t t, + size_t y, size_t salt_len, size_t output_len) + { + std::vector<uint8_t> salt(salt_len); + rng.randomize(salt.data(), salt.size()); + + std::vector<uint8_t> output(output_len); + argon2(output.data(), output.size(), + password, password_len, + salt.data(), salt.size(), + nullptr, 0, + nullptr, 0, + y, p, M, t); + + std::ostringstream oss; + + if(y == 0) + oss << "$argon2d$"; + else if(y == 1) + oss << "$argon2i$"; + else + oss << "$argon2id$"; + + oss << "v=19$m=" << M << ",t=" << t << ",p=" << p << "$"; + oss << strip_padding(base64_encode(salt)) << "$" << strip_padding(base64_encode(output)); + + return oss.str(); + } + +bool argon2_check_pwhash(const char* password, size_t password_len, + const std::string& input_hash) + { + const std::vector<std::string> parts = split_on(input_hash, '$'); + + if(parts.size() != 5) + return false; + + size_t family = 0; + + if(parts[0] == "argon2d") + family = 0; + else if(parts[0] == "argon2i") + family = 1; + else if(parts[0] == "argon2id") + family = 2; + else + return false; + + if(parts[1] != "v=19") + return false; + + const std::vector<std::string> params = split_on(parts[2], ','); + + if(params.size() != 3) + return false; + + size_t M = 0, t = 0, p = 0; + + for(auto param_str : params) + { + const std::vector<std::string> param = split_on(param_str, '='); + + if(param.size() != 2) + return false; + + const std::string key = param[0]; + const size_t val = to_u32bit(param[1]); + if(key == "m") + M = val; + else if(key == "t") + t = val; + else if(key == "p") + p = val; + else + return false; + } + + std::vector<uint8_t> salt(base64_decode_max_output(parts[3].size())); + salt.resize(base64_decode(salt.data(), parts[3], false)); + + std::vector<uint8_t> hash(base64_decode_max_output(parts[4].size())); + hash.resize(base64_decode(hash.data(), parts[4], false)); + + if(hash.size() < 4) + return false; + + std::vector<uint8_t> generated(hash.size()); + argon2(generated.data(), generated.size(), + password, password_len, + salt.data(), salt.size(), + nullptr, 0, + nullptr, 0, + family, p, M, t); + + return constant_time_compare(generated.data(), hash.data(), generated.size()); + } + +} diff --git a/src/lib/pbkdf/argon2/argon2pwhash.cpp b/src/lib/pbkdf/argon2/argon2pwhash.cpp new file mode 100644 index 000000000..c31c90878 --- /dev/null +++ b/src/lib/pbkdf/argon2/argon2pwhash.cpp @@ -0,0 +1,151 @@ +/** +* (C) 2019 Jack Lloyd +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#include <botan/argon2.h> +#include <botan/exceptn.h> +#include <botan/internal/timer.h> + +namespace Botan { + +Argon2::Argon2(uint8_t family, size_t M, size_t t, size_t p) : + m_family(family), + m_M(M), + m_t(t), + m_p(p) + {} + +void Argon2::derive_key(uint8_t output[], size_t output_len, + const char* password, const size_t password_len, + const uint8_t salt[], size_t salt_len) const + { + argon2(output, output_len, + password, password_len, + salt, salt_len, + nullptr, 0, + nullptr, 0, + m_family, m_p, m_M, m_t); + } + +namespace { + +std::string argon2_family_name(uint8_t f) + { + switch(f) + { + case 0: + return "Argon2d"; + case 1: + return "Argon2i"; + case 2: + return "Argon2id"; + default: + throw Invalid_Argument("Unknown Argon2 parameter"); + } + } + +} + +std::string Argon2::to_string() const + { + return argon2_family_name(m_family) + "(" + + std::to_string(m_M) + "," + + std::to_string(m_t) + "," + + std::to_string(m_p) + ")"; + } + +Argon2_Family::Argon2_Family(uint8_t family) : m_family(family) + { + if(m_family != 0 && m_family != 1 && m_family != 2) + throw Invalid_Argument("Unknown Argon2 family identifier"); + } + +std::string Argon2_Family::name() const + { + return argon2_family_name(m_family); + } + +std::unique_ptr<PasswordHash> Argon2_Family::tune(size_t /*output_length*/, + std::chrono::milliseconds msec, + size_t max_memory) const + { + const size_t max_kib = (max_memory == 0) ? 256*1024 : max_memory*1024; + + // Tune with a large memory otherwise we measure cache vs RAM speeds and underestimate + // costs for larger params + size_t M = 32*1024; // in KiB + size_t p = 1; + size_t t = 1; + + Timer timer("Argon2"); + const auto tune_time = BOTAN_PBKDF_TUNING_TIME; + + timer.run_until_elapsed(tune_time, [&]() { + uint8_t output[32] = { 0 }; + argon2(output, sizeof(output), "test", 4, nullptr, 0, nullptr, 0, nullptr, 0, m_family, p, M, t); + }); + + if(timer.events() == 0 || timer.value() == 0) + return default_params(); + + const uint64_t measured_time = timer.value() / timer.events(); + + const uint64_t target_nsec = msec.count() * static_cast<uint64_t>(1000000); + + /* + * Argon2 scaling rules: + * k*M, k*t, k*p all increase cost by about k + * + * Since we don't even take advantage of p > 1, we prefer increasing + * t or M instead. + * + * If possible to increase M, prefer that. + */ + + uint64_t est_nsec = measured_time; + + if(est_nsec < target_nsec && M < max_kib) + { + const size_t desired_cost_increase = ((target_nsec + est_nsec - 1) / est_nsec); + const size_t mem_headroom = max_kib / M; + + const size_t M_mult = std::min(desired_cost_increase, mem_headroom); + M *= M_mult; + est_nsec *= M_mult; + } + + if(est_nsec < target_nsec) + { + const size_t desired_cost_increase = ((target_nsec + est_nsec - 1) / est_nsec); + t *= desired_cost_increase; + } + + return this->from_params(M, t, p); + } + +std::unique_ptr<PasswordHash> Argon2_Family::default_params() const + { + return this->from_params(64*1024*1024, 3, 4); + } + +std::unique_ptr<PasswordHash> Argon2_Family::from_iterations(size_t iter) const + { + /* + These choices are arbitrary, but should not change in future + releases since they will break applications expecting deterministic + mapping from iteration count to params + */ + const size_t M = iter; + const size_t t = 3; + const size_t p = 1; + return this->from_params(M, t, p); + } + +std::unique_ptr<PasswordHash> Argon2_Family::from_params(size_t M, size_t t, size_t p) const + { + return std::unique_ptr<PasswordHash>(new Argon2(m_family, M, t, p)); + } + +} diff --git a/src/lib/pbkdf/pwdhash.cpp b/src/lib/pbkdf/pwdhash.cpp index 610ae7ac7..f5ee26c6a 100644 --- a/src/lib/pbkdf/pwdhash.cpp +++ b/src/lib/pbkdf/pwdhash.cpp @@ -20,6 +20,10 @@ #include <botan/scrypt.h> #endif +#if defined(BOTAN_HAS_ARGON2) + #include <botan/argon2.h> +#endif + namespace Botan { std::unique_ptr<PasswordHashFamily> PasswordHashFamily::create(const std::string& algo_spec, @@ -52,6 +56,21 @@ std::unique_ptr<PasswordHashFamily> PasswordHashFamily::create(const std::string } #endif +#if defined(BOTAN_HAS_ARGON2) + if(req.algo_name() == "Argon2d") + { + return std::unique_ptr<PasswordHashFamily>(new Argon2_Family(0)); + } + else if(req.algo_name() == "Argon2i") + { + return std::unique_ptr<PasswordHashFamily>(new Argon2_Family(1)); + } + else if(req.algo_name() == "Argon2id") + { + return std::unique_ptr<PasswordHashFamily>(new Argon2_Family(2)); + } +#endif + #if defined(BOTAN_HAS_PGP_S2K) if(req.algo_name() == "OpenPGP-S2K" && req.arg_count() == 1) { diff --git a/src/tests/data/passhash/argon2.vec b/src/tests/data/passhash/argon2.vec new file mode 100644 index 000000000..6de6724d5 --- /dev/null +++ b/src/tests/data/passhash/argon2.vec @@ -0,0 +1,38 @@ + +[Verify] +Password = pass +Passhash = $argon2i$v=19$m=8,t=1,p=1$YWFhYWFhYWE$3ney028aI7naIJ/5U///1ICfSVF0Ta4jh2SpJ1jhsCE + +Password = pass +Passhash = $argon2d$v=19$m=8,t=1,p=1$YWFhYWFhYWE$0WM+IC/fpCF2boiNXmu0lnBXDAKes/BHiYuq9abKsWQ + +Password = pass +Passhash = $argon2id$v=19$m=8,t=1,p=1$YWFhYWFhYWE$tPAla38/iYe0rtvQKVaPv04WYar67QEGlc4fhxU185s + +[Generate] +Password = pass +Mode = 2 +M = 8 +T = 1 +P = 1 +Salt = 313233343536373839616263646566 +OutLen = 32 +Passhash = $argon2id$v=19$m=8,t=1,p=1$MTIzNDU2Nzg5YWJjZGVm$+iAchMa6urtGUvqS2c2ly5SxSb3Jj9S/nq4SZaIgLaI + +Password = pass +Mode = 0 +M = 8192 +T = 3 +P = 1 +Salt = 313233343536373839616263646566 +OutLen = 32 +Passhash = $argon2d$v=19$m=8192,t=3,p=1$MTIzNDU2Nzg5YWJjZGVm$6C4pewOLgibFqWOo9mKTN2xV8KBRq7wjD8PM7DsoV0k + +Password = pass +Mode = 1 +M = 8192 +T = 3 +P = 1 +Salt = 313233343536373839616263646566 +OutLen = 32 +Passhash = $argon2i$v=19$m=8192,t=3,p=1$MTIzNDU2Nzg5YWJjZGVm$7iO3QHobBZHBgjSM+u92dRHJeKpsMdbZ+sLxPjcm9MI diff --git a/src/tests/test_passhash.cpp b/src/tests/test_passhash.cpp index b6bd268b1..0b39e7ffc 100644 --- a/src/tests/test_passhash.cpp +++ b/src/tests/test_passhash.cpp @@ -14,6 +14,11 @@ #include <botan/passhash9.h> #endif +#if defined(BOTAN_HAS_ARGON2) + #include <botan/argon2.h> + #include "test_rng.h" +#endif + namespace Botan_Tests { namespace { @@ -76,6 +81,54 @@ BOTAN_REGISTER_TEST("bcrypt", Bcrypt_Tests); #endif +#if defined(BOTAN_HAS_ARGON2) +class Argon2_Tests final : public Text_Based_Test + { + public: + Argon2_Tests() : Text_Based_Test("passhash/argon2.vec", "Password,Passhash", "Mode,M,T,P,Salt,OutLen") {} + + Test::Result run_one_test(const std::string& header, const VarMap& vars) override + { + const std::string password = vars.get_req_str("Password"); + const std::string passhash = vars.get_req_str("Passhash"); + + Test::Result result("Argon2 password hash"); + + if(header == "Verify") + { + const bool accepted = Botan::argon2_check_pwhash(password.data(), password.size(), passhash); + result.test_eq("correct hash accepted", accepted, true); + } + else if(header == "Generate") + { + const std::vector<uint8_t> salt = vars.get_req_bin("Salt"); + const size_t y = vars.get_req_sz("Mode"); + const size_t M = vars.get_req_sz("M"); + const size_t t = vars.get_req_sz("T"); + const size_t p = vars.get_req_sz("P"); + const size_t out_len = vars.get_req_sz("OutLen"); + + Fixed_Output_RNG rng(salt); + + const std::string generated = Botan::argon2_generate_pwhash(password.data(), password.size(), + rng, + p, M, t, y, salt.size(), out_len); + + result.test_eq("expected hash generated", generated, passhash); + const bool accepted = Botan::argon2_check_pwhash(password.data(), password.size(), generated); + result.test_eq("generated hash accepted", accepted, true); + } + else + throw Test_Error("Unexpected header in Argon2 password hash test file"); + + return result; + } + }; + +BOTAN_REGISTER_TEST("argon2_pass", Argon2_Tests); + +#endif + #if defined(BOTAN_HAS_PASSHASH9) class Passhash9_Tests final : public Text_Based_Test { |