forked from cory/tildefriends
		
	Make auth use JWTs.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@3991 ed5197a5-7fde-0310-b194-c3ffbd925b24
This commit is contained in:
		
							
								
								
									
										94
									
								
								core/auth.js
									
									
									
									
									
								
							
							
						
						
									
										94
									
								
								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 { | ||||
|   | ||||
							
								
								
									
										75
									
								
								src/ssb.js.c
									
									
									
									
									
								
							
							
						
						
									
										75
									
								
								src/ssb.js.c
									
									
									
									
									
								
							| @@ -7,6 +7,7 @@ | ||||
| #include "task.h" | ||||
| #include "util.js.h" | ||||
|  | ||||
| #include <base64c.h> | ||||
| #include <sodium/crypto_hash_sha256.h> | ||||
| #include <sodium/crypto_sign.h> | ||||
| #include <string.h> | ||||
| @@ -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)); | ||||
|   | ||||
							
								
								
									
										57
									
								
								src/tests.c
									
									
									
									
									
								
							
							
						
						
									
										57
									
								
								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"); | ||||
| } | ||||
|   | ||||
| @@ -4,8 +4,8 @@ | ||||
| #include "task.h" | ||||
| #include "trace.h" | ||||
|  | ||||
| #include "quickjs-libc.h" | ||||
|  | ||||
| #include <base64c.h> | ||||
| #include <quickjs-libc.h> | ||||
| #include <uv.h> | ||||
|  | ||||
| #include <string.h> | ||||
| @@ -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); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user