| /** |
| * Copyright © 2017 IBM Corporation |
| * |
| * 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 <cstring> |
| #include <unistd.h> |
| #include <sys/types.h> |
| #include <sys/stat.h> |
| #include <shadow.h> |
| #include <array> |
| #include <random> |
| #include <errno.h> |
| #include <xyz/openbmc_project/Common/error.hpp> |
| #include <phosphor-logging/log.hpp> |
| #include <phosphor-logging/elog.hpp> |
| #include <phosphor-logging/elog-errors.hpp> |
| #include "user.hpp" |
| #include "file.hpp" |
| #include "shadowlock.hpp" |
| #include "config.h" |
| namespace phosphor |
| { |
| namespace user |
| { |
| |
| constexpr auto SHADOW_FILE = "/etc/shadow"; |
| |
| // See crypt(3) |
| constexpr int SALT_LENGTH = 16; |
| |
| using namespace phosphor::logging; |
| using InsufficientPermission = |
| sdbusplus::xyz::openbmc_project::Common::Error::InsufficientPermission; |
| using InternalFailure = |
| sdbusplus::xyz::openbmc_project::Common::Error::InternalFailure; |
| // Sets or updates the password |
| void User::setPassword(std::string newPassword) |
| { |
| // Gate any access to /etc/shadow |
| phosphor::user::shadow::Lock lock(); |
| |
| // rewind to the start of shadow entry |
| setspent(); |
| |
| // Generate a random string from set [A-Za-z0-9./] |
| std::string salt{}; |
| salt.resize(SALT_LENGTH); |
| salt = randomString(SALT_LENGTH); |
| |
| // Apply the change. Updates could be directly made to shadow |
| // but then any update must be contained within the boundary |
| // of that user, else it would run into next entry and thus |
| // corrupting it. Classic example is when a password is set on |
| // a user ID that does not have a prior password |
| applyPassword(SHADOW_FILE, newPassword, salt); |
| return; |
| } |
| |
| void User::applyPassword(const std::string& shadowFile, |
| const std::string& password, const std::string& salt) |
| { |
| // Needed by getspnam_r |
| struct spwd shdp; |
| struct spwd* pshdp; |
| |
| // This should be fine even if SHA512 is used. |
| std::array<char, 1024> buffer{}; |
| |
| // Open the shadow file for reading |
| phosphor::user::File shadow(shadowFile, "r"); |
| if ((shadow)() == NULL) |
| { |
| return raiseException(errno, "Error opening shadow file"); |
| } |
| |
| // open temp shadow file, by suffixing random name in shadow file name. |
| std::vector<char> tempFileName(shadowFile.begin(), shadowFile.end()); |
| std::vector<char> fileTemplate = {'_', '_', 'X', 'X', 'X', |
| 'X', 'X', 'X', '\0'}; |
| tempFileName.insert(tempFileName.end(), fileTemplate.begin(), |
| fileTemplate.end()); |
| |
| int fd = mkstemp(tempFileName.data()); |
| if (fd == -1) |
| { |
| return raiseException(errno, "Error creating temp shadow file"); |
| } |
| |
| std::string strTempFileName(tempFileName.data()); |
| // Open the temp shadow file for writing from provided fd |
| // By "true", remove it at exit if still there. |
| // This is needed to cleanup the temp file at exception |
| phosphor::user::File temp(fd, strTempFileName, "w", true); |
| if ((temp)() == NULL) |
| { |
| close(fd); |
| return raiseException(errno, "Error opening temp shadow file"); |
| } |
| fd = -1; // don't use fd anymore, as the File object owns it |
| |
| // Change the permission of this new temp file |
| // to be same as shadow so that it's secure |
| struct stat st |
| { |
| }; |
| auto r = fstat(fileno((shadow)()), &st); |
| if (r < 0) |
| { |
| return raiseException(errno, "Error reading shadow file mode"); |
| } |
| |
| r = fchmod(fileno((temp)()), st.st_mode); |
| if (r < 0) |
| { |
| return raiseException(errno, "Error setting temp file mode"); |
| } |
| |
| // Read shadow file and process |
| while (true) |
| { |
| auto r = fgetspent_r((shadow)(), &shdp, buffer.data(), |
| buffer.max_size(), &pshdp); |
| if (r) |
| { |
| if (errno == EACCES || errno == ERANGE) |
| { |
| return raiseException(errno, "Error reading shadow file"); |
| } |
| else |
| { |
| // Seem to have run over all |
| break; |
| } |
| } |
| |
| // Hash of password if the user matches |
| std::string hash{}; |
| |
| // Matched user |
| if (user == shdp.sp_namp) |
| { |
| // Update with new hashed password |
| hash = hashPassword(shdp.sp_pwdp, password, salt); |
| shdp.sp_pwdp = const_cast<char*>(hash.c_str()); |
| } |
| |
| // Apply |
| r = putspent(&shdp, (temp)()); |
| if (r < 0) |
| { |
| return raiseException(errno, "Error updating temp shadow file"); |
| } |
| } // All entries |
| |
| // Done |
| endspent(); |
| // flush contents to file first, before renaming to avoid |
| // corruption during power failure |
| fflush((temp)()); |
| |
| // Everything must be fine at this point |
| fs::rename(strTempFileName, shadowFile); |
| return; |
| } |
| |
| void User::raiseException(int errNo, const std::string& errMsg) |
| { |
| using namespace std::string_literals; |
| if (errNo == EACCES) |
| { |
| auto message = "Access denied "s + errMsg; |
| log<level::ERR>(message.c_str()); |
| elog<InsufficientPermission>(); |
| } |
| else |
| { |
| log<level::ERR>(errMsg.c_str(), entry("USER=%s", user.c_str()), |
| entry("ERRNO=%d", errNo)); |
| elog<InternalFailure>(); |
| } |
| } |
| |
| std::string User::hashPassword(char* spPwdp, const std::string& password, |
| const std::string& salt) |
| { |
| // Parse and get crypt algo |
| auto cryptAlgo = getCryptField(spPwdp); |
| if (cryptAlgo.empty()) |
| { |
| log<level::ERR>("Error finding crypt algo", |
| entry("USER=%s", user.c_str())); |
| elog<InternalFailure>(); |
| } |
| |
| // Update shadow password pointer with hash |
| auto saltString = getSaltString(cryptAlgo, salt); |
| return generateHash(password, saltString); |
| } |
| |
| // Returns a random string in set [A-Za-z0-9./] |
| // of size numChars |
| const std::string User::randomString(int length) |
| { |
| // Populated random string |
| std::string random{}; |
| |
| // Needed per crypt(3) |
| std::string set = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijk" |
| "lmnopqrstuvwxyz0123456789./"; |
| |
| // Will be used to obtain a seed for the random number engine |
| std::random_device rd; |
| |
| // Standard mersenne_twister_engine seeded with rd() |
| std::mt19937 gen(rd()); |
| |
| std::uniform_int_distribution<> dis(0, set.size() - 1); |
| for (int count = 0; count < length; count++) |
| { |
| // Use dis to transform the random unsigned int generated by |
| // gen into a int in [1, SALT_LENGTH] |
| random.push_back(set.at(dis(gen))); |
| } |
| return random; |
| } |
| |
| // Extract crypto algorithm field |
| CryptAlgo User::getCryptField(char* spPwdp) |
| { |
| char* savePtr{}; |
| if (std::string{spPwdp}.front() != '$') |
| { |
| return DEFAULT_CRYPT_ALGO; |
| } |
| return strtok_r(spPwdp, "$", &savePtr); |
| } |
| |
| // Returns specific format of salt string |
| std::string User::getSaltString(const std::string& crypt, |
| const std::string& salt) |
| { |
| return '$' + crypt + '$' + salt + '$'; |
| } |
| |
| // Given a password and salt, generates hash |
| std::string User::generateHash(const std::string& password, |
| const std::string& salt) |
| { |
| return crypt(password.c_str(), salt.c_str()); |
| } |
| |
| } // namespace user |
| } // namespace phosphor |