| 1 |
/* |
| 2 |
* Copyright (c) 2021-2022 joshua stein <jcs@jcs.org> |
| 3 |
* |
| 4 |
* Permission to use, copy, modify, and distribute this software for any |
| 5 |
* purpose with or without fee is hereby granted, provided that the above |
| 6 |
* copyright notice and this permission notice appear in all copies. |
| 7 |
* |
| 8 |
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES |
| 9 |
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
| 10 |
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR |
| 11 |
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
| 12 |
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN |
| 13 |
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF |
| 14 |
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
| 15 |
*/ |
| 16 |
|
| 17 |
#include <ctype.h> |
| 18 |
#include <string.h> |
| 19 |
|
| 20 |
#include "bile.h" |
| 21 |
#include "db.h" |
| 22 |
#include "sha2.h" |
| 23 |
#include "subtext.h" |
| 24 |
#include "user.h" |
| 25 |
#include "util.h" |
| 26 |
|
| 27 |
static const char *BANNED_USERNAMES[] = { |
| 28 |
"admin", "administrator", "\"admin", "admin1", |
| 29 |
"client", "contact", |
| 30 |
"daemon", "default", |
| 31 |
"enable", |
| 32 |
"fraud", |
| 33 |
"guest", |
| 34 |
"help", "hostmaster", |
| 35 |
"mailer-daemon", |
| 36 |
"moderator", "moderators", |
| 37 |
"nobody", "notme", |
| 38 |
"postmaster", |
| 39 |
"root", "\"root", |
| 40 |
"security", "server", "support", "sventek", "sysop", "system", |
| 41 |
"test", |
| 42 |
"ubnt", "unknown", |
| 43 |
"webmaster", |
| 44 |
}; |
| 45 |
|
| 46 |
void |
| 47 |
user_cache_usernames(void) |
| 48 |
{ |
| 49 |
struct user user; |
| 50 |
struct username_cache *muser, *new_username_cache; |
| 51 |
struct bile_object *o; |
| 52 |
size_t new_nusers, nuser, len; |
| 53 |
|
| 54 |
new_nusers = bile_count_by_type(db->bile, DB_USER_RTYPE); |
| 55 |
new_username_cache = xreallocarray(db->username_cache, |
| 56 |
sizeof(struct username_cache), new_nusers); |
| 57 |
if (new_username_cache == NULL) |
| 58 |
return; |
| 59 |
|
| 60 |
db->nusers = new_nusers; |
| 61 |
db->username_cache = new_username_cache; |
| 62 |
|
| 63 |
nuser = 0; |
| 64 |
while ((o = bile_get_nth_of_type(db->bile, nuser, DB_USER_RTYPE))) { |
| 65 |
muser = &db->username_cache[nuser]; |
| 66 |
muser->id = o->id; |
| 67 |
|
| 68 |
len = bile_read(db->bile, DB_USER_RTYPE, o->id, (char *)&user, |
| 69 |
sizeof(user)); |
| 70 |
if (len == sizeof(user)) |
| 71 |
strncpy(muser->username, user.username, sizeof(muser->username)); |
| 72 |
else { |
| 73 |
warn("user_update_cache_map: user %lu read size %lu != %lu (%d)", |
| 74 |
o->id, len, sizeof(user), bile_error(db->bile)); |
| 75 |
memset(muser->username, 0, sizeof(muser->username)); |
| 76 |
} |
| 77 |
|
| 78 |
xfree(&o); |
| 79 |
nuser++; |
| 80 |
} |
| 81 |
} |
| 82 |
|
| 83 |
bool |
| 84 |
user_save(struct user *user) |
| 85 |
{ |
| 86 |
size_t len; |
| 87 |
|
| 88 |
if (!user->id) { |
| 89 |
user->id = bile_next_id(db->bile, DB_USER_RTYPE); |
| 90 |
if (!user->id) |
| 91 |
return false; |
| 92 |
user->created_at = Time; |
| 93 |
} |
| 94 |
|
| 95 |
len = bile_write(db->bile, DB_USER_RTYPE, user->id, |
| 96 |
user, sizeof(struct user)); |
| 97 |
if (len != sizeof(struct user)) |
| 98 |
panic("user_save: failed to write: %d", bile_error(db->bile)); |
| 99 |
|
| 100 |
bile_flush(db->bile, true); |
| 101 |
return true; |
| 102 |
} |
| 103 |
|
| 104 |
struct user * |
| 105 |
user_find(unsigned long id) |
| 106 |
{ |
| 107 |
struct user *user; |
| 108 |
char *data; |
| 109 |
size_t len; |
| 110 |
|
| 111 |
len = bile_read_alloc(db->bile, DB_USER_RTYPE, id, &data); |
| 112 |
if (len == 0 || data == NULL) |
| 113 |
return NULL; |
| 114 |
if (len != sizeof(struct user)) |
| 115 |
panic("user_find: bad user record size %lu != %lu", len, |
| 116 |
sizeof(struct user)); |
| 117 |
user = xmalloczero(sizeof(struct user)); |
| 118 |
if (user != NULL) |
| 119 |
memcpy(user, data, sizeof(struct user)); |
| 120 |
xfree(&data); |
| 121 |
return user; |
| 122 |
} |
| 123 |
|
| 124 |
struct user * |
| 125 |
user_find_by_username(const char *username) |
| 126 |
{ |
| 127 |
struct user suser; |
| 128 |
struct username_cache *muser; |
| 129 |
short n; |
| 130 |
size_t len; |
| 131 |
|
| 132 |
len = strlen(username); |
| 133 |
if (len > sizeof(suser.username)) |
| 134 |
return NULL; |
| 135 |
|
| 136 |
if (db->nusers == 0) |
| 137 |
user_cache_usernames(); |
| 138 |
|
| 139 |
for (n = 0; n < db->nusers; n++) { |
| 140 |
muser = &db->username_cache[n]; |
| 141 |
|
| 142 |
if (strcasecmp(muser->username, username) == 0) |
| 143 |
return user_find(muser->id); |
| 144 |
} |
| 145 |
|
| 146 |
return NULL; |
| 147 |
} |
| 148 |
|
| 149 |
struct username_cache * |
| 150 |
user_username(unsigned long id) |
| 151 |
{ |
| 152 |
struct username_cache *muser; |
| 153 |
size_t n; |
| 154 |
|
| 155 |
for (n = 0; n < db->nusers; n++) { |
| 156 |
muser = &db->username_cache[n]; |
| 157 |
if (muser->id == id) |
| 158 |
return muser; |
| 159 |
} |
| 160 |
|
| 161 |
return NULL; |
| 162 |
} |
| 163 |
|
| 164 |
short |
| 165 |
user_authenticate(struct user *user, const char *password) |
| 166 |
{ |
| 167 |
char hash[SHA256_DIGEST_STRING_LENGTH]; |
| 168 |
char *salted; |
| 169 |
size_t plen, slen; |
| 170 |
short n; |
| 171 |
unsigned char res; |
| 172 |
|
| 173 |
plen = strlen(password); |
| 174 |
slen = SHA256_DIGEST_STRING_LENGTH - 1 + plen; |
| 175 |
salted = xmalloc(slen); |
| 176 |
if (salted == NULL) |
| 177 |
return AUTH_USER_FAILED; |
| 178 |
memcpy(salted, user->password_salt, SHA256_DIGEST_STRING_LENGTH - 1); |
| 179 |
memcpy(salted + SHA256_DIGEST_STRING_LENGTH - 1, password, plen); |
| 180 |
|
| 181 |
SHA256Data((const u_int8_t *)salted, slen, hash); |
| 182 |
|
| 183 |
/* timing-safe comparison */ |
| 184 |
for (res = 0, n = 0; n < SHA256_DIGEST_STRING_LENGTH; n++) |
| 185 |
res |= (hash[n] ^ user->password_hash[n]); |
| 186 |
|
| 187 |
memset(&hash, 0, sizeof(hash)); |
| 188 |
memset(salted, 0, slen); |
| 189 |
xfree(&salted); |
| 190 |
|
| 191 |
if (res == 0) |
| 192 |
return AUTH_USER_OK; |
| 193 |
|
| 194 |
return AUTH_USER_FAILED; |
| 195 |
} |
| 196 |
|
| 197 |
bool |
| 198 |
user_set_password(struct user *user, const char *password) |
| 199 |
{ |
| 200 |
char hash[SHA256_DIGEST_STRING_LENGTH]; |
| 201 |
char salt[SHA256_DIGEST_STRING_LENGTH]; |
| 202 |
char *salted; |
| 203 |
unsigned long tmp[8]; |
| 204 |
size_t plen, slen; |
| 205 |
short n; |
| 206 |
|
| 207 |
/* make a random salt out of a sha256 of some random longs */ |
| 208 |
for (n = 0; n < nitems(tmp) - 1; n++) |
| 209 |
tmp[n] = xorshift32(); |
| 210 |
|
| 211 |
SHA256Data((const u_int8_t *)tmp, sizeof(tmp), salt); |
| 212 |
memcpy(&user->password_salt, &salt, sizeof(salt)); |
| 213 |
|
| 214 |
plen = strlen(password); |
| 215 |
slen = plen + sizeof(salt) - 1; |
| 216 |
salted = xmalloc(slen); |
| 217 |
if (salted == NULL) |
| 218 |
return false; |
| 219 |
memcpy(salted, salt, sizeof(salt) - 1); /* exclude null */ |
| 220 |
memcpy(salted + sizeof(salt) - 1, password, plen); |
| 221 |
SHA256Data((const u_int8_t *)salted, slen, hash); |
| 222 |
memset(salted, 0, slen); |
| 223 |
xfree(&salted); |
| 224 |
|
| 225 |
memcpy(&user->password_hash, &hash, sizeof(user->password_hash)); |
| 226 |
|
| 227 |
memset(&hash, 0, sizeof(hash)); |
| 228 |
memset(&salt, 0, sizeof(salt)); |
| 229 |
return true; |
| 230 |
} |
| 231 |
|
| 232 |
bool |
| 233 |
user_valid_username(struct user *user, char *username, char **error) |
| 234 |
{ |
| 235 |
struct user *ouser; |
| 236 |
unsigned long ouser_id; |
| 237 |
size_t len, n; |
| 238 |
char c; |
| 239 |
|
| 240 |
if (username == NULL) { |
| 241 |
*error = xstrdup("username cannot be empty"); |
| 242 |
return false; |
| 243 |
} |
| 244 |
|
| 245 |
len = strlen(username); |
| 246 |
if (len > DB_USERNAME_LENGTH) { |
| 247 |
*error = xmalloc(61); |
| 248 |
if (*error) |
| 249 |
snprintf(*error, 60, "username cannot be more than %d " |
| 250 |
"characters", DB_USERNAME_LENGTH); |
| 251 |
return false; |
| 252 |
} |
| 253 |
|
| 254 |
for (n = 0; n < len; n++) { |
| 255 |
c = username[n]; |
| 256 |
|
| 257 |
if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || |
| 258 |
(c >= 'a' && c <= 'z') || c == '_')) { |
| 259 |
*error = xstrdup("username cannot contain non-alphanumeric " |
| 260 |
"characters"); |
| 261 |
return false; |
| 262 |
} |
| 263 |
} |
| 264 |
|
| 265 |
if (strcasecmp(username, GUEST_USERNAME) == 0) { |
| 266 |
*error = xstrdup("username is already in use"); |
| 267 |
return false; |
| 268 |
} |
| 269 |
|
| 270 |
if (!(user != NULL && user->is_sysop) && |
| 271 |
user_username_is_banned(username)) { |
| 272 |
*error = xstrdup("username is not permitted"); |
| 273 |
return false; |
| 274 |
} |
| 275 |
|
| 276 |
if ((ouser = user_find_by_username(username))) { |
| 277 |
ouser_id = ouser->id; |
| 278 |
xfree(&ouser); |
| 279 |
|
| 280 |
if (user == NULL || ouser_id != user->id) { |
| 281 |
*error = xstrdup("username is already in use"); |
| 282 |
return false; |
| 283 |
} |
| 284 |
} |
| 285 |
|
| 286 |
return true; |
| 287 |
} |
| 288 |
|
| 289 |
bool |
| 290 |
user_username_is_banned(char *username) |
| 291 |
{ |
| 292 |
size_t n; |
| 293 |
|
| 294 |
for (n = 0; n < nitems(BANNED_USERNAMES); n++) { |
| 295 |
if (strcasecmp(username, BANNED_USERNAMES[n]) == 0) |
| 296 |
return true; |
| 297 |
} |
| 298 |
|
| 299 |
return false; |
| 300 |
} |
| 301 |
|
| 302 |
unsigned long |
| 303 |
user_first_sysop_id(void) |
| 304 |
{ |
| 305 |
struct user user; |
| 306 |
struct bile_object *o; |
| 307 |
size_t nuser; |
| 308 |
|
| 309 |
nuser = 0; |
| 310 |
while ((o = bile_get_nth_of_type(db->bile, nuser, DB_USER_RTYPE))) { |
| 311 |
bile_read(db->bile, DB_USER_RTYPE, o->id, (char *)&user, |
| 312 |
sizeof(user)); |
| 313 |
xfree(&o); |
| 314 |
if (user.is_sysop) |
| 315 |
return user.id; |
| 316 |
nuser++; |
| 317 |
} |
| 318 |
|
| 319 |
return 0; |
| 320 |
} |
| 321 |
|
| 322 |
void |
| 323 |
user_change_password(struct session *s, struct user *user) |
| 324 |
{ |
| 325 |
char *password = NULL, *password_confirm = NULL; |
| 326 |
|
| 327 |
if (!user) { |
| 328 |
session_printf(s, "{{B}}Error{{/B}}: Guest accounts " |
| 329 |
"cannot change passwords\r\n"); |
| 330 |
return; |
| 331 |
} |
| 332 |
|
| 333 |
while (!s->ending) { |
| 334 |
session_printf(s, "{{B}}New Password:{{/B}} "); |
| 335 |
session_flush(s); |
| 336 |
password = session_field_input(s, 64, 64, NULL, false, '*'); |
| 337 |
session_output(s, "\r\n", 2); |
| 338 |
session_flush(s); |
| 339 |
|
| 340 |
if (password == NULL || s->ending) |
| 341 |
break; |
| 342 |
|
| 343 |
if (password[0] == '\0') { |
| 344 |
session_printf(s, "{{B}}Error:{{/B}} " |
| 345 |
"Password cannot be blank\r\n"); |
| 346 |
xfree(&password); |
| 347 |
password = NULL; |
| 348 |
continue; |
| 349 |
} |
| 350 |
|
| 351 |
session_printf(s, "{{B}}New Password (again):{{/B}} "); |
| 352 |
session_flush(s); |
| 353 |
password_confirm = session_field_input(s, 64, 64, NULL, false, '*'); |
| 354 |
session_output(s, "\r\n", 2); |
| 355 |
session_flush(s); |
| 356 |
|
| 357 |
if (password_confirm == NULL || s->ending) |
| 358 |
break; |
| 359 |
|
| 360 |
if (strcmp(password_confirm, password) != 0) { |
| 361 |
session_printf(s, "{{B}}Error:{{/B}} " |
| 362 |
"Passwords do not match\r\n"); |
| 363 |
memset(password, 0, strlen(password)); |
| 364 |
xfree(&password); |
| 365 |
memset(password_confirm, 0, strlen(password_confirm)); |
| 366 |
xfree(&password_confirm); |
| 367 |
continue; |
| 368 |
} |
| 369 |
|
| 370 |
if (!user_set_password(user, password)) { |
| 371 |
session_logf(s, "Failed saving new password"); |
| 372 |
session_printf(s, "{{B}}Error:{{/B}} " |
| 373 |
"Failed saving new password\r\n"); |
| 374 |
break; |
| 375 |
} |
| 376 |
|
| 377 |
if (!user_save(user)) { |
| 378 |
session_logf(s, "Failed saving user account"); |
| 379 |
session_printf(s, "{{B}}Error:{{/B}} " |
| 380 |
"Failed saving user account\r\n"); |
| 381 |
break; |
| 382 |
} |
| 383 |
|
| 384 |
if (strcmp(s->user->username, user->username) == 0) { |
| 385 |
session_logf(s, "User changed password"); |
| 386 |
session_printf(s, "{{B}}Your password has been " |
| 387 |
"changed{{/B}}\r\n"); |
| 388 |
} else { |
| 389 |
session_logf(s, "User %s changed password for %s", |
| 390 |
s->user->username, user->username); |
| 391 |
session_printf(s, "{{B}}Password has been " |
| 392 |
"changed{{/B}}\r\n"); |
| 393 |
} |
| 394 |
|
| 395 |
break; |
| 396 |
} |
| 397 |
|
| 398 |
if (password != NULL) { |
| 399 |
memset(password, 0, strlen(password)); |
| 400 |
xfree(&password); |
| 401 |
} |
| 402 |
if (password_confirm != NULL) { |
| 403 |
memset(password_confirm, 0, strlen(password_confirm)); |
| 404 |
xfree(&password_confirm); |
| 405 |
} |
| 406 |
} |
| 407 |
|
| 408 |
void |
| 409 |
user_delete(struct user *user) |
| 410 |
{ |
| 411 |
struct bile_object *o; |
| 412 |
unsigned long msg_user_id, id; |
| 413 |
size_t n; |
| 414 |
|
| 415 |
/* delete the user's mail */ |
| 416 |
n = 0; |
| 417 |
while ((o = bile_get_nth_of_type(db->mail_bile, n, |
| 418 |
MAIL_SPOOL_MESSAGE_RTYPE))) { |
| 419 |
id = o->id; |
| 420 |
bile_read(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE, id, |
| 421 |
(char *)&msg_user_id, sizeof(msg_user_id)); |
| 422 |
xfree(&o); |
| 423 |
|
| 424 |
if (msg_user_id != user->id) { |
| 425 |
n++; |
| 426 |
continue; |
| 427 |
} |
| 428 |
|
| 429 |
bile_delete(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE, id, |
| 430 |
BILE_DELETE_FLAG_ZERO | BILE_DELETE_FLAG_PURGE); |
| 431 |
|
| 432 |
/* don't increase n, the remaining messages will shift down */ |
| 433 |
} |
| 434 |
|
| 435 |
/* delete the user */ |
| 436 |
bile_delete(db->bile, DB_USER_RTYPE, user->id, |
| 437 |
BILE_DELETE_FLAG_ZERO | BILE_DELETE_FLAG_PURGE); |
| 438 |
} |