aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXavier Del Campo Romero <xavi92@disroot.org>2025-10-08 13:50:52 +0200
committerXavier Del Campo Romero <xavi92@disroot.org>2025-10-08 22:55:44 +0200
commit10e42591ac72285736d5cc4ee5e7c2f68dbf1e4b (patch)
tree3bb586177e375a6f7f91c0335876faefc28b805c
parent805630dbfcd409a5d49bc89102f4183b71f713f9 (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--.gitignore3
-rw-r--r--CMakeLists.txt18
-rw-r--r--README.md5
-rw-r--r--auth.c95
-rw-r--r--base64.c126
-rw-r--r--base64.h9
-rw-r--r--cmake/Findlibsodium.cmake58
-rwxr-xr-xconfigure31
-rw-r--r--doc/man1/usergen.174
-rw-r--r--jwt.c114
-rw-r--r--main.c18
-rwxr-xr-xusergen97
-rw-r--r--usergen.c546
13 files changed, 845 insertions, 349 deletions
diff --git a/.gitignore b/.gitignore
index dbef56b..759af02 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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)
diff --git a/README.md b/README.md
index 80e850f..685d58c 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/auth.c b/auth.c
index ec8c75c..8ff23fd 100644
--- a/auth.c
+++ b/auth.c
@@ -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()
diff --git a/configure b/configure
index 73d5610..cc9e0dc 100755
--- a/configure
+++ b/configure
@@ -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
diff --git a/jwt.c b/jwt.c
index 94f2f6e..fd5adb1 100644
--- a/jwt.c
+++ b/jwt.c
@@ -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;
}
diff --git a/main.c b/main.c
index 436c50d..1b70542 100644
--- a/main.c
+++ b/main.c
@@ -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(&quota)
+ || 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;
+}