Serenity Operating System
1/*
2 * Copyright (c) 2020, Peter Elliott <pelliott@serenityos.org>
3 * Copyright (c) 2021-2022, Brian Gianforcaro <bgianf@serenityos.org>
4 *
5 * SPDX-License-Identifier: BSD-2-Clause
6 */
7
8#include <AK/Base64.h>
9#include <AK/Memory.h>
10#include <AK/Random.h>
11#include <AK/ScopeGuard.h>
12#include <LibCore/Account.h>
13#include <LibCore/Directory.h>
14#include <LibCore/System.h>
15#include <LibCore/UmaskScope.h>
16#include <errno.h>
17#include <grp.h>
18#include <pwd.h>
19#ifndef AK_OS_BSD_GENERIC
20# include <crypt.h>
21# include <shadow.h>
22#endif
23#include <stdio.h>
24#include <string.h>
25#include <sys/stat.h>
26#include <unistd.h>
27
28namespace Core {
29
30static DeprecatedString get_salt()
31{
32 char random_data[12];
33 fill_with_random(random_data, sizeof(random_data));
34
35 StringBuilder builder;
36 builder.append("$5$"sv);
37
38 // FIXME: change to TRY() and make method fallible
39 auto salt_string = MUST(encode_base64({ random_data, sizeof(random_data) }));
40 builder.append(salt_string);
41
42 return builder.to_deprecated_string();
43}
44
45static Vector<gid_t> get_extra_gids(passwd const& pwd)
46{
47 StringView username { pwd.pw_name, strlen(pwd.pw_name) };
48 Vector<gid_t> extra_gids;
49 setgrent();
50 for (auto* group = getgrent(); group; group = getgrent()) {
51 if (group->gr_gid == pwd.pw_gid)
52 continue;
53 for (size_t i = 0; group->gr_mem[i]; ++i) {
54 if (username == group->gr_mem[i]) {
55 extra_gids.append(group->gr_gid);
56 break;
57 }
58 }
59 }
60 endgrent();
61 return extra_gids;
62}
63
64ErrorOr<Account> Account::from_passwd(passwd const& pwd, spwd const& spwd)
65{
66 Account account(pwd, spwd, get_extra_gids(pwd));
67 endpwent();
68#ifndef AK_OS_BSD_GENERIC
69 endspent();
70#endif
71 return account;
72}
73
74ErrorOr<Account> Account::self([[maybe_unused]] Read options)
75{
76 Vector<gid_t> extra_gids = TRY(Core::System::getgroups());
77
78 auto pwd = TRY(Core::System::getpwuid(getuid()));
79 if (!pwd.has_value())
80 return Error::from_string_literal("No such user");
81
82 spwd spwd = {};
83#ifndef AK_OS_BSD_GENERIC
84 if (options != Read::PasswdOnly) {
85 auto maybe_spwd = TRY(Core::System::getspnam({ pwd->pw_name, strlen(pwd->pw_name) }));
86 if (!maybe_spwd.has_value())
87 return Error::from_string_literal("No shadow entry for user");
88 spwd = maybe_spwd.release_value();
89 }
90#endif
91
92 return Account(*pwd, spwd, extra_gids);
93}
94
95ErrorOr<Account> Account::from_name(StringView username, [[maybe_unused]] Read options)
96{
97 auto pwd = TRY(Core::System::getpwnam(username));
98 if (!pwd.has_value())
99 return Error::from_string_literal("No such user");
100
101 spwd spwd = {};
102#ifndef AK_OS_BSD_GENERIC
103 if (options != Read::PasswdOnly) {
104 auto maybe_spwd = TRY(Core::System::getspnam({ pwd->pw_name, strlen(pwd->pw_name) }));
105 if (!maybe_spwd.has_value())
106 return Error::from_string_literal("No shadow entry for user");
107 spwd = maybe_spwd.release_value();
108 }
109#endif
110 return from_passwd(*pwd, spwd);
111}
112
113ErrorOr<Account> Account::from_uid(uid_t uid, [[maybe_unused]] Read options)
114{
115 auto pwd = TRY(Core::System::getpwuid(uid));
116 if (!pwd.has_value())
117 return Error::from_string_literal("No such user");
118
119 spwd spwd = {};
120#ifndef AK_OS_BSD_GENERIC
121 if (options != Read::PasswdOnly) {
122 auto maybe_spwd = TRY(Core::System::getspnam({ pwd->pw_name, strlen(pwd->pw_name) }));
123 if (!maybe_spwd.has_value())
124 return Error::from_string_literal("No shadow entry for user");
125 spwd = maybe_spwd.release_value();
126 }
127#endif
128 return from_passwd(*pwd, spwd);
129}
130
131ErrorOr<Vector<Account>> Account::all([[maybe_unused]] Read options)
132{
133 Vector<Account> accounts;
134 char buffer[1024] = { 0 };
135
136 ScopeGuard pwent_guard([] { endpwent(); });
137 setpwent();
138
139 while (true) {
140 auto pwd = TRY(Core::System::getpwent({ buffer, sizeof(buffer) }));
141 if (!pwd.has_value())
142 break;
143
144 spwd spwd = {};
145
146#ifndef AK_OS_BSD_GENERIC
147 ScopeGuard spent_guard([] { endspent(); });
148 if (options != Read::PasswdOnly) {
149 auto maybe_spwd = TRY(Core::System::getspnam({ pwd->pw_name, strlen(pwd->pw_name) }));
150 if (!maybe_spwd.has_value())
151 return Error::from_string_literal("No shadow entry for user");
152 spwd = maybe_spwd.release_value();
153 }
154#endif
155
156 accounts.append({ *pwd, spwd, get_extra_gids(*pwd) });
157 }
158
159 return accounts;
160}
161
162bool Account::authenticate(SecretString const& password) const
163{
164 // If there was no shadow entry for this account, authentication always fails.
165 if (m_password_hash.is_null())
166 return false;
167
168 // An empty passwd field indicates that no password is required to log in.
169 if (m_password_hash.is_empty())
170 return true;
171
172 // FIXME: Use crypt_r if it can be built in lagom.
173 char* hash = crypt(password.characters(), m_password_hash.characters());
174 return hash != nullptr && AK::timing_safe_compare(hash, m_password_hash.characters(), m_password_hash.length());
175}
176
177ErrorOr<void> Account::login() const
178{
179 TRY(Core::System::setgroups(m_extra_gids));
180 TRY(Core::System::setgid(m_gid));
181 TRY(Core::System::setuid(m_uid));
182
183 return {};
184}
185
186void Account::set_password(SecretString const& password)
187{
188 m_password_hash = crypt(password.characters(), get_salt().characters());
189}
190
191void Account::set_password_enabled(bool enabled)
192{
193 if (enabled && m_password_hash != "" && m_password_hash[0] == '!') {
194 m_password_hash = m_password_hash.substring(1, m_password_hash.length() - 1);
195 } else if (!enabled && (m_password_hash == "" || m_password_hash[0] != '!')) {
196 StringBuilder builder;
197 builder.append('!');
198 builder.append(m_password_hash);
199 m_password_hash = builder.to_deprecated_string();
200 }
201}
202
203void Account::delete_password()
204{
205 m_password_hash = "";
206}
207
208Account::Account(passwd const& pwd, spwd const& spwd, Vector<gid_t> extra_gids)
209 : m_username(pwd.pw_name)
210 , m_password_hash(spwd.sp_pwdp)
211 , m_uid(pwd.pw_uid)
212 , m_gid(pwd.pw_gid)
213 , m_gecos(pwd.pw_gecos)
214 , m_home_directory(pwd.pw_dir)
215 , m_shell(pwd.pw_shell)
216 , m_extra_gids(move(extra_gids))
217{
218}
219
220ErrorOr<DeprecatedString> Account::generate_passwd_file() const
221{
222 StringBuilder builder;
223 char buffer[1024] = { 0 };
224
225 ScopeGuard pwent_guard([] { endpwent(); });
226 setpwent();
227
228 while (true) {
229 auto pwd = TRY(Core::System::getpwent({ buffer, sizeof(buffer) }));
230 if (!pwd.has_value())
231 break;
232
233 if (pwd->pw_name == m_username) {
234 if (m_deleted)
235 continue;
236 builder.appendff("{}:!:{}:{}:{}:{}:{}\n",
237 m_username,
238 m_uid, m_gid,
239 m_gecos,
240 m_home_directory,
241 m_shell);
242
243 } else {
244 builder.appendff("{}:!:{}:{}:{}:{}:{}\n",
245 pwd->pw_name, pwd->pw_uid,
246 pwd->pw_gid, pwd->pw_gecos, pwd->pw_dir,
247 pwd->pw_shell);
248 }
249 }
250
251 return builder.to_deprecated_string();
252}
253
254#ifndef AK_OS_BSD_GENERIC
255ErrorOr<DeprecatedString> Account::generate_shadow_file() const
256{
257 StringBuilder builder;
258
259 setspent();
260
261 struct spwd* p;
262 errno = 0;
263 while ((p = getspent())) {
264 if (p->sp_namp == m_username) {
265 if (m_deleted)
266 continue;
267 builder.appendff("{}:{}", m_username, m_password_hash);
268 } else
269 builder.appendff("{}:{}", p->sp_namp, p->sp_pwdp);
270
271 builder.appendff(":{}:{}:{}:{}:{}:{}:{}\n",
272 (p->sp_lstchg == -1) ? "" : DeprecatedString::formatted("{}", p->sp_lstchg),
273 (p->sp_min == -1) ? "" : DeprecatedString::formatted("{}", p->sp_min),
274 (p->sp_max == -1) ? "" : DeprecatedString::formatted("{}", p->sp_max),
275 (p->sp_warn == -1) ? "" : DeprecatedString::formatted("{}", p->sp_warn),
276 (p->sp_inact == -1) ? "" : DeprecatedString::formatted("{}", p->sp_inact),
277 (p->sp_expire == -1) ? "" : DeprecatedString::formatted("{}", p->sp_expire),
278 (p->sp_flag == 0) ? "" : DeprecatedString::formatted("{}", p->sp_flag));
279 }
280 endspent();
281
282 if (errno)
283 return Error::from_errno(errno);
284
285 return builder.to_deprecated_string();
286}
287#endif
288
289ErrorOr<void> Account::sync()
290{
291 Core::UmaskScope umask_scope(0777);
292
293 auto new_passwd_file_content = TRY(generate_passwd_file());
294#ifndef AK_OS_BSD_GENERIC
295 auto new_shadow_file_content = TRY(generate_shadow_file());
296#endif
297
298 char new_passwd_file[] = "/etc/passwd.XXXXXX";
299#ifndef AK_OS_BSD_GENERIC
300 char new_shadow_file[] = "/etc/shadow.XXXXXX";
301#endif
302
303 {
304 auto new_passwd_fd = TRY(Core::System::mkstemp(new_passwd_file));
305 ScopeGuard new_passwd_fd_guard = [new_passwd_fd] { close(new_passwd_fd); };
306 TRY(Core::System::fchmod(new_passwd_fd, 0644));
307
308#ifndef AK_OS_BSD_GENERIC
309 auto new_shadow_fd = TRY(Core::System::mkstemp(new_shadow_file));
310 ScopeGuard new_shadow_fd_guard = [new_shadow_fd] { close(new_shadow_fd); };
311 TRY(Core::System::fchmod(new_shadow_fd, 0600));
312#endif
313
314 auto nwritten = TRY(Core::System::write(new_passwd_fd, new_passwd_file_content.bytes()));
315 VERIFY(static_cast<size_t>(nwritten) == new_passwd_file_content.length());
316
317#ifndef AK_OS_BSD_GENERIC
318 nwritten = TRY(Core::System::write(new_shadow_fd, new_shadow_file_content.bytes()));
319 VERIFY(static_cast<size_t>(nwritten) == new_shadow_file_content.length());
320#endif
321 }
322
323 auto new_passwd_file_view = StringView { new_passwd_file, sizeof(new_passwd_file) };
324 TRY(Core::System::rename(new_passwd_file_view, "/etc/passwd"sv));
325#ifndef AK_OS_BSD_GENERIC
326 auto new_shadow_file_view = StringView { new_shadow_file, sizeof(new_shadow_file) };
327 TRY(Core::System::rename(new_shadow_file_view, "/etc/shadow"sv));
328#endif
329
330 return {};
331 // FIXME: Sync extra groups.
332}
333
334}