diff --git a/src/httpd.js.c b/src/httpd.js.c index 45b9656c..2c954c15 100644 --- a/src/httpd.js.c +++ b/src/httpd.js.c @@ -18,8 +18,6 @@ #include "sodium/crypto_sign.h" #include "sodium/utils.h" -#include "sqlite3.h" - #include #include #include @@ -36,7 +34,6 @@ const int64_t k_refresh_interval = 1ULL * 7 * 24 * 60 * 60 * 1000; static JSValue _authenticate_jwt(JSContext* context, const char* jwt); -static const char* _get_property(tf_ssb_t* ssb, const char* id, const char* key); static JSValue _httpd_websocket_upgrade(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv); static const char* _make_session_jwt(tf_ssb_t* ssb, const char* name); static const char* _make_set_session_cookie_header(tf_http_request_t* request, const char* session_cookie); @@ -473,7 +470,7 @@ static JSValue _httpd_auth_query(JSContext* context, JSValueConst this_val, int JSValue name = JS_GetPropertyStr(context, entry, "name"); const char* name_string = JS_ToCString(context, name); - const char* settings = _get_property(ssb, "core", "settings"); + const char* settings = tf_ssb_db_get_property(ssb, "core", "settings"); JSValue settings_value = settings ? JS_ParseJSON(context, settings, strlen(settings), NULL) : JS_UNDEFINED; JSValue permissions = JS_GetPropertyStr(context, settings_value, "permissions"); JSValue user_permissions = JS_GetPropertyStr(context, permissions, name_string); @@ -1096,106 +1093,6 @@ static bool _is_name_valid(const char* name) return true; } -static bool _read_account(tf_ssb_t* ssb, const char* name, char* out_passwd, size_t passwd_size) -{ - bool result = false; - sqlite3* db = tf_ssb_acquire_db_reader(ssb); - sqlite3_stmt* statement = NULL; - if (sqlite3_prepare(db, "SELECT value ->> '$.password' FROM properties WHERE id = 'auth' AND key = 'user:' || ?", -1, &statement, NULL) == SQLITE_OK) - { - if (sqlite3_bind_text(statement, 1, name, -1, NULL) == SQLITE_OK) - { - if (sqlite3_step(statement) == SQLITE_ROW) - { - snprintf(out_passwd, passwd_size, "%s", (const char*)sqlite3_column_text(statement, 0)); - result = true; - } - } - sqlite3_finalize(statement); - } - tf_ssb_release_db_reader(ssb, db); - return result; -} - -static bool _set_account_password(JSContext* context, sqlite3* db, const char* name, const char* password) -{ - bool result = false; - static const int k_salt_length = 12; - - char buffer[16]; - tf_task_t* task = tf_task_get(context); - size_t bytes = uv_random(tf_task_get_loop(task), &(uv_random_t) { 0 }, buffer, sizeof(buffer), 0, NULL) == 0 ? sizeof(buffer) : 0; - char output[7 + 22 + 1]; - char* salt = crypt_gensalt_rn("$2b$", k_salt_length, buffer, bytes, output, sizeof(output)); - char hash_output[7 + 22 + 31 + 1]; - char* hash = crypt_rn(password, salt, hash_output, sizeof(hash_output)); - - JSValue user_entry = JS_NewObject(context); - JS_SetPropertyStr(context, user_entry, "password", JS_NewString(context, hash)); - JSValue user_json = JS_JSONStringify(context, user_entry, JS_NULL, JS_NULL); - size_t user_length = 0; - const char* user_string = JS_ToCStringLen(context, &user_length, user_json); - - sqlite3_stmt* statement = NULL; - if (sqlite3_prepare(db, "INSERT OR REPLACE INTO properties (id, key, value) VALUES ('auth', 'user:' || ?, ?)", -1, &statement, NULL) == SQLITE_OK) - { - if (sqlite3_bind_text(statement, 1, name, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, user_string, user_length, NULL) == SQLITE_OK) - { - result = sqlite3_step(statement) == SQLITE_DONE; - } - sqlite3_finalize(statement); - } - - JS_FreeCString(context, user_string); - JS_FreeValue(context, user_json); - JS_FreeValue(context, user_entry); - return result; -} - -static bool _register_account(tf_ssb_t* ssb, const char* name, const char* password) -{ - bool result = false; - JSContext* context = tf_ssb_get_context(ssb); - JSValue users_array = JS_UNDEFINED; - - sqlite3* db = tf_ssb_acquire_db_writer(ssb); - sqlite3_stmt* statement = NULL; - if (sqlite3_prepare(db, "SELECT value FROM properties WHERE id = 'auth' AND key = 'users'", -1, &statement, NULL) == SQLITE_OK) - { - if (sqlite3_step(statement) == SQLITE_ROW) - { - users_array = JS_ParseJSON(context, (const char*)sqlite3_column_text(statement, 0), sqlite3_column_bytes(statement, 0), NULL); - } - sqlite3_finalize(statement); - } - if (JS_IsUndefined(users_array)) - { - users_array = JS_NewArray(context); - } - int length = tf_util_get_length(context, users_array); - JS_SetPropertyUint32(context, users_array, length, JS_NewString(context, name)); - - JSValue json = JS_JSONStringify(context, users_array, JS_NULL, JS_NULL); - JS_FreeValue(context, users_array); - size_t value_length = 0; - const char* value = JS_ToCStringLen(context, &value_length, json); - if (sqlite3_prepare(db, "INSERT OR REPLACE INTO properties (id, key, value) VALUES ('auth', 'users', ?)", -1, &statement, NULL) == SQLITE_OK) - { - if (sqlite3_bind_text(statement, 1, value, value_length, NULL) == SQLITE_OK) - { - result = sqlite3_step(statement) == SQLITE_DONE; - } - sqlite3_finalize(statement); - } - JS_FreeCString(context, value); - JS_FreeValue(context, json); - - result = result && _set_account_password(context, db, name, password); - - tf_ssb_release_db_writer(ssb, db); - return result; -} - static void _visit_auth_identity(const char* identity, void* user_data) { if (!*(char*)user_data) @@ -1275,47 +1172,6 @@ static bool _verify_password(const char* password, const char* hash) return out_hash && strcmp(hash, out_hash) == 0; } -static const char* _get_property(tf_ssb_t* ssb, const char* id, const char* key) -{ - char* result = NULL; - sqlite3* db = tf_ssb_acquire_db_reader(ssb); - sqlite3_stmt* statement = NULL; - if (sqlite3_prepare(db, "SELECT value FROM properties WHERE id = ? AND key = ?", -1, &statement, NULL) == SQLITE_OK) - { - if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, key, -1, NULL) == SQLITE_OK) - { - if (sqlite3_step(statement) == SQLITE_ROW) - { - size_t length = sqlite3_column_bytes(statement, 0); - result = tf_malloc(length + 1); - memcpy(result, sqlite3_column_text(statement, 0), length); - result[length] = '\0'; - } - } - sqlite3_finalize(statement); - } - tf_ssb_release_db_reader(ssb, db); - return result; -} - -static bool _set_property(tf_ssb_t* ssb, const char* id, const char* key, const char* value) -{ - bool result = false; - sqlite3* db = tf_ssb_acquire_db_writer(ssb); - sqlite3_stmt* statement = NULL; - if (sqlite3_prepare(db, "INSERT OR REPLACE INTO properties (id, key, value) VALUES (?, ?, ?)", -1, &statement, NULL) == SQLITE_OK) - { - if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, key, -1, NULL) == SQLITE_OK && - sqlite3_bind_text(statement, 3, value, -1, NULL) == SQLITE_OK) - { - result = sqlite3_step(statement) == SQLITE_DONE; - } - sqlite3_finalize(statement); - } - tf_ssb_release_db_writer(ssb, db); - return result; -} - static void _httpd_endpoint_login(tf_http_request_t* request) { tf_task_t* task = request->user_data; @@ -1362,11 +1218,11 @@ static void _httpd_endpoint_login(tf_http_request_t* request) const char* change = _form_data_get(post_form_data, "change"); const char* form_register = _form_data_get(post_form_data, "register"); char account_passwd[256] = { 0 }; - bool have_account = _read_account(ssb, _form_data_get(post_form_data, "name"), account_passwd, sizeof(account_passwd)); + bool have_account = tf_ssb_db_get_account_password_hash(ssb, _form_data_get(post_form_data, "name"), account_passwd, sizeof(account_passwd)); if (form_register && strcmp(form_register, "1") == 0) { - if (!have_account && _is_name_valid(account_name) && password && confirm && strcmp(password, confirm) == 0 && _register_account(ssb, account_name, password)) + if (!have_account && _is_name_valid(account_name) && password && confirm && strcmp(password, confirm) == 0 && tf_ssb_db_register_account(ssb, account_name, password)) { tf_free((void*)send_session); send_session = _make_session_jwt(ssb, account_name); @@ -1379,9 +1235,8 @@ static void _httpd_endpoint_login(tf_http_request_t* request) } else if (change && strcmp(change, "1") == 0) { - sqlite3* db = tf_ssb_acquire_db_writer(ssb); if (have_account && _is_name_valid(account_name) && new_password && confirm && strcmp(new_password, confirm) == 0 && _verify_password(password, account_passwd) && - _set_account_password(context, db, account_name, new_password)) + tf_ssb_db_set_account_password(ssb, account_name, new_password)) { tf_free((void*)send_session); send_session = _make_session_jwt(ssb, account_name); @@ -1390,7 +1245,6 @@ static void _httpd_endpoint_login(tf_http_request_t* request) { login_error = "Error changing password."; } - tf_ssb_release_db_writer(ssb, db); } else { @@ -1438,7 +1292,7 @@ static void _httpd_endpoint_login(tf_http_request_t* request) { tf_http_request_ref(request); - const char* settings = _get_property(ssb, "core", "settings"); + const char* settings = tf_ssb_db_get_property(ssb, "core", "settings"); JSValue settings_value = settings ? JS_ParseJSON(context, settings, strlen(settings), NULL) : JS_UNDEFINED; JSValue code_of_conduct_value = JS_GetPropertyStr(context, settings_value, "code_of_conduct"); const char* code_of_conduct = JS_ToCString(context, code_of_conduct_value); @@ -1494,7 +1348,7 @@ static void _httpd_endpoint_login(tf_http_request_t* request) JSValue settings_json = JS_JSONStringify(context, settings_value, JS_NULL, JS_NULL); const char* settings_string = JS_ToCString(context, settings_json); - _set_property(ssb, "core", "settings", settings_string); + tf_ssb_db_set_property(ssb, "core", "settings", settings_string); JS_FreeCString(context, settings_string); JS_FreeValue(context, settings_json); } diff --git a/src/ssb.db.c b/src/ssb.db.c index c3095ebf..10a8b228 100644 --- a/src/ssb.db.c +++ b/src/ssb.db.c @@ -6,6 +6,7 @@ #include "trace.h" #include "util.js.h" +#include "ow-crypt.h" #include "sodium/crypto_hash_sha256.h" #include "sodium/crypto_sign.h" #include "sqlite3.h" @@ -1592,3 +1593,145 @@ void tf_ssb_db_forget_stored_connection(tf_ssb_t* ssb, const char* address, int } tf_ssb_release_db_writer(ssb, db); } + +bool tf_ssb_db_get_account_password_hash(tf_ssb_t* ssb, const char* name, char* out_password, size_t password_size) +{ + bool result = false; + sqlite3* db = tf_ssb_acquire_db_reader(ssb); + sqlite3_stmt* statement = NULL; + if (sqlite3_prepare(db, "SELECT value ->> '$.password' FROM properties WHERE id = 'auth' AND key = 'user:' || ?", -1, &statement, NULL) == SQLITE_OK) + { + if (sqlite3_bind_text(statement, 1, name, -1, NULL) == SQLITE_OK) + { + if (sqlite3_step(statement) == SQLITE_ROW) + { + snprintf(out_password, password_size, "%s", (const char*)sqlite3_column_text(statement, 0)); + result = true; + } + } + sqlite3_finalize(statement); + } + tf_ssb_release_db_reader(ssb, db); + return result; +} + +bool tf_ssb_db_set_account_password(tf_ssb_t* ssb, const char* name, const char* password) +{ + JSContext* context = tf_ssb_get_context(ssb); + bool result = false; + static const int k_salt_length = 12; + + char buffer[16]; + size_t bytes = uv_random(tf_ssb_get_loop(ssb), &(uv_random_t) { 0 }, buffer, sizeof(buffer), 0, NULL) == 0 ? sizeof(buffer) : 0; + char output[7 + 22 + 1]; + char* salt = crypt_gensalt_rn("$2b$", k_salt_length, buffer, bytes, output, sizeof(output)); + char hash_output[7 + 22 + 31 + 1]; + char* hash = crypt_rn(password, salt, hash_output, sizeof(hash_output)); + + JSValue user_entry = JS_NewObject(context); + JS_SetPropertyStr(context, user_entry, "password", JS_NewString(context, hash)); + JSValue user_json = JS_JSONStringify(context, user_entry, JS_NULL, JS_NULL); + size_t user_length = 0; + const char* user_string = JS_ToCStringLen(context, &user_length, user_json); + + sqlite3* db = tf_ssb_acquire_db_writer(ssb); + sqlite3_stmt* statement = NULL; + if (sqlite3_prepare(db, "INSERT OR REPLACE INTO properties (id, key, value) VALUES ('auth', 'user:' || ?, ?)", -1, &statement, NULL) == SQLITE_OK) + { + if (sqlite3_bind_text(statement, 1, name, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, user_string, user_length, NULL) == SQLITE_OK) + { + result = sqlite3_step(statement) == SQLITE_DONE; + } + sqlite3_finalize(statement); + } + tf_ssb_release_db_writer(ssb, db); + + JS_FreeCString(context, user_string); + JS_FreeValue(context, user_json); + JS_FreeValue(context, user_entry); + return result; +} + +bool tf_ssb_db_register_account(tf_ssb_t* ssb, const char* name, const char* password) +{ + bool result = false; + JSContext* context = tf_ssb_get_context(ssb); + JSValue users_array = JS_UNDEFINED; + + sqlite3* db = tf_ssb_acquire_db_writer(ssb); + sqlite3_stmt* statement = NULL; + if (sqlite3_prepare(db, "SELECT value FROM properties WHERE id = 'auth' AND key = 'users'", -1, &statement, NULL) == SQLITE_OK) + { + if (sqlite3_step(statement) == SQLITE_ROW) + { + users_array = JS_ParseJSON(context, (const char*)sqlite3_column_text(statement, 0), sqlite3_column_bytes(statement, 0), NULL); + } + sqlite3_finalize(statement); + } + if (JS_IsUndefined(users_array)) + { + users_array = JS_NewArray(context); + } + int length = tf_util_get_length(context, users_array); + JS_SetPropertyUint32(context, users_array, length, JS_NewString(context, name)); + + JSValue json = JS_JSONStringify(context, users_array, JS_NULL, JS_NULL); + JS_FreeValue(context, users_array); + size_t value_length = 0; + const char* value = JS_ToCStringLen(context, &value_length, json); + if (sqlite3_prepare(db, "INSERT OR REPLACE INTO properties (id, key, value) VALUES ('auth', 'users', ?)", -1, &statement, NULL) == SQLITE_OK) + { + if (sqlite3_bind_text(statement, 1, value, value_length, NULL) == SQLITE_OK) + { + result = sqlite3_step(statement) == SQLITE_DONE; + } + sqlite3_finalize(statement); + } + JS_FreeCString(context, value); + JS_FreeValue(context, json); + tf_ssb_release_db_writer(ssb, db); + + result = result && tf_ssb_db_set_account_password(ssb, name, password); + return result; +} + +const char* tf_ssb_db_get_property(tf_ssb_t* ssb, const char* id, const char* key) +{ + char* result = NULL; + sqlite3* db = tf_ssb_acquire_db_reader(ssb); + sqlite3_stmt* statement = NULL; + if (sqlite3_prepare(db, "SELECT value FROM properties WHERE id = ? AND key = ?", -1, &statement, NULL) == SQLITE_OK) + { + if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, key, -1, NULL) == SQLITE_OK) + { + if (sqlite3_step(statement) == SQLITE_ROW) + { + size_t length = sqlite3_column_bytes(statement, 0); + result = tf_malloc(length + 1); + memcpy(result, sqlite3_column_text(statement, 0), length); + result[length] = '\0'; + } + } + sqlite3_finalize(statement); + } + tf_ssb_release_db_reader(ssb, db); + return result; +} + +bool tf_ssb_db_set_property(tf_ssb_t* ssb, const char* id, const char* key, const char* value) +{ + bool result = false; + sqlite3* db = tf_ssb_acquire_db_writer(ssb); + sqlite3_stmt* statement = NULL; + if (sqlite3_prepare(db, "INSERT OR REPLACE INTO properties (id, key, value) VALUES (?, ?, ?)", -1, &statement, NULL) == SQLITE_OK) + { + if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, key, -1, NULL) == SQLITE_OK && + sqlite3_bind_text(statement, 3, value, -1, NULL) == SQLITE_OK) + { + result = sqlite3_step(statement) == SQLITE_DONE; + } + sqlite3_finalize(statement); + } + tf_ssb_release_db_writer(ssb, db); + return result; +} diff --git a/src/ssb.db.h b/src/ssb.db.h index 6990aaec..d1fa685c 100644 --- a/src/ssb.db.h +++ b/src/ssb.db.h @@ -311,6 +311,53 @@ tf_ssb_db_stored_connection_t* tf_ssb_db_get_stored_connections(tf_ssb_t* ssb, i */ void tf_ssb_db_forget_stored_connection(tf_ssb_t* ssb, const char* address, int port, const char* pubkey); +/** +** Retrieve a user's hashed password from the database. +** @param ssb The SSB instance. +** @param name The username. +** @param[out] out_password Populated with the password. +** @param password_size The size of the out_password buffer. +** @return true if the password hash was successfully retrieved. +*/ +bool tf_ssb_db_get_account_password_hash(tf_ssb_t* ssb, const char* name, char* out_password, size_t password_size); + +/** +** Insert or update a user's hashed password in the database. +** @param ssb The SSB instance. +** @param name The username. +** @param password The raw password. +** @return true if the hash of the password was successfully stored. +*/ +bool tf_ssb_db_set_account_password(tf_ssb_t* ssb, const char* name, const char* password); + +/** +** Add a user account to the database. +** @param ssb The SSB instance. +** @param name The username to add. +** @param password The user's raw password. +** @return true If the user was added successfully. +*/ +bool tf_ssb_db_register_account(tf_ssb_t* ssb, const char* name, const char* password); + +/** +** Get an entry from the properties table. +** @param ssb The SSB instance. +** @param id The user. +** @param key The property key. +** @return The property value or null. Free with tf_free(). +*/ +const char* tf_ssb_db_get_property(tf_ssb_t* ssb, const char* id, const char* key); + +/** +** Store an entry in the properties table. +** @param ssb The SSB instance. +** @param id The user. +** @param key The property key. +** @param value The property value. +** @return true if the property was stored successfully. +*/ +bool tf_ssb_db_set_property(tf_ssb_t* ssb, const char* id, const char* key, const char* value); + /** ** An SQLite authorizer callback. See https://www.sqlite.org/c3ref/set_authorizer.html for use. ** @param user_data User data registered with the authorizer.