/* * nanobbs, a tiny forums software. * Copyright (C) 2025-2026 Xavier Del Campo Romero * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ #define _POSIX_C_SOURCE 200809L #include "endpoints.h" #include "auth.h" #include "db.h" #include "defs.h" #include "form.h" #include #include #include #include #include #include #include #include #include #include #include struct passwd { bool valid; sqlite3 *db; sqlite3_stmt *stmt; char *username, *old, *new; }; static void free_passwd(struct passwd *const p) { int error; if (!p) return; sqlite3_finalize(p->stmt); if ((error = sqlite3_close(p->db) != SQLITE_OK)) fprintf(stderr, "%s: sqlite3_close: %s\n", __func__, sqlite3_errstr(error)); free(p->username); free(p->old); free(p->new); free(p); } static int row(struct passwd *const p) { char *const hashpwd = db_str(p->db, p->stmt, "password"); if (!hashpwd) { fprintf(stderr, "%s: missing password\n", __func__); return -1; } else if (!crypto_pwhash_str_verify(hashpwd, p->old, strlen(p->old))) p->valid = true; free(hashpwd); return 0; } static int finalize_update(struct passwd *const p, struct http_response *const r) { *r = (const struct http_response){.status = HTTP_STATUS_SEE_OTHER}; if (http_response_add_header(r, "Location", "/")) { fprintf(stderr, "%s: http_response_add_header failed\n", __func__); return -1; } free_passwd(p); return 0; } static int update(const struct http_payload *const pl, struct http_response *const r, void *const user, void *const args) { struct passwd *const p = args; sqlite3_stmt *const stmt = p->stmt; const int error = sqlite3_step(stmt); switch (error) { case SQLITE_BUSY: break; case SQLITE_DONE: if (finalize_update(p, r)) goto failure; break; default: fprintf(stderr, "%s: sqlite3_step: %s\n", __func__, sqlite3_errstr(error)); sqlite3_reset(stmt); goto failure; } return 0; failure: free_passwd(p); return -1; } static int prepare_change(struct passwd *const p, struct http_response *const r) { int ret = -1, error; struct dynstr d; sqlite3_stmt *stmt = NULL; char hashpwd[crypto_pwhash_STRBYTES]; dynstr_init(&d); if (crypto_pwhash_str(hashpwd, p->new, strlen(p->new), crypto_pwhash_OPSLIMIT_INTERACTIVE, crypto_pwhash_MEMLIMIT_INTERACTIVE)) { fprintf(stderr, "%s: crypto_pwhash_str failed\n", __func__); goto end; } else if (dynstr_append(&d, "UPDATE users SET password='%s' WHERE name = '%s';", hashpwd, p->username)) { fprintf(stderr, "%s: dynstr_append failed\n", __func__); goto end; } else if ((error = sqlite3_prepare_v2(p->db, d.str, d.len, &stmt, NULL)) != SQLITE_OK) { fprintf(stderr, "%s: sqlite3_prepare_v2 \"%s\": %s\n", __func__, d.str, sqlite3_errstr(error)); goto end; } p->stmt = stmt; *r = (const struct http_response) { .step.payload = update, .args = p }; ret = 0; end: dynstr_free(&d); return ret; } static int finalize(struct passwd *const p, struct http_response *const r) { const int error = sqlite3_finalize(p->stmt); p->stmt = NULL; if (error != SQLITE_OK) { fprintf(stderr, "%s: sqlite3_finalize: %s\n", __func__, sqlite3_errstr(error)); return -1; } else if (!p->valid) { const int ret = form_unauthorized("Invalid current password", r); free_passwd(p); return ret; } else if (prepare_change(p, r)) { fprintf(stderr, "%s: prepare_change failed\n", __func__); return -1; } return 0; } static int run_query(const struct http_payload *const pl, struct http_response *const r, void *const user, void *const args) { struct passwd *const p = args; sqlite3_stmt *const stmt = p->stmt; const int error = sqlite3_step(stmt); switch (error) { case SQLITE_BUSY: break; case SQLITE_DONE: if (finalize(p, r)) goto failure; break; case SQLITE_ROW: if (row(p)) goto failure; break; default: fprintf(stderr, "%s: sqlite3_step: %s\n", __func__, sqlite3_errstr(error)); sqlite3_reset(stmt); goto failure; } return 0; failure: free_passwd(p); return -1; } static int passwd(const struct cfg *const cfg, const char *const username, const char *const old, const char *const new, sqlite3 *const db, struct http_response *const r) { int ret = -1, error; struct dynstr d; sqlite3_stmt *stmt = NULL; char *userdup = NULL, *olddup = NULL, *newdup = NULL; struct passwd *p = NULL; dynstr_init(&d); if (!(userdup = strdup(username)) || !(olddup = strdup(old)) || !(newdup = strdup(new))) { fprintf(stderr, "%s: strdup(3): %s\n", __func__, strerror(errno)); goto end; } else if (dynstr_append(&d, "SELECT password from users WHERE name = '%s'", username)) { fprintf(stderr, "%s: dynstr_append failed\n", __func__); goto end; } else if ((error = sqlite3_prepare_v2(db, d.str, d.len, &stmt, NULL)) != SQLITE_OK) { fprintf(stderr, "%s: sqlite3_prepare_v2 \"%s\": %s\n", __func__, d.str, sqlite3_errstr(error)); goto end; } else if (!(p = malloc(sizeof *p))) { fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno)); goto end; } *p = (const struct passwd) { .username = userdup, .old = olddup, .new = newdup, .stmt = stmt, .db = db }; *r = (const struct http_response) { .step.payload = run_query, .args = p }; ret = 0; end: if (ret) { free(p); free(userdup); free(olddup); free(newdup); sqlite3_finalize(stmt); } dynstr_free(&d); return ret; } static int setup(const struct http_payload *const p, struct http_response *const r, void *const user, sqlite3 *const db, const struct auth_user *const u) { int ret = -1, error; struct form *f = NULL; const char *old, *new, *cnew; const struct cfg *const cfg = user; *r = (const struct http_response){0}; if (!u) { ret = form_unauthorized("Authentication required", r); goto failure; } else if ((error = form_alloc(p->u.post.data, &f))) { if ((ret = error) < 0) fprintf(stderr, "%s: form_alloc failed\n", __func__); else ret = form_badreq("Invalid request", r); goto failure; } else if (!(old = form_value(f, "old"))) { ret = form_badreq("Missing old password", r); goto failure; } else if (!(new = form_value(f, "new"))) { ret = form_badreq("Missing new password", r); goto failure; } else if (!(cnew = form_value(f, "cnew"))) { ret = form_badreq("Missing confirmation password", r); goto failure; } else if (strcmp(new, cnew)) { ret = form_badreq("New password mismatch", r); goto failure; } else if (strlen(new) < MINPWDLEN) { ret = form_shortpwd(r); goto failure; } else if ((ret = passwd(cfg, u->username, old, new, db, r))) { fprintf(stderr, "%s: passwd failed\n", __func__); goto failure; } form_free(f); return 0; failure: if ((error = sqlite3_close(db)) != SQLITE_OK) fprintf(stderr, "%s: sqlite3_close: %s\n", __func__, sqlite3_errstr(error)); form_free(f); return ret; } int ep_passwd(const struct http_payload *const p, struct http_response *const r, void *const user) { const int n = auth_validate(p, r, user, setup); if (n < 0) fprintf(stderr, "%s: auth_validate failed\n", __func__); else if (n) { const struct cfg *const cfg = user; sqlite3 *db; if (db_open(cfg->dir, &db)) { fprintf(stderr, "%s: db_open failed\n", __func__); return -1; } return setup(p, r, user, db, NULL); } return n; }