Merge pull request #1879 from DarthGandalf/argon

Update password hashes from SHA-256 to Argon2id
This commit is contained in:
Alexey Sokolov
2023-09-27 09:25:53 +01:00
committed by GitHub
15 changed files with 183 additions and 40 deletions
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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
+8 -1
View File
@@ -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}")
+2
View File
@@ -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 \
Vendored
+1
View File
@@ -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):
+10 -7
View File
@@ -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();
+6 -5
View File
@@ -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 <Pass> 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 = "",
+8 -1
View File
@@ -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
+1
View File
@@ -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
+3
View File
@@ -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})
+51 -6
View File
@@ -25,6 +25,10 @@
#include <time.h>
#include <algorithm>
#ifdef ZNC_HAVE_ARGON
#include <argon2.h>
#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; }
+42 -7
View File
@@ -52,6 +52,10 @@
#include <unicode/errorcode.h>
#endif
#ifdef ZNC_HAVE_ARGON
#include <argon2.h>
#endif
// Required with GCC 4.3+ if openssl is disabled
#include <cstring>
#include <cstdlib>
@@ -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("<Pass password>");
#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("</Pass>");
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
+2 -7
View File
@@ -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 <User> section of your config with "
"this:");
// Not PrintMessage(), to remove [**] from the beginning, to ease
// copypasting
std::cout << "<Pass password>" << std::endl;
std::cout << "\tMethod = " << CUtils::sDefaultHash << std::endl;
std::cout << "\tHash = " << sHash << std::endl;
std::cout << "\tSalt = " << sSalt << std::endl;
std::cout << "</Pass>" << 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.");
+2 -4
View File
@@ -734,10 +734,8 @@ bool CZNC::WriteNewConfig(const CString& sConfigFile) {
} while (!CUser::IsValidUsername(sUser));
vsLines.push_back("<User " + sUser + ">");
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");
+45
View File
@@ -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"(
<User foo>
<Pass pass>
Method = MD5
Salt = abc
Hash = defdf93cef7fa7a8ee88e65d0e277b99
</Pass>
</User>
)";
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