diff --git a/CMakeLists.txt b/CMakeLists.txt index a41613b1..23eadb61 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -255,14 +255,14 @@ if(ENABLE_SSH_TLS) if (MBEDTLS_FOUND) # dependencies - mbedtls set(HAVE_MBEDTLS TRUE) - list(APPEND libsrc src/session_mbedtls.c) + list(APPEND libsrc src/session_mbedtls.c src/crypt.c src/crypt_mbedtls.c) include_directories(${MBEDTLS_INCLUDE_DIRS}) target_link_libraries(netconf2 ${MBEDTLS_LIBRARIES}) list(APPEND CMAKE_REQUIRED_LIBRARIES ${MBEDTLS_LIBRARIES}) else() # dependencies - openssl find_package(OpenSSL 3.0.0 REQUIRED) - list(APPEND libsrc src/session_openssl.c) + list(APPEND libsrc src/session_openssl.c src/crypt.c src/crypt_openssl.c) include_directories(${OPENSSL_INCLUDE_DIR}) target_link_libraries(netconf2 ${OPENSSL_LIBRARIES}) list(APPEND CMAKE_REQUIRED_LIBRARIES ${OPENSSL_LIBRARIES}) @@ -290,18 +290,6 @@ if(ENABLE_SSH_TLS) include_directories(${CURL_INCLUDE_DIRS}) endif() - # crypt (if not found, assume no library needs to be linked) - if(${CMAKE_SYSTEM_NAME} MATCHES "QNX") - set(LIBCRYPT login) - else() - set(LIBCRYPT crypt) - endif() - check_library_exists(${LIBCRYPT} crypt "" HAVE_CRYPT) - if(HAVE_CRYPT) - target_link_libraries(netconf2 ${LIBCRYPT}) - list(APPEND CMAKE_REQUIRED_LIBRARIES ${LIBCRYPT}) - endif() - # libpam if(ENABLE_PAM) find_package(LibPAM) diff --git a/CMakeModules/UseCompat.cmake b/CMakeModules/UseCompat.cmake index 55e67e08..d5c997e3 100644 --- a/CMakeModules/UseCompat.cmake +++ b/CMakeModules/UseCompat.cmake @@ -69,20 +69,6 @@ macro(USE_COMPAT) check_include_file("alloca.h" HAVE_ALLOCA_H) - # crypt - check_include_file("crypt.h" HAVE_CRYPT_H) - - if(${CMAKE_SYSTEM_NAME} MATCHES "QNX") - list(APPEND CMAKE_REQUIRED_LIBRARIES -llogin) - elseif(NOT APPLE) - list(APPEND CMAKE_REQUIRED_LIBRARIES -lcrypt) - endif() - if(HAVE_CRYPT_H) - check_symbol_exists(crypt_r "crypt.h" HAVE_CRYPT_R) - else() - check_symbol_exists(crypt_r "unistd.h" HAVE_CRYPT_R) - endif() - # unsock_get_uid check_symbol_exists(SO_PEERCRED "sys/socket.h" HAVE_SO_PEERCRED) if(NOT HAVE_SO_PEERCRED) diff --git a/README.md b/README.md index 422688b4..613a68dd 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,6 @@ the `distro` directory. * C compiler (gcc >= 4.8.4, clang >= 3.0, ...) * cmake >= 3.5.0 -* crypt(3) * [libyang](https://github.com/CESNET/libyang) * libssh >= 0.9.5 (for SSH support) * OpenSSL >= 3.0.0 or MbedTLS >= 3.5.0 (for TLS support) diff --git a/compat/compat.c b/compat/compat.c index 1dd9dc8e..0c9079ff 100644 --- a/compat/compat.c +++ b/compat/compat.c @@ -380,24 +380,6 @@ get_current_dir_name(void) #endif -#ifndef HAVE_CRYPT_R -char * -crypt_r(const char *phrase, const char *setting, struct crypt_data *data) -{ - static pthread_mutex_t crypt_lock = PTHREAD_MUTEX_INITIALIZER; - char *hash; - - (void) data; - - pthread_mutex_lock(&crypt_lock); - hash = crypt(phrase, setting); - pthread_mutex_unlock(&crypt_lock); - - return hash; -} - -#endif - #ifndef HAVE_TIMEGM time_t timegm(struct tm *tm) diff --git a/compat/compat.h.in b/compat/compat.h.in index 4059f918..771e4dab 100644 --- a/compat/compat.h.in +++ b/compat/compat.h.in @@ -17,11 +17,6 @@ #define _GNU_SOURCE /* pthread_rwlock_t */ -#cmakedefine HAVE_CRYPT_H -#ifdef HAVE_CRYPT_H -# include -#endif - #cmakedefine HAVE_ALLOCA_H #ifdef HAVE_ALLOCA_H # include @@ -80,7 +75,6 @@ #cmakedefine HAVE_STRCHRNUL #cmakedefine HAVE_GET_CURRENT_DIR_NAME #cmakedefine HAVE_TIMEGM -#cmakedefine HAVE_CRYPT_R #cmakedefine HAVE_SO_PEERCRED #cmakedefine HAVE_GETPEEREID @@ -228,16 +222,6 @@ char *strchrnul(const char *s, int c); char *get_current_dir_name(void); #endif -#ifndef HAVE_CRYPT_R - -/* unused anyway */ -struct crypt_data { - char a; -}; - -char *crypt_r(const char *phrase, const char *setting, struct crypt_data *data); -#endif - #ifndef HAVE_TIMEGM time_t timegm(struct tm *tm); #endif diff --git a/src/crypt.c b/src/crypt.c new file mode 100644 index 00000000..4780e121 --- /dev/null +++ b/src/crypt.c @@ -0,0 +1,706 @@ +/** + * @file crypt.c + * @author Petr Hanzlik + * @brief libnetconf2 - password hashing core + * + * @copyright + * Copyright (c) 2026 CESNET, z.s.p.o. + * + * This source code is licensed under BSD 3-Clause License (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + */ + +#include +#include + +#include "compat.h" +#include "crypt_p.h" +#include "log_p.h" + +/** salt length limits defined by the formats */ +#define NC_CRYPT_MD5_SALT_MAX 8 +#define NC_CRYPT_SHA_SALT_MAX 16 + +/** SHA-crypt "rounds=" limits, part of the format */ +#define NC_CRYPT_SHA_ROUNDS_MIN 1000 +#define NC_CRYPT_SHA_ROUNDS_MAX 999999999UL +#define NC_CRYPT_SHA_ROUNDS_DEFAULT 5000 + +/** number of MD5-crypt stretching rounds, fixed by the format */ +#define NC_CRYPT_MD5_ROUNDS 1000 + +/** lengths of the base64-encoded digests */ +#define NC_CRYPT_MD5_HASH_LEN 22 +#define NC_CRYPT_SHA256_HASH_LEN 43 +#define NC_CRYPT_SHA512_HASH_LEN 86 + +/** + * @brief Base64 alphabet shared by the hashing schemes. + */ +static const char nc_crypt_b64tab[] = + "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + +/** + * @brief Parsed setting string. + */ +struct nc_crypt_setting { + enum nc_crypt_alg alg; /**< digest algorithm */ + unsigned long rounds; /**< SHA-crypt rounds */ + int rounds_custom; /**< "rounds=" was present in the setting */ + char salt[NC_CRYPT_SHA_SALT_MAX + 1]; /**< salt, truncated and NUL-terminated */ + size_t salt_len; /**< salt length */ + const char *stored; /**< stored digest part of the setting, NULL if absent */ +}; + +/** + * @brief Check that a string consists only of the crypt base64 alphabet characters. + * + * @param[in] str String to check. + * @param[in] len Length of @p str. + * @return 1 if all the characters are valid, 0 otherwise. + */ +static int +nc_crypt_valid_b64(const char *str, size_t len) +{ + size_t i; + char c; + + for (i = 0; i < len; ++i) { + c = str[i]; + if (((c < 'a') || (c > 'z')) && ((c < 'A') || (c > 'Z')) && ((c < '0') || (c > '9')) && + (c != '.') && (c != '/')) { + return 0; + } + } + + return 1; +} + +/** + * @brief Zero a buffer in a way the compiler cannot optimize away. + * + * @param[in] p Buffer to zero. + * @param[in] len Length of @p p. + */ +static void +nc_crypt_memzero(void *p, size_t len) +{ + volatile unsigned char *vp = p; + size_t i; + + for (i = 0; i < len; ++i) { + vp[i] = 0; + } +} + +/** + * @brief Parse a setting string "$$[rounds=N>$][$]". + * + * @param[in] setting Setting to parse. + * @param[out] set Parsed setting. + * @return 0 on success. + * @return -2 on unsupported hash id. + * @return -3 on malformed setting. + */ +static int +nc_crypt_parse_setting(const char *setting, struct nc_crypt_setting *set) +{ + const char *p; + char *end = NULL; + unsigned long rounds; + size_t salt_run_len, salt_max; + + if ((setting[0] != '$') || !setting[1] || !setting[2]) { + WRN(NULL, "Setting is not a valid hash setting."); + return -3; + } + if (setting[2] != '$') { + WRN(NULL, "Hash id in the setting is not supported."); + return -2; + } + + /* parse the hash id */ + switch (setting[1]) { + case '1': + set->alg = NC_CRYPT_ALG_MD5; + set->rounds = NC_CRYPT_MD5_ROUNDS; + salt_max = NC_CRYPT_MD5_SALT_MAX; + break; + case '5': + set->alg = NC_CRYPT_ALG_SHA256; + set->rounds = NC_CRYPT_SHA_ROUNDS_DEFAULT; + salt_max = NC_CRYPT_SHA_SALT_MAX; + break; + case '6': + set->alg = NC_CRYPT_ALG_SHA512; + set->rounds = NC_CRYPT_SHA_ROUNDS_DEFAULT; + salt_max = NC_CRYPT_SHA_SALT_MAX; + break; + default: + WRN(NULL, "Hash id \"$%c$\" is not supported.", setting[1]); + return -2; + } + + p = setting + 3; + + /* optional "rounds=$" prefix of the SHA-crypt schemes */ + if (((set->alg == NC_CRYPT_ALG_SHA256) || (set->alg == NC_CRYPT_ALG_SHA512)) && + !strncmp(p, "rounds=", 7)) { + + rounds = strtoul(p + 7, &end, 10); + if ((end == (p + 7)) || (*end != '$')) { + WRN(NULL, "Invalid \"rounds=\" specification in the setting."); + return -3; + } + if ((rounds < NC_CRYPT_SHA_ROUNDS_MIN) || (rounds > NC_CRYPT_SHA_ROUNDS_MAX)) { + WRN(NULL, "\"rounds=%lu\" is out of range [%d, %lu].", + rounds, NC_CRYPT_SHA_ROUNDS_MIN, NC_CRYPT_SHA_ROUNDS_MAX); + return -3; + } + set->rounds = rounds; + set->rounds_custom = 1; + p = end + 1; + } + + /* parse the salt */ + salt_run_len = strcspn(p, "$"); + if ((salt_run_len > 0) && !nc_crypt_valid_b64(p, salt_run_len)) { + WRN(NULL, "Invalid characters in the salt of the setting."); + return -3; + } + set->salt_len = (salt_run_len < salt_max) ? salt_run_len : salt_max; + memcpy(set->salt, p, set->salt_len); + set->salt[set->salt_len] = '\0'; + + if (p[salt_run_len] == '$') { + /* everything after the salt is the stored digest (VERIFY target) */ + set->stored = p + salt_run_len + 1; + } + + return 0; +} + +/** + * @brief Encode 3 bytes as up to 4 base64 characters. + * + * The 24 bits are consumed 6 at a time starting from the least significant + * bit. + * + * @param[in,out] out Output position, advanced by @p nchars characters. + * @param[in] b2 First byte, most significant in the 24-bit word. + * @param[in] b1 Second byte. + * @param[in] b0 Third byte, least significant in the 24-bit word. + * @param[in] nchars Number of characters to emit (2-4). + */ +static void +nc_crypt_b64_group(char **out, unsigned char b2, unsigned char b1, unsigned char b0, int nchars) +{ + /* assemble one 24-bit word from the 3 bytes, b2 is the most significant + * byte (the encode tables pass the first digest byte of a group as b2) */ + unsigned int w = ((unsigned int)b2 << 16) | ((unsigned int)b1 << 8) | b0; + + /* emit one character per 6 bits starting from the least significant bits, + * the reverse of the standard base64 bit order; the final partial groups + * pass their leftover bytes as b0/b1 so that all the significant bits + * are covered by the emitted characters */ + while (nchars-- > 0) { + *(*out)++ = nc_crypt_b64tab[w & 0x3f]; + w >>= 6; + } +} + +/** + * @brief Encode a raw digest in the base64 variant of the given scheme. + * + * @param[in] alg Algorithm of the digest. + * @param[in] digest Raw digest bytes. + * @param[out] out Buffer for the encoded digest, NUL-terminated by the caller. + */ +static void +nc_crypt_encode(enum nc_crypt_alg alg, const unsigned char *digest, char *out) +{ + switch (alg) { + case NC_CRYPT_ALG_MD5: + nc_crypt_b64_group(&out, digest[0], digest[6], digest[12], 4); + nc_crypt_b64_group(&out, digest[1], digest[7], digest[13], 4); + nc_crypt_b64_group(&out, digest[2], digest[8], digest[14], 4); + nc_crypt_b64_group(&out, digest[3], digest[9], digest[15], 4); + nc_crypt_b64_group(&out, digest[4], digest[10], digest[5], 4); + nc_crypt_b64_group(&out, 0, 0, digest[11], 2); + break; + case NC_CRYPT_ALG_SHA256: + nc_crypt_b64_group(&out, digest[0], digest[10], digest[20], 4); + nc_crypt_b64_group(&out, digest[21], digest[1], digest[11], 4); + nc_crypt_b64_group(&out, digest[12], digest[22], digest[2], 4); + nc_crypt_b64_group(&out, digest[3], digest[13], digest[23], 4); + nc_crypt_b64_group(&out, digest[24], digest[4], digest[14], 4); + nc_crypt_b64_group(&out, digest[15], digest[25], digest[5], 4); + nc_crypt_b64_group(&out, digest[6], digest[16], digest[26], 4); + nc_crypt_b64_group(&out, digest[27], digest[7], digest[17], 4); + nc_crypt_b64_group(&out, digest[18], digest[28], digest[8], 4); + nc_crypt_b64_group(&out, digest[9], digest[19], digest[29], 4); + nc_crypt_b64_group(&out, 0, digest[31], digest[30], 3); + break; + case NC_CRYPT_ALG_SHA512: + nc_crypt_b64_group(&out, digest[0], digest[21], digest[42], 4); + nc_crypt_b64_group(&out, digest[22], digest[43], digest[1], 4); + nc_crypt_b64_group(&out, digest[44], digest[2], digest[23], 4); + nc_crypt_b64_group(&out, digest[3], digest[24], digest[45], 4); + nc_crypt_b64_group(&out, digest[25], digest[46], digest[4], 4); + nc_crypt_b64_group(&out, digest[47], digest[5], digest[26], 4); + nc_crypt_b64_group(&out, digest[6], digest[27], digest[48], 4); + nc_crypt_b64_group(&out, digest[28], digest[49], digest[7], 4); + nc_crypt_b64_group(&out, digest[50], digest[8], digest[29], 4); + nc_crypt_b64_group(&out, digest[9], digest[30], digest[51], 4); + nc_crypt_b64_group(&out, digest[31], digest[52], digest[10], 4); + nc_crypt_b64_group(&out, digest[53], digest[11], digest[32], 4); + nc_crypt_b64_group(&out, digest[12], digest[33], digest[54], 4); + nc_crypt_b64_group(&out, digest[34], digest[55], digest[13], 4); + nc_crypt_b64_group(&out, digest[56], digest[14], digest[35], 4); + nc_crypt_b64_group(&out, digest[15], digest[36], digest[57], 4); + nc_crypt_b64_group(&out, digest[37], digest[58], digest[16], 4); + nc_crypt_b64_group(&out, digest[59], digest[17], digest[38], 4); + nc_crypt_b64_group(&out, digest[18], digest[39], digest[60], 4); + nc_crypt_b64_group(&out, digest[40], digest[61], digest[19], 4); + nc_crypt_b64_group(&out, digest[62], digest[20], digest[41], 4); + nc_crypt_b64_group(&out, 0, 0, digest[63], 2); + break; + } +} + +/** + * @brief One operand of a stretching round. + */ +struct nc_crypt_feed { + const void *data; /**< operand bytes */ + size_t len; /**< operand length */ +}; + +/** + * @brief Run the stretching rounds. + * + * @param[in,out] digest_ctx Digest context with ::alg set, reused every round. + * @param[in] pw_seq Password-derived byte sequence. + * @param[in] pw_seq_len Length of @p pw_seq. + * @param[in] salt_seq Salt-derived byte sequence. + * @param[in] salt_seq_len Length of @p salt_seq. + * @param[in,out] digest Digest from the preliminary phase, replaced every round. + * @param[in] digest_len Digest length. + * @param[in] rounds Number of rounds. + * @return 0 on success, -5 on digest failure. + */ +static int +nc_crypt_stretch(struct nc_crypt_digest *digest_ctx, const void *pw_seq, size_t pw_seq_len, + const void *salt_seq, size_t salt_seq_len, unsigned char *digest, size_t digest_len, + unsigned long rounds) +{ + struct nc_crypt_feed feeds[4]; + int n; + + for (size_t i = 0; i < rounds; ++i) { + feeds[0].data = (i & 1) ? pw_seq : digest; + feeds[0].len = (i & 1) ? pw_seq_len : digest_len; + + n = 1; + if (i % 3) { + feeds[n].data = salt_seq; + feeds[n].len = salt_seq_len; + ++n; + } + if (i % 7) { + feeds[n].data = pw_seq; + feeds[n].len = pw_seq_len; + ++n; + } + + feeds[n].data = (i & 1) ? digest : pw_seq; + feeds[n].len = (i & 1) ? digest_len : pw_seq_len; + ++n; + + if (nc_crypt_digest_init(digest_ctx)) { + return -5; + } + for (int j = 0; j < n; ++j) { + if (nc_crypt_digest_update(digest_ctx, feeds[j].data, feeds[j].len)) { + return -5; + } + } + if (nc_crypt_digest_final(digest_ctx, digest)) { + return -5; + } + } + + return 0; +} + +/** + * @brief Compute the raw MD5-crypt ("$1$") digest. + * + * @param[in] password Password. + * @param[in] pw_len Password length. + * @param[in] salt Salt, already truncated. + * @param[in] salt_len Salt length. + * @param[out] digest Resulting 16 bytes. + * @return 0 on success, -5 on digest failure. + */ +static int +nc_crypt_md5_digest(const char *password, size_t pw_len, const char *salt, size_t salt_len, + unsigned char digest[16]) +{ + struct nc_crypt_digest digest_ctx = {0}; + unsigned char alt[16] = {0}; + int rc = 0; + size_t n; + + digest_ctx.alg = NC_CRYPT_ALG_MD5; + + /* alternate sum B (password + salt + password) */ + if (nc_crypt_digest_init(&digest_ctx) || + nc_crypt_digest_update(&digest_ctx, password, pw_len) || + nc_crypt_digest_update(&digest_ctx, salt, salt_len) || + nc_crypt_digest_update(&digest_ctx, password, pw_len) || + nc_crypt_digest_final(&digest_ctx, alt)) { + rc = -5; + goto cleanup; + } + + /* open digest A (password + "$1$" + salt) */ + if (nc_crypt_digest_init(&digest_ctx) || + nc_crypt_digest_update(&digest_ctx, password, pw_len) || + nc_crypt_digest_update(&digest_ctx, "$1$", 3) || + nc_crypt_digest_update(&digest_ctx, salt, salt_len)) { + rc = -5; + goto cleanup; + } + + /* for every full 16 bytes of password length add B, then the first n bytes */ + for (n = pw_len; n > 16; n -= 16) { + if (nc_crypt_digest_update(&digest_ctx, alt, 16)) { + rc = -5; + goto cleanup; + } + } + if (nc_crypt_digest_update(&digest_ctx, alt, n)) { + rc = -5; + goto cleanup; + } + + /* for every set bit of the password length a NUL byte, otherwise the first password byte */ + for (size_t i = pw_len; i; i >>= 1) { + if (nc_crypt_digest_update(&digest_ctx, (i & 1) ? "" : password, 1)) { + rc = -5; + goto cleanup; + } + } + + if (nc_crypt_digest_final(&digest_ctx, digest)) { + rc = -5; + goto cleanup; + } + + rc = nc_crypt_stretch(&digest_ctx, password, pw_len, salt, salt_len, digest, 16, NC_CRYPT_MD5_ROUNDS); + +cleanup: + /* the alternate sum is derived from the password */ + nc_crypt_memzero(alt, sizeof alt); + nc_crypt_digest_cleanup(&digest_ctx); + return rc; +} + +/** + * @brief Compute the raw SHA-crypt ("$5$"/"$6$") digest. + * + * @param[in] alg SHA256 or SHA512. + * @param[in] password Password. + * @param[in] pw_len Password length. + * @param[in] salt Salt, already truncated. + * @param[in] salt_len Salt length. + * @param[in] rounds Number of rounds. + * @param[out] digest Resulting 32 or 64 bytes. + * @return 0 on success, -5 on digest failure. + */ +static int +nc_crypt_sha_digest(enum nc_crypt_alg alg, const char *password, size_t pw_len, + const char *salt, size_t salt_len, unsigned long rounds, unsigned char *digest) +{ + struct nc_crypt_digest digest_ctx = {0}; + unsigned char alt[64] = {0}, dp[64] = {0}, ds[64] = {0}; + unsigned char *p_seq = NULL, *s_seq = NULL; + size_t n, size, i; + int rc = 0; + + size = (alg == NC_CRYPT_ALG_SHA256) ? 32 : 64; + digest_ctx.alg = alg; + + /* alternate sum B = SHA(password + salt + password) */ + if (nc_crypt_digest_init(&digest_ctx) || + nc_crypt_digest_update(&digest_ctx, password, pw_len) || + nc_crypt_digest_update(&digest_ctx, salt, salt_len) || + nc_crypt_digest_update(&digest_ctx, password, pw_len) || + nc_crypt_digest_final(&digest_ctx, alt)) { + rc = -5; + goto cleanup; + } + + /* open digest A (password + salt) */ + if (nc_crypt_digest_init(&digest_ctx) || + nc_crypt_digest_update(&digest_ctx, password, pw_len) || + nc_crypt_digest_update(&digest_ctx, salt, salt_len)) { + rc = -5; + goto cleanup; + } + + /* for every full digest-sized block of password length add B, then the first n bytes */ + for (n = pw_len; n > size; n -= size) { + if (nc_crypt_digest_update(&digest_ctx, alt, size)) { + rc = -5; + goto cleanup; + } + } + if (nc_crypt_digest_update(&digest_ctx, alt, n)) { + rc = -5; + goto cleanup; + } + + /* for every set bit of the password length the whole B, otherwise the whole password */ + for (i = pw_len; i; i >>= 1) { + if (i & 1) { + if (nc_crypt_digest_update(&digest_ctx, alt, size)) { + rc = -5; + goto cleanup; + } + } else if (nc_crypt_digest_update(&digest_ctx, password, pw_len)) { + rc = -5; + goto cleanup; + } + } + + if (nc_crypt_digest_final(&digest_ctx, digest)) { + rc = -5; + goto cleanup; + } + + /* password precompression DP = SHA(password for every byte of the password) */ + if (nc_crypt_digest_init(&digest_ctx)) { + rc = -5; + goto cleanup; + } + for (i = 0; i < pw_len; ++i) { + if (nc_crypt_digest_update(&digest_ctx, password, pw_len)) { + rc = -5; + goto cleanup; + } + } + if (nc_crypt_digest_final(&digest_ctx, dp)) { + rc = -5; + goto cleanup; + } + + /* salt precompression DS = SHA(salt repeated 16 + digest[0] times) */ + if (nc_crypt_digest_init(&digest_ctx)) { + rc = -5; + goto cleanup; + } + for (i = 0; i < (size_t)(16 + digest[0]); ++i) { + if (nc_crypt_digest_update(&digest_ctx, salt, salt_len)) { + rc = -5; + goto cleanup; + } + } + if (nc_crypt_digest_final(&digest_ctx, ds)) { + rc = -5; + goto cleanup; + } + + /* P and S byte sequences: DP and DS cycled to the password and salt lengths */ + p_seq = calloc(pw_len ? pw_len : 1, sizeof *p_seq); + if (p_seq) { + s_seq = calloc(salt_len ? salt_len : 1, sizeof *s_seq); + } + if (!p_seq || !s_seq) { + ERRMEM; + rc = -5; + goto cleanup; + } + for (i = 0; i < pw_len; ++i) { + p_seq[i] = dp[i % size]; + } + for (i = 0; i < salt_len; ++i) { + s_seq[i] = ds[i % size]; + } + + rc = nc_crypt_stretch(&digest_ctx, p_seq, pw_len, s_seq, salt_len, digest, size, rounds); + +cleanup: + /* P, S and the alternate sums are derived from the password */ + if (p_seq) { + nc_crypt_memzero(p_seq, pw_len ? pw_len : 1); + } + if (s_seq) { + nc_crypt_memzero(s_seq, salt_len ? salt_len : 1); + } + free(p_seq); + free(s_seq); + nc_crypt_memzero(alt, sizeof alt); + nc_crypt_memzero(dp, sizeof dp); + nc_crypt_memzero(ds, sizeof ds); + nc_crypt_digest_cleanup(&digest_ctx); + return rc; +} + +/** + * @brief Compare two equal-length strings in constant time. + * + * @param[in] a First string. + * @param[in] b Second string. + * @param[in] len Length of both strings. + * @return 0 if the strings are equal, 1 otherwise. + */ +static int +nc_crypt_ct_equal(const char *a, const char *b, size_t len) +{ + /* Volatile to prevent time-based attacks */ + volatile unsigned char diff = 0; + size_t i; + + for (i = 0; i < len; ++i) { + diff |= a[i] ^ b[i]; + } + + return diff != 0; +} + +int +nc_crypt(const char *password, const char *setting, struct nc_crypt *crypt) +{ + struct nc_crypt_setting set = {0}; + unsigned char digest[64] = {0}; + char b64[NC_CRYPT_SHA512_HASH_LEN + 1] = {0}; + size_t pw_len, hash_len, stored_len, needed, digits; + char id; + int rc; + + if (!password || !setting || !crypt) { + ERR(NULL, "Invalid arguments for nc_crypt()."); + return -1; + } + if ((crypt->action != NC_CRYPT_CREATE) && (crypt->action != NC_CRYPT_VERIFY)) { + ERR(NULL, "Invalid nc_crypt action (%d).", (int)crypt->action); + return -1; + } + if (crypt->action == NC_CRYPT_VERIFY) { + /* a failed verification must always report a mismatch */ + crypt->match = 1; + } + + rc = nc_crypt_parse_setting(setting, &set); + if (rc) { + return rc; + } + + if (crypt->action == NC_CRYPT_VERIFY) { + if (!set.stored || !*set.stored) { + WRN(NULL, "Setting has no stored digest to verify against."); + return -3; + } + if (!nc_crypt_valid_b64(set.stored, strlen(set.stored))) { + WRN(NULL, "Invalid characters in the stored digest of the setting."); + return -3; + } + } + + /* learn the encoded digest length */ + switch (set.alg) { + case NC_CRYPT_ALG_MD5: + hash_len = NC_CRYPT_MD5_HASH_LEN; + break; + case NC_CRYPT_ALG_SHA256: + hash_len = NC_CRYPT_SHA256_HASH_LEN; + break; + case NC_CRYPT_ALG_SHA512: + hash_len = NC_CRYPT_SHA512_HASH_LEN; + break; + } + + /* validate the output parameters */ + if (crypt->action == NC_CRYPT_CREATE) { + id = (set.alg == NC_CRYPT_ALG_MD5) ? '1' : (set.alg == NC_CRYPT_ALG_SHA256) ? '5' : '6'; + + needed = 3 + set.salt_len + 1 + hash_len + 1; + if (set.rounds_custom) { + needed += 8; /* "rounds=" and the trailing '$' */ + digits = set.rounds; + do { + ++needed; + digits /= 10; + } while (digits); + } + + if (!crypt->output) { + ERR(NULL, "No output buffer provided for hash creation."); + return -1; + } + if (crypt->output_len < needed) { + ERR(NULL, "Output buffer too small (%zu bytes, at least %zu needed).", crypt->output_len, needed); + return -4; + } + } + + pw_len = strlen(password); + + if (set.alg == NC_CRYPT_ALG_MD5) { + rc = nc_crypt_md5_digest(password, pw_len, set.salt, set.salt_len, digest); + } else { + rc = nc_crypt_sha_digest(set.alg, password, pw_len, set.salt, set.salt_len, set.rounds, digest); + } + if (rc) { + /* the digest may be partially computed */ + nc_crypt_memzero(digest, sizeof digest); + return rc; + } + + nc_crypt_encode(set.alg, digest, b64); + b64[hash_len] = '\0'; + + if (crypt->action == NC_CRYPT_CREATE) { + if (set.rounds_custom) { + snprintf(crypt->output, crypt->output_len, "$%c$rounds=%lu$%s$%s", id, set.rounds, set.salt, b64); + } else { + snprintf(crypt->output, crypt->output_len, "$%c$%s$%s", id, set.salt, b64); + } + } else { + /* a mismatch is a valid outcome, not an error */ + stored_len = strlen(set.stored); + if (stored_len == hash_len) { + crypt->match = nc_crypt_ct_equal(set.stored, b64, hash_len); + } + } + + /* the digest and its encoding are derived from the password */ + nc_crypt_memzero(digest, sizeof digest); + nc_crypt_memzero(b64, sizeof b64); + + return 0; +} + +const char * +nc_crypt_strerr(int err) +{ + switch (err) { + case -1: + return "invalid arguments"; + case -2: + return "unsupported hash id"; + case -3: + return "malformed setting"; + case -4: + return "output buffer too small"; + case -5: + return "digest computation failed"; + default: + return "unknown error"; + } +} diff --git a/src/crypt_mbedtls.c b/src/crypt_mbedtls.c new file mode 100644 index 00000000..ea81d503 --- /dev/null +++ b/src/crypt_mbedtls.c @@ -0,0 +1,134 @@ +/** + * @file crypt_mbedtls.c + * @author Petr Hanzlik + * @brief libnetconf2 - mbed TLS digest backend for password hashing + * + * @copyright + * Copyright (c) 2026 CESNET, z.s.p.o. + * + * This source code is licensed under BSD 3-Clause License (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + */ + +#include +#include + +#include "crypt_p.h" + +/** + * @brief Get the mbed TLS message digest for an algorithm. + * + * @param[in] alg Algorithm. + * @return mbed TLS digest, MBEDTLS_MD_NONE on unsupported algorithm. + */ +static mbedtls_md_type_t +nc_crypt_digest_md(enum nc_crypt_alg alg) +{ + switch (alg) { + case NC_CRYPT_ALG_MD5: + return MBEDTLS_MD_MD5; + case NC_CRYPT_ALG_SHA256: + return MBEDTLS_MD_SHA256; + case NC_CRYPT_ALG_SHA512: + return MBEDTLS_MD_SHA512; + } + + return MBEDTLS_MD_NONE; +} + +/** + * @brief mbed TLS digest context together with the algorithm it is set up for. + */ +struct nc_crypt_mbedtls_ctx { + mbedtls_md_context_t md; + mbedtls_md_type_t type; +}; + +int +nc_crypt_digest_init(struct nc_crypt_digest *digest_ctx) +{ + struct nc_crypt_mbedtls_ctx *ctx = digest_ctx->ctx; + const mbedtls_md_info_t *info; + mbedtls_md_type_t type; + int owned = digest_ctx->ctx != NULL; + + /* Get the mbed TLS digest for the algorithm */ + type = nc_crypt_digest_md(digest_ctx->alg); + info = mbedtls_md_info_from_type(type); + if (!info) { + return 1; + } + + /* Create a new context if not already allocated */ + if (!owned) { + /* Allocate memory for the context, use calloc instead of mbedtls_md_init */ + ctx = calloc(1, sizeof *ctx); + if (!ctx) { + return 1; + } + } else if (ctx->type != type) { + /* The context is reused for a different algorithm, it must be set up again */ + mbedtls_md_free(&ctx->md); + ctx->type = MBEDTLS_MD_NONE; + } + + if (ctx->type == MBEDTLS_MD_NONE) { + /* Setup the mbed TLS context with the selected algorithm */ + if (mbedtls_md_setup(&ctx->md, info, 0)) { + mbedtls_md_free(&ctx->md); + if (!owned) { + free(ctx); + } + return 1; + } + ctx->type = type; + } + + digest_ctx->size = mbedtls_md_get_size(info); + digest_ctx->ctx = ctx; + + if (mbedtls_md_starts(&ctx->md)) { + return 1; + } + + return 0; +} + +int +nc_crypt_digest_update(struct nc_crypt_digest *digest_ctx, const void *data, size_t len) +{ + struct nc_crypt_mbedtls_ctx *ctx = digest_ctx->ctx; + + if (mbedtls_md_update(&ctx->md, data, len)) { + return 1; + } + + return 0; +} + +int +nc_crypt_digest_final(struct nc_crypt_digest *digest_ctx, void *out) +{ + struct nc_crypt_mbedtls_ctx *ctx = digest_ctx->ctx; + + if (mbedtls_md_finish(&ctx->md, out)) { + return 1; + } + + return 0; +} + +void +nc_crypt_digest_cleanup(struct nc_crypt_digest *digest_ctx) +{ + struct nc_crypt_mbedtls_ctx *ctx = digest_ctx->ctx; + + if (ctx) { + mbedtls_md_free(&ctx->md); + free(ctx); + } + digest_ctx->ctx = NULL; +} diff --git a/src/crypt_openssl.c b/src/crypt_openssl.c new file mode 100644 index 00000000..81b46a30 --- /dev/null +++ b/src/crypt_openssl.c @@ -0,0 +1,94 @@ +/** + * @file crypt_openssl.c + * @author Petr Hanzlik + * @brief libnetconf2 - OpenSSL digest backend for password hashing + * + * @copyright + * Copyright (c) 2026 CESNET, z.s.p.o. + * + * This source code is licensed under BSD 3-Clause License (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + */ + +#include + +#include "crypt_p.h" + +/** + * @brief Get the EVP message digest for an algorithm. + * + * @param[in] alg Algorithm. + * @return EVP digest, NULL on unsupported algorithm. + */ +static const EVP_MD * +nc_crypt_digest_md(enum nc_crypt_alg alg) +{ + switch (alg) { + case NC_CRYPT_ALG_MD5: + return EVP_md5(); + case NC_CRYPT_ALG_SHA256: + return EVP_sha256(); + case NC_CRYPT_ALG_SHA512: + return EVP_sha512(); + } + + return NULL; +} + +int +nc_crypt_digest_init(struct nc_crypt_digest *digest_ctx) +{ + const EVP_MD *md; + + /* Get the EVP digest for the algorithm */ + md = nc_crypt_digest_md(digest_ctx->alg); + if (!md) { + return 1; + } + + /* Create a new EVP_MD_CTX if not already allocated */ + if (!digest_ctx->ctx) { + digest_ctx->ctx = EVP_MD_CTX_new(); + if (!digest_ctx->ctx) { + return 1; + } + } + + /* Initialize the digest context with the selected algorithm */ + digest_ctx->size = EVP_MD_get_size(md); + if (!EVP_DigestInit_ex(digest_ctx->ctx, md, NULL)) { + return 1; + } + + return 0; +} + +int +nc_crypt_digest_update(struct nc_crypt_digest *digest_ctx, const void *data, size_t len) +{ + if (!EVP_DigestUpdate(digest_ctx->ctx, data, len)) { + return 1; + } + + return 0; +} + +int +nc_crypt_digest_final(struct nc_crypt_digest *digest_ctx, void *out) +{ + if (!EVP_DigestFinal_ex(digest_ctx->ctx, out, NULL)) { + return 1; + } + + return 0; +} + +void +nc_crypt_digest_cleanup(struct nc_crypt_digest *digest_ctx) +{ + EVP_MD_CTX_free(digest_ctx->ctx); + digest_ctx->ctx = NULL; +} diff --git a/src/crypt_p.h b/src/crypt_p.h new file mode 100644 index 00000000..d7437d1b --- /dev/null +++ b/src/crypt_p.h @@ -0,0 +1,146 @@ +/** + * @file crypt_p.h + * @author Petr Hanzlik + * @brief libnetconf2 - password hashing core header + * + * @copyright + * Copyright (c) 2026 CESNET, z.s.p.o. + * + * This source code is licensed under BSD 3-Clause License (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + */ + +#ifndef NC_CRYPT_P_H_ +#define NC_CRYPT_P_H_ + +#include + +/** + * @brief Recommended output buffer size. + * + * The longest possible output is "$6$rounds=999999999$" + 16 salt chars + + * '$' + 86 digest chars + NUL = 124 bytes. + */ +#define NC_CRYPT_BUFSIZE 128 + +/** + * @brief Actions for ::nc_crypt(). + */ +enum nc_crypt_action { + NC_CRYPT_CREATE, /**< create a new hash from password and the setting salt */ + NC_CRYPT_VERIFY /**< compare password against a full stored hash in setting */ +}; + +/** + * @brief Caller-owned input/output data for ::nc_crypt(). + */ +struct nc_crypt { + enum nc_crypt_action action; /**< action to perform */ + char *output; /**< caller-provided output buffer (may be NULL for VERIFY) */ + size_t output_len; /**< capacity of output including terminating NUL */ + int match; /**< VERIFY result: 0 if password matches, 1 otherwise (always set, even on failure) */ +}; + +/** + * @brief Hash or verify a password in the "$$" password hash format. + * + * Supported ids are "$1$" (MD5-crypt), "$5$" (SHA-256-crypt) and "$6$" + * (SHA-512-crypt). + * + * The salt (CREATE and VERIFY) as well as the stored digest (VERIFY) must + * consist only of the crypt base64 alphabet characters [a-zA-Z0-9./], the + * same restriction the iana-crypt-hash YANG type puts on them. Any digest + * stored in the setting is ignored for CREATE. + * + * @param[in] password Clear-text password. + * @param[in] setting Setting string. For CREATE a salt prefix such as + * "$6$[rounds=N$]$" (rounds only for "$5$"/"$6$"), for VERIFY the complete + * stored hash "$6$[rounds=N$]$". + * @param[in,out] crypt Operation selector and output data. + * @return 0 on success. + * @return -1 on invalid arguments. + * @return -2 on unsupported hash id. + * @return -3 on malformed setting (including invalid salt/digest characters). + * @return -4 if the output buffer is too small. + * @return -5 on internal (digest backend) failure. + * + * Note that some inputs accepted by the crypt(3) implementations are + * deliberately rejected: + * - salt characters outside the crypt base64 alphabet [a-zA-Z0-9./] + * (e.g. '=', which glibc tolerates in MD5-crypt salts), in line with + * the iana-crypt-hash YANG type; + * - "rounds=" outside [1000, 999999999], the range defined by the + * SHA-crypt format (glibc clamps instead, but it always prints the + * clamped value, so glibc-generated hashes are never affected); + * - settings without a "$$" prefix (e.g. legacy DES hashes), which + * do not match any form allowed by the iana-crypt-hash YANG type. + */ +int nc_crypt(const char *password, const char *setting, struct nc_crypt *crypt); + +/** + * @brief Get a static description of an ::nc_crypt() error code. + * + * @param[in] err Error code returned by ::nc_crypt(). + * @return Static string describing the error. + */ +const char *nc_crypt_strerr(int err); + +/** + * @brief Digest algorithms of the supported schemes. + */ +enum nc_crypt_alg { + NC_CRYPT_ALG_MD5, /**< MD5, 16-byte digest */ + NC_CRYPT_ALG_SHA256, /**< SHA-256, 32-byte digest */ + NC_CRYPT_ALG_SHA512 /**< SHA-512, 64-byte digest */ +}; + +/** + * @brief Digest context for one streaming hash computation. + * + * Implemented by the crypto backend (OpenSSL, mbedTLS), the algorithm core + * uses it exclusively through ::nc_crypt_digest_init() and other functions. + */ +struct nc_crypt_digest { + enum nc_crypt_alg alg; /**< algorithm to compute */ + size_t size; /**< digest length in bytes */ + void *ctx; /* backend-private state */ +}; + +/** + * @brief Start (or restart) a digest computation. + * + * @param[in,out] digest_ctx Digest with ::alg set. May be reused after a final. + * @return 0 on success, non-zero on failure. + */ +int nc_crypt_digest_init(struct nc_crypt_digest *digest_ctx); + +/** + * @brief Append data to a digest computation. + * + * @param[in,out] digest_ctx Initialized digest. + * @param[in] data Bytes to hash. + * @param[in] len Number of bytes. + * @return 0 on success, non-zero on failure. + */ +int nc_crypt_digest_update(struct nc_crypt_digest *digest_ctx, const void *data, size_t len); + +/** + * @brief Finish the computation and write the digest bytes. + * + * @param[in,out] digest_ctx Initialized digest. + * @param[out] out Buffer for at least ::size bytes. + * @return 0 on success, non-zero on failure. + */ +int nc_crypt_digest_final(struct nc_crypt_digest *digest_ctx, void *out); + +/** + * @brief Free all resources of a digest. + * + * @param[in,out] digest_ctx Digest to clean up. + */ +void nc_crypt_digest_cleanup(struct nc_crypt_digest *digest_ctx); + +#endif /* NC_CRYPT_P_H_ */ diff --git a/src/server_config.h b/src/server_config.h index 45f3155e..55289860 100644 --- a/src/server_config.h +++ b/src/server_config.h @@ -501,7 +501,7 @@ int nc_server_config_del_ssh_user_authkey(const char *endpt_name, const char *us /** * @brief Hashes a clear-text 'iana-crypt-hash' password the way the server stores it in its configuration. * - * Only "$0$" is a clear-text password, which is hashed into a crypt(3) SHA-512 digest + * Only "$0$" is a clear-text password, which is hashed into a SHA-512 digest * under a freshly generated random salt ("$6$$"). Any other @p crypt_hash is already * hashed, so nothing is done and NULL is returned in @p hashed_password. * diff --git a/src/server_config_util_ssh.c b/src/server_config_util_ssh.c index f15456c1..f7ec4065 100644 --- a/src/server_config_util_ssh.c +++ b/src/server_config_util_ssh.c @@ -27,6 +27,7 @@ #include "compat.h" #include "config.h" +#include "crypt_p.h" #include "log_p.h" #include "server_config.h" #include "session_p.h" @@ -491,7 +492,7 @@ nc_server_config_ch_del_ssh_user_authkey(const char *client_name, const char *en } /** - * @brief Hash a clear-text password into a crypt(3) SHA-512 digest. + * @brief Hash a clear-text password into a SHA-512 digest. * * @param[in] password Clear-text password to hash. * @param[out] hashed_password Generated "$6$$" value. @@ -502,16 +503,16 @@ nc_server_config_crypt_password(const char *password, char **hashed_password) { int ret = 0; size_t i; - char *hashed_pw = NULL; char salt[3 /* "$6$" */ + 16 /* random chars */ + 1 /* trailing '$' */ + 1 /* NUL */]; - struct crypt_data *cdata = NULL; + struct nc_crypt cdata = {0}; + char output[NC_CRYPT_BUFSIZE]; unsigned char rnd[16]; static const char itoa64[] = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; *hashed_password = NULL; - - cdata = calloc(1, sizeof *cdata); - NC_CHECK_ERRMEM_GOTO(!cdata, ret = 1, cleanup); + cdata.output = output; + cdata.output_len = NC_CRYPT_BUFSIZE; + cdata.action = NC_CRYPT_CREATE; /* generate a random salt compatible with crypt SHA-512: "$6$$" */ if (nc_tls_generate_random_bytes_wrap(rnd, sizeof rnd)) { @@ -528,19 +529,17 @@ nc_server_config_crypt_password(const char *password, char **hashed_password) salt[3 + sizeof rnd] = '$'; salt[3 + sizeof rnd + 1] = '\0'; - hashed_pw = crypt_r(password, salt, cdata); - if (!hashed_pw) { - ERR(NULL, "Hashing password failed (%s).", strerror(errno)); + ret = nc_crypt(password, salt, &cdata); + if (ret) { + ERR(NULL, "Hashing password failed (%s).", nc_crypt_strerr(ret)); ret = 1; goto cleanup; } - /* crypt_r() returns a pointer into cdata, which is freed below */ - *hashed_password = strdup(hashed_pw); + *hashed_password = strdup(cdata.output); NC_CHECK_ERRMEM_GOTO(!*hashed_password, ret = 1, cleanup); cleanup: - free(cdata); return ret; } diff --git a/src/session_server_ssh.c b/src/session_server_ssh.c index afa7cd72..669972a7 100644 --- a/src/session_server_ssh.c +++ b/src/session_server_ssh.c @@ -42,6 +42,7 @@ #endif #include "compat.h" +#include "crypt_p.h" #include "log_p.h" #include "nc_version.h" #include "session.h" @@ -1468,8 +1469,7 @@ nc_server_ssh_kbdint_verify_passwd(struct nc_session *session, const char *usern int nc_server_ssh_compare_password(const char *stored_pw, const char *received_pw) { - char *received_pw_hash = NULL; - struct crypt_data *cdata; + struct nc_crypt cdata = {0}; int ret; NC_CHECK_ARG_RET(NULL, stored_pw, received_pw, 1); @@ -1490,18 +1490,15 @@ nc_server_ssh_compare_password(const char *stored_pw, const char *received_pw) return strcmp(stored_pw + 3, received_pw); } - cdata = calloc(1, sizeof *cdata); - NC_CHECK_ERRMEM_RET(!cdata, 1); + cdata.action = NC_CRYPT_VERIFY; - received_pw_hash = crypt_r(received_pw, stored_pw, cdata); - if (!received_pw_hash) { - ERR(NULL, "Hashing the password failed (%s).", strerror(errno)); - free(cdata); + ret = nc_crypt(received_pw, stored_pw, &cdata); + if (ret) { + ERR(NULL, "Verifying the password failed (%s).", nc_crypt_strerr(ret)); return 1; } - ret = strcmp(received_pw_hash, stored_pw); - free(cdata); + ret = cdata.match; return ret; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e1fac8ef..895307d8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -75,6 +75,7 @@ if(ENABLE_SSH_TLS) libnetconf2_test(NAME test_ch PORT_COUNT 2) libnetconf2_test(NAME test_client_monitoring) libnetconf2_test(NAME test_config PORT_COUNT 6) + libnetconf2_test(NAME test_crypt) libnetconf2_test(NAME test_endpt_share_clients PORT_COUNT 4) libnetconf2_test(NAME test_ks_ts) if (LIBPAM_HAVE_CONFDIR) diff --git a/tests/test_crypt.c b/tests/test_crypt.c new file mode 100644 index 00000000..9897966e --- /dev/null +++ b/tests/test_crypt.c @@ -0,0 +1,397 @@ +/** + * @file test_crypt.c + * @author Petr Hanzlik + * @brief libnetconf2 tests - internal password hashing + * + * @copyright + * Copyright (c) 2026 CESNET, z.s.p.o. + * + * This source code is licensed under BSD 3-Clause License (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + */ + +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "compat.h" +#include "crypt_p.h" + +/* complete "$6$" hash of the password "hello" with the salt "abcdefghijklmnop", used by several tests */ +#define TEST_HASH_SHA512_HELLO \ + "$6$abcdefghijklmnop$vRqpf9WzVv13MKJy4hd5jALhNQnt4y0RZsj0MrdHWdwA8G8gihD6oj69IwfuemaVekHG0M6jA5HNgthaIaLRR/" + +/** + * @brief Reference vectors for all the supported schemes. + * + * The expected values were generated by the reference glibc crypt_r(3) + * implementation, called with the exact password and setting of each vector: + * + * struct crypt_data data = {0}; + * crypt_r(password, setting, &data); + * + * The vectors with a salt longer than 8 ("$1$") or 16 ("$5$"/"$6$") + * characters verify the salt truncation, crypt_r(3) truncates the salts the + * same way. All the salts (and digests) are within the crypt base64 + * alphabet [a-zA-Z0-9./]. + */ +static const struct test_vector { + const char *setting; /* "$$[rounds=N$]$" */ + const char *password; /* clear-text password */ + const char *expected; /* complete hash */ +} test_vectors[] = { + /* MD5-crypt */ + {"$1$abc$", "hello", "$1$abc$jWy0FsfCoVBK.I85XpXf30"}, + {"$1$abc$", "", "$1$abc$Or2rbeUYTvt12aiVzMuS/."}, + {"$1$abc$", "0123456789abcdef", "$1$abc$RWqMNvoz3MlAm/dUNBly11"}, + {"$1$abc$", "0123456789abcdef7", "$1$abc$UoisR6zt/vyX.GVBejn/s/"}, + {"$1$abc$", "0123456789abcdef78", "$1$abc$AiQKSpZM70XZD/J5cq1zo0"}, + {"$1$abc$", "0123456789012345678901234567890123456789012345678901234567890123", "$1$abc$o6f5nh6VgFk5PuY00GZ4q."}, + {"$1$abc$", "h\xc3\xa9llo", "$1$abc$olqAE9a3sVQf1DzmPrTaz1"}, + /* '$' in the password is not parsed as part of the setting */ + {"$1$abcdefgh$", "pa$$w0rd$x", "$1$abcdefgh$h1ixzB.L8DxmzznmUj2I40"}, + {"$1$abcdefgh$", "hello", "$1$abcdefgh$rwnEbRiN0agqVgZBovWNQ/"}, + /* the salt of "$1$" is truncated to 8 characters */ + {"$1$abcdefghijklmnop$", "hello", "$1$abcdefgh$rwnEbRiN0agqVgZBovWNQ/"}, + {"$1$verylongsaltvaluewayover16chars$", "hello", "$1$verylong$G.EmESvPcN/dfh2zVVLo4."}, + {"$1$", "hello", "$1$$/PWPe740uvaQxKzRH.Zxj1"}, + {"$1$", "", "$1$$qRPK7m23GJusamGpoGLby/"}, + + /* SHA-256-crypt */ + {"$5$abc$", "hello", "$5$abc$W6SISEcmf6GfVdGFK0lGBXz2NPDhGZ.FBBMOz418YA7"}, + {"$5$abc$", "0123456789abcdef", "$5$abc$YpspK6zwM3epwlxYlNbhj1uz8M0olliID/rZqdtH7uC"}, + {"$5$abc$", "0123456789abcdef7", "$5$abc$g.imuXJ8IzZ8mWpgqnfFLnjgo1XuwbBvNqfBSIXDZ1/"}, + {"$5$abc$", "0123456789abcdef78", "$5$abc$K5aj4AYNi23Mtsp4uB.Tuuxxgqn6L6g4XkK/Utg1q.4"}, + {"$5$abc$", "0123456789012345678901234567890123456789012345678901234567890123", "$5$abc$fICUW/9OYTl38W5To0V79TTVmJMhLE6YYvM9Qs9uAk4"}, + {"$5$abc$", "h\xc3\xa9llo", "$5$abc$inGrvBqHz8uZkN.P/YPvmi1zgtyg/YXaTiIUXx6HTK0"}, + {"$5$abcdefgh$", "hello", "$5$abcdefgh$UUoko/Tf0b.8LrDH6BE28ZvVbc7xd//GfzsD0NKugU7"}, + /* the salt of "$5$"/"$6$" is truncated to 16 characters */ + {"$5$abcdefghijklmnop$", "hello", "$5$abcdefghijklmnop$PWitSpu2soJsfZ6ATsTxeTHC527GEr8VkIcYoKnV6G/"}, + {"$5$abcdefghijklmnopq$", "hello", "$5$abcdefghijklmnop$PWitSpu2soJsfZ6ATsTxeTHC527GEr8VkIcYoKnV6G/"}, + {"$5$verylongsaltvaluewayover16chars$", "hello", "$5$verylongsaltvalu$Efvl8TQO5e9pnzw6o64VsLpL3QQT9VQpZfJI198GR61"}, + /* "rounds=" specification */ + {"$5$rounds=1000$abcdefghijklmnop$", "hello", "$5$rounds=1000$abcdefghijklmnop$WrXWnj4kGA329xgo6OuYgg4dCUBFRerEyEW8GtoN6I5"}, + {"$5$rounds=10000$abcdefghijklmnop$", "hello", "$5$rounds=10000$abcdefghijklmnop$gakfHbxNcYmu8/VVGCdhagYkdMmRhzRqQyQhHYWi9k7"}, + {"$5$", "hello", "$5$$TCRu/ts4Npu8OJyeWy2WnUHCe/6bKVMSi0sROUrPh48"}, + + /* SHA-512-crypt */ + {"$6$abc$", "hello", "$6$abc$34O8oRzThM7onc3iRQAcdWBv2H/sMGJlnsnG6uIkasTEvQkG9apXR/oBhPpqIepkul8wg.bf4wxoel67eJND./"}, + {"$6$abcdefgh$", "hello", "$6$abcdefgh$bp46WUxDu2cphMRE0PvVJlqas2imuSq186YS793XKfmqn9XUEL17HECskUTZInaNigNndLLYc7A6yWBmFEFn3/"}, + /* '.' and '/' are valid salt characters */ + {"$6$ab.cd/ef$", "hello", "$6$ab.cd/ef$sV4BC2Kqe0jWhdQioxhq0MCijtPO/TSfuBdFg63JbMMpm6iDkLX8LEnLB5PSaFMJp13pjvWAH46wN.vvu6z45."}, + {"$6$abcdefghijklmnop$", "hello", TEST_HASH_SHA512_HELLO}, + {"$6$abcdefghijklmnop$", "0123456789abcdef", "$6$abcdefghijklmnop$WQAzULv0Ukk44nlNhVKoNi3QBj9GOrOUYqHJK7K/cen5os5qmscMn/GK9p68EawC37X.H..TBeQL1We6POnYI0"}, + {"$6$abcdefghijklmnop$", "h\xc3\xa9llo", "$6$abcdefghijklmnop$BjRFgpCnoEl8cfIQCpEqVHEVCTsjvYInzBjb6J.KLIXKs8JGYiDPELluRy8z3LVO5wmWST41bCrlm6zcGCUgM1"}, + /* a password longer than one SHA-512 digest block (64 bytes) */ + { + "$6$abcdefghijklmnop$", "01234567890123456789012345678901234567890123456789012345678901234", + "$6$abcdefghijklmnop$TlFyOXCcL/GeVkvPqzyw2viFND3yFhb0VAIhztQ0HGiafEntT9M4dW5ZpllPgHHEt80.ZslzQtj8nMOA0TYLx0" + }, + /* '$' in the password is not parsed as part of the setting */ + { + "$6$abcdefghijklmnop$", "pa$$w0rd$x", + "$6$abcdefghijklmnop$tX9iusQ5hZnqodRSRZMi9nHnFWheE7aqHOsEu9rYD0EI65BiFhsJIhtOnuuGV/msAaCoiBOahQXQdPyptwBee." + }, + {"$6$verylongsaltvaluewayover16chars$", "hello", "$6$verylongsaltvalu$6hTjPqyUed7wu3zyqaBUB.p0vw6sqLZhMoYW7x7jQqlBk2lFs0rcfDlx1HP14WHuUz7ISsstvqNHsf65BHACJ0"}, + /* "rounds=" specification */ + {"$6$rounds=1000$abcdefghijklmnop$", "hello", "$6$rounds=1000$abcdefghijklmnop$GsvqqORKveGP9eM50U7hoZ2RaYgO9vXD6Ludjc08Zj00u1Yk0AHH7rzYh6a.UMZ62y6Z7G/DVTJslcByAE9NI/"}, + {"$6$rounds=10000$abcdefghijklmnop$", "hello", "$6$rounds=10000$abcdefghijklmnop$9sNcVihoH1.aunYOPqyb.bKPu4qE59NF9J2D9VKvsbdqNWuzqKAsS7bzjInP5HL0DXOwpM/6/J6FYcGDV4MNo/"}, + {"$6$abcdefghijklmnop$", "", "$6$abcdefghijklmnop$6.vC8ffobuN7AxcHvesxeeksF2DXFfpYyFt3PFU8pYpEQPhWFSN7hwaUQRfHg/LkfB3jIPEitUcU7ZTqjaQUp1"}, + {"$6$", "hello", "$6$$8E8LfwilfZ5LGh4IJH.CRzu9SqEBIk11xcvH1HL2h7nPXSOL41SgqNeOe1lSKb0D0r.6tMA2NFig55Fr3bO1V0"}, +}; + +/** + * @brief Create a hash for the given password and setting. + * + * @param[in] password Password. + * @param[in] setting Setting "$$[rounds=N$]$". + * @param[out] out Output buffer. + * @param[out] out_len Capacity of @p out including the terminating NUL. + * @return nc_crypt() return value. + */ +static int +test_crypt_create(const char *password, const char *setting, char *out, size_t out_len) +{ + struct nc_crypt c; + + memset(&c, 0, sizeof c); + c.action = NC_CRYPT_CREATE; + c.output = out; + c.output_len = out_len; + + return nc_crypt(password, setting, &c); +} + +/** + * @brief Verify a password against a stored hash. + * + * @param[in] password Password. + * @param[in] setting Complete stored hash. + * @param[in] match_init Initial value of the match result, to check that it is always set. + * @param[out] match Resulting match value. + * @return nc_crypt() return value. + */ +static int +test_crypt_verify(const char *password, const char *setting, int match_init, int *match) +{ + struct nc_crypt c; + int rc; + + memset(&c, 0, sizeof c); + c.action = NC_CRYPT_VERIFY; + c.match = match_init; + + rc = nc_crypt(password, setting, &c); + *match = c.match; + + return rc; +} + +/** + * @brief Check that every reference vector is created exactly. + * + * @param[in] state Test state. + */ +static void +test_algo_reference(void **state) +{ + char out[NC_CRYPT_BUFSIZE]; + size_t i; + + (void)state; + + for (i = 0; i < (sizeof test_vectors / sizeof *test_vectors); ++i) { + assert_int_equal(test_crypt_create(test_vectors[i].password, test_vectors[i].setting, out, sizeof out), 0); + assert_string_equal(out, test_vectors[i].expected); + } +} + +/** + * @brief Check handling of invalid arguments. + * + * @param[in] state Test state. + */ +static void +test_invalid_args(void **state) +{ + char out[NC_CRYPT_BUFSIZE]; + struct nc_crypt c; + + (void)state; + + /* NULL arguments */ + memset(&c, 0, sizeof c); + c.action = NC_CRYPT_CREATE; + c.output = out; + c.output_len = sizeof out; + assert_int_equal(nc_crypt(NULL, "$6$abc$", &c), -1); + assert_int_equal(nc_crypt("hello", NULL, &c), -1); + assert_int_equal(nc_crypt("hello", "$6$abc$", NULL), -1); + + /* no output buffer for hash creation */ + memset(&c, 0, sizeof c); + c.action = NC_CRYPT_CREATE; + c.output_len = sizeof out; + assert_int_equal(nc_crypt("hello", "$6$abc$", &c), -1); +} + +/** + * @brief Check handling of a small output buffer, an unknown action, an unknown + * hash id and a malformed hash string. + * + * @param[in] state Test state. + */ +static void +test_invalid_input(void **state) +{ + static const char *sha512_hello = TEST_HASH_SHA512_HELLO; + static const char *sha256_rounds_hello = "$5$rounds=1000$abcdefghijklmnop$WrXWnj4kGA329xgo6OuYgg4dCUBFRerEyEW8GtoN6I5"; + char out[NC_CRYPT_BUFSIZE]; + struct nc_crypt c; + int match; + + (void)state; + + /* an output buffer of the exact size works */ + assert_int_equal(test_crypt_create("hello", "$6$abcdefghijklmnop$", out, strlen(sha512_hello) + 1), 0); + assert_string_equal(out, sha512_hello); + assert_int_equal(test_crypt_create("hello", "$5$rounds=1000$abcdefghijklmnop$", out, strlen(sha256_rounds_hello) + 1), 0); + assert_string_equal(out, sha256_rounds_hello); + + /* an output buffer one byte too small fails before the hash is computed and the buffer is not modified at all */ + memset(out, (char)0xAA, sizeof out); + assert_int_equal(test_crypt_create("hello", "$6$abcdefghijklmnop$", out, strlen(sha512_hello)), -4); + assert_int_equal(out[0], (char)0xAA); + assert_int_equal(out[strlen(sha512_hello) / 2], (char)0xAA); + assert_int_equal(out[sizeof out - 1], (char)0xAA); + + /* unknown action */ + memset(&c, 0, sizeof c); + c.action = (enum nc_crypt_action)42; + assert_int_equal(nc_crypt("hello", "$6$abc$", &c), -1); + + /* unknown hash id */ + assert_int_equal(test_crypt_create("hello", "$2b$abcdefghijklmnop$", out, sizeof out), -2); + assert_int_equal(test_crypt_create("hello", "$2y$abcdefghijklmnop$", out, sizeof out), -2); + assert_int_equal(test_crypt_create("hello", "$7$abc$", out, sizeof out), -2); + assert_int_equal(test_crypt_create("hello", "$x$abc$", out, sizeof out), -2); + assert_int_equal(test_crypt_create("hello", "$0$password", out, sizeof out), -2); + assert_int_equal(test_crypt_create("hello", "$6abc$", out, sizeof out), -2); + assert_int_equal(test_crypt_verify("hello", "$2b$abcdefghijklmnop$hash", 0, &match), -2); + + /* malformed setting string */ + assert_int_equal(test_crypt_create("hello", "", out, sizeof out), -3); + assert_int_equal(test_crypt_create("hello", "$", out, sizeof out), -3); + assert_int_equal(test_crypt_create("hello", "$6", out, sizeof out), -3); + assert_int_equal(test_crypt_create("hello", "6$abc$", out, sizeof out), -3); + + /* salt characters outside the crypt base64 alphabet [a-zA-Z0-9./] */ + assert_int_equal(test_crypt_create("hello", "$6$a!b@c#$", out, sizeof out), -3); + assert_int_equal(test_crypt_create("hello", "$1$a!b@c#$", out, sizeof out), -3); + assert_int_equal(test_crypt_create("hello", "$6$ab cd$", out, sizeof out), -3); + /* crypt_r(3) tolerates '=' in a "$1$" salt, but the iana-crypt-hash + * YANG type excludes it as well */ + assert_int_equal(test_crypt_create("hello", "$1$rounds=5$", out, sizeof out), -3); + /* the whole salt run is validated, even past the truncation length */ + assert_int_equal(test_crypt_create("hello", "$6$abcdefghijklmnop!xyz$", out, sizeof out), -3); + /* invalid characters in the stored digest of a verification */ + assert_int_equal(test_crypt_verify("hello", "$6$abcdefghijklmnop$vRqpf9WzVv13MKJy4hd5!ALhNQnt4y0RZsj0MrdHWdwA8G8gihD6oj69IwfuemaVekHG0M6jA5HNgthaIaLRR/", 0, &match), -3); + assert_int_equal(match, 1); + + /* invalid "rounds=" specification */ + assert_int_equal(test_crypt_create("hello", "$5$rounds=abc$abc$", out, sizeof out), -3); + assert_int_equal(test_crypt_create("hello", "$5$rounds=$abc$", out, sizeof out), -3); + assert_int_equal(test_crypt_create("hello", "$5$rounds=1x$abc$", out, sizeof out), -3); + + /* "rounds=" out of the valid range [1000, 999999999] */ + assert_int_equal(test_crypt_create("hello", "$5$rounds=0$abc$", out, sizeof out), -3); + assert_int_equal(test_crypt_create("hello", "$5$rounds=999$abc$", out, sizeof out), -3); + assert_int_equal(test_crypt_create("hello", "$5$rounds=1000000000$abc$", out, sizeof out), -3); + assert_int_equal(test_crypt_create("hello", "$6$rounds=99999999999999999999$abc$", out, sizeof out), -3); + + /* no stored digest to verify against */ + assert_int_equal(test_crypt_verify("hello", "$6$abcdefghijklmnop$", 0, &match), -3); + assert_int_equal(match, 1); +} + +/** + * @brief Check password verification against stored hashes. + * + * @param[in] state Test state. + */ +static void +test_verify_password(void **state) +{ + int match; + + (void)state; + + /* correct password of every scheme */ + assert_int_equal(test_crypt_verify("hello", "$1$abcdefgh$rwnEbRiN0agqVgZBovWNQ/", 1, &match), 0); + assert_int_equal(match, 0); + assert_int_equal(test_crypt_verify("hello", "$5$rounds=10000$abcdefghijklmnop$gakfHbxNcYmu8/VVGCdhagYkdMmRhzRqQyQhHYWi9k7", 1, &match), 0); + assert_int_equal(match, 0); + assert_int_equal(test_crypt_verify("hello", TEST_HASH_SHA512_HELLO, 1, &match), 0); + assert_int_equal(match, 0); + + /* empty password */ + assert_int_equal(test_crypt_verify("", "$1$$qRPK7m23GJusamGpoGLby/", 1, &match), 0); + assert_int_equal(match, 0); + + /* wrong password */ + assert_int_equal(test_crypt_verify("hellp", TEST_HASH_SHA512_HELLO, 0, &match), 0); + assert_int_equal(match, 1); + assert_int_equal(test_crypt_verify("", TEST_HASH_SHA512_HELLO, 0, &match), 0); + assert_int_equal(match, 1); +} + +/** + * @brief Check handling of unusual salt formats. + * + * @param[in] state Test state. + */ +static void +test_salt_format(void **state) +{ + char out[NC_CRYPT_BUFSIZE]; + int match; + + (void)state; + + /* a '$' in the setting terminates the salt, the rest is the stored digest */ + assert_int_equal(test_crypt_create("hello", "$6$ab$cd$", out, sizeof out), 0); + assert_string_equal(out, "$6$ab$kCEdajKfECjkWwo5i4GMTY4WW.ADIevHyGdlZb3r8wskFWlOJqWZgemJ96NLPhkdsWWH5zZrOY6OzTC0hpeuU1"); + + /* empty salt round-trip */ + assert_int_equal(test_crypt_create("hello", "$6$$", out, sizeof out), 0); + assert_int_equal(test_crypt_verify("hello", out, 1, &match), 0); + assert_int_equal(match, 0); +} + +/** + * @brief Check verification against corrupted stored hashes. + * + * @param[in] state Test state. + */ +static void +test_corrupt_stored_hash(void **state) +{ + char buf[NC_CRYPT_BUFSIZE]; + int match; + + (void)state; + + /* truncated digest */ + strcpy(buf, TEST_HASH_SHA512_HELLO); + buf[strlen(buf) - 1] = '\0'; + assert_int_equal(test_crypt_verify("hello", buf, 0, &match), 0); + assert_int_equal(match, 1); + + /* extended digest */ + strcpy(buf, TEST_HASH_SHA512_HELLO); + strcat(buf, "x"); + assert_int_equal(test_crypt_verify("hello", buf, 0, &match), 0); + assert_int_equal(match, 1); + + /* corrupted digest character */ + strcpy(buf, TEST_HASH_SHA512_HELLO); + buf[60] = (buf[60] == 'A') ? 'B' : 'A'; + assert_int_equal(test_crypt_verify("hello", buf, 0, &match), 0); + assert_int_equal(match, 1); + + /* character outside the base64 alphabet in the digest */ + strcpy(buf, TEST_HASH_SHA512_HELLO); + buf[25] = '!'; + assert_int_equal(test_crypt_verify("hello", buf, 0, &match), -3); + assert_int_equal(match, 1); + + /* no digest at all */ + assert_int_equal(test_crypt_verify("hello", "$6$abcdefghijklmnop$", 0, &match), -3); + assert_int_equal(match, 1); +} + +int +main(void) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_algo_reference), + cmocka_unit_test(test_invalid_args), + cmocka_unit_test(test_invalid_input), + cmocka_unit_test(test_verify_password), + cmocka_unit_test(test_salt_format), + cmocka_unit_test(test_corrupt_stored_hash), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +}