/* * Copyright (c) 2021-2022 joshua stein * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #include #include "bile.h" #include "db.h" #include "sha2.h" #include "subtext.h" #include "user.h" #include "util.h" static const char *BANNED_USERNAMES[] = { "admin", "administrator", "\"admin", "admin1", "client", "contact", "daemon", "default", "enable", "fraud", "guest", "help", "hostmaster", "mailer-daemon", "moderator", "moderators", "nobody", "notme", "postmaster", "root", "\"root", "security", "server", "support", "sventek", "sysop", "system", "test", "ubnt", "unknown", "webmaster", }; void user_cache_usernames(void) { struct user user; struct username_cache *muser, *new_username_cache; struct bile_object *o; size_t new_nusers, nuser, len; new_nusers = bile_count_by_type(db->bile, DB_USER_RTYPE); new_username_cache = xreallocarray(db->username_cache, sizeof(struct username_cache), new_nusers); if (new_username_cache == NULL) return; db->nusers = new_nusers; db->username_cache = new_username_cache; nuser = 0; while ((o = bile_get_nth_of_type(db->bile, nuser, DB_USER_RTYPE))) { muser = &db->username_cache[nuser]; muser->id = o->id; len = bile_read(db->bile, DB_USER_RTYPE, o->id, (char *)&user, sizeof(user)); if (len == sizeof(user)) strncpy(muser->username, user.username, sizeof(muser->username)); else { warn("user_update_cache_map: user %lu read size %lu != %lu (%d)", o->id, len, sizeof(user), bile_error(db->bile)); memset(muser->username, 0, sizeof(muser->username)); } xfree(&o); nuser++; } } bool user_save(struct user *user) { size_t len; if (!user->id) { user->id = bile_next_id(db->bile, DB_USER_RTYPE); if (!user->id) return false; user->created_at = Time; } len = bile_write(db->bile, DB_USER_RTYPE, user->id, user, sizeof(struct user)); if (len != sizeof(struct user)) panic("user_save: failed to write: %d", bile_error(db->bile)); bile_flush(db->bile, true); return true; } struct user * user_find(unsigned long id) { struct user *user; char *data; size_t len; len = bile_read_alloc(db->bile, DB_USER_RTYPE, id, &data); if (len == 0 || data == NULL) return NULL; if (len != sizeof(struct user)) panic("user_find: bad user record size %lu != %lu", len, sizeof(struct user)); user = xmalloczero(sizeof(struct user)); if (user != NULL) memcpy(user, data, sizeof(struct user)); xfree(&data); return user; } struct user * user_find_by_username(const char *username) { struct user suser; struct username_cache *muser; short n; size_t len; len = strlen(username); if (len > sizeof(suser.username)) return NULL; if (db->nusers == 0) user_cache_usernames(); for (n = 0; n < db->nusers; n++) { muser = &db->username_cache[n]; if (strcasecmp(muser->username, username) == 0) return user_find(muser->id); } return NULL; } struct username_cache * user_username(unsigned long id) { struct username_cache *muser; size_t n; for (n = 0; n < db->nusers; n++) { muser = &db->username_cache[n]; if (muser->id == id) return muser; } return NULL; } short user_authenticate(struct user *user, const char *password) { char hash[SHA256_DIGEST_STRING_LENGTH]; char *salted; size_t plen, slen; short n; unsigned char res; plen = strlen(password); slen = SHA256_DIGEST_STRING_LENGTH - 1 + plen; salted = xmalloc(slen); if (salted == NULL) return AUTH_USER_FAILED; memcpy(salted, user->password_salt, SHA256_DIGEST_STRING_LENGTH - 1); memcpy(salted + SHA256_DIGEST_STRING_LENGTH - 1, password, plen); SHA256Data((const u_int8_t *)salted, slen, hash); /* timing-safe comparison */ for (res = 0, n = 0; n < SHA256_DIGEST_STRING_LENGTH; n++) res |= (hash[n] ^ user->password_hash[n]); memset(&hash, 0, sizeof(hash)); memset(salted, 0, slen); xfree(&salted); if (res == 0) return AUTH_USER_OK; return AUTH_USER_FAILED; } bool user_set_password(struct user *user, const char *password) { char hash[SHA256_DIGEST_STRING_LENGTH]; char salt[SHA256_DIGEST_STRING_LENGTH]; char *salted; unsigned long tmp[8]; size_t plen, slen; short n; /* make a random salt out of a sha256 of some random longs */ for (n = 0; n < nitems(tmp) - 1; n++) tmp[n] = xorshift32(); SHA256Data((const u_int8_t *)tmp, sizeof(tmp), salt); memcpy(&user->password_salt, &salt, sizeof(salt)); plen = strlen(password); slen = plen + sizeof(salt) - 1; salted = xmalloc(slen); if (salted == NULL) return false; memcpy(salted, salt, sizeof(salt) - 1); /* exclude null */ memcpy(salted + sizeof(salt) - 1, password, plen); SHA256Data((const u_int8_t *)salted, slen, hash); memset(salted, 0, slen); xfree(&salted); memcpy(&user->password_hash, &hash, sizeof(user->password_hash)); memset(&hash, 0, sizeof(hash)); memset(&salt, 0, sizeof(salt)); return true; } bool user_valid_username(struct user *user, char *username, char **error) { struct user *ouser; unsigned long ouser_id; size_t len, n; char c; if (username == NULL) { *error = xstrdup("username cannot be empty"); return false; } len = strlen(username); if (len > DB_USERNAME_LENGTH) { *error = xmalloc(61); if (*error) snprintf(*error, 60, "username cannot be more than %d " "characters", DB_USERNAME_LENGTH); return false; } for (n = 0; n < len; n++) { c = username[n]; if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_')) { *error = xstrdup("username cannot contain non-alphanumeric " "characters"); return false; } } if (strcasecmp(username, GUEST_USERNAME) == 0) { *error = xstrdup("username is already in use"); return false; } if (!(user != NULL && user->is_sysop) && user_username_is_banned(username)) { *error = xstrdup("username is not permitted"); return false; } if ((ouser = user_find_by_username(username))) { ouser_id = ouser->id; xfree(&ouser); if (user == NULL || ouser_id != user->id) { *error = xstrdup("username is already in use"); return false; } } return true; } bool user_username_is_banned(char *username) { size_t n; for (n = 0; n < nitems(BANNED_USERNAMES); n++) { if (strcasecmp(username, BANNED_USERNAMES[n]) == 0) return true; } return false; } unsigned long user_first_sysop_id(void) { struct user user; struct bile_object *o; size_t nuser; nuser = 0; while ((o = bile_get_nth_of_type(db->bile, nuser, DB_USER_RTYPE))) { bile_read(db->bile, DB_USER_RTYPE, o->id, (char *)&user, sizeof(user)); xfree(&o); if (user.is_sysop) return user.id; nuser++; } return 0; } void user_change_password(struct session *s, struct user *user) { char *password = NULL, *password_confirm = NULL; if (!user) { session_printf(s, "{{B}}Error{{/B}}: Guest accounts " "cannot change passwords\r\n"); return; } while (!s->ending) { session_printf(s, "{{B}}New Password:{{/B}} "); session_flush(s); password = session_field_input(s, 64, 64, NULL, false, '*'); session_output(s, "\r\n", 2); session_flush(s); if (password == NULL || s->ending) break; if (password[0] == '\0') { session_printf(s, "{{B}}Error:{{/B}} " "Password cannot be blank\r\n"); xfree(&password); password = NULL; continue; } session_printf(s, "{{B}}New Password (again):{{/B}} "); session_flush(s); password_confirm = session_field_input(s, 64, 64, NULL, false, '*'); session_output(s, "\r\n", 2); session_flush(s); if (password_confirm == NULL || s->ending) break; if (strcmp(password_confirm, password) != 0) { session_printf(s, "{{B}}Error:{{/B}} " "Passwords do not match\r\n"); memset(password, 0, strlen(password)); xfree(&password); memset(password_confirm, 0, strlen(password_confirm)); xfree(&password_confirm); continue; } if (!user_set_password(user, password)) { session_logf(s, "Failed saving new password"); session_printf(s, "{{B}}Error:{{/B}} " "Failed saving new password\r\n"); break; } if (!user_save(user)) { session_logf(s, "Failed saving user account"); session_printf(s, "{{B}}Error:{{/B}} " "Failed saving user account\r\n"); break; } if (strcmp(s->user->username, user->username) == 0) { session_logf(s, "User changed password"); session_printf(s, "{{B}}Your password has been " "changed{{/B}}\r\n"); } else { session_logf(s, "User %s changed password for %s", s->user->username, user->username); session_printf(s, "{{B}}Password has been " "changed{{/B}}\r\n"); } break; } if (password != NULL) { memset(password, 0, strlen(password)); xfree(&password); } if (password_confirm != NULL) { memset(password_confirm, 0, strlen(password_confirm)); xfree(&password_confirm); } } void user_delete(struct user *user) { struct bile_object *o; unsigned long msg_user_id, id; size_t n; /* delete the user's mail */ n = 0; while ((o = bile_get_nth_of_type(db->mail_bile, n, MAIL_SPOOL_MESSAGE_RTYPE))) { id = o->id; bile_read(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE, id, (char *)&msg_user_id, sizeof(msg_user_id)); xfree(&o); if (msg_user_id != user->id) { n++; continue; } bile_delete(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE, id, BILE_DELETE_FLAG_ZERO | BILE_DELETE_FLAG_PURGE); /* don't increase n, the remaining messages will shift down */ } /* delete the user */ bile_delete(db->bile, DB_USER_RTYPE, user->id, BILE_DELETE_FLAG_ZERO | BILE_DELETE_FLAG_PURGE); }