diff --git a/core/app.js b/core/app.js index 601dceba..2735e9e4 100644 --- a/core/app.js +++ b/core/app.js @@ -1,4 +1,3 @@ -import * as auth from './auth.js'; import * as core from './core.js'; let g_next_id = 1; @@ -87,7 +86,7 @@ App.prototype.send = function (message) { function socket(request, response, client) { let process; let options = {}; - let credentials = auth.query(request.headers); + let credentials = httpd.auth_query(request.headers); response.onClose = async function () { if (process && process.task) { diff --git a/core/auth.js b/core/auth.js deleted file mode 100644 index 4114b1d8..00000000 --- a/core/auth.js +++ /dev/null @@ -1,130 +0,0 @@ -import * as core from './core.js'; - -/** - * TODOC - * @param {string} value - * @returns - */ -function unb64url(value) { - value = value.replaceAll('-', '+').replaceAll('_', '/'); - let remainder = value.length % 4; - - if (remainder == 3) { - return value + '='; - } else if (remainder == 2) { - return value + '=='; - } else { - return value; - } -} - -/** - * Validates a JWT ? - * @param {*} session TODOC - * @returns - */ -function readSession(session) { - let jwt_parts = session?.split('.'); - - if (jwt_parts?.length === 3) { - let [header, payload, signature] = jwt_parts; - header = JSON.parse(utf8Decode(base64Decode(unb64url(header)))); - - if (header.typ === 'JWT' && header.alg === 'HS256') { - signature = unb64url(signature); - let id = ssb.getIdentities(':auth'); - - if (id?.length && ssb.hmacsha256verify(id[0], payload, signature)) { - const result = JSON.parse(utf8Decode(base64Decode(unb64url(payload)))); - const now = new Date().valueOf(); - - if (now < result.exp) { - print(`JWT valid for another ${(result.exp - now) / 1000} seconds.`); - return result; - } else { - print(`JWT expired by ${(now - result.exp) / 1000} seconds.`); - } - } else { - print('JWT verification failed.'); - } - } else { - print('Invalid JWT header.'); - } - } -} - -/** - * TODOC - * @param {*} headers most likely an object - * @returns - */ -function getCookies(headers) { - let cookies = {}; - - if (headers.cookie) { - let parts = headers.cookie.split(/,|;/); - for (let i in parts) { - let equals = parts[i].indexOf('='); - let name = parts[i].substring(0, equals).trim(); - let value = parts[i].substring(equals + 1).trim(); - cookies[name] = value; - } - } - - return cookies; -} - -/** - * Gets a user's permissions based on it's session ? - * @param {*} session TODOC - * @returns - */ -function getPermissions(session) { - let permissions; - let entry = readSession(session); - if (entry) { - permissions = getPermissionsForUser(entry.name); - permissions.authenticated = entry.name !== 'guest'; - } - return permissions || {}; -} - -/** - * Get a user's permissions ? - * @param {string} userName TODOC - * @returns - */ -function getPermissionsForUser(userName) { - let permissions = {}; - if ( - core.globalSettings && - core.globalSettings.permissions && - core.globalSettings.permissions[userName] - ) { - for (let i in core.globalSettings.permissions[userName]) { - permissions[core.globalSettings.permissions[userName][i]] = true; - } - } - return permissions; -} - -/** - * TODOC - * @param {*} headers - * @returns - */ -function query(headers) { - let session = getCookies(headers).session; - let entry; - let autologin = tildefriends.args.autologin; - if ((entry = autologin ? {name: autologin} : readSession(session))) { - return { - session: entry, - permissions: autologin - ? getPermissionsForUser(autologin) - : getPermissions(session), - }; - } -} - -export {query}; diff --git a/core/core.js b/core/core.js index cd331ba8..f2698f59 100644 --- a/core/core.js +++ b/core/core.js @@ -1,5 +1,4 @@ import * as app from './app.js'; -import * as auth from './auth.js'; import * as form from './form.js'; import * as http from './http.js'; @@ -967,7 +966,7 @@ async function useAppHandler( }, respond: do_resolve, }, - credentials: auth.query(headers), + credentials: httpd.auth_query(headers), packageOwner: packageOwner, packageName: packageName, } @@ -1098,7 +1097,7 @@ async function blobHandler(request, response, blobId, uri) { if ((match = /^\/\~(\w+)\/(\w+)$/.exec(blobId))) { let user = match[1]; let appName = match[2]; - let credentials = auth.query(request.headers); + let credentials = httpd.auth_query(request.headers); if ( credentials && credentials.session && @@ -1161,7 +1160,7 @@ async function blobHandler(request, response, blobId, uri) { if ((match = /^\/\~(\w+)\/(\w+)$/.exec(blobId))) { let user = match[1]; let appName = match[2]; - let credentials = auth.query(request.headers); + let credentials = https.auth_query(request.headers); if ( credentials && credentials.session && diff --git a/src/httpd.js.c b/src/httpd.js.c index b7057ae9..45b9656c 100644 --- a/src/httpd.js.c +++ b/src/httpd.js.c @@ -36,6 +36,7 @@ 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); @@ -443,6 +444,58 @@ static JSValue _httpd_set_http_redirect(JSContext* context, JSValueConst this_va return JS_UNDEFINED; } +static JSValue _httpd_auth_query(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) +{ + tf_task_t* task = tf_task_get(context); + tf_ssb_t* ssb = tf_task_get_ssb(task); + JSValue headers = argv[0]; + if (JS_IsUndefined(headers)) + { + return JS_UNDEFINED; + } + + JSValue cookie = JS_GetPropertyStr(context, headers, "cookie"); + const char* cookie_string = JS_ToCString(context, cookie); + const char* session = tf_http_get_cookie(cookie_string, "session"); + JSValue entry = _authenticate_jwt(context, session); + tf_free((void*)session); + JS_FreeCString(context, cookie_string); + JS_FreeValue(context, cookie); + + JSValue result = JS_UNDEFINED; + if (!JS_IsUndefined(entry)) + { + result = JS_NewObject(context); + JS_SetPropertyStr(context, result, "session", entry); + JSValue out_permissions = JS_NewObject(context); + JS_SetPropertyStr(context, result, "permissions", out_permissions); + + JSValue name = JS_GetPropertyStr(context, entry, "name"); + const char* name_string = JS_ToCString(context, name); + + const char* settings = _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); + int length = tf_util_get_length(context, user_permissions); + for (int i = 0; i < length; i++) + { + JSValue permission = JS_GetPropertyUint32(context, user_permissions, i); + const char* permission_string = JS_ToCString(context, permission); + JS_SetPropertyStr(context, out_permissions, permission_string, JS_TRUE); + JS_FreeCString(context, permission_string); + JS_FreeValue(context, permission); + } + JS_FreeValue(context, user_permissions); + JS_FreeValue(context, permissions); + JS_FreeValue(context, settings_value); + tf_free((void*)settings); + JS_FreeCString(context, name_string); + JS_FreeValue(context, name); + } + return result; +} + static void _httpd_finalizer(JSRuntime* runtime, JSValue value) { tf_http_t* http = JS_GetOpaque(value, _httpd_class_id); @@ -965,13 +1018,14 @@ static JSValue _authenticate_jwt(JSContext* context, const char* jwt) return JS_UNDEFINED; } - uint8_t header[256]; + uint8_t header[256] = { 0 }; size_t actual_length = 0; - if (sodium_base642bin(header, sizeof(header), jwt, dot[0], NULL, &actual_length, NULL, sodium_base64_VARIANT_URLSAFE_NO_PADDING) != 0) + if (sodium_base642bin(header, sizeof(header), jwt, dot[0], NULL, &actual_length, NULL, sodium_base64_VARIANT_URLSAFE_NO_PADDING) != 0 || actual_length >= sizeof(header)) { return JS_UNDEFINED; } + header[actual_length] = '\0'; JSValue header_value = JS_ParseJSON(context, (const char*)header, actual_length, NULL); bool header_valid = _string_property_equals(context, header_value, "typ", "JWT") && _string_property_equals(context, header_value, "alg", "HS256"); JS_FreeValue(context, header_value); @@ -986,19 +1040,21 @@ static JSValue _authenticate_jwt(JSContext* context, const char* jwt) tf_ssb_db_identity_visit(ssb, ":auth", _public_key_visit, public_key_b64); const char* payload = jwt + dot[0] + 1; - size_t payload_length = dot[1] - dot[0]; - if (!tf_ssb_hmacsha256_verify(public_key_b64, payload, payload_length, jwt + dot[1] + 1)) + size_t payload_length = dot[1] - dot[0] - 1; + if (!tf_ssb_hmacsha256_verify(public_key_b64, payload, payload_length, jwt + dot[1] + 1, true)) { return JS_UNDEFINED; } uint8_t payload_bin[256]; size_t actual_payload_length = 0; - if (sodium_base642bin(payload_bin, sizeof(payload_bin), payload, payload_length, NULL, &actual_payload_length, NULL, sodium_base64_VARIANT_URLSAFE_NO_PADDING) != 0) + if (sodium_base642bin(payload_bin, sizeof(payload_bin), payload, payload_length, NULL, &actual_payload_length, NULL, sodium_base64_VARIANT_URLSAFE_NO_PADDING) != 0 || + actual_payload_length >= sizeof(payload_bin)) { return JS_UNDEFINED; } + payload_bin[actual_payload_length] = '\0'; JSValue parsed = JS_ParseJSON(context, (const char*)payload_bin, actual_payload_length, NULL); JSValue exp = JS_GetPropertyStr(context, parsed, "exp"); int64_t exp_value = 0; @@ -1539,6 +1595,7 @@ void tf_httpd_register(JSContext* context) JS_SetPropertyStr(context, httpd, "all", JS_NewCFunction(context, _httpd_endpoint_all, "all", 2)); JS_SetPropertyStr(context, httpd, "start", JS_NewCFunction(context, _httpd_endpoint_start, "start", 2)); JS_SetPropertyStr(context, httpd, "set_http_redirect", JS_NewCFunction(context, _httpd_set_http_redirect, "set_http_redirect", 1)); + JS_SetPropertyStr(context, httpd, "auth_query", JS_NewCFunction(context, _httpd_auth_query, "auth_query", 1)); JS_SetPropertyStr(context, global, "httpd", httpd); JS_FreeValue(context, global); } diff --git a/src/ssb.c b/src/ssb.c index 989a56e0..1b4a0776 100644 --- a/src/ssb.c +++ b/src/ssb.c @@ -3911,7 +3911,7 @@ void tf_ssb_schedule_work(tf_ssb_t* ssb, int delay_ms, void (*callback)(tf_ssb_t uv_unref((uv_handle_t*)&timer->timer); } -bool tf_ssb_hmacsha256_verify(const char* public_key, const void* payload, size_t payload_length, const char* signature) +bool tf_ssb_hmacsha256_verify(const char* public_key, const void* payload, size_t payload_length, const char* signature, bool signature_is_urlb64) { bool result = false; @@ -3926,7 +3926,8 @@ bool tf_ssb_hmacsha256_verify(const char* public_key, const void* payload, size_ if (tf_base64_decode(public_key_start, public_key_end - public_key_start, bin_public_key, sizeof(bin_public_key)) > 0) { uint8_t bin_signature[crypto_sign_BYTES] = { 0 }; - if (tf_base64_decode(signature, strlen(signature), bin_signature, sizeof(bin_signature)) > 0) + if (sodium_base642bin(bin_signature, sizeof(bin_signature), signature, strlen(signature), NULL, NULL, NULL, + signature_is_urlb64 ? sodium_base64_VARIANT_URLSAFE_NO_PADDING : sodium_base64_VARIANT_ORIGINAL) == 0) { if (crypto_sign_verify_detached(bin_signature, (const uint8_t*)payload, payload_length, bin_public_key) == 0) { diff --git a/src/ssb.db.c b/src/ssb.db.c index 26858b82..c3095ebf 100644 --- a/src/ssb.db.c +++ b/src/ssb.db.c @@ -1179,8 +1179,16 @@ bool tf_ssb_db_identity_get_private_key(tf_ssb_t* ssb, const char* user, const c } } } + else + { + tf_printf("Bind failed: %s.\n", sqlite3_errmsg(db)); + } sqlite3_finalize(statement); } + else + { + tf_printf("Prepare failed: %s.\n", sqlite3_errmsg(db)); + } tf_ssb_release_db_reader(ssb, db); return success; } diff --git a/src/ssb.h b/src/ssb.h index 9f9e7f3a..c6cd733a 100644 --- a/src/ssb.h +++ b/src/ssb.h @@ -957,6 +957,15 @@ void tf_ssb_set_room_name(tf_ssb_t* ssb, const char* room_name); */ void tf_ssb_schedule_work(tf_ssb_t* ssb, int delay_ms, void (*callback)(tf_ssb_t* ssb, void* user_data), void* user_data); -bool tf_ssb_hmacsha256_verify(const char* public_key, const void* payload, size_t payload_length, const char* signature); +/** +** Verify a signature. +** @param public_key The public key for which the message was signed. +** @param payload The signed payload. +** @param payload_length The length of the signed payload in bytes. +** @param signature The signature. +** @param signature_is_urlb64 True if the signature is in URL base64 format, otherwise standard base64. +** @return true If the message was successfully verified. +*/ +bool tf_ssb_hmacsha256_verify(const char* public_key, const void* payload, size_t payload_length, const char* signature, bool signature_is_urlb64); /** @} */ diff --git a/src/tests.c b/src/tests.c index 4545b59e..73abadb2 100644 --- a/src/tests.c +++ b/src/tests.c @@ -606,7 +606,7 @@ static void _test_file(const tf_test_options_t* options) "});\n"); char command[256]; - snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); + snprintf(command, sizeof(command), "%s run --db-path=out/test.db -s out/test.js" TEST_ARGS, options->exe_path); tf_printf("%s\n", command); int result = system(command); tf_printf("returned %d\n", WEXITSTATUS(result)); @@ -634,13 +634,15 @@ static void _test_sign(const tf_test_options_t* options) " exit(1);\n" "}\n"); + unlink("out/test_db0.sqlite"); char command[256]; - snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); + snprintf(command, sizeof(command), "%s run --db-path=out/test_db0.sqlite -s out/test.js" TEST_ARGS, options->exe_path); tf_printf("%s\n", command); int result = system(command); tf_printf("returned %d\n", WEXITSTATUS(result)); assert(WIFEXITED(result)); assert(WEXITSTATUS(result) == 0); + unlink("out/test_db0.sqlite"); unlink("out/test.js"); }