diff options
| author | Xavier Del Campo Romero <xavi92@disroot.org> | 2025-10-08 13:50:52 +0200 |
|---|---|---|
| committer | Xavier Del Campo Romero <xavi92@disroot.org> | 2025-10-08 22:55:44 +0200 |
| commit | 10e42591ac72285736d5cc4ee5e7c2f68dbf1e4b (patch) | |
| tree | 3bb586177e375a6f7f91c0335876faefc28b805c | |
| parent | 805630dbfcd409a5d49bc89102f4183b71f713f9 (diff) | |
Replace OpenSSL with libsodium and argon2id
The SHA256-based password hashing algorithm used by slcl(1) and
usergen(1) is considered insecure against several kinds of attacks,
including brute force attacks. [1]
Therefore, a stronger password hashing algorithm based on the Argon2id
key derivation function is now used by default. While OpenSSL does
support Argon2id, it is only supported by very recent versions [2],
which are still not packaged by most distributions as of the time of
this writing. [3]
As an alternative to OpenSSL, libsodium [4] had several benefits:
- It provides easy-to-use functions for password hashing, base64
encoding/decoding and other cryptographic primitives used by slcl(1)
and usergen(1).
- It is packaged by most distributions [5], and most often only the patch
version differs, which ensures good compatibility across distributions.
Unfortunately, and as opposed to OpenSSL, libsodium does not come with
command-line tools. Therefore, usergen(1) had to be rewritten in C.
In order to maintain backwards compatiblity with existing databases,
slcl(1) and usergen(1) shall support the insecure, SHA256-based password
hashing algorithm. However, Argon2id shall now be the default choice for
usergen(1).
[1]: https://security.stackexchange.com/questions/195563/why-is-sha-256-not-good-for-passwords
[2]: https://docs.openssl.org/3.3/man7/EVP_KDF-ARGON2/
[3]: https://repology.org/project/openssl/versions
[4]: https://www.libsodium.org/
[5]: https://repology.org/project/libsodium/versions
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | CMakeLists.txt | 18 | ||||
| -rw-r--r-- | README.md | 5 | ||||
| -rw-r--r-- | auth.c | 95 | ||||
| -rw-r--r-- | base64.c | 126 | ||||
| -rw-r--r-- | base64.h | 9 | ||||
| -rw-r--r-- | cmake/Findlibsodium.cmake | 58 | ||||
| -rwxr-xr-x | configure | 31 | ||||
| -rw-r--r-- | doc/man1/usergen.1 | 74 | ||||
| -rw-r--r-- | jwt.c | 114 | ||||
| -rw-r--r-- | main.c | 18 | ||||
| -rwxr-xr-x | usergen | 97 | ||||
| -rw-r--r-- | usergen.c | 546 |
13 files changed, 845 insertions, 349 deletions
@@ -1,5 +1,6 @@ build/ -slcl +/slcl +/usergen thumbnail/thumbnail *.a *.o diff --git a/CMakeLists.txt b/CMakeLists.txt index 4d234c3..c3e4ea1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,6 @@ cmake_minimum_required(VERSION 3.13) project(slcl LANGUAGES C VERSION 0.2.0) add_executable(${PROJECT_NAME} auth.c - base64.c cftw.c crealpath.c hex.c @@ -12,6 +11,10 @@ add_executable(${PROJECT_NAME} style.c zip.c ) +add_executable(usergen + hex.c + usergen.c +) target_compile_options(${PROJECT_NAME} PRIVATE -Wall) target_compile_definitions(${PROJECT_NAME} PRIVATE _FILE_OFFSET_BITS=64) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_LIST_DIR}/cmake) @@ -26,7 +29,7 @@ else() endif() find_package(cJSON 1.0 REQUIRED) -find_package(OpenSSL 1.0 REQUIRED) +find_package(libsodium 1.0.0 REQUIRED) find_package(fdzipstream) if(NOT fdzipstream_FOUND) @@ -34,13 +37,8 @@ if(NOT fdzipstream_FOUND) add_subdirectory(fdzipstream) endif() -target_link_libraries(${PROJECT_NAME} PRIVATE web dynstr cjson OpenSSL::SSL +target_link_libraries(${PROJECT_NAME} PRIVATE web dynstr cjson libsodium fdzipstream) -install(TARGETS ${PROJECT_NAME}) -install(FILES usergen - TYPE BIN - PERMISSIONS - OWNER_READ OWNER_WRITE OWNER_EXECUTE - GROUP_READ GROUP_EXECUTE - WORLD_READ WORLD_EXECUTE) +target_link_libraries(usergen PRIVATE dynstr cjson libsodium) +install(TARGETS ${PROJECT_NAME} usergen) add_subdirectory(doc) @@ -58,7 +58,6 @@ to `slcl`. If required, encryption should be done before uploading e.g.: using (provided as a `git` submodule). - [`fdzipstream`](https://github.com/CTrabant/fdzipstream.git) (provided as a `git` submodule). -- `jq` (for [`usergen`](usergen) only). - CMake (optional). ### Ubuntu / Debian @@ -66,7 +65,7 @@ to `slcl`. If required, encryption should be done before uploading e.g.: using #### Mandatory packages ```sh -sudo apt install build-essential libcjson-dev libssl-dev m4 jq zlib1g +sudo apt install build-essential libcjson-dev libssl-dev m4 zlib1g ``` #### Optional packages @@ -176,7 +175,7 @@ schema: } ``` -[`usergen`](usergen) is an interactive script that consumes a directory, +[`usergen`](usergen) is an interactive program that consumes a directory, a username, a password and, optionally, a user quota in MiB. A salt is randomly generated using `openssl` and passwords are hashed multiple times beforehand - see [`usergen`](usergen) and [`auth.c`](auth.c) for further @@ -5,7 +5,7 @@ #include <libweb/http.h> #include <cjson/cJSON.h> #include <dynstr.h> -#include <openssl/sha.h> +#include <sodium.h> #include <errno.h> #include <limits.h> #include <stddef.h> @@ -195,17 +195,21 @@ end: return ret; } -static int compare_pwd(const char *const salt, const char *const password, - const char *const exp_password) +static int compare_pwd_sha256(const char *const salt, + const char *const password, const char *const exp_password) { int ret = -1; - enum {SALT_LEN = SHA256_DIGEST_LENGTH}; - unsigned char dec_salt[SALT_LEN], sha256[SHA256_DIGEST_LENGTH]; - const size_t slen = strlen(salt), - len = strlen(password), n = sizeof dec_salt + len; + unsigned char dec_salt[crypto_hash_sha256_BYTES], + sha256[sizeof dec_salt]; + size_t len = strlen(password), n = sizeof dec_salt + len, slen; unsigned char *const salted = malloc(n); - if (slen != SALT_LEN * 2) + if (!salt) + { + fprintf(stderr, "%s: missing salt\n", __func__); + goto end; + } + else if ((slen = strlen(salt)) != sizeof dec_salt * 2) { fprintf(stderr, "%s: unexpected salt length: %zu\n", __func__, slen); goto end; @@ -224,18 +228,19 @@ static int compare_pwd(const char *const salt, const char *const password, memcpy(salted, dec_salt, sizeof dec_salt); memcpy(salted + sizeof dec_salt, password, len); - if (!SHA256(salted, n, sha256)) + if (crypto_hash_sha256(sha256, salted, n)) { - fprintf(stderr, "%s: SHA256 (first round) failed\n", __func__); + fprintf(stderr, "%s: crypto_hash_sha256 (first round) failed\n", + __func__); goto end; } enum {ROUNDS = 1000 - 1}; for (int i = 0; i < ROUNDS; i++) - if (!SHA256(sha256, sizeof sha256, sha256)) + if (crypto_hash_sha256(sha256, sha256, sizeof sha256)) { - fprintf(stderr, "%s: SHA256 failed\n", __func__); + fprintf(stderr, "%s: crypto_hash_sha256 failed\n", __func__); goto end; } @@ -269,6 +274,37 @@ end: return ret; } +static int compare_pwd_argon2id(const char *const salt, + const char *const password, const char *const exp_password) +{ + return !!crypto_pwhash_str_verify(exp_password, password, strlen(password)); +} + +static int compare_pwd(const char *const method, const char *const salt, + const char *const password, const char *const exp_password) +{ + static const struct m + { + const char *s; + int (*fn)(const char *, const char *, const char *); + } methods[] = + { + {.s = "sha256", .fn = compare_pwd_sha256}, + {.s = "argon2id", .fn = compare_pwd_argon2id} + }; + + for (size_t i = 0; i < sizeof methods / sizeof *methods; i++) + { + const struct m *const m = &methods[i]; + + if (!strcmp(m->s, method)) + return m->fn(salt, password, exp_password); + } + + fprintf(stderr, "%s: unknown method: %s\n", __func__, method); + return -1; +} + int auth_login(const struct auth *const a, const char *const user, const char *const password, char **const cookie) { @@ -308,19 +344,16 @@ int auth_login(const struct auth *const a, const char *const user, const cJSON *const n = cJSON_GetObjectItem(u, "name"), *const s = cJSON_GetObjectItem(u, "salt"), *const p = cJSON_GetObjectItem(u, "password"), - *const k = cJSON_GetObjectItem(u, "key"); - const char *name, *salt, *pwd, *key; + *const k = cJSON_GetObjectItem(u, "key"), + *const m = cJSON_GetObjectItem(u, "method"); + const char *name, *pwd, *key, + *const salt = s ? cJSON_GetStringValue(s) : NULL; if (!n || !(name = cJSON_GetStringValue(n))) { fprintf(stderr, "%s: missing username\n", __func__); goto end; } - else if (!s || !(salt = cJSON_GetStringValue(s))) - { - fprintf(stderr, "%s: missing salt\n", __func__); - goto end; - } else if (!p || !(pwd = cJSON_GetStringValue(p))) { fprintf(stderr, "%s: missing password\n", __func__); @@ -333,14 +366,30 @@ int auth_login(const struct auth *const a, const char *const user, } else if (!strcmp(name, user)) { - const int res = compare_pwd(salt, password, pwd); + int res; - if (res < 0) + if (m) { - fprintf(stderr, "%s: compare_pwd failed\n", __func__); + const char *const method = cJSON_GetStringValue(m); + + if (!method) + { + fprintf(stderr, "%s: missing method\n", __func__); + goto end; + } + else if ((res = compare_pwd(method, salt, password, pwd)) < 0) + { + fprintf(stderr, "%s: compare_pwd failed\n", __func__); + goto end; + } + } + else if ((res = compare_pwd_sha256(salt, password, pwd)) < 0) + { + fprintf(stderr, "%s: compare_pwd_sha256 failed\n", __func__); goto end; } - else if (!res) + + if (!res) { if (generate_cookie(name, key, cookie)) { diff --git a/base64.c b/base64.c deleted file mode 100644 index b1d4737..0000000 --- a/base64.c +++ /dev/null @@ -1,126 +0,0 @@ -#include "base64.h" -#include <openssl/evp.h> -#include <errno.h> -#include <stddef.h> -#include <stdlib.h> -#include <stdio.h> -#include <string.h> - -static void remove_lf(char *b64) -{ - while ((b64 = strchr(b64, '\n'))) - memcpy(b64, b64 + 1, strlen(b64)); -} - -static size_t base64len(const size_t n) -{ - /* Read EVP_EncodeInit(3) for further reference. */ - return ((n / 48) * 65) + (n % 48 ? 1 + ((n / 3) + 1) * 4 : 0); -} - -static size_t decodedlen(const size_t n) -{ - return ((n / 64) * 48) + (n % 64 ? (n / 4) * 3 : 0); -} - -char *base64_encode(const void *const buf, const size_t n) -{ - EVP_ENCODE_CTX *const ctx = EVP_ENCODE_CTX_new(); - char *ret = NULL; - unsigned char *b64 = NULL; - - if (!ctx) - { - fprintf(stderr, "%s: EVP_ENCODE_CTX_new failed\n", __func__); - goto end; - } - - const size_t b64len = base64len(n); - - if (!(b64 = malloc(b64len + 1))) - { - fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno)); - goto end; - } - - EVP_EncodeInit(ctx); - - size_t rem = n, done = 0; - int outl = b64len; - - while (rem) - { - const size_t i = n - rem, inl = rem > 48 ? 48 : rem; - const unsigned char *const in = buf; - - if (!EVP_EncodeUpdate(ctx, &b64[done], &outl, &in[i], inl)) - { - fprintf(stderr, "%s: EVP_EncodeUpdate failed\n", __func__); - goto end; - } - - done += outl; - rem -= inl; - } - - EVP_EncodeFinal(ctx, b64, &outl); - ret = (char *)b64; - remove_lf(ret); - -end: - if (!ret) - free(b64); - - EVP_ENCODE_CTX_free(ctx); - return ret; -} - -void *base64_decode(const char *const b64, size_t *const n) -{ - void *ret = NULL; - const size_t len = strlen(b64), dlen = decodedlen(len); - EVP_ENCODE_CTX *const ctx = EVP_ENCODE_CTX_new(); - unsigned char *const buf = malloc(dlen); - - if (!buf) - { - fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno)); - goto end; - } - else if (!ctx) - { - fprintf(stderr, "%s: EVP_ENCODE_CTX_new failed\n", __func__); - goto end; - } - - EVP_DecodeInit(ctx); - - size_t rem = len, done = 0; - int outl = dlen; - - while (rem) - { - const size_t i = len - rem, inl = rem > 64 ? 64 : rem; - const unsigned char *const in = (const unsigned char *)b64; - - if (EVP_DecodeUpdate(ctx, &buf[done], &outl, &in[i], inl) < 0) - { - fprintf(stderr, "%s: EVP_EncodeUpdate failed\n", __func__); - goto end; - } - - done += outl; - rem -= inl; - } - - EVP_DecodeFinal(ctx, buf, &outl); - *n = done; - ret = buf; - -end: - if (!ret) - free(buf); - - EVP_ENCODE_CTX_free(ctx); - return ret; -} diff --git a/base64.h b/base64.h deleted file mode 100644 index 6ca0f18..0000000 --- a/base64.h +++ /dev/null @@ -1,9 +0,0 @@ -#ifndef BASE64_H -#define BASE64_H - -#include <stddef.h> - -char *base64_encode(const void *buf, size_t n); -void *base64_decode(const char *b64, size_t *n); - -#endif /* BASE64_H */ diff --git a/cmake/Findlibsodium.cmake b/cmake/Findlibsodium.cmake new file mode 100644 index 0000000..0844e74 --- /dev/null +++ b/cmake/Findlibsodium.cmake @@ -0,0 +1,58 @@ +find_package(PkgConfig) + +if(PKG_CONFIG_FOUND) + pkg_check_modules(libsodium libsodium) +endif() + +if(NOT libsodium_FOUND) + find_library(libsodium_LIBRARIES NAMES sodium) + find_path(libsodium_INCLUDE_DIRS NAMES + sodium.h + sodium/version.h + PATH_SUFFIXES include) + + if(libsodium_INCLUDE_DIRS) + set(libsodium_VERSION ${libsodium_INCLUDE_DIRS}/sodium/version.h) + + if(NOT EXISTS ${libsodium_VERSION}) + message(FATAL_ERROR "Missing file: ${libsodium_VERSION}") + endif() + endif() + + file(STRINGS ${libsodium_VERSION} libsodium_VERSION_LINE REGEX "^#define[ \t]+SODIUM_VERSION_STRING[ \t]+\"[0-9\.]+\"$") + set(libsodium_expr "^#define[ \t]+SODIUM_VERSION_STRING[ \t]+\"([0-9]+)\.([0-9]+)\.([0-9]+)\"$") + string(REGEX REPLACE ${libsodium_expr} "\\1" libsodium_VERSION_MAJOR "${libsodium_VERSION_LINE}") + string(REGEX REPLACE ${libsodium_expr} "\\2" libsodium_VERSION_MINOR "${libsodium_VERSION_LINE}") + string(REGEX REPLACE ${libsodium_expr} "\\3" libsodium_VERSION_PATCH "${libsodium_VERSION_LINE}") + set(libsodium_VERSION_STRING ${libsodium_VERSION_MAJOR}.${libsodium_VERSION_MINOR}.${libsodium_VERSION_PATCH}) + unset(libsodium_VERSION_MAJOR_LINE) + unset(libsodium_VERSION_MINOR_LINE) + unset(libsodium_VERSION_PATCH_LINE) + unset(libsodium_VERSION_MAJOR) + unset(libsodium_VERSION_MINOR) + unset(libsodium_VERSION_PATCH) + unset(libsodium_expr) + + if (libsodium_LIBRARIES STREQUAL "libsodium_LIBRARIES-NOTFOUND") + message(FATAL_ERROR "libsodium not found") + elseif (libsodium_INCLUDE_DIRS STREQUAL "libsodium_INCLUDE_DIRS-NOTFOUND") + message(FATAL_ERROR "libsodium headers not found") + endif() + + set(libsodium_LINK_LIBRARIES ${libsodium_LIBRARIES}) +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(libsodium + REQUIRED_VARS + libsodium_LINK_LIBRARIES + libsodium_INCLUDE_DIRS + VERSION_VAR libsodium_VERSION_STRING +) + +if(NOT TARGET libsodium) + add_library(libsodium INTERFACE IMPORTED) + set_target_properties(libsodium PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${libsodium_INCLUDE_DIRS} + INTERFACE_LINK_LIBRARIES ${libsodium_LINK_LIBRARIES}) +endif() @@ -7,11 +7,12 @@ prefix=$default_prefix default_CC='c99' # FILE_OFFSET_BITS=64 is required for large file support on 32-bit platforms. default_CFLAGS='-O1 -g -D_FILE_OFFSET_BITS=64 -Wall -MD' -default_LDFLAGS="-lcjson -lssl -lm -lcrypto" +default_LDFLAGS="-lcjson -lm" CC=${CC:-$default_CC} CFLAGS=${CFLAGS:-"$default_CFLAGS $default_NPCFLAGS"} LDFLAGS=${LDFLAGS:-$default_LDFLAGS} +USERGEN_LDFLAGS=${LDFLAGS} help() { @@ -47,16 +48,27 @@ while true; do esac done +if pkg-config libsodium +then + CFLAGS="$CFLAGS $(pkg-config --cflags libsodium)" + LDFLAGS="$LDFLAGS $(pkg-config --libs libsodium)" +else + echo "Error: libsodium not found." >&2 + exit 1 +fi + if pkg-config dynstr then in_tree_dynstr=0 CFLAGS="$CFLAGS $(pkg-config --cflags dynstr)" LDFLAGS="$LDFLAGS $(pkg-config --libs dynstr)" + USERGEN_LDFLAGS="$USERGEN_LDFLAGS $(pkg-config --libs dynstr)" else echo "Info: dynstr not found. Using in-tree copy" >&2 in_tree_dynstr=1 CFLAGS="$CFLAGS -Ilibweb/dynstr/include" LDFLAGS="$LDFLAGS -Llibweb/dynstr -ldynstr" + USERGEN_LDFLAGS="$LDFLAGS -Llibweb/dynstr -ldynstr" fi if pkg-config libweb @@ -99,6 +111,7 @@ PREFIX = $prefix DST = $prefix/bin CFLAGS = $CFLAGS LDFLAGS = $LDFLAGS +USERGEN_LDFLAGS = $USERGEN_LDFLAGS EOF cat <<"EOF" >> $F @@ -106,7 +119,6 @@ PROJECT = slcl DEPS = $(OBJECTS:.o=.d) OBJECTS = \ auth.o \ - base64.o \ cftw.o \ crealpath.o \ hex.o \ @@ -116,9 +128,13 @@ OBJECTS = \ style.o \ zip.o -all: $(PROJECT) +USERGEN_OBJECTS = \ + hex.o \ + usergen.o \ + +all: $(PROJECT) usergen -install: all usergen +install: all mkdir -p $(DST) cp slcl usergen $(DST) chmod 0755 $(DST)/slcl @@ -129,6 +145,9 @@ FORCE: $(PROJECT): $(OBJECTS) $(CC) $(OBJECTS) $(LDFLAGS) -o $@ + +usergen: $(USERGEN_OBJECTS) + $(CC) $(USERGEN_OBJECTS) $(USERGEN_LDFLAGS) -o $@ EOF if [ $in_tree_dynstr -ne 0 ] @@ -156,7 +175,7 @@ then cat <<"EOF" >> $F FDZIPSTREAM = fdzipstream/libfdzipstream.a $(PROJECT): $(FDZIPSTREAM) -$(FDZIPSTREAM): FORCE +$(FDZIPSTREAM): FORCE $(DYNSTR) +cd fdzipstream && $(MAKE) CC=$(CC) EOF fi @@ -189,7 +208,7 @@ fi cat <<"EOF" >> $F distclean: clean - rm -f slcl + rm -f slcl usergen rm Makefile EOF diff --git a/doc/man1/usergen.1 b/doc/man1/usergen.1 index 857d9df..1a52e31 100644 --- a/doc/man1/usergen.1 +++ b/doc/man1/usergen.1 @@ -1,4 +1,4 @@ -.TH USERGEN 1 usergen +.TH USERGEN 1 2025-10-08 0.4.0 "slcl user manual" .SH NAME usergen \- append a user into a slcl database @@ -12,11 +12,29 @@ dir performs the following steps: .B 1. -Reads user credentials and quota from standard input. +Reads user credentials, password hashing algorithm and quota from standard +input. + +Two password hashing algorithms are defined: + +.IP \(bu 2 +.BR sha256 : +a multi-round, SHA256-based hashing algorithm. +.BR "It is deprecated and considered insecure" , +see +.B NOTES +for further reference. + +.IP \(bu 2 +.BR argon2id : +considered more secure and enabled by default. It is based on the Argon2id +key derivation function. + +.LP .B 2. -Generates a JSON object with the read credentials and quota, as well as -a random salt and signing key. +Generates a JSON object with the read credentials, password hashing algorithm +and quota, as well as a signing key and optional, algorithm-specific data. .B 3. Appends the newly generated JSON object into the @@ -37,10 +55,21 @@ is located. .SH NOTES For security reasons, passwords are never stored in plaintext into .BR dir/db.json . -Instead, a salted, multi-round hashed password is calculated and -stored. Then, -.B slcl(1) -performs the same operations to ensure both tokens match. +Historically, +.IR usergen (1) +and +.IR slcl (1) +relied on a salted, multi-round SHA256-based password hashing algorithm. +However, +.BR "this is considered insecure against brute-forcing and other attacks" . +Therefore, since version 0.4.0, both +.IR usergen (1) +and +.IR slcl (1) +have been improved in order to support the more secure +Argon2id key derivation function. +Nevertheless, in order to keep backwards compatibility with existing +databases, the older hashing algorithm is still supported. .SH EXAMPLES @@ -48,18 +77,20 @@ Below, there is an example of a new user called .B johndoe with password .B secret -and a specified quota of 512 MiB: +(not echoed to the terminal) and a specified quota of 512 MiB: .PP .EX $ ./usergen ~/db +./usergen ~/db Username: johndoe Password: -secret + Quota, in MiB (leave empty for unlimited quota): 512 -1000/1000 +Password hashing (sha256 [deprecated], argon2id): [argon2id] + .EE Then, @@ -67,22 +98,23 @@ Then, should be updated to something similar to: .PP +.in +4n .EX { - "users": [ - { - "name": "johndoe", - "password": "4c48385ec2be4798dc772d3c8f5649d8411afbdfc4708ada79379e3562af5abb", - "salt": "835324df29527731f3faad663c58c3b19a07c193e97dc77f33e10d3942cdc91c", - "key": "d0ae360b9af1177ce73eef3f499eea2627cd61b69df79dcb7a5c70bc658a4e63", - "quota": "512" - } - ] + "users": [{ + "name": "johndoe", + "key": "2bce6ac030d0737d17678d073a3b16841f08abd2f3757fb463d14edf869cf1ff", + "method": "argon2id", + "quota": "512", + "password": "$argon2id$v=19$m=65536,t=2,p=1$TV6tsKdVRyCO6U5a/5GVhw$eRO0T4wc0WYtnYTIIh84XThVB+J5BJOsOufLF/6JaV8" + }] } .EE +.in +.PP .SH COPYRIGHT -Copyright (C) 2023 Xavier Del Campo Romero. +Copyright (C) 2023-2025 Xavier Del Campo Romero .P 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 @@ -1,21 +1,17 @@ #include "jwt.h" -#include "base64.h" #include <dynstr.h> -#include <cjson/cJSON.h> -#include <openssl/evp.h> -#include <openssl/hmac.h> -#include <openssl/sha.h> +#include <sodium.h> #include <errno.h> #include <stddef.h> #include <stdlib.h> #include <stdio.h> #include <string.h> -static const char jwt_header[] = "{\"alg\": \"HS256\", \"typ\": \"JWT\"}"; +#define VARIANT sodium_base64_VARIANT_URLSAFE static char *get_payload(const char *const name) { - char *ret = NULL; + char *ret = NULL, *s = NULL; struct dynstr d; dynstr_init(&d); @@ -25,13 +21,27 @@ static char *get_payload(const char *const name) fprintf(stderr, "%s: dynstr_append name failed\n", __func__); goto end; } - else if (!(ret = base64_encode(d.str, d.len))) + + const size_t n = sodium_base64_encoded_len(d.len, VARIANT); + const unsigned char *const in = (const unsigned char *)d.str; + + if (!(s = malloc(n))) { - fprintf(stderr, "%s: base64 failed\n", __func__); + fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno)); goto end; } + else if (!sodium_bin2base64(s, n, in, d.len, VARIANT)) + { + fprintf(stderr, "%s: sodium_bin2base64 failed\n", __func__); + goto end; + } + + ret = s; end: + if (!ret) + free(s); + dynstr_free(&d); return ret; } @@ -39,47 +49,66 @@ end: static char *get_hmac(const void *const buf, const size_t n, const void *const key, const size_t keyn) { - unsigned char hmac[SHA256_DIGEST_LENGTH]; - const EVP_MD *const md = EVP_sha256(); + unsigned char hmac[crypto_auth_hmacsha256_KEYBYTES]; char *ret = NULL; - if (!md) + if (keyn != crypto_auth_hmacsha256_KEYBYTES) { - fprintf(stderr, "%s: EVP_sha256 failed\n", __func__); - return NULL; + fprintf(stderr, "%s: invalid key size (%zu), expected %u\n", __func__, + n, crypto_auth_hmacsha256_KEYBYTES); + goto failure; } - else if (!HMAC(md, key, keyn, buf, n, hmac, NULL)) + else if (crypto_auth_hmacsha256(hmac, buf, n, key)) { fprintf(stderr, "%s: HMAC failed\n", __func__); - return NULL; + goto failure; + } + + const size_t b64n = sodium_base64_encoded_len(sizeof hmac, VARIANT); + + if (!(ret = malloc(b64n))) + { + fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno)); + goto failure; } - else if (!(ret = base64_encode(hmac, sizeof hmac))) + else if (!sodium_bin2base64(ret, b64n, hmac, sizeof hmac, VARIANT)) { - fprintf(stderr, "%s: base64 failed\n", __func__); - return NULL; + fprintf(stderr, "%s: sodium_bin2base64 failed\n", __func__); + goto failure; } return ret; + +failure: + free(ret); + return NULL; } char *jwt_encode(const char *const name, const void *const key, const size_t n) { - char *ret = NULL; - char *const header = base64_encode(jwt_header, strlen(jwt_header)), - *const payload = get_payload(name), - *hmac = NULL; + static const char jwt_header[] = "{\"alg\": \"HS256\", \"typ\": \"JWT\"}"; struct dynstr jwt; + const size_t hlen = strlen(jwt_header), + sz = sodium_base64_encoded_len(hlen, VARIANT); + char *ret = NULL, *header = NULL, *hmac = NULL, + *const payload = get_payload(name); dynstr_init(&jwt); - if (!header) + if (!payload) { - fprintf(stderr, "%s: base64_encode header failed\n", __func__); + fprintf(stderr, "%s: get_payload failed\n", __func__); goto end; } - else if (!payload) + else if (!(header = malloc(sz))) { - fprintf(stderr, "%s: get_payload failed\n", __func__); + fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno)); + goto end; + } + else if (!sodium_bin2base64(header, sz, (const unsigned char *)jwt_header, + hlen, VARIANT)) + { + fprintf(stderr, "%s: sodium_bin2base64 failed\n", __func__); goto end; } else if (dynstr_append(&jwt, "%s.%s", header, payload)) @@ -111,17 +140,17 @@ end: return ret; } -int jwt_check(const char *const jwt, const void *const key, const size_t n) +int jwt_check(const char *const jwt, const void *const key, + const size_t n) { const char *const p = strrchr(jwt, '.'); - const EVP_MD *const md = EVP_sha256(); - unsigned char hmac[SHA256_DIGEST_LENGTH]; - char *dhmac = NULL; - size_t hmaclen; + const unsigned char *const in = (const unsigned char *)jwt; + unsigned char hmac[crypto_auth_hmacsha256_KEYBYTES]; - if (!md) + if (n != crypto_auth_hmacsha256_KEYBYTES) { - fprintf(stderr, "%s: EVP_sha256 failed\n", __func__); + fprintf(stderr, "%s: invalid key size (%zu), expected %u\n", __func__, + n, crypto_auth_hmacsha256_KEYBYTES); return -1; } else if (!p) @@ -129,19 +158,14 @@ int jwt_check(const char *const jwt, const void *const key, const size_t n) fprintf(stderr, "%s: expected '.'\n", __func__); return 1; } - else if (!HMAC(md, key, n, (const unsigned char *)jwt, p - jwt, hmac, NULL)) + else if (sodium_base642bin(hmac, sizeof hmac, p + 1, strlen(p + 1), + NULL, NULL, NULL, VARIANT)) { - fprintf(stderr, "%s: HMAC failed\n", __func__); - return -1; - } - else if (!(dhmac = base64_decode(p + 1, &hmaclen))) - { - fprintf(stderr, "%s: base64_decode failed\n", __func__); + fprintf(stderr, "%s: sodium_base642bin failed\n", __func__); return 1; } + else if (crypto_auth_hmacsha256_verify(hmac, in, p - jwt, key)) + return 1; - const int r = memcmp(dhmac, hmac, hmaclen); - - free(dhmac); - return !!r; + return 0; } @@ -18,6 +18,7 @@ #include <openssl/err.h> #include <openssl/rand.h> #include <dynstr.h> +#include <sodium.h> #include <dirent.h> #include <libgen.h> #include <fcntl.h> @@ -522,13 +523,9 @@ static char *create_symlink(const char *const dir, const char *user) dynstr_init(&abs); dynstr_init(&rel); - if (RAND_bytes(buf, sizeof buf) != 1) - { - fprintf(stderr, "%s: RAND_bytes failed with %lu\n", - __func__, ERR_get_error()); - goto end; - } - else if (hex_encode(buf, dbuf, sizeof buf, sizeof dbuf)) + randombytes_buf(buf, sizeof buf); + + if (hex_encode(buf, dbuf, sizeof buf, sizeof dbuf)) { fprintf(stderr, "%s: hex_encode failed\n", __func__); goto end; @@ -2650,7 +2647,12 @@ int main(int argc, char *argv[]) dynstr_init(&ua.d); - if (parse_args(argc, argv, &dir, &port, &tmpdir, &ua.fifo) + if (sodium_init()) + { + fprintf(stderr, "%s: sodium_init failed\n", __func__); + goto end; + } + else if (parse_args(argc, argv, &dir, &port, &tmpdir, &ua.fifo) || init_dirs(dir) || ensure_style(dir) || !(ua.a = auth_alloc(dir)) diff --git a/usergen b/usergen deleted file mode 100755 index eac072b..0000000 --- a/usergen +++ /dev/null @@ -1,97 +0,0 @@ -#! /bin/sh - -set -e - -usage() -{ - echo "$0 <dir>" -} - -to_hex() -{ - od -An -t x1 | tr -d ' ' | tr -d '\n' -} - -to_bin() -{ - sed -e 's,\([0-9a-f]\{2\}\),\\\\\\x\1,g' | xargs printf -} - -mktemp_posix() -{ - m4 <<EOF -mkstemp(${TMPDIR:-/tmp}/tmp.XXXXXX) -EOF -} - -if [ $# != 1 ]; then - usage >&2 - exit 1 -fi - -DIR=$1 - -echo Username: >&2 -IFS= read -r USER - -if printf '%s' "$USER" | grep -qe '[[:space:]]' -then - echo Username cannot contain whitespaces >&2 - exit 1 -fi - -DB="$DIR/db.json" - -if jq '.users[].name' "$DB" | grep -q $USER -then - echo User $USER already in $DB >&2 - exit 1 -fi - -TTYCFG=$(stty -g) -trap "stty $TTYCFG" INT QUIT TERM EXIT -stty -echo -echo Password: >&2 -IFS= read -r PWD -stty echo -# Force newline -echo - -echo "Quota, in MiB (leave empty for unlimited quota):" >&2 -read -r QUOTA -QUOTA="${QUOTA:+"$(printf '%d' "$QUOTA")"}" - -PWD=$(printf '%s' "$PWD" | to_hex) -SALT=$(openssl rand -hex 32) -KEY=$(openssl rand -hex 32) -PWD=$(printf '%s%s' "$SALT" "$PWD") - -ROUNDS=1000 - -for i in $(seq $ROUNDS) -do - printf "\r%d/$ROUNDS" $i >&2 - PWD=$(printf '%s' "$PWD" | to_bin | openssl sha256 -r | cut -d' ' -f1) -done - -echo >&2 -TMP=$(mktemp_posix) - -cleanup() -{ - rm -f $TMP -} - -trap cleanup EXIT - -jq ".users += [ -{ - \"name\": \"$USER\", - \"password\": \""$PWD"\", - \"salt\": \"$SALT\", - \"key\": \"$KEY\", - \"quota\": \"$QUOTA\" -}]" "$DB" > $TMP - -mkdir -p "$DIR/user/$USER" -mv $TMP "$DB" diff --git a/usergen.c b/usergen.c new file mode 100644 index 0000000..5a0cf3c --- /dev/null +++ b/usergen.c @@ -0,0 +1,546 @@ +#define _POSIX_C_SOURCE 200809L + +#include "hex.h" +#include <dynstr.h> +#include <cjson/cJSON.h> +#include <sodium.h> +#include <fcntl.h> +#include <sys/stat.h> +#include <termios.h> +#include <unistd.h> +#include <errno.h> +#include <stddef.h> +#include <stdint.h> +#include <stdlib.h> +#include <stdio.h> +#include <string.h> + +static cJSON *dump_db(const char *const dir) +{ + int fd = -1; + cJSON *ret = NULL, *c = NULL; + char *buf = NULL; + struct dynstr d; + struct stat sb; + + dynstr_init(&d); + + if (dynstr_append(&d, "%s/db.json", dir)) + { + fprintf(stderr, "%s: dynstr_append failed\n", __func__); + goto end; + } + else if ((fd = open(d.str, O_RDONLY)) < 0) + { + fprintf(stderr, "%s: open(2) %s: %s\n", __func__, d.str, + strerror(errno)); + goto end; + } + else if (fstat(fd, &sb)) + { + fprintf(stderr, "%s: fstat(2) %s: %s\n", __func__, d.str, + strerror(errno)); + goto end; + } + else if (sb.st_size > SIZE_MAX - 1) + { + fprintf(stderr, "%s: size for %s (%ju) exceeds maximum size (%zu)\n", + __func__, d.str, (uintmax_t)sb.st_size, SIZE_MAX); + goto end; + } + else if (!(buf = malloc(sb.st_size + 1))) + { + fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno)); + goto end; + } + + size_t rem = sb.st_size; + char *p = buf; + + while (rem) + { + const ssize_t r = read(fd, p, rem); + + if (r < 0) + { + fprintf(stderr, "%s: read(2): %s\n", __func__, strerror(errno)); + goto end; + } + + rem -= r; + p += r; + } + + buf[sb.st_size] = '\0'; + + if (!(c = cJSON_Parse(buf))) + { + fprintf(stderr, "%s: cJSON_Parse failed\n", __func__); + goto end; + } + + ret = c; + +end: + + if (fd >= 0 && close(fd)) + { + fprintf(stderr, "%s: close(2): %s\n", __func__, strerror(errno)); + ret = NULL; + } + + if (!ret) + cJSON_Delete(c); + + dynstr_free(&d); + free(buf); + return ret; +} + +static int getuser(char *const username, const size_t n) +{ + fputs("Username:\n", stderr); + + if (!fgets(username, n, stdin)) + { + fputs("Failed to obtain username\n", stderr); + return -1; + } + else if (strcspn(username, " \t") != strlen(username)) + { + fputs("Username cannot contain whitespaces\n", stderr); + return -1; + } + + *strchr(username, '\n') = '\0'; + return 0; +} + +static int checkuser(const char *const username, const cJSON *const c) +{ + const cJSON *entry, *const users = cJSON_GetObjectItem(c, "users"); + + if (!users) + { + fputs("Could not find users in database\n", stderr); + return -1; + } + else if (!cJSON_IsArray(users)) + { + fputs("Expected JSON array for users\n", stderr); + return -1; + } + + cJSON_ArrayForEach(entry, users) + { + const cJSON *const u = cJSON_GetObjectItem(entry, "name"); + const char *dbuser; + + if (!u || !(dbuser = cJSON_GetStringValue(u))) + { + fputs("Missing username field in database\n", stderr); + return -1; + } + else if (!strcmp(username, dbuser)) + { + fprintf(stderr, "User %s already in database\n", username); + return -1; + } + } + + return 0; +} + +static int getpass(char *const password, const size_t n) +{ + int ret = -1; + struct termios t, ne; + + fputs("Password:\n", stderr); + + if (tcgetattr(STDIN_FILENO, &t)) + { + fprintf(stderr, "%s: tcgetattr(3): %s\n", __func__, strerror(errno)); + return -1; + } + + ne = t; + ne.c_lflag ^= ECHO; + + if (tcsetattr(STDIN_FILENO, TCSANOW, &ne)) + { + fprintf(stderr, "%s: tcgetattr(3): %s\n", __func__, strerror(errno)); + return -1; + } + else if (!fgets(password, n, stdin)) + { + fputs("Failed to obtain username\n", stderr); + goto restore; + } + + *strchr(password, '\n') = '\0'; + putchar('\n'); + ret = 0; + +restore: + + if (tcsetattr(STDIN_FILENO, TCSANOW, &t)) + { + fprintf(stderr, "%s: tcgetattr(3): %s\n", __func__, strerror(errno)); + ret = -1; + } + + return ret; +} + +static int getquota(unsigned long long *const q) +{ + char s[256], *end; + + fputs("Quota, in MiB (leave empty for unlimited quota):\n", stderr); + + if (!fgets(s, sizeof s, stdin)) + { + fprintf(stderr, "%s: fgets(3): %s\n", __func__, strerror(errno)); + return -1; + } + + *strchr(s, '\n') = '\0'; + + if (!*s) + { + *q = 0; + return 0; + } + + errno = 0; + *q = strtoull(s, &end, 10); + + if (errno) + { + fprintf(stderr, "%s: strtoull(3): %s\n", __func__, strerror(errno)); + return -1; + } + else if (*end) + { + fprintf(stderr, "Invalid quota: %s\n", s); + return -1; + } + + return 0; +} + +static int getmethod(char *const method, const size_t n) +{ + fputs("Password hashing (sha256 [deprecated], argon2id): " + "[argon2id]\n", stderr); + + if (!fgets(method, n, stdin)) + { + fprintf(stderr, "%s: fgets(3): %s\n", __func__, strerror(errno)); + return -1; + } + + *strchr(method, '\n') = '\0'; + + if (!*method) + strcpy(method, "argon2id"); + + return 0; +} + +static cJSON *createobj(cJSON *const c, const char *const username, + const char *const key, const char *const method, + const unsigned long long quota) +{ + cJSON *const ret = cJSON_CreateObject(); + char sq[sizeof "18446744073709551615"]; + const int n = snprintf(sq, sizeof sq, "%llu", quota); + + if (!ret) + { + fprintf(stderr, "%s: cJSON_CreateObject failed\n", __func__); + goto failure; + } + else if (n < 0 || n >= sizeof sq) + { + fprintf(stderr, "%s: snprintf(3) failed with %d\n", __func__, n); + goto failure; + } + else if (!cJSON_AddStringToObject(ret, "name", username) + || !cJSON_AddStringToObject(ret, "key", key) + || !cJSON_AddStringToObject(ret, "method", method) + || (quota && !cJSON_AddStringToObject(ret, "quota", sq))) + { + fprintf(stderr, "%s: cJSON_AddStringToObject failed\n", __func__); + goto failure; + } + + return ret; + +failure: + cJSON_Delete(ret); + return NULL; +} + +static int runsha256(const char *const password, cJSON *const o) +{ + int ret = -1; + enum {ROUNDS = 1000}; + unsigned char salt[32], sha256[crypto_hash_sha256_BYTES]; + const size_t plen = strlen(password), bufsz = sizeof salt + plen; + char hexsha256[sizeof sha256 * 2 + 1], hexsalt[sizeof salt * 2 + 1]; + unsigned char *const buf = malloc(bufsz); + + randombytes_buf(salt, sizeof salt); + + if (!buf) + { + fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno)); + goto end; + } + + memcpy(buf, salt, sizeof salt); + memcpy(buf + sizeof salt, password, plen); + + if (crypto_hash_sha256(sha256, buf, bufsz)) + { + fprintf(stderr, "%s: crypto_hash_sha256 failed\n", __func__); + goto end; + } + + fprintf(stderr, "1/%d", ROUNDS); + + for (int i = 1; i < ROUNDS; i++) + { + if (crypto_hash_sha256(sha256, sha256, sizeof sha256)) + { + fprintf(stderr, "%s: crypto_hash_sha256 failed\n", __func__); + goto end; + } + + fprintf(stderr, "\r%d/%d", i + 1, ROUNDS); + } + + fputc('\n', stderr); + + if (hex_encode(salt, hexsalt, sizeof salt, sizeof hexsalt) + || hex_encode(sha256, hexsha256, sizeof sha256, sizeof hexsha256)) + { + fprintf(stderr, "%s: hex_encode failed\n", __func__); + goto end; + } + else if (!cJSON_AddStringToObject(o, "salt", hexsalt) + || !cJSON_AddStringToObject(o, "password", hexsha256)) + { + fprintf(stderr, "%s: cJSON_AddStringToObject failed\n", __func__); + goto end; + } + + ret = 0; + +end: + free(buf); + return ret; +} + +static int runargon2id(const char *const password, cJSON *const o) +{ + char hashpwd[crypto_pwhash_STRBYTES]; + + if (crypto_pwhash_str(hashpwd, password, strlen(password), + crypto_pwhash_OPSLIMIT_INTERACTIVE, crypto_pwhash_MEMLIMIT_INTERACTIVE)) + { + fprintf(stderr, "%s: crypto_pwhash_str failed\n", __func__); + return -1; + } + else if (!cJSON_AddStringToObject(o, "password", hashpwd)) + { + fprintf(stderr, "%s: cJSON_AddStringToObject failed\n", __func__); + return -1; + } + + return 0; +} + +static int runmethod(const char *const method, const char *const password, + cJSON *const o) +{ + static const struct m + { + const char *s; + int (*fn)(const char *, cJSON *); + } methods[] = + { + {.s = "sha256", .fn = runsha256}, + {.s = "argon2id", .fn = runargon2id} + }; + + for (size_t i = 0; i < sizeof methods / sizeof *methods; i++) + { + const struct m *const m = &methods[i]; + + if (!strcmp(m->s, method)) + return m->fn(password, o); + } + + fprintf(stderr, "Invalid password hashing method: %s\n", method); + return -1; +} + +static int save_db(const cJSON *const c, const char *const dir) +{ + int ret = -1; + FILE *f = NULL; + char *s = NULL; + struct dynstr d; + + dynstr_init(&d); + + if (dynstr_append(&d, "%s/db.json", dir)) + { + fprintf(stderr, "%s: dynstr_append failed\n", __func__); + goto end; + } + else if (!(f = fopen(d.str, "wb"))) + { + fprintf(stderr, "%s: fopen(3) %s: %s\n", __func__, d.str, + strerror(errno)); + goto end; + } + else if (!(s = cJSON_Print(c))) + { + fprintf(stderr, "%s: cJSON_Print failed\n", __func__); + goto end; + } + else if (fprintf(f, "%s", s) < 0) + { + fprintf(stderr, "%s: fprintf(3) failed\n", __func__); + goto end; + } + + ret = 0; + +end: + + if (f && fclose(f)) + { + fprintf(stderr, "%s: fclose(3) %s: %s\n", __func__, d.str ? d.str : "", + strerror(errno)); + ret = -1; + } + + cJSON_free(s); + dynstr_free(&d); + return ret; +} + +static int mkuserdir(const char *const dir, const char *const username) +{ + int ret = -1; + struct dynstr d; + + dynstr_init(&d); + + if (dynstr_append(&d, "%s/user", dir)) + { + fprintf(stderr, "%s: dynstr_append failed\n", __func__); + goto end; + } + else if (mkdir(d.str, 0700) && errno != EEXIST) + { + fprintf(stderr, "%s: mkdir(2) %s: %s\n", __func__, d.str, + strerror(errno)); + goto end; + } + else if (dynstr_append(&d, "/%s", username)) + { + fprintf(stderr, "%s: dynstr_append failed\n", __func__); + goto end; + } + else if (mkdir(d.str, 0700) && errno != EEXIST) + { + fprintf(stderr, "%s: mkdir(2) %s: %s\n", __func__, d.str, + strerror(errno)); + goto end; + } + + ret = 0; + +end: + dynstr_free(&d); + return ret; +} + +int main(int argc, char *argv[]) +{ + int ret = EXIT_FAILURE; + cJSON *c = NULL, *o, *users; + unsigned long long quota; + unsigned char key[32]; + char username[256], password[sizeof username], method[sizeof username], + hexkey[sizeof key * 2 + 1]; + const char *dir; + + if (argc != 2) + { + fprintf(stderr, "%s <dir>\n", *argv); + goto end; + } + else if (sodium_init()) + { + fprintf(stderr, "%s: sodium_init failed\n", __func__); + goto end; + } + else if (!(c = dump_db((dir = argv[1])))) + { + fprintf(stderr, "%s: dump_db failed\n", __func__); + goto end; + } + + randombytes_buf(key, sizeof key); + + if (hex_encode(key, hexkey, sizeof key, sizeof hexkey)) + { + fprintf(stderr, "%s: hex_encode failed\n", __func__); + goto end; + } + + if (getuser(username, sizeof username) + || checkuser(username, c) + || getpass(password, sizeof password) + || getquota("a) + || getmethod(method, sizeof method) + || !(o = createobj(c, username, hexkey, method, quota))) + goto end; + else if (runmethod(method, password, o)) + { + cJSON_Delete(o); + goto end; + } + else if (!(users = cJSON_GetObjectItem(c, "users"))) + { + fputs("Missing \"users\" array in database\n", stderr); + goto end; + } + else if (!cJSON_IsArray(users)) + { + fputs("Database item \"users\" not an array\n", stderr); + goto end; + } + else if (!cJSON_AddItemToArray(users, o)) + { + fprintf(stderr, "%s: cJSON_AddItemToArray failed\n", __func__); + cJSON_Delete(o); + goto end; + } + else if (mkuserdir(dir, username) + || save_db(c, dir)) + goto end; + + ret = EXIT_SUCCESS; + +end: + cJSON_Delete(c); + return ret; +} |
