/* * Copyright (C) 2004-2026 ZNC, see the NOTICE file for details. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #include class UserTest : public ::testing::Test { protected: // A CZNC instance is required to instantiate CUsers UserTest() { CZNC::CreateInstance(); } ~UserTest() { CZNC::DestroyInstance(); } }; TEST_F(UserTest, IsHostAllowed) { struct hostTest { CString sMask; CString sIP; bool bExpectedResult; }; hostTest aHostTests[] = { {"127.0.0.1", "127.0.0.1", true}, {"127.0.0.20", "127.0.0.1", false}, {"127.0.0.20/24", "127.0.0.1", true}, {"127.0.0.20/0", "127.0.0.1", true}, {"127.0.0.20/32", "127.0.0.1", false}, {"127.0.0.1", "127.0.0.0", false}, {"127.0.0.20", "127.0.0.0", false}, {"127.0.0.20/24", "127.0.0.0", true}, {"127.0.0.20/0", "127.0.0.0", true}, {"127.0.0.20/32", "127.0.0.0", false}, {"127.0.0.1", "127.0.0.255", false}, {"127.0.0.20", "127.0.0.255", false}, {"127.0.0.20/24", "127.0.0.255", true}, {"127.0.0.20/0", "127.0.0.255", true}, {"127.0.0.20/32", "127.0.0.255", false}, {"127.0.0.1", "127.0.1.1", false}, {"127.0.0.20", "127.0.1.1", false}, {"127.0.0.20/24", "127.0.1.1", false}, {"127.0.0.20/16", "127.0.1.1", true}, {"127.0.0.20/0", "127.0.1.1", true}, {"127.0.0.20/32", "127.0.1.1", false}, {"127.0.0.1", "0.0.0.0", false}, {"127.0.0.20", "0.0.0.0", false}, {"127.0.0.20/24", "0.0.0.0", false}, {"127.0.0.20/16", "0.0.0.0", false}, {"127.0.0.20/0", "0.0.0.0", true}, {"127.0.0.20/32", "0.0.0.0", false}, {"127.0.0.1", "255.255.255.255", false}, {"127.0.0.20", "255.255.255.255", false}, {"127.0.0.20/24", "255.255.255.255", false}, {"127.0.0.20/16", "255.255.255.255", false}, {"127.0.0.20/0", "255.255.255.255", true}, {"127.0.0.20/32", "255.255.255.255", false}, {"127.0.0.1", "::1", false}, {"::1", "::1", true}, {"::20/120", "::1", true}, {"::20/120", "::ffff", false}, {"::ff/0", "::1", true}, {"::ff/0", "127.0.0.1", false}, {"127.0.0.20/0", "::1", false}, {"127.0.0.1/-1", "127.0.0.1", false}, {"::0/-1", "::0", false}, {"127.0.0.a/0", "127.0.0.1", false}, {"::g/0", "::0", false}, {"127.0.0.1/0", "127.0.0.a", false}, {"::0/0", "::g", false}, {"::0/0/0", "::0", false}, {"2001:db8::/33", "2001:db9:0::", false}, {"2001:db8::/32", "2001:db8:8000::", true}, {"2001:db8::/33", "2001:db8:8000::", false}, {"0.0.0.0/0", "::1", false}, {"0.0.0.0/0", "127.0.0.1", true}, {"::0/0", "127.0.0.1", false}, {"::0/0", "::1", true}, {"0.0.0.0", "127.0.0.1", false}, {"0.0.0.0", "::1", false}, {"::0", "::1", false}, {"::0", "127.0.0.1", false}, {"127.0.0.2/abc", "127.0.0.1", false}, {"::2/abc", "::1", false}, {"127.0.0.1/33", "127.0.0.1", false}, {"::1/129", "::1", false}, {"::2/00000000000", "::1", false}, {"::2/0a", "::1", false}, {"192.168.*", "192.168.0.1", true}, }; for (const hostTest& h : aHostTests) { CUser user("user"); user.AddAllowedHost(h.sMask); EXPECT_EQ(user.IsHostAllowed(h.sIP), h.bExpectedResult) << "Allow-host is " << h.sMask; } } TEST_F(UserTest, TestAuthOnlyViaModule) { CUser user("user"); user.SetPass("password", CUser::HASH_NONE); bool bAuthOnlyViaModuleDefault = CZNC::Get().GetAuthOnlyViaModule(); CZNC::Get().SetAuthOnlyViaModule(false); user.SetAuthOnlyViaModule(false); EXPECT_TRUE(user.CheckPass("password")); // user-level only user.SetAuthOnlyViaModule(true); EXPECT_FALSE(user.CheckPass("password")); // re-enabling built-in authentication user.SetAuthOnlyViaModule(false); EXPECT_TRUE(user.CheckPass("password")); // on at global level, off at user level CZNC::Get().SetAuthOnlyViaModule(true); EXPECT_FALSE(user.CheckPass("password")); // on at both levels user.SetAuthOnlyViaModule(true); EXPECT_FALSE(user.CheckPass("password")); CZNC::Get().SetAuthOnlyViaModule(bAuthOnlyViaModuleDefault); } // Functional regression for the constant-time CheckPass paths (#2011). // We can't measure timing in a unit test, but we verify the boolean // contract for each hash mode: correct password matches, wrong password // (including same-length and length-mismatch variants) does not. TEST_F(UserTest, CheckPassPlain) { CUser user("user"); user.SetPass("plaintext", CUser::HASH_NONE); EXPECT_FALSE(user.CheckPass("")); EXPECT_FALSE(user.CheckPass("plaintex")); // shorter EXPECT_FALSE(user.CheckPass("plaintextX")); // longer EXPECT_FALSE(user.CheckPass("plaintexT")); // case-sensitive EXPECT_FALSE(user.CheckPass("Xlaintext")); // first byte differs EXPECT_FALSE(user.CheckPass("plaintexX")); // last byte differs EXPECT_TRUE(user.CheckPass("plaintext")); } TEST_F(UserTest, CheckPassMD5) { CUser user("user"); CString sSalt = "the-salt"; CString sCorrect = "correct-password"; user.SetPass(CUtils::SaltedMD5Hash(sCorrect, sSalt), CUser::HASH_MD5, sSalt); // Wrong inputs first; the success path may upgrade the stored hash // to argon2id, after which the hash type is no longer MD5. EXPECT_FALSE(user.CheckPass("")); EXPECT_FALSE(user.CheckPass("wrong-password")); EXPECT_FALSE(user.CheckPass("correct-passworX")); // last byte differs EXPECT_FALSE(user.CheckPass("Xorrect-password")); // first byte differs EXPECT_FALSE(user.CheckPass("Correct-password")); // case-sensitive EXPECT_TRUE(user.CheckPass(sCorrect)); } TEST_F(UserTest, CheckPassSHA256) { CUser user("user"); CString sSalt = "another-salt"; CString sCorrect = "another-password"; user.SetPass(CUtils::SaltedSHA256Hash(sCorrect, sSalt), CUser::HASH_SHA256, sSalt); EXPECT_FALSE(user.CheckPass("")); EXPECT_FALSE(user.CheckPass("wrong")); EXPECT_FALSE(user.CheckPass("another-passworX")); EXPECT_FALSE(user.CheckPass("Another-password")); EXPECT_TRUE(user.CheckPass(sCorrect)); }