ladybird/Userland/Libraries/LibCore/Account.cpp
brapru 3cf835af6d LibCore: Do not write disabled spwd values in generate_shadow_file
When LibC/shadow.cpp parses shadow entries in getspent, it sets the
spwd member value to disabled (-1) if the value is empty. When
Core::Account::sync calls getspent to generate a new shadow file, it
would recieve the -1 values and write them in the shadow file. This
would cause the /etc/shadow file to be cluttered with disabled values
after any password change.

This patch checks if the spwd member value is disabled, and prints the
appropriate value to the shadow file.
2021-05-29 18:23:10 +01:00

326 lines
8.7 KiB
C++

/*
* Copyright (c) 2020, Peter Elliott <pelliott@ualberta.ca>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Base64.h>
#include <AK/Random.h>
#include <AK/ScopeGuard.h>
#include <LibCore/Account.h>
#include <LibCore/File.h>
#ifndef AK_OS_MACOS
# include <crypt.h>
#endif
#include <errno.h>
#include <grp.h>
#include <pwd.h>
#ifndef AK_OS_MACOS
# include <shadow.h>
#else
# include <LibC/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$");
builder.append(encode_base64(ReadonlyBytes(random_data, sizeof(random_data))));
return builder.build();
}
static Vector<gid_t> get_extra_gids(const passwd& pwd)
{
StringView username { 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;
}
Result<Account, String> Account::from_passwd(const passwd& pwd, const spwd& spwd)
{
Account account(pwd, spwd, get_extra_gids(pwd));
endpwent();
#ifndef AK_OS_MACOS
endspent();
#endif
return account;
}
Result<Account, String> Account::from_name(const char* username)
{
errno = 0;
auto* pwd = getpwnam(username);
if (!pwd) {
if (errno == 0)
return String("No such user");
return String(strerror(errno));
}
spwd spwd_dummy = {};
spwd_dummy.sp_namp = const_cast<char*>(username);
spwd_dummy.sp_pwdp = const_cast<char*>("");
#ifndef AK_OS_MACOS
auto* spwd = getspnam(username);
if (!spwd)
spwd = &spwd_dummy;
#else
auto* spwd = &spwd_dummy;
#endif
return from_passwd(*pwd, *spwd);
}
Result<Account, String> Account::from_uid(uid_t uid)
{
errno = 0;
auto* pwd = getpwuid(uid);
if (!pwd) {
if (errno == 0)
return String("No such user");
return String(strerror(errno));
}
spwd spwd_dummy = {};
spwd_dummy.sp_namp = pwd->pw_name;
spwd_dummy.sp_pwdp = const_cast<char*>("");
#ifndef AK_OS_MACOS
auto* spwd = getspnam(pwd->pw_name);
if (!spwd)
spwd = &spwd_dummy;
#else
auto* spwd = &spwd_dummy;
#endif
return from_passwd(*pwd, *spwd);
}
bool Account::authenticate(const char* 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, m_password_hash.characters());
return hash != nullptr && strcmp(hash, m_password_hash.characters()) == 0;
}
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(const char* password)
{
m_password_hash = crypt(password, 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(const passwd& pwd, const spwd& 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))
{
}
String Account::generate_passwd_file() const
{
StringBuilder builder;
setpwent();
struct passwd* p;
errno = 0;
while ((p = getpwent())) {
if (p->pw_uid == m_uid) {
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) {
dbgln("errno was non-zero after generating new passwd file.");
return {};
}
return builder.to_string();
}
#ifndef AK_OS_MACOS
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) {
dbgln("errno was non-zero after generating new passwd file.");
return {};
}
return builder.to_string();
}
#endif
bool Account::sync()
{
auto new_passwd_file_content = generate_passwd_file();
VERIFY(!new_passwd_file_content.is_null());
#ifndef AK_OS_MACOS
auto new_shadow_file_content = generate_shadow_file();
VERIFY(!new_shadow_file_content.is_null());
#endif
char new_passwd_name[] = "/etc/passwd.XXXXXX";
#ifndef AK_OS_MACOS
char new_shadow_name[] = "/etc/shadow.XXXXXX";
#endif
{
auto new_passwd_fd = mkstemp(new_passwd_name);
if (new_passwd_fd < 0) {
perror("mkstemp");
VERIFY_NOT_REACHED();
}
ScopeGuard new_passwd_fd_guard = [new_passwd_fd] { close(new_passwd_fd); };
#ifndef AK_OS_MACOS
auto new_shadow_fd = mkstemp(new_shadow_name);
if (new_shadow_fd < 0) {
perror("mkstemp");
VERIFY_NOT_REACHED();
}
ScopeGuard new_shadow_fd_guard = [new_shadow_fd] { close(new_shadow_fd); };
#endif
if (fchmod(new_passwd_fd, 0644) < 0) {
perror("fchmod");
VERIFY_NOT_REACHED();
}
auto nwritten = write(new_passwd_fd, new_passwd_file_content.characters(), new_passwd_file_content.length());
if (nwritten < 0) {
perror("write");
VERIFY_NOT_REACHED();
}
VERIFY(static_cast<size_t>(nwritten) == new_passwd_file_content.length());
#ifndef AK_OS_MACOS
nwritten = write(new_shadow_fd, new_shadow_file_content.characters(), new_shadow_file_content.length());
if (nwritten < 0) {
perror("write");
VERIFY_NOT_REACHED();
}
VERIFY(static_cast<size_t>(nwritten) == new_shadow_file_content.length());
#endif
}
if (rename(new_passwd_name, "/etc/passwd") < 0) {
perror("Failed to install new /etc/passwd");
return false;
}
#ifndef AK_OS_MACOS
if (rename(new_shadow_name, "/etc/shadow") < 0) {
perror("Failed to install new /etc/shadow");
return false;
}
#endif
return true;
// FIXME: Sync extra groups.
}
}