MFA feature: Enable google authenticator

Enabling multi-factor authentication for BMC. This feature enables
google authenticator using TOTP method.
This commit implements interface published [here][1]
and [here][2]

The implementation supports features such as create secret key,verify
TOTP token, enable system level MFA, and enable bypass options.

Currently the support is only for GoogleAuthenticator.

[1]: https://github.com/openbmc/phosphor-dbus-interfaces/blob/master/yaml/xyz/openbmc_project/User/MultiFactorAuthConfiguration.interface.yaml

[2]: https://github.com/openbmc/phosphor-dbus-interfaces/blob/master/yaml/xyz/openbmc_project/User/TOTPAuthenticator.interface.yaml

Tested By:
Unit test
https://gerrit.openbmc.org/c/openbmc/phosphor-user-manager/+/78583/1

Change-Id: I053095763c65963ff865b487ab08f05039d2fc3a
Signed-off-by: Abhilash Raju <abhilash.kollam@gmail.com>
diff --git a/meson.build b/meson.build
index cbe0e81..6bbea64 100644
--- a/meson.build
+++ b/meson.build
@@ -103,11 +103,13 @@
     assert(cereal_proj.found(), 'cereal is required')
     cereal_dep = cereal_proj.dependency('cereal')
 endif
-
+pam_dep = dependency('pam')
 user_manager_src = ['mainapp.cpp', 'user_mgr.cpp', 'users.cpp']
 
+
 user_manager_deps = [
     boost_dep,
+    pam_dep,
     sdbusplus_dep,
     phosphor_logging_dep,
     phosphor_dbus_interfaces_dep,
@@ -150,6 +152,8 @@
     install_dir: get_option('datadir') / 'phosphor-certificate-manager',
 )
 
+install_data('mfa_pam', install_dir: '/etc/pam.d/')
+
 # Figure out how to use install_symlink to install symlink to a file of another
 # recipe
 #install_symlink(
diff --git a/mfa_pam b/mfa_pam
new file mode 100644
index 0000000..980c5b9
--- /dev/null
+++ b/mfa_pam
@@ -0,0 +1,4 @@
+# /etc/pam.d/mfa
+
+# Google Authenticator for two-factor authentication.
+auth       required     pam_google_authenticator.so secret=/home/${USER}/.config/phosphor-user-manager/.google_authenticator.tmp
\ No newline at end of file
diff --git a/totp.hpp b/totp.hpp
new file mode 100644
index 0000000..12cb925
--- /dev/null
+++ b/totp.hpp
@@ -0,0 +1,162 @@
+#pragma once
+
+#include "config.h"
+
+#include <security/pam_appl.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <phosphor-logging/elog-errors.hpp>
+#include <phosphor-logging/elog.hpp>
+#include <phosphor-logging/lg2.hpp>
+
+#include <cstring>
+#include <memory>
+#include <span>
+#include <string_view>
+
+struct PasswordData
+{
+    struct Response
+    {
+        std::string_view prompt;
+        std::string value;
+    };
+
+    std::vector<Response> responseData;
+
+    int addPrompt(std::string_view prompt, std::string_view value)
+    {
+        if (value.size() + 1 > PAM_MAX_MSG_SIZE)
+        {
+            lg2::error("value length error{PROMPT}", "PROMPT", prompt);
+            return PAM_CONV_ERR;
+        }
+        responseData.emplace_back(prompt, std::string(value));
+        return PAM_SUCCESS;
+    }
+
+    int makeResponse(const pam_message& msg, pam_response& response)
+    {
+        switch (msg.msg_style)
+        {
+            case PAM_PROMPT_ECHO_ON:
+                break;
+            case PAM_PROMPT_ECHO_OFF:
+            {
+                std::string prompt(msg.msg);
+                auto iter = std::ranges::find_if(
+                    responseData, [&prompt](const Response& data) {
+                        return prompt.starts_with(data.prompt);
+                    });
+                if (iter == responseData.end())
+                {
+                    return PAM_CONV_ERR;
+                }
+                response.resp = strdup(iter->value.c_str());
+                return PAM_SUCCESS;
+            }
+            break;
+            case PAM_ERROR_MSG:
+            {
+                lg2::error("Pam error {MSG}", "MSG", msg.msg);
+            }
+            break;
+            case PAM_TEXT_INFO:
+            {
+                lg2::error("Pam info {MSG}", "MSG", msg.msg);
+            }
+            break;
+            default:
+            {
+                return PAM_CONV_ERR;
+            }
+        }
+        return PAM_SUCCESS;
+    }
+};
+
+// function used to get user input
+inline int pamFunctionConversation(int numMsg, const struct pam_message** msgs,
+                                   struct pam_response** resp, void* appdataPtr)
+{
+    if ((appdataPtr == nullptr) || (msgs == nullptr) || (resp == nullptr))
+    {
+        return PAM_CONV_ERR;
+    }
+
+    if (numMsg <= 0 || numMsg >= PAM_MAX_NUM_MSG)
+    {
+        return PAM_CONV_ERR;
+    }
+    // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
+    PasswordData* appPass = reinterpret_cast<PasswordData*>(appdataPtr);
+    auto msgCount = static_cast<size_t>(numMsg);
+    // NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays)
+    // auto responseArrPtr = std::make_unique<pam_response[]>(msgCount);
+    auto pamResponseDeleter = [](pam_response* ptr) { free(ptr); };
+
+    std::unique_ptr<pam_response[], decltype(pamResponseDeleter)> responses(
+        static_cast<pam_response*>(calloc(msgCount, sizeof(pam_response))),
+        pamResponseDeleter);
+
+    auto messagePtrs = std::span(msgs, msgCount);
+    for (size_t i = 0; i < msgCount; ++i)
+    {
+        const pam_message& msg = *(messagePtrs[i]);
+
+        pam_response& response = responses[i];
+        response.resp_retcode = 0;
+        response.resp = nullptr;
+
+        int r = appPass->makeResponse(msg, response);
+        if (r != PAM_SUCCESS)
+        {
+            return r;
+        }
+    }
+
+    *resp = responses.release();
+    return PAM_SUCCESS;
+}
+
+struct Totp
+{
+    /**
+     * @brief Attempt username/password authentication via PAM.
+     * @param username The provided username aka account name.
+     * @param token The provided MFA token.
+     * @returns PAM error code or PAM_SUCCESS for success. */
+    static inline int verify(std::string_view username, std::string token)
+    {
+        std::string userStr(username);
+        PasswordData data;
+
+        if (int ret = data.addPrompt("Verification code: ", token);
+            ret != PAM_SUCCESS)
+        {
+            return ret;
+        }
+
+        const struct pam_conv localConversation = {pamFunctionConversation,
+                                                   &data};
+        pam_handle_t* localAuthHandle = nullptr; // this gets set by pam_start
+
+        int retval = pam_start("mfa_pam", userStr.c_str(), &localConversation,
+                               &localAuthHandle);
+        if (retval != PAM_SUCCESS)
+        {
+            return retval;
+        }
+
+        retval = pam_authenticate(localAuthHandle,
+                                  PAM_SILENT | PAM_DISALLOW_NULL_AUTHTOK);
+        if (retval != PAM_SUCCESS)
+        {
+            pam_end(localAuthHandle, PAM_SUCCESS); // ignore retval
+            return retval;
+        }
+        return pam_end(localAuthHandle, PAM_SUCCESS);
+    }
+};
diff --git a/user_mgr.cpp b/user_mgr.cpp
index d1c525b..129e4b4 100644
--- a/user_mgr.cpp
+++ b/user_mgr.cpp
@@ -1207,6 +1207,8 @@
                          user.get()->userLockedForFailedAttempt());
         userInfo.emplace("UserPasswordExpired",
                          user.get()->userPasswordExpired());
+        userInfo.emplace("TOTPSecretkeyRequired",
+                         user.get()->secretKeyGenerationRequired());
         userInfo.emplace("RemoteUser", false);
     }
     else
@@ -1539,5 +1541,39 @@
     return executeCmd("/usr/sbin/faillock", "--user", userName);
 }
 
+MultiFactorAuthType UserMgr::enabled(MultiFactorAuthType value, bool skipSignal)
+{
+    if (value == enabled())
+    {
+        return value;
+    }
+    switch (value)
+    {
+        case MultiFactorAuthType::None:
+            for (auto type : {MultiFactorAuthType::GoogleAuthenticator})
+            {
+                for (auto& u : usersList)
+                {
+                    u.second->enableMultiFactorAuth(type, false);
+                }
+            }
+            break;
+        default:
+            for (auto& u : usersList)
+            {
+                u.second->enableMultiFactorAuth(value, true);
+            }
+            break;
+    }
+    return MultiFactorAuthConfigurationIface::enabled(value, skipSignal);
+}
+bool UserMgr::secretKeyRequired(std::string userName)
+{
+    if (usersList.contains(userName))
+    {
+        return usersList[userName]->secretKeyGenerationRequired();
+    }
+    return false;
+}
 } // namespace user
 } // namespace phosphor
diff --git a/user_mgr.hpp b/user_mgr.hpp
index 701ab3b..624b2ba 100644
--- a/user_mgr.hpp
+++ b/user_mgr.hpp
@@ -26,6 +26,8 @@
 #include <xyz/openbmc_project/Common/error.hpp>
 #include <xyz/openbmc_project/User/AccountPolicy/server.hpp>
 #include <xyz/openbmc_project/User/Manager/server.hpp>
+#include <xyz/openbmc_project/User/MultiFactorAuthConfiguration/server.hpp>
+#include <xyz/openbmc_project/User/TOTPState/server.hpp>
 
 #include <span>
 #include <string>
@@ -50,7 +52,14 @@
 using AccountPolicyIface =
     sdbusplus::xyz::openbmc_project::User::server::AccountPolicy;
 
-using Ifaces = sdbusplus::server::object_t<UserMgrIface, AccountPolicyIface>;
+using MultiFactorAuthConfigurationIface =
+    sdbusplus::xyz::openbmc_project::User::server::MultiFactorAuthConfiguration;
+
+using TOTPStateIface = sdbusplus::xyz::openbmc_project::User::server::TOTPState;
+
+using Ifaces = sdbusplus::server::object_t<UserMgrIface, AccountPolicyIface,
+                                           MultiFactorAuthConfigurationIface,
+                                           TOTPStateIface>;
 
 using Privilege = std::string;
 using GroupList = std::vector<std::string>;
@@ -73,6 +82,8 @@
 
 using DbusUserObj = std::map<DbusUserObjPath, DbusUserObjValue>;
 
+using MultiFactorAuthType = sdbusplus::common::xyz::openbmc_project::user::
+    MultiFactorAuthConfiguration::Type;
 std::string getCSVFromVector(std::span<const std::string> vec);
 
 bool removeStringFromCSV(std::string& csvStr, const std::string& delStr);
@@ -259,7 +270,13 @@
     void createGroup(std::string groupName) override;
 
     void deleteGroup(std::string groupName) override;
-
+    MultiFactorAuthType enabled() const override
+    {
+        return MultiFactorAuthConfigurationIface::enabled();
+    }
+    MultiFactorAuthType enabled(MultiFactorAuthType value,
+                                bool skipSignal) override;
+    bool secretKeyRequired(std::string userName) override;
     static std::vector<std::string> readAllGroupsOnSystem();
 
   protected:
diff --git a/users.cpp b/users.cpp
index 3440b94..0300358 100644
--- a/users.cpp
+++ b/users.cpp
@@ -18,8 +18,10 @@
 
 #include "users.hpp"
 
+#include "totp.hpp"
 #include "user_mgr.hpp"
 
+#include <pwd.h>
 #include <sys/types.h>
 #include <sys/wait.h>
 #include <unistd.h>
@@ -31,7 +33,8 @@
 #include <xyz/openbmc_project/User/Common/error.hpp>
 
 #include <filesystem>
-
+#include <fstream>
+#include <map>
 namespace phosphor
 {
 namespace user
@@ -46,8 +49,14 @@
     sdbusplus::xyz::openbmc_project::Common::Error::InvalidArgument;
 using NoResource =
     sdbusplus::xyz::openbmc_project::User::Common::Error::NoResource;
+using UnsupportedRequest =
+    sdbusplus::xyz::openbmc_project::Common::Error::UnsupportedRequest;
 
 using Argument = xyz::openbmc_project::Common::InvalidArgument;
+static constexpr auto authAppPath = "/usr/bin/google-authenticator";
+static constexpr auto secretKeyPath = "/home/{}/.google_authenticator";
+static constexpr auto secretKeyTempPath =
+    "/home/{}/.config/phosphor-user-manager/.google_authenticator.tmp";
 
 /** @brief Constructs UserMgr object.
  *
@@ -193,6 +202,165 @@
 {
     return manager.userPasswordExpired(userName);
 }
+bool changeFileOwnership(const std::string& filePath,
+                         const std::string& userName)
+{
+    // Get the user ID
+    passwd* pwd = getpwnam(userName.c_str());
+    if (pwd == nullptr)
+    {
+        lg2::error("Failed to get user ID for user:{USER}", "USER", userName);
+        return false;
+    }
+    // Change the ownership of the file
+    if (chown(filePath.c_str(), pwd->pw_uid, pwd->pw_gid) != 0)
+    {
+        lg2::error("Ownership change error {PATH}", "PATH", filePath);
+        return false;
+    }
+    return true;
+}
+bool Users::checkMfaStatus() const
+{
+    return (manager.enabled() != MultiFactorAuthType::None &&
+            Interfaces::bypassedProtocol() == MultiFactorAuthType::None);
+}
+std::string Users::createSecretKey()
+{
+    if (!std::filesystem::exists(authAppPath))
+    {
+        lg2::error("No authenticator app found at {PATH}", "PATH", authAppPath);
+        throw UnsupportedRequest();
+    }
+    std::string path = std::format(secretKeyTempPath, userName);
+    if (!std::filesystem::exists(std::filesystem::path(path).parent_path()))
+    {
+        std::filesystem::create_directories(
+            std::filesystem::path(path).parent_path());
+    }
+    /*
+    -u no-rate-limit
+    -W minimal-window
+    -Q qr-mode (NONE, ANSI, UTF8)
+    -t time-based
+    -f force file
+    -d disallow-reuse
+    -C no-confirm no confirmation required for code provisioned
+    */
+    executeCmd(authAppPath, "-s", path.c_str(), "-u", "-W", "-Q", "NONE", "-t",
+               "-f", "-d", "-C");
+    if (!std::filesystem::exists(path))
+    {
+        lg2::error("Failed to create secret key for user {USER}", "USER",
+                   userName);
+        throw UnsupportedRequest();
+    }
+    std::ifstream file(path);
+    if (!file.is_open())
+    {
+        lg2::error("Failed to open secret key file {PATH}", "PATH", path);
+        throw UnsupportedRequest();
+    }
+    std::string secret;
+    std::getline(file, secret);
+    file.close();
+    if (!changeFileOwnership(path, userName))
+    {
+        throw UnsupportedRequest();
+    }
+    return secret;
+}
+bool Users::verifyOTP(std::string otp)
+{
+    if (Totp::verify(getUserName(), otp) == PAM_SUCCESS)
+    {
+        // If MFA is enabled for the user register the secret key
+        if (checkMfaStatus())
+        {
+            try
+            {
+                std::filesystem::rename(
+                    std::format(secretKeyTempPath, getUserName()),
+                    std::format(secretKeyPath, getUserName()));
+            }
+            catch (const std::filesystem::filesystem_error& e)
+            {
+                lg2::error("Failed to rename file: {CODE}", "CODE", e);
+                return false;
+            }
+        }
+        else
+        {
+            std::filesystem::remove(
+                std::format(secretKeyTempPath, getUserName()));
+        }
+        return true;
+    }
+    return false;
+}
+static void clearSecretFile(const std::string& path)
+{
+    if (std::filesystem::exists(path))
+    {
+        std::filesystem::remove(path);
+    }
+}
+static void clearGoogleAuthenticator(Users& thisp)
+{
+    clearSecretFile(std::format(secretKeyPath, thisp.getUserName()));
+    clearSecretFile(std::format(secretKeyTempPath, thisp.getUserName()));
+}
+static std::map<MultiFactorAuthType, std::function<void(Users&)>>
+    mfaBypassHandlers{{MultiFactorAuthType::GoogleAuthenticator,
+                       clearGoogleAuthenticator},
+                      {MultiFactorAuthType::None, [](Users&) {}}};
+
+MultiFactorAuthType Users::bypassedProtocol(MultiFactorAuthType value,
+                                            bool skipSignal)
+{
+    auto iter = mfaBypassHandlers.find(value);
+    if (iter != end(mfaBypassHandlers))
+    {
+        iter->second(*this);
+    }
+    return Interfaces::bypassedProtocol(value, skipSignal);
+}
+
+bool Users::secretKeyIsValid() const
+{
+    std::string path = std::format(secretKeyPath, getUserName());
+    return std::filesystem::exists(path);
+}
+
+inline void googleAuthenticatorEnabled(Users& user, bool /*unused*/)
+{
+    clearGoogleAuthenticator(user);
+}
+static std::map<MultiFactorAuthType, std::function<void(Users&, bool)>>
+    mfaEnableHandlers{{MultiFactorAuthType::GoogleAuthenticator,
+                       googleAuthenticatorEnabled},
+                      {MultiFactorAuthType::None, [](Users&, bool) {}}};
+
+void Users::enableMultiFactorAuth(MultiFactorAuthType type, bool value)
+{
+    auto iter = mfaEnableHandlers.find(type);
+    if (iter != end(mfaEnableHandlers))
+    {
+        iter->second(*this, value);
+    }
+}
+bool Users::secretKeyGenerationRequired() const
+{
+    return checkMfaStatus() && !secretKeyIsValid();
+}
+void Users::clearSecretKey()
+{
+    if (!checkMfaStatus())
+    {
+        throw UnsupportedRequest();
+    }
+    clearGoogleAuthenticator(*this);
+}
 
 } // namespace user
 } // namespace phosphor
diff --git a/users.hpp b/users.hpp
index 3e0a891..a5ff131 100644
--- a/users.hpp
+++ b/users.hpp
@@ -18,7 +18,8 @@
 #include <sdbusplus/server/object.hpp>
 #include <xyz/openbmc_project/Object/Delete/server.hpp>
 #include <xyz/openbmc_project/User/Attributes/server.hpp>
-
+#include <xyz/openbmc_project/User/MultiFactorAuthConfiguration/server.hpp>
+#include <xyz/openbmc_project/User/TOTPAuthenticator/server.hpp>
 namespace phosphor
 {
 namespace user
@@ -26,8 +27,13 @@
 
 namespace Base = sdbusplus::xyz::openbmc_project;
 using UsersIface = Base::User::server::Attributes;
+
+using TOTPAuthenticatorIface = Base::User::server::TOTPAuthenticator;
 using DeleteIface = Base::Object::server::Delete;
-using Interfaces = sdbusplus::server::object_t<UsersIface, DeleteIface>;
+using Interfaces = sdbusplus::server::object_t<UsersIface, DeleteIface,
+                                               TOTPAuthenticatorIface>;
+using MultiFactorAuthType = sdbusplus::common::xyz::openbmc_project::user::
+    MultiFactorAuthConfiguration::Type;
 // Place where all user objects has to be created
 constexpr auto usersObjPath = "/xyz/openbmc_project/user";
 
@@ -121,7 +127,21 @@
      **/
     bool userPasswordExpired(void) const override;
 
+    std::string getUserName() const
+    {
+        return userName;
+    }
+    bool secretKeyIsValid() const override;
+    std::string createSecretKey() override;
+    bool verifyOTP(std::string otp) override;
+    bool secretKeyGenerationRequired() const override;
+    void clearSecretKey() override;
+    MultiFactorAuthType bypassedProtocol(MultiFactorAuthType value,
+                                         bool skipSignal) override;
+    void enableMultiFactorAuth(MultiFactorAuthType type, bool value);
+
   private:
+    bool checkMfaStatus() const;
     std::string userName;
     UserMgr& manager;
 };