From 113a82b382074caef65d3f609934b081d4771884 Mon Sep 17 00:00:00 2001 From: Cory McWilliams Date: Wed, 28 Sep 2022 23:52:44 +0000 Subject: [PATCH] Make auth use JWTs. git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@3991 ed5197a5-7fde-0310-b194-c3ffbd925b24 --- core/auth.js | 94 ++++++++++++++++++++++++++++++++------------------- src/ssb.js.c | 75 ++++++++++++++++++++++++++++++++++++++++ src/tests.c | 57 +++++++++++++++++++++++++++++++ src/util.js.c | 42 +++++++++++++++++++++-- 4 files changed, 232 insertions(+), 36 deletions(-) diff --git a/core/auth.js b/core/auth.js index a9e9069e..39c2caf4 100644 --- a/core/auth.js +++ b/core/auth.js @@ -5,38 +5,66 @@ import * as form from './form.js'; var gTokens = {}; var gDatabase = new Database("auth"); +const kRefreshInterval = 1 * 60 * 60 * 1000; + +function b64url(value) { + value = value.replaceAll('+', '-').replaceAll('/', '_'); + let equals = value.indexOf('='); + if (equals !== -1) { + return value.substring(0, equals); + } else { + return value; + } +} + +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; + } +} + +function makeJwt(payload) { + let ids = ssb.getIdentities(':auth'); + let id; + if (ids?.length) { + id = ids[0]; + } else { + id = ssb.createIdentity(':auth'); + } + + let final_payload = b64url(base64Encode(JSON.stringify(Object.assign({}, payload, {exp: (new Date().valueOf()) + kRefreshInterval})))); + let jwt = [b64url(base64Encode(JSON.stringify({alg: 'HS256', typ: 'JWT'}))), final_payload, b64url(ssb.hmacsha256sign(final_payload, ':auth', id))].join('.'); + return jwt; +} + function readSession(session) { - var result = session ? gDatabase.get("session:" + session) : null; - - if (result) { - result = JSON.parse(result); - - let kRefreshInterval = 1 * 60 * 60 * 1000; - let now = Date.now(); - if (!result.lastAccess || result.lastAccess < now - kRefreshInterval) { - result.lastAccess = now; - writeSession(session, result); + let jwt_parts = session?.split('.'); + if (jwt_parts?.length === 3) { + let [header, payload, signature] = jwt_parts; + header = JSON.parse(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)) { + let result = JSON.parse(base64Decode(unb64url(payload))); + if ((new Date()).valueOf() < result.exp) { + return result; + } else { + print('JWT expired.'); + } + } else { + print('JWT verification failed.'); + } + } else { + print('Invalid JWT header.'); } } - - return result; -} - -function writeSession(session, value) { - gDatabase.set("session:" + session, JSON.stringify(value)); -} - -function removeSession(session, value) { - gDatabase.remove("session:" + session); -} - -function newSession() { - var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - var result = ""; - for (var i = 0; i < 32; i++) { - result += alphabet.charAt(Math.floor(Math.random() * alphabet.length)); - } - return result; } function verifyPassword(password, hash) { @@ -92,7 +120,6 @@ function handler(request, response) { var formData = form.decodeForm(request.query); if (request.method == "POST" || formData.submit) { - session = newSession(); sessionIsNew = true; formData = form.decodeForm(utf8Decode(request.body), formData); if (formData.submit == "Login") { @@ -114,7 +141,7 @@ function handler(request, response) { if (users !== users_original) { gDatabase.set('users', users); } - writeSession(session, {name: formData.name}); + session = makeJwt({name: formData.name}); account = {password: hashPassword(formData.password)}; gDatabase.set("user:" + formData.name, JSON.stringify(account)); if (noAdministrator()) { @@ -127,7 +154,7 @@ function handler(request, response) { if (account && account.password && verifyPassword(formData.password, account.password)) { - writeSession(session, {name: formData.name}); + session = makeJwt({name: formData.name}); if (noAdministrator()) { makeAdministrator(formData.name); } @@ -137,7 +164,7 @@ function handler(request, response) { } } else { // Proceed as Guest - writeSession(session, {name: "guest"}); + session = makeJwt({name: 'guest'}); } } @@ -191,7 +218,6 @@ function handler(request, response) { }); } } else if (request.uri == "/login/logout") { - removeSession(session); response.writeHead(303, {"Set-Cookie": "session=; path=/; secure; SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT", "Location": "/login" + (request.query ? "?" + request.query : "")}); response.end(); } else { diff --git a/src/ssb.js.c b/src/ssb.js.c index 4905ded4..dffea0af 100644 --- a/src/ssb.js.c +++ b/src/ssb.js.c @@ -7,6 +7,7 @@ #include "task.h" #include "util.js.h" +#include #include #include #include @@ -815,6 +816,78 @@ static JSValue _tf_ssb_remove_event_listener(JSContext* context, JSValueConst th return result; } +static JSValue _tf_ssb_hmacsha256_sign(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) +{ + JSValue result = JS_UNDEFINED; + tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId); + + size_t payload_length = 0; + const char* payload = JS_ToCStringLen(context, &payload_length, argv[0]); + const char* user = JS_ToCString(context, argv[1]); + const char* public_key = JS_ToCString(context, argv[2]); + + uint8_t private_key[crypto_sign_SECRETKEYBYTES]; + if (tf_ssb_db_identity_get_private_key(ssb, user, public_key, private_key, sizeof(private_key))) + { + uint8_t signature[crypto_sign_BYTES]; + unsigned long long siglen; + if (crypto_sign_detached(signature, &siglen, (const uint8_t*)payload, payload_length, private_key) == 0) + { + char signature_base64[crypto_sign_BYTES * 2]; + base64c_encode(signature, sizeof(signature), (uint8_t*)signature_base64, sizeof(signature_base64)); + result = JS_NewString(context, signature_base64); + } + } + else + { + result = JS_ThrowInternalError(context, "Private key not found."); + } + + JS_FreeCString(context, public_key); + JS_FreeCString(context, user); + JS_FreeCString(context, payload); + + return result; +} + +static JSValue _tf_ssb_hmacsha256_verify(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) +{ + JSValue result = JS_UNDEFINED; + + size_t public_key_length = 0; + const char* public_key = JS_ToCStringLen(context, &public_key_length, argv[0]); + size_t payload_length = 0; + const char* payload = JS_ToCStringLen(context, &payload_length, argv[1]); + size_t signature_length = 0; + const char* signature = JS_ToCStringLen(context, &signature_length, argv[2]); + + const char* public_key_start = public_key && *public_key == '@' ? public_key + 1 : public_key; + const char* public_key_end = strstr(public_key_start, ".ed25519"); + if (!public_key_end) + { + public_key_end = public_key_start + strlen(public_key_start); + } + + uint8_t bin_public_key[crypto_sign_PUBLICKEYBYTES] = { 0 }; + if (base64c_decode((const uint8_t*)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 (base64c_decode((const uint8_t*)signature, signature_length, bin_signature, sizeof(bin_signature)) > 0) + { + if (crypto_sign_verify_detached(bin_signature, (const uint8_t*)payload, payload_length, bin_public_key) == 0) + { + result = JS_TRUE; + } + } + } + + JS_FreeCString(context, signature); + JS_FreeCString(context, payload); + JS_FreeCString(context, public_key); + + return result; +} + void tf_ssb_register(JSContext* context, tf_ssb_t* ssb) { JS_NewClassID(&_tf_ssb_classId); @@ -836,6 +909,8 @@ void tf_ssb_register(JSContext* context, tf_ssb_t* ssb) JS_SetPropertyStr(context, object, "createIdentity", JS_NewCFunction(context, _tf_ssb_createIdentity, "createIdentity", 1)); JS_SetPropertyStr(context, object, "getIdentities", JS_NewCFunction(context, _tf_ssb_getIdentities, "getIdentities", 1)); JS_SetPropertyStr(context, object, "appendMessageWithIdentity", JS_NewCFunction(context, _tf_ssb_appendMessageWithIdentity, "appendMessageWithIdentity", 3)); + JS_SetPropertyStr(context, object, "hmacsha256sign", JS_NewCFunction(context, _tf_ssb_hmacsha256_sign, "hmacsha256sign", 3)); + JS_SetPropertyStr(context, object, "hmacsha256verify", JS_NewCFunction(context, _tf_ssb_hmacsha256_verify, "hmacsha256verify", 3)); /* Does not require an identity. */ JS_SetPropertyStr(context, object, "getAllIdentities", JS_NewCFunction(context, _tf_ssb_getAllIdentities, "getAllIdentities", 0)); diff --git a/src/tests.c b/src/tests.c index 255f0dca..448baa6d 100644 --- a/src/tests.c +++ b/src/tests.c @@ -585,6 +585,61 @@ static void _test_file(const tf_test_options_t* options) unlink("out/test.js"); } +static void _test_sign(const tf_test_options_t* options) +{ + FILE* file = fopen("out/test.js", "w"); + fprintf(file, + "'use strict';\n" + "let id = ssb.createIdentity('test');\n" + "print(id);\n" + "let sig = ssb.hmacsha256sign('hello', 'test', id);\n" + "print(sig);\n" + "if (!ssb.hmacsha256verify(id, 'hello', sig)) {\n" + " exit(1);\n" + "}\n" + "if (ssb.hmacsha256verify(id, 'world', sig)) {\n" + " exit(1);\n" + "}\n" + "if (ssb.hmacsha256verify(id, 'hello1', sig)) {\n" + " exit(1);\n" + "}\n" + ); + fclose(file); + + char command[256]; + snprintf(command, sizeof(command), "%s run --ssb-port=0 -s out/test.js", options->exe_path); + printf("%s\n", command); + int result = system(command); + printf("returned %d\n", WEXITSTATUS(result)); + assert(WIFEXITED(result)); + assert(WEXITSTATUS(result) == 0); + + unlink("out/test.js"); +} + +static void _test_b64(const tf_test_options_t* options) +{ + FILE* file = fopen("out/test.js", "w"); + fprintf(file, + "'use strict';\n" + "print(base64Encode('hello'));\n" + "if (base64Decode(base64Encode('hello')) !== 'hello') {\n" + " exit(1);\n" + "}\n" + ); + fclose(file); + + char command[256]; + snprintf(command, sizeof(command), "%s run --ssb-port=0 -s out/test.js", options->exe_path); + printf("%s\n", command); + int result = system(command); + printf("returned %d\n", WEXITSTATUS(result)); + assert(WIFEXITED(result)); + assert(WEXITSTATUS(result) == 0); + + unlink("out/test.js"); +} + static void _tf_test_run(const tf_test_options_t* options, const char* name, void (*test)(const tf_test_options_t* options)) { bool specified = false; @@ -639,5 +694,7 @@ void tf_tests(const tf_test_options_t* options) _tf_test_run(options, "uint8array", _test_uint8array); _tf_test_run(options, "socket", _test_socket); _tf_test_run(options, "file", _test_file); + _tf_test_run(options, "sign", _test_sign); + _tf_test_run(options, "b64", _test_b64); printf("Tests completed.\n"); } diff --git a/src/util.js.c b/src/util.js.c index 9bd3b488..901aa7ad 100644 --- a/src/util.js.c +++ b/src/util.js.c @@ -4,8 +4,8 @@ #include "task.h" #include "trace.h" -#include "quickjs-libc.h" - +#include +#include #include #include @@ -65,6 +65,42 @@ JSValue tf_util_utf8_decode(JSContext* context, JSValue value) return _util_utf8_decode(context, JS_NULL, 1, &value); } +static JSValue _util_base64_encode(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) +{ + JSValue result = JS_UNDEFINED; + size_t length = 0; + const char* value = JS_ToCStringLen(context, &length, argv[0]); + char* encoded = tf_malloc(length * 4); + + int r = base64c_encode((const uint8_t*)value, length, (uint8_t*)encoded, length * 4); + if (r >= 0) + { + result = JS_NewStringLen(context, encoded, r); + } + + tf_free(encoded); + JS_FreeCString(context, value); + return result; +} + +static JSValue _util_base64_decode(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) +{ + JSValue result = JS_UNDEFINED; + size_t length = 0; + const char* value = JS_ToCStringLen(context, &length, argv[0]); + char* encoded = tf_malloc(length); + + int r = base64c_decode((const uint8_t*)value, length, (uint8_t*)encoded, length); + if (r >= 0) + { + result = JS_NewStringLen(context, encoded, r); + } + + tf_free(encoded); + JS_FreeCString(context, value); + return result; +} + uint8_t* tf_util_try_get_array_buffer(JSContext* context, size_t* psize, JSValueConst obj) { uint8_t* result = JS_GetArrayBuffer(context, psize, obj); @@ -197,6 +233,8 @@ void tf_util_register(JSContext* context) JSValue global = JS_GetGlobalObject(context); JS_SetPropertyStr(context, global, "utf8Decode", JS_NewCFunction(context, _util_utf8_decode, "utf8Decode", 1)); JS_SetPropertyStr(context, global, "utf8Encode", JS_NewCFunction(context, _util_utf8_encode, "utf8Encode", 1)); + JS_SetPropertyStr(context, global, "base64Decode", JS_NewCFunction(context, _util_base64_decode, "base64Decode", 1)); + JS_SetPropertyStr(context, global, "base64Encode", JS_NewCFunction(context, _util_base64_encode, "base64Encode", 1)); JS_SetPropertyStr(context, global, "print", JS_NewCFunction(context, _util_print, "print", 1)); JS_SetPropertyStr(context, global, "setTimeout", JS_NewCFunction(context, _util_setTimeout, "setTimeout", 2)); JS_FreeValue(context, global);