diff --git a/common/Crypto.cc b/common/Crypto.cc index a7e4dbb..439d85f 100644 --- a/common/Crypto.cc +++ b/common/Crypto.cc @@ -2,6 +2,9 @@ #include +#include + +#include #include #include #include @@ -9,6 +12,39 @@ namespace visione::crypto { +namespace +{ +constexpr int passwordIterations = 210000; +constexpr size_t passwordHashBytes = 32; + +std::string toHex(const unsigned char *data, size_t size) +{ + std::ostringstream stream; + stream << std::hex << std::uppercase << std::setfill('0'); + for (size_t i = 0; i < size; ++i) + { + stream << std::setw(2) << static_cast(data[i]); + } + return stream.str(); +} + +std::string pbkdf2(const std::string &password, + const std::string &salt, + int iterations) +{ + std::array output{}; + PKCS5_PBKDF2_HMAC(password.c_str(), + static_cast(password.size()), + reinterpret_cast(salt.data()), + static_cast(salt.size()), + iterations, + EVP_sha256(), + static_cast(output.size()), + output.data()); + return toHex(output.data(), output.size()); +} +} // namespace + std::string sha256(const std::string &value) { return drogon::utils::getSha256(value); @@ -36,27 +72,53 @@ std::string randomToken() std::string hashPassword(const std::string &password) { - const auto salt = drogon::utils::getUuid(); - return "sha256$" + salt + "$" + sha256(salt + ":" + password); + const auto salt = drogon::utils::getUuid() + drogon::utils::getUuid(); + return "pbkdf2_sha256$" + std::to_string(passwordIterations) + "$" + salt + + "$" + pbkdf2(password, salt, passwordIterations); } bool verifyPassword(const std::string &password, const std::string &hash) { - constexpr std::string_view prefix = "sha256$"; - if (!hash.starts_with(prefix)) + constexpr std::string_view legacyPrefix = "sha256$"; + constexpr std::string_view pbkdf2Prefix = "pbkdf2_sha256$"; + + if (hash.starts_with(legacyPrefix)) + { + const auto saltStart = legacyPrefix.size(); + const auto hashStart = hash.find('$', saltStart); + if (hashStart == std::string::npos) + { + return false; + } + + const auto salt = hash.substr(saltStart, hashStart - saltStart); + const auto expected = + "sha256$" + salt + "$" + sha256(salt + ":" + password); + return expected == hash; + } + + if (!hash.starts_with(pbkdf2Prefix)) { return false; } - const auto saltStart = prefix.size(); - const auto hashStart = hash.find('$', saltStart); - if (hashStart == std::string::npos) + const auto iterationsStart = pbkdf2Prefix.size(); + const auto saltStart = hash.find('$', iterationsStart); + if (saltStart == std::string::npos) + { + return false; + } + const auto digestStart = hash.find('$', saltStart + 1); + if (digestStart == std::string::npos) { return false; } - const auto salt = hash.substr(saltStart, hashStart - saltStart); - const auto expected = "sha256$" + salt + "$" + sha256(salt + ":" + password); + const auto iterations = + std::stoi(hash.substr(iterationsStart, saltStart - iterationsStart)); + const auto salt = hash.substr(saltStart + 1, digestStart - saltStart - 1); + const auto expected = "pbkdf2_sha256$" + std::to_string(iterations) + "$" + + salt + "$" + pbkdf2(password, salt, iterations); return expected == hash; } @@ -65,4 +127,3 @@ std::string hashSecret(const std::string &secret) return sha256("visione:" + secret); } } // namespace visione::crypto - diff --git a/docs/MILESTONE_01.md b/docs/MILESTONE_01.md index 9feff25..2e8febe 100644 --- a/docs/MILESTONE_01.md +++ b/docs/MILESTONE_01.md @@ -10,9 +10,10 @@ - Focused regression tests for technical endpoints and core validation helpers. - Local admin seed: `admin@visione.local` / `ChangeMe123!`, with forced password change flag. - Multipart report photo upload endpoint storing files under `uploads/reports/`. +- PBKDF2-HMAC-SHA256 password hashes for new credentials, with legacy SHA-256 verification kept for existing local rows. ## To do next -- Harden password hashing with Argon2id or bcrypt before production. +- Consider Argon2id/bcrypt and password reset flows before production hardening. - Add real email provider configuration for non-local environments. - Add CIE/SPID integration design and provider adapters. - Expand frontend-driven API coverage as Vue screens are implemented. diff --git a/migrations/003_admin_pbkdf2_password.sql b/migrations/003_admin_pbkdf2_password.sql new file mode 100644 index 0000000..2b7dd9b --- /dev/null +++ b/migrations/003_admin_pbkdf2_password.sql @@ -0,0 +1,5 @@ +UPDATE users +SET password_hash = 'pbkdf2_sha256$210000$local-dev-admin-salt$A4683BDB63C1598400B6B196F53C830F8434BA645C4A4A70AB311B278223E698', + must_change_password = TRUE +WHERE email = 'admin@visione.local'; +