mirror of
				https://github.com/RGBCube/serenity
				synced 2025-10-31 15:42:44 +00:00 
			
		
		
		
	 c1af2f28e3
			
		
	
	
		c1af2f28e3
		
	
	
	
	
		
			
			Since we already have the directory open, let's have an API to fchown() the underlying file descriptor instead of forcing clients to do another path lookup.
		
			
				
	
	
		
			329 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			329 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /*
 | |
|  * Copyright (c) 2020, Peter Elliott <pelliott@serenityos.org>
 | |
|  * Copyright (c) 2021-2022, Brian Gianforcaro <bgianf@serenityos.org>
 | |
|  *
 | |
|  * SPDX-License-Identifier: BSD-2-Clause
 | |
|  */
 | |
| 
 | |
| #include <AK/Base64.h>
 | |
| #include <AK/Memory.h>
 | |
| #include <AK/Random.h>
 | |
| #include <AK/ScopeGuard.h>
 | |
| #include <LibCore/Account.h>
 | |
| #include <LibCore/Directory.h>
 | |
| #include <LibCore/System.h>
 | |
| #include <LibCore/UmaskScope.h>
 | |
| #include <errno.h>
 | |
| #include <grp.h>
 | |
| #include <pwd.h>
 | |
| #ifndef AK_OS_BSD_GENERIC
 | |
| #    include <crypt.h>
 | |
| #    include <shadow.h>
 | |
| #endif
 | |
| #include <stdio.h>
 | |
| #include <string.h>
 | |
| #include <sys/stat.h>
 | |
| #include <unistd.h>
 | |
| 
 | |
| namespace Core {
 | |
| 
 | |
| static String get_salt()
 | |
| {
 | |
|     char random_data[12];
 | |
|     fill_with_random(random_data, sizeof(random_data));
 | |
| 
 | |
|     StringBuilder builder;
 | |
|     builder.append("$5$"sv);
 | |
|     builder.append(encode_base64(ReadonlyBytes(random_data, sizeof(random_data))));
 | |
| 
 | |
|     return builder.build();
 | |
| }
 | |
| 
 | |
| static Vector<gid_t> get_extra_gids(passwd const& pwd)
 | |
| {
 | |
|     StringView username { pwd.pw_name, strlen(pwd.pw_name) };
 | |
|     Vector<gid_t> extra_gids;
 | |
|     setgrent();
 | |
|     for (auto* group = getgrent(); group; group = getgrent()) {
 | |
|         if (group->gr_gid == pwd.pw_gid)
 | |
|             continue;
 | |
|         for (size_t i = 0; group->gr_mem[i]; ++i) {
 | |
|             if (username == group->gr_mem[i]) {
 | |
|                 extra_gids.append(group->gr_gid);
 | |
|                 break;
 | |
|             }
 | |
|         }
 | |
|     }
 | |
|     endgrent();
 | |
|     return extra_gids;
 | |
| }
 | |
| 
 | |
| ErrorOr<Account> Account::from_passwd(passwd const& pwd, spwd const& spwd)
 | |
| {
 | |
|     Account account(pwd, spwd, get_extra_gids(pwd));
 | |
|     endpwent();
 | |
| #ifndef AK_OS_BSD_GENERIC
 | |
|     endspent();
 | |
| #endif
 | |
|     return account;
 | |
| }
 | |
| 
 | |
| String Account::parse_path_with_uid(StringView general_path, Optional<uid_t> uid)
 | |
| {
 | |
|     if (general_path.contains("%uid"sv)) {
 | |
|         auto const final_uid = uid.has_value() ? uid.value() : getuid();
 | |
|         return general_path.replace("%uid"sv, String::number(final_uid), ReplaceMode::All);
 | |
|     }
 | |
|     return general_path;
 | |
| }
 | |
| 
 | |
| ErrorOr<Account> Account::self([[maybe_unused]] Read options)
 | |
| {
 | |
|     Vector<gid_t> extra_gids = TRY(Core::System::getgroups());
 | |
| 
 | |
|     auto pwd = TRY(Core::System::getpwuid(getuid()));
 | |
|     if (!pwd.has_value())
 | |
|         return Error::from_string_literal("No such user");
 | |
| 
 | |
|     spwd spwd = {};
 | |
| #ifndef AK_OS_BSD_GENERIC
 | |
|     if (options != Read::PasswdOnly) {
 | |
|         auto maybe_spwd = TRY(Core::System::getspnam({ pwd->pw_name, strlen(pwd->pw_name) }));
 | |
|         if (!maybe_spwd.has_value())
 | |
|             return Error::from_string_literal("No shadow entry for user");
 | |
|         spwd = maybe_spwd.release_value();
 | |
|     }
 | |
| #endif
 | |
| 
 | |
|     return Account(*pwd, spwd, extra_gids);
 | |
| }
 | |
| 
 | |
| ErrorOr<Account> Account::from_name(char const* username, [[maybe_unused]] Read options)
 | |
| {
 | |
|     auto pwd = TRY(Core::System::getpwnam({ username, strlen(username) }));
 | |
|     if (!pwd.has_value())
 | |
|         return Error::from_string_literal("No such user");
 | |
| 
 | |
|     spwd spwd = {};
 | |
| #ifndef AK_OS_BSD_GENERIC
 | |
|     if (options != Read::PasswdOnly) {
 | |
|         auto maybe_spwd = TRY(Core::System::getspnam({ pwd->pw_name, strlen(pwd->pw_name) }));
 | |
|         if (!maybe_spwd.has_value())
 | |
|             return Error::from_string_literal("No shadow entry for user");
 | |
|         spwd = maybe_spwd.release_value();
 | |
|     }
 | |
| #endif
 | |
|     return from_passwd(*pwd, spwd);
 | |
| }
 | |
| 
 | |
| ErrorOr<Account> Account::from_uid(uid_t uid, [[maybe_unused]] Read options)
 | |
| {
 | |
|     auto pwd = TRY(Core::System::getpwuid(uid));
 | |
|     if (!pwd.has_value())
 | |
|         return Error::from_string_literal("No such user");
 | |
| 
 | |
|     spwd spwd = {};
 | |
| #ifndef AK_OS_BSD_GENERIC
 | |
|     if (options != Read::PasswdOnly) {
 | |
|         auto maybe_spwd = TRY(Core::System::getspnam({ pwd->pw_name, strlen(pwd->pw_name) }));
 | |
|         if (!maybe_spwd.has_value())
 | |
|             return Error::from_string_literal("No shadow entry for user");
 | |
|         spwd = maybe_spwd.release_value();
 | |
|     }
 | |
| #endif
 | |
|     return from_passwd(*pwd, spwd);
 | |
| }
 | |
| 
 | |
| bool Account::authenticate(SecretString const& password) const
 | |
| {
 | |
|     // If there was no shadow entry for this account, authentication always fails.
 | |
|     if (m_password_hash.is_null())
 | |
|         return false;
 | |
| 
 | |
|     // An empty passwd field indicates that no password is required to log in.
 | |
|     if (m_password_hash.is_empty())
 | |
|         return true;
 | |
| 
 | |
|     // FIXME: Use crypt_r if it can be built in lagom.
 | |
|     char* hash = crypt(password.characters(), m_password_hash.characters());
 | |
|     return hash != nullptr && AK::timing_safe_compare(hash, m_password_hash.characters(), m_password_hash.length());
 | |
| }
 | |
| 
 | |
| ErrorOr<void> Account::create_user_temporary_directory_if_needed() const
 | |
| {
 | |
|     auto const temporary_directory = String::formatted("/tmp/user/{}", m_uid);
 | |
|     auto directory = TRY(Core::Directory::create(temporary_directory, Core::Directory::CreateDirectories::Yes));
 | |
|     TRY(directory.chown(m_uid, m_gid));
 | |
|     return {};
 | |
| }
 | |
| 
 | |
| bool Account::login() const
 | |
| {
 | |
|     if (setgroups(m_extra_gids.size(), m_extra_gids.data()) < 0)
 | |
|         return false;
 | |
| 
 | |
|     if (setgid(m_gid) < 0)
 | |
|         return false;
 | |
| 
 | |
|     if (setuid(m_uid) < 0)
 | |
|         return false;
 | |
| 
 | |
|     return true;
 | |
| }
 | |
| 
 | |
| void Account::set_password(SecretString const& password)
 | |
| {
 | |
|     m_password_hash = crypt(password.characters(), get_salt().characters());
 | |
| }
 | |
| 
 | |
| void Account::set_password_enabled(bool enabled)
 | |
| {
 | |
|     if (enabled && m_password_hash != "" && m_password_hash[0] == '!') {
 | |
|         m_password_hash = m_password_hash.substring(1, m_password_hash.length() - 1);
 | |
|     } else if (!enabled && (m_password_hash == "" || m_password_hash[0] != '!')) {
 | |
|         StringBuilder builder;
 | |
|         builder.append('!');
 | |
|         builder.append(m_password_hash);
 | |
|         m_password_hash = builder.build();
 | |
|     }
 | |
| }
 | |
| 
 | |
| void Account::delete_password()
 | |
| {
 | |
|     m_password_hash = "";
 | |
| }
 | |
| 
 | |
| Account::Account(passwd const& pwd, spwd const& spwd, Vector<gid_t> extra_gids)
 | |
|     : m_username(pwd.pw_name)
 | |
|     , m_password_hash(spwd.sp_pwdp)
 | |
|     , m_uid(pwd.pw_uid)
 | |
|     , m_gid(pwd.pw_gid)
 | |
|     , m_gecos(pwd.pw_gecos)
 | |
|     , m_home_directory(pwd.pw_dir)
 | |
|     , m_shell(pwd.pw_shell)
 | |
|     , m_extra_gids(move(extra_gids))
 | |
| {
 | |
| }
 | |
| 
 | |
| ErrorOr<String> Account::generate_passwd_file() const
 | |
| {
 | |
|     StringBuilder builder;
 | |
| 
 | |
|     setpwent();
 | |
| 
 | |
|     struct passwd* p;
 | |
|     errno = 0;
 | |
|     while ((p = getpwent())) {
 | |
|         if (p->pw_name == m_username) {
 | |
|             builder.appendff("{}:!:{}:{}:{}:{}:{}\n",
 | |
|                 m_username,
 | |
|                 m_uid, m_gid,
 | |
|                 m_gecos,
 | |
|                 m_home_directory,
 | |
|                 m_shell);
 | |
| 
 | |
|         } else {
 | |
|             builder.appendff("{}:!:{}:{}:{}:{}:{}\n",
 | |
|                 p->pw_name, p->pw_uid,
 | |
|                 p->pw_gid, p->pw_gecos, p->pw_dir,
 | |
|                 p->pw_shell);
 | |
|         }
 | |
|     }
 | |
|     endpwent();
 | |
| 
 | |
|     if (errno)
 | |
|         return Error::from_errno(errno);
 | |
| 
 | |
|     return builder.to_string();
 | |
| }
 | |
| 
 | |
| #ifndef AK_OS_BSD_GENERIC
 | |
| ErrorOr<String> Account::generate_shadow_file() const
 | |
| {
 | |
|     StringBuilder builder;
 | |
| 
 | |
|     setspent();
 | |
| 
 | |
|     struct spwd* p;
 | |
|     errno = 0;
 | |
|     while ((p = getspent())) {
 | |
|         if (p->sp_namp == m_username) {
 | |
|             builder.appendff("{}:{}:{}:{}:{}:{}:{}:{}:{}\n",
 | |
|                 m_username, m_password_hash,
 | |
|                 (p->sp_lstchg == -1) ? "" : String::formatted("{}", p->sp_lstchg),
 | |
|                 (p->sp_min == -1) ? "" : String::formatted("{}", p->sp_min),
 | |
|                 (p->sp_max == -1) ? "" : String::formatted("{}", p->sp_max),
 | |
|                 (p->sp_warn == -1) ? "" : String::formatted("{}", p->sp_warn),
 | |
|                 (p->sp_inact == -1) ? "" : String::formatted("{}", p->sp_inact),
 | |
|                 (p->sp_expire == -1) ? "" : String::formatted("{}", p->sp_expire),
 | |
|                 (p->sp_flag == 0) ? "" : String::formatted("{}", p->sp_flag));
 | |
| 
 | |
|         } else {
 | |
|             builder.appendff("{}:{}:{}:{}:{}:{}:{}:{}:{}\n",
 | |
|                 p->sp_namp, p->sp_pwdp,
 | |
|                 (p->sp_lstchg == -1) ? "" : String::formatted("{}", p->sp_lstchg),
 | |
|                 (p->sp_min == -1) ? "" : String::formatted("{}", p->sp_min),
 | |
|                 (p->sp_max == -1) ? "" : String::formatted("{}", p->sp_max),
 | |
|                 (p->sp_warn == -1) ? "" : String::formatted("{}", p->sp_warn),
 | |
|                 (p->sp_inact == -1) ? "" : String::formatted("{}", p->sp_inact),
 | |
|                 (p->sp_expire == -1) ? "" : String::formatted("{}", p->sp_expire),
 | |
|                 (p->sp_flag == 0) ? "" : String::formatted("{}", p->sp_flag));
 | |
|         }
 | |
|     }
 | |
|     endspent();
 | |
| 
 | |
|     if (errno)
 | |
|         return Error::from_errno(errno);
 | |
| 
 | |
|     return builder.to_string();
 | |
| }
 | |
| #endif
 | |
| 
 | |
| ErrorOr<void> Account::sync()
 | |
| {
 | |
|     Core::UmaskScope umask_scope(0777);
 | |
| 
 | |
|     auto new_passwd_file_content = TRY(generate_passwd_file());
 | |
| #ifndef AK_OS_BSD_GENERIC
 | |
|     auto new_shadow_file_content = TRY(generate_shadow_file());
 | |
| #endif
 | |
| 
 | |
|     // FIXME: mkstemp taking Span<char> makes this code entirely un-AKable.
 | |
|     //        Make this code less char-pointery.
 | |
|     char new_passwd_name[] = "/etc/passwd.XXXXXX";
 | |
|     size_t new_passwd_name_length = strlen(new_passwd_name);
 | |
| #ifndef AK_OS_BSD_GENERIC
 | |
|     char new_shadow_name[] = "/etc/shadow.XXXXXX";
 | |
|     size_t new_shadow_name_length = strlen(new_shadow_name);
 | |
| #endif
 | |
| 
 | |
|     {
 | |
|         auto new_passwd_fd = TRY(Core::System::mkstemp({ new_passwd_name, new_passwd_name_length }));
 | |
|         ScopeGuard new_passwd_fd_guard = [new_passwd_fd] { close(new_passwd_fd); };
 | |
|         TRY(Core::System::fchmod(new_passwd_fd, 0644));
 | |
| 
 | |
| #ifndef AK_OS_BSD_GENERIC
 | |
|         auto new_shadow_fd = TRY(Core::System::mkstemp({ new_shadow_name, new_shadow_name_length }));
 | |
|         ScopeGuard new_shadow_fd_guard = [new_shadow_fd] { close(new_shadow_fd); };
 | |
|         TRY(Core::System::fchmod(new_shadow_fd, 0600));
 | |
| #endif
 | |
| 
 | |
|         auto nwritten = TRY(Core::System::write(new_passwd_fd, new_passwd_file_content.bytes()));
 | |
|         VERIFY(static_cast<size_t>(nwritten) == new_passwd_file_content.length());
 | |
| 
 | |
| #ifndef AK_OS_BSD_GENERIC
 | |
|         nwritten = TRY(Core::System::write(new_shadow_fd, new_shadow_file_content.bytes()));
 | |
|         VERIFY(static_cast<size_t>(nwritten) == new_shadow_file_content.length());
 | |
| #endif
 | |
|     }
 | |
| 
 | |
|     TRY(Core::System::rename({ new_passwd_name, new_passwd_name_length }, "/etc/passwd"sv));
 | |
| #ifndef AK_OS_BSD_GENERIC
 | |
|     TRY(Core::System::rename({ new_shadow_name, new_shadow_name_length }, "/etc/shadow"sv));
 | |
| #endif
 | |
| 
 | |
|     return {};
 | |
|     // FIXME: Sync extra groups.
 | |
| }
 | |
| 
 | |
| }
 |