diff --git a/.appveyor.yml b/.appveyor.yml index 3fabe3e4..d5bcd2e9 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -7,7 +7,7 @@ clone_depth: 10 install: - ps: Invoke-WebRequest https://cygwin.com/setup-x86_64.exe -OutFile c:\cygwin-setup.exe # libcrypt-devel is needed only on x86_64 and only for modperl... probably some dependency problem. - - c:\cygwin-setup.exe --quiet-mode --no-shortcuts --no-startmenu --no-desktop --upgrade-also --only-site --site http://cygwin.mirror.constant.com/ --root c:\cygwin-root --local-package-dir c:\cygwin-setup-cache --packages gcc-g++,make,pkg-config,wget,libssl-devel,libicu-devel,zlib-devel,libcrypt-devel,perl,python3-devel,swig,libsasl2-devel,libQt5Core-devel,cmake,libboost-devel,gettext-devel + - c:\cygwin-setup.exe --quiet-mode --no-shortcuts --no-startmenu --no-desktop --upgrade-also --only-site --site http://cygwin.mirror.constant.com/ --root c:\cygwin-root --local-package-dir c:\cygwin-setup-cache --packages gcc-g++,make,pkg-config,wget,libssl-devel,libicu-devel,zlib-devel,libcrypt-devel,perl,python3-devel,swig,libsasl2-devel,libQt5Core-devel,cmake,libboost-devel,gettext-devel,libargon2-devel - c:\cygwin-root\bin\sh -lc "echo Hi" - c:\cygwin-root\bin\sh -lc "uname -a" - c:\cygwin-root\bin\sh -lc "cat /proc/cpuinfo" diff --git a/.github/ubuntu_deps.sh b/.github/ubuntu_deps.sh index 30684cd8..2e6d12b2 100644 --- a/.github/ubuntu_deps.sh +++ b/.github/ubuntu_deps.sh @@ -1,4 +1,4 @@ sudo apt-get update -sudo apt-get install -y tcl-dev libsasl2-dev libicu-dev swig qtbase5-dev libboost-locale-dev libperl-dev cpanminus gettext clang llvm lcov +sudo apt-get install -y tcl-dev libsasl2-dev libicu-dev swig qtbase5-dev libboost-locale-dev libperl-dev libargon2-dev cpanminus gettext clang llvm lcov sudo apt-get upgrade -y diff --git a/CMakeLists.txt b/CMakeLists.txt index be6f93f0..1e76842c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,7 +14,7 @@ # limitations under the License. # -cmake_minimum_required(VERSION 3.1) +cmake_minimum_required(VERSION 3.13) project(ZNC VERSION 1.9.0 LANGUAGES CXX) set(ZNC_VERSION 1.9.x) set(append_git_version true) @@ -135,6 +135,12 @@ if(WANT_CYRUS) endif() endif() +tristate_option(ARGON "Store password hashes using Argon2id instead of SHA-256") +if(WANT_ARGON) + pkg_check_modules(ARGON ${TRISTATE_ARGON_REQUIRED} IMPORTED_TARGET libargon2) +endif() +set(ZNC_HAVE_ARGON "${ARGON_FOUND}") + tristate_option(ICU "Support character encodings") if(WANT_ICU) pkg_check_modules(ICU ${TRISTATE_ICU_REQUIRED} icu-uc) @@ -447,6 +453,7 @@ summary_line("Cyrus " "${CYRUS_FOUND}") summary_line("Charset " "${ICU_FOUND}") summary_line("Zlib " "${ZLIB_FOUND}") summary_line("i18n " "${HAVE_I18N}") +summary_line("Argon2 " "${ZNC_HAVE_ARGON}") include(render_framed_multiline) render_framed_multiline("${summary_lines}") diff --git a/Dockerfile b/Dockerfile index d13c57fe..798f1062 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,6 +15,7 @@ RUN set -x \ && adduser -S znc \ && addgroup -S znc RUN apk add --no-cache \ + argon2-libs \ boost \ build-base \ ca-certificates \ @@ -30,6 +31,7 @@ RUN apk add --no-cache \ tini \ tzdata RUN apk add --no-cache --virtual build-dependencies \ + argon2-dev \ boost-dev \ cyrus-sasl-dev \ perl-dev \ diff --git a/configure b/configure index 2ba1f2c9..c20e8c1e 100755 --- a/configure +++ b/configure @@ -83,6 +83,7 @@ tristate('cyrus') tristate('charset', 'ICU') tristate('tcl') tristate('i18n') +tristate('argon') class HandlePython(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): diff --git a/include/znc/User.h b/include/znc/User.h index fddd73e9..17f735c2 100644 --- a/include/znc/User.h +++ b/include/znc/User.h @@ -45,24 +45,27 @@ class CUser : private CCoreTranslationMixin { bool ParseConfig(CConfig* Config, CString& sError); - // TODO refactor this enum eHashType { HASH_NONE, HASH_MD5, HASH_SHA256, + HASH_ARGON2ID, - HASH_DEFAULT = HASH_SHA256 + // This should be kept in sync with CUtils::SaltedHash +#if ZNC_HAVE_ARGON + HASH_DEFAULT = HASH_ARGON2ID, +#else + HASH_DEFAULT = HASH_SHA256, +#endif }; - // If you change the default hash here and in HASH_DEFAULT, - // don't forget CUtils::sDefaultHash! - // TODO refactor this static CString SaltedHash(const CString& sPass, const CString& sSalt) { - return CUtils::SaltedSHA256Hash(sPass, sSalt); + return CUtils::SaltedHash(sPass, sSalt); } CConfig ToConfig() const; - bool CheckPass(const CString& sPass) const; + /** Checks password, may upgrade the hash method. */ + bool CheckPass(const CString& sPass); bool AddAllowedHost(const CString& sHostMask); bool RemAllowedHost(const CString& sHostMask); void ClearAllowedHosts(); diff --git a/include/znc/Utils.h b/include/znc/Utils.h index b4047195..c549dce6 100644 --- a/include/znc/Utils.h +++ b/include/znc/Utils.h @@ -51,15 +51,16 @@ class CUtils { static void PrintAction(const CString& sMessage); static void PrintStatus(bool bSuccess, const CString& sMessage = ""); -#ifndef SWIGPERL - // TODO refactor this - static const CString sDefaultHash; -#endif + /** Asks password from stdin, with confirmation. + * + * @returns Piece of znc.conf with block + * */ + static CString AskSaltedHashPassForConfig(); - static CString GetSaltedHashPass(CString& sSalt); static CString GetSalt(); static CString SaltedMD5Hash(const CString& sPass, const CString& sSalt); static CString SaltedSHA256Hash(const CString& sPass, const CString& sSalt); + static CString SaltedHash(const CString& sPass, const CString& sSalt); static CString GetPass(const CString& sPrompt); static bool GetInput(const CString& sPrompt, CString& sRet, const CString& sDefault = "", diff --git a/include/znc/version.h b/include/znc/version.h index 8cb9479a..191cf0b5 100644 --- a/include/znc/version.h +++ b/include/znc/version.h @@ -57,9 +57,16 @@ extern const char* ZNC_VERSION_EXTRA; #define ZNC_VERSION_TEXT_I18N "no" #endif +// This is only here because HASH_DEFAULT has different value +#ifdef ZNC_HAVE_ARGON +#define ZNC_VERSION_TEXT_ARGON "yes" +#else +#define ZNC_VERSION_TEXT_ARGON "no" +#endif + #define ZNC_COMPILE_OPTIONS_STRING \ "IPv6: " ZNC_VERSION_TEXT_IPV6 ", SSL: " ZNC_VERSION_TEXT_SSL \ ", DNS: " ZNC_VERSION_TEXT_DNS ", charset: " ZNC_VERSION_TEXT_ICU \ - ", i18n: " ZNC_VERSION_TEXT_I18N + ", i18n: " ZNC_VERSION_TEXT_I18N ", Argon2: " ZNC_VERSION_TEXT_ARGON #endif // !ZNC_VERSION_H diff --git a/include/znc/zncconfig.h.cmake.in b/include/znc/zncconfig.h.cmake.in index d2097502..42fc50a3 100644 --- a/include/znc/zncconfig.h.cmake.in +++ b/include/znc/zncconfig.h.cmake.in @@ -35,6 +35,7 @@ #cmakedefine HAVE_IPV6 1 #cmakedefine HAVE_ZLIB 1 #cmakedefine HAVE_I18N 1 +#cmakedefine ZNC_HAVE_ARGON 1 #cmakedefine CSOCK_USE_POLL 1 #cmakedefine HAVE_GETOPT_LONG 1 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c10767d2..c3eb3008 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -82,6 +82,9 @@ if(Boost_FOUND) target_link_libraries(znclib PRIVATE ${Boost_LIBRARIES}) list(APPEND znc_include_dirs ${Boost_INCLUDE_DIRS}) endif() +if(ZNC_HAVE_ARGON) + target_link_libraries(znclib PRIVATE PkgConfig::ARGON) +endif() target_link_libraries(znclib PRIVATE cctz::cctz) target_include_directories(znc PUBLIC ${znc_include_dirs}) target_include_directories(znclib PUBLIC ${znc_include_dirs}) diff --git a/src/User.cpp b/src/User.cpp index 5a240f97..00b43283 100644 --- a/src/User.cpp +++ b/src/User.cpp @@ -25,6 +25,10 @@ #include #include +#ifdef ZNC_HAVE_ARGON +#include +#endif + using std::vector; using std::set; @@ -346,7 +350,12 @@ bool CUser::ParseConfig(CConfig* pConfig, CString& sError) { method = CUser::HASH_MD5; else if (sMethod.Equals("sha256")) method = CUser::HASH_SHA256; - else { + else if (sMethod.Equals("Argon2id")) { + method = CUser::HASH_ARGON2ID; +#ifndef ZNC_HAVE_ARGON + CUtils::PrintError("ZNC is built without Argon2 support, " + GetUsername() + " won't be able to authenticate"); +#endif + } else { sError = "Invalid hash method"; CUtils::PrintError(sError); return false; @@ -958,6 +967,9 @@ CConfig CUser::ToConfig() const { case HASH_SHA256: sHash = "SHA256"; break; + case HASH_ARGON2ID: + sHash = "Argon2id"; + break; } passConfig.AddKeyValuePair("Salt", m_sPassSalt); passConfig.AddKeyValuePair("Method", sHash); @@ -1042,20 +1054,43 @@ CConfig CUser::ToConfig() const { return config; } -bool CUser::CheckPass(const CString& sPass) const { +bool CUser::CheckPass(const CString& sPass) { if(AuthOnlyViaModule() || CZNC::Get().GetAuthOnlyViaModule()) { return false; } + bool bResult = false; + bool bUpgrade = false; switch (m_eHashType) { case HASH_MD5: - return m_sPass.Equals(CUtils::SaltedMD5Hash(sPass, m_sPassSalt)); + bResult = m_sPass.Equals(CUtils::SaltedMD5Hash(sPass, m_sPassSalt)); + bUpgrade = true; + break; case HASH_SHA256: - return m_sPass.Equals(CUtils::SaltedSHA256Hash(sPass, m_sPassSalt)); + bResult = m_sPass.Equals(CUtils::SaltedSHA256Hash(sPass, m_sPassSalt)); +#if ZNC_HAVE_ARGON + bUpgrade = true; +#endif + break; + case HASH_ARGON2ID: +#if ZNC_HAVE_ARGON + return argon2id_verify(m_sPass.c_str(), sPass.data(), sPass.length()) == ARGON2_OK; +#else + CUtils::PrintError("ZNC is built without Argon2 support, " + GetUsername() + " cannot authenticate"); + return false; +#endif case HASH_NONE: - default: + // Don't upgrade hash, since the only valid use case for plain are + // manual tests, where it's simpler this way return (sPass == m_sPass); } + + if (bResult && bUpgrade) { + CString sSalt = CUtils::GetSalt(); + CString sHash = CUser::SaltedHash(sPass, sSalt); + SetPass(sHash, CUser::HASH_DEFAULT, sSalt); + } + return bResult; } /*CClient* CUser::GetClient() { @@ -1271,7 +1306,17 @@ void CUser::SetDCCBindHost(const CString& s) { m_sDCCBindHost = s; } void CUser::SetPass(const CString& s, eHashType eHash, const CString& sSalt) { m_sPass = s; m_eHashType = eHash; - m_sPassSalt = sSalt; + switch (eHash) { + case HASH_NONE: + case HASH_ARGON2ID: + // Salt is embedded in the encoded "hash" in argon + m_sPassSalt = ""; + break; + case HASH_MD5: + case HASH_SHA256: + m_sPassSalt = sSalt; + break; + } } void CUser::SetMultiClients(bool b) { m_bMultiClients = b; } void CUser::SetDenyLoadMod(bool b) { m_bDenyLoadMod = b; } diff --git a/src/Utils.cpp b/src/Utils.cpp index c10dfa60..c8cebc79 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -52,6 +52,10 @@ #include #endif +#ifdef ZNC_HAVE_ARGON +#include +#endif + // Required with GCC 4.3+ if openssl is disabled #include #include @@ -176,12 +180,24 @@ unsigned long CUtils::GetLongIP(const CString& sIP) { return ret; } -// If you change this here and in GetSaltedHashPass(), -// don't forget CUser::HASH_DEFAULT! -// TODO refactor this -const CString CUtils::sDefaultHash = "sha256"; -CString CUtils::GetSaltedHashPass(CString& sSalt) { - sSalt = GetSalt(); +#ifdef ZNC_HAVE_ARGON +static CString SaltedArgonHash(const CString& sPass, const CString& sSalt) { +#define ZNC_ARGON_PARAMS /* iterations */ 6, /* memory */ 6144, /* parallelism */ 1 + constexpr int iHashLen = 32; + CString sOut; + sOut.resize(argon2_encodedlen(ZNC_ARGON_PARAMS, sSalt.length(), iHashLen, Argon2_id) + 1); + int err = argon2id_hash_encoded(ZNC_ARGON_PARAMS, sPass.data(), sPass.length(), sSalt.data(), sSalt.length(), iHashLen, &sOut[0], sOut.length()); + if (err) { + CUtils::PrintError(argon2_error_message(err)); + sOut.clear(); + } + return sOut; +} +#undef ZNC_ARGON_PARAMS +#endif + +CString CUtils::AskSaltedHashPassForConfig() { + CString sSalt = GetSalt(); while (true) { CString pass1; @@ -195,7 +211,18 @@ CString CUtils::GetSaltedHashPass(CString& sSalt) { CUtils::PrintError("The supplied passwords did not match"); } else { // Construct the salted pass - return SaltedSHA256Hash(pass1, sSalt); + VCString vsLines; + vsLines.push_back(""); +#if ZNC_HAVE_ARGON + vsLines.push_back("\tMethod = Argon2id"); + vsLines.push_back("\tHash = " + SaltedArgonHash(pass1, sSalt)); +#else + vsLines.push_back("\tMethod = SHA256"); + vsLines.push_back("\tHash = " + SaltedSHA256Hash(pass1, sSalt)); + vsLines.push_back("\tSalt = " + sSalt); +#endif + vsLines.push_back(""); + return CString("\n").Join(vsLines.begin(), vsLines.end()); } } } @@ -210,6 +237,14 @@ CString CUtils::SaltedSHA256Hash(const CString& sPass, const CString& sSalt) { return CString(sPass + sSalt).SHA256(); } +CString CUtils::SaltedHash(const CString& sPass, const CString& sSalt) { +#ifdef ZNC_HAVE_ARGON + return SaltedArgonHash(sPass, sSalt); +#else + return SaltedSHA256Hash(sPass, sSalt); +#endif +} + CString CUtils::GetPass(const CString& sPrompt) { #ifdef HAVE_TCSETATTR // Disable echo diff --git a/src/main.cpp b/src/main.cpp index d8955c33..b76ce42a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -383,20 +383,15 @@ int main(int argc, char** argv) { #endif /* HAVE_LIBSSL */ if (bMakePass) { - CString sSalt; CUtils::PrintMessage("Type your new password."); - CString sHash = CUtils::GetSaltedHashPass(sSalt); + CString sPass = CUtils::AskSaltedHashPassForConfig(); CUtils::PrintMessage("Kill ZNC process, if it's running."); CUtils::PrintMessage( "Then replace password in the section of your config with " "this:"); // Not PrintMessage(), to remove [**] from the beginning, to ease // copypasting - std::cout << "" << std::endl; - std::cout << "\tMethod = " << CUtils::sDefaultHash << std::endl; - std::cout << "\tHash = " << sHash << std::endl; - std::cout << "\tSalt = " << sSalt << std::endl; - std::cout << "" << std::endl; + std::cout << sPass << std::endl; CUtils::PrintMessage( "After that start ZNC again, and you should be able to login with " "the new password."); diff --git a/src/znc.cpp b/src/znc.cpp index 89dd30ed..b19d7d7a 100644 --- a/src/znc.cpp +++ b/src/znc.cpp @@ -734,10 +734,8 @@ bool CZNC::WriteNewConfig(const CString& sConfigFile) { } while (!CUser::IsValidUsername(sUser)); vsLines.push_back(""); - CString sSalt; - sAnswer = CUtils::GetSaltedHashPass(sSalt); - vsLines.push_back("\tPass = " + CUtils::sDefaultHash + "#" + sAnswer + - "#" + sSalt + "#"); + sAnswer = CUtils::AskSaltedHashPassForConfig(); + vsLines.push_back(sAnswer); vsLines.push_back("\tAdmin = true"); diff --git a/test/integration/tests/core.cpp b/test/integration/tests/core.cpp index 95a7490d..602b995c 100644 --- a/test/integration/tests/core.cpp +++ b/test/integration/tests/core.cpp @@ -19,6 +19,7 @@ #include "znctest.h" using testing::HasSubstr; +using testing::ContainsRegex; namespace znc_inttest { namespace { @@ -430,5 +431,49 @@ TEST_F(ZNCTest, DenyOptions) { client2.ReadUntil("Access denied!"); } +TEST_F(ZNCTest, HashUpgrade) { + QFile conf(m_dir.path() + "/configs/znc.conf"); + ASSERT_TRUE(conf.open(QIODevice::Append | QIODevice::Text)); + QTextStream out(&conf); + out << R"( + + + Method = MD5 + Salt = abc + Hash = defdf93cef7fa7a8ee88e65d0e277b99 + + + )"; + out.flush(); + conf.close(); + auto znc = Run(); + auto ircd = ConnectIRCd(); + + auto client = ConnectClient(); + client.Write("PASS :hunter2"); + client.Write("NICK nick"); + client.Write("USER foo x x :x"); + client.ReadUntil("Welcome"); + client.Close(); + + client = LoginClient(); + client.Write("znc saveconfig"); + client.ReadUntil("Wrote config"); + + ASSERT_TRUE(conf.open(QIODevice::ReadOnly | QIODevice::Text)); + QTextStream in(&conf); + QString config = in.readAll(); + // It was upgraded to either Argon2 or SHA256 + EXPECT_THAT(config.toStdString(), Not(ContainsRegex("Method.*MD5"))); + + // Check that still can login after the upgrade + client = ConnectClient(); + client.Write("PASS :hunter2"); + client.Write("NICK nick"); + client.Write("USER foo x x :x"); + client.ReadUntil("Welcome"); + client.Close(); +} + } // namespace } // namespace znc_inttest