diff --git a/meson.build b/meson.build index e6804900a6..ce913c2ff6 100644 --- a/meson.build +++ b/meson.build @@ -882,6 +882,17 @@ libm = cc.find_library('m') libdl = cc.find_library('dl') libcrypt = cc.find_library('crypt') +crypt_header = conf.get('HAVE_CRYPT_H') == 1 ? \ + '''#include ''' : '''#include ''' +foreach ident : [ + ['crypt_ra', crypt_header], + ['crypt_gensalt_ra', crypt_header]] + + have = cc.has_function(ident[0], prefix : ident[1], args : '-D_GNU_SOURCE', + dependencies : libcrypt) + conf.set10('HAVE_' + ident[0].to_upper(), have) +endforeach + libcap = dependency('libcap', required : false) if not libcap.found() # Compat with Ubuntu 14.04 which ships libcap w/o .pc file diff --git a/src/firstboot/firstboot.c b/src/firstboot/firstboot.c index f8499a6ffd..3109f9cdfc 100644 --- a/src/firstboot/firstboot.c +++ b/src/firstboot/firstboot.c @@ -802,7 +802,7 @@ static int write_root_shadow(const char *shadow_path, const char *hashed_passwor static int process_root_args(void) { _cleanup_close_ int lock = -1; - struct crypt_data cd = {}; + _cleanup_(erase_and_freep) char *_hashed_password = NULL; const char *password, *hashed_password; const char *etc_passwd, *etc_shadow; int r; @@ -866,20 +866,13 @@ static int process_root_args(void) { password = "x"; hashed_password = arg_root_password; } else if (arg_root_password) { - _cleanup_free_ char *salt = NULL; - /* hashed_password points inside cd after crypt_r returns so cd has function scope. */ + r = hash_password(arg_root_password, &_hashed_password); + if (r < 0) + return log_error_errno(r, "Failed to hash password: %m"); password = "x"; + hashed_password = _hashed_password; - r = make_salt(&salt); - if (r < 0) - return log_error_errno(r, "Failed to get salt: %m"); - - errno = 0; - hashed_password = crypt_r(arg_root_password, salt, &cd); - if (!hashed_password) - return log_error_errno(errno == 0 ? SYNTHETIC_ERRNO(EINVAL) : errno, - "Failed to encrypt password: %m"); } else if (arg_delete_root_password) password = hashed_password = ""; else diff --git a/src/home/home-util.c b/src/home/home-util.c index 3fd57639f8..8e28e3ab76 100644 --- a/src/home/home-util.c +++ b/src/home/home-util.c @@ -1,7 +1,6 @@ /* SPDX-License-Identifier: LGPL-2.1+ */ #include "dns-domain.h" -#include "errno-util.h" #include "home-util.h" #include "libcrypt-util.h" #include "memory-util.h" @@ -134,35 +133,3 @@ int bus_message_append_secret(sd_bus_message *m, UserRecord *secret) { return sd_bus_message_append(m, "s", formatted); } - -int test_password_one(const char *hashed_password, const char *password) { - struct crypt_data cc = {}; - const char *k; - bool b; - - errno = 0; - k = crypt_r(password, hashed_password, &cc); - if (!k) { - explicit_bzero_safe(&cc, sizeof(cc)); - return errno_or_else(EINVAL); - } - - b = streq(k, hashed_password); - explicit_bzero_safe(&cc, sizeof(cc)); - return b; -} - -int test_password_many(char **hashed_password, const char *password) { - char **hpw; - int r; - - STRV_FOREACH(hpw, hashed_password) { - r = test_password_one(*hpw, password); - if (r < 0) - return r; - if (r > 0) - return true; - } - - return false; -} diff --git a/src/home/home-util.h b/src/home/home-util.h index 6161d4c3d0..73602e4f8e 100644 --- a/src/home/home-util.h +++ b/src/home/home-util.h @@ -21,6 +21,3 @@ int bus_message_append_secret(sd_bus_message *m, UserRecord *secret); /* Many of our operations might be slow due to crypto, fsck, recursive chown() and so on. For these * operations permit a *very* long timeout */ #define HOME_SLOW_BUS_CALL_TIMEOUT_USEC (2*USEC_PER_MINUTE) - -int test_password_one(const char *hashed_password, const char *password); -int test_password_many(char **hashed_password, const char *password); diff --git a/src/home/homectl-fido2.c b/src/home/homectl-fido2.c index 0d087c79f0..eb9098fda9 100644 --- a/src/home/homectl-fido2.c +++ b/src/home/homectl-fido2.c @@ -70,31 +70,23 @@ static int add_fido2_salt( size_t secret_size) { _cleanup_(json_variant_unrefp) JsonVariant *l = NULL, *w = NULL, *e = NULL; - _cleanup_(erase_and_freep) char *base64_encoded = NULL; - _cleanup_free_ char *unix_salt = NULL; - struct crypt_data cd = {}; - char *k; + _cleanup_(erase_and_freep) char *base64_encoded = NULL, *hashed = NULL; int r; - r = make_salt(&unix_salt); - if (r < 0) - return log_error_errno(r, "Failed to generate salt: %m"); - /* Before using UNIX hashing on the supplied key we base64 encode it, since crypt_r() and friends * expect a NUL terminated string, and we use a binary key */ r = base64mem(secret, secret_size, &base64_encoded); if (r < 0) return log_error_errno(r, "Failed to base64 encode secret key: %m"); - errno = 0; - k = crypt_r(base64_encoded, unix_salt, &cd); - if (!k) + r = hash_password(base64_encoded, &hashed); + if (r < 0) return log_error_errno(errno_or_else(EINVAL), "Failed to UNIX hash secret key: %m"); r = json_build(&e, JSON_BUILD_OBJECT( JSON_BUILD_PAIR("credential", JSON_BUILD_BASE64(cid, cid_size)), JSON_BUILD_PAIR("salt", JSON_BUILD_BASE64(fido2_salt, fido2_salt_size)), - JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_STRING(k)))); + JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_STRING(hashed)))); if (r < 0) return log_error_errno(r, "Failed to build FIDO2 salt JSON key object: %m"); diff --git a/src/home/homectl-pkcs11.c b/src/home/homectl-pkcs11.c index 3507841faa..0583171f04 100644 --- a/src/home/homectl-pkcs11.c +++ b/src/home/homectl-pkcs11.c @@ -134,10 +134,7 @@ static int add_pkcs11_encrypted_key( const void *decrypted_key, size_t decrypted_key_size) { _cleanup_(json_variant_unrefp) JsonVariant *l = NULL, *w = NULL, *e = NULL; - _cleanup_(erase_and_freep) char *base64_encoded = NULL; - _cleanup_free_ char *salt = NULL; - struct crypt_data cd = {}; - char *k; + _cleanup_(erase_and_freep) char *base64_encoded = NULL, *hashed = NULL; int r; assert(v); @@ -147,25 +144,20 @@ static int add_pkcs11_encrypted_key( assert(decrypted_key); assert(decrypted_key_size > 0); - r = make_salt(&salt); - if (r < 0) - return log_error_errno(r, "Failed to generate salt: %m"); - /* Before using UNIX hashing on the supplied key we base64 encode it, since crypt_r() and friends * expect a NUL terminated string, and we use a binary key */ r = base64mem(decrypted_key, decrypted_key_size, &base64_encoded); if (r < 0) return log_error_errno(r, "Failed to base64 encode secret key: %m"); - errno = 0; - k = crypt_r(base64_encoded, salt, &cd); - if (!k) + r = hash_password(base64_encoded, &hashed); + if (r < 0) return log_error_errno(errno_or_else(EINVAL), "Failed to UNIX hash secret key: %m"); r = json_build(&e, JSON_BUILD_OBJECT( JSON_BUILD_PAIR("uri", JSON_BUILD_STRING(uri)), JSON_BUILD_PAIR("data", JSON_BUILD_BASE64(encrypted_key, encrypted_key_size)), - JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_STRING(k)))); + JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_STRING(hashed)))); if (r < 0) return log_error_errno(r, "Failed to build encrypted JSON key object: %m"); diff --git a/src/home/homectl-recovery-key.c b/src/home/homectl-recovery-key.c index 9d7f345f1e..c63d3415f4 100644 --- a/src/home/homectl-recovery-key.c +++ b/src/home/homectl-recovery-key.c @@ -183,9 +183,7 @@ static int print_qr_code(const char *secret) { } int identity_add_recovery_key(JsonVariant **v) { - _cleanup_(erase_and_freep) char *unix_salt = NULL, *password = NULL; - struct crypt_data cd = {}; - char *k; + _cleanup_(erase_and_freep) char *password = NULL, *hashed = NULL; int r; assert(v); @@ -196,17 +194,12 @@ int identity_add_recovery_key(JsonVariant **v) { return r; /* Let's UNIX hash it */ - r = make_salt(&unix_salt); + r = hash_password(password, &hashed); if (r < 0) - return log_error_errno(r, "Failed to generate salt: %m"); - - errno = 0; - k = crypt_r(password, unix_salt, &cd); - if (!k) return log_error_errno(errno_or_else(EINVAL), "Failed to UNIX hash secret key: %m"); /* Let's now add the "privileged" version of the recovery key */ - r = add_privileged(v, k); + r = add_privileged(v, hashed); if (r < 0) return r; diff --git a/src/home/homework.c b/src/home/homework.c index 594c4a05bb..986ce2b3f0 100644 --- a/src/home/homework.c +++ b/src/home/homework.c @@ -17,6 +17,7 @@ #include "homework-mount.h" #include "homework-pkcs11.h" #include "homework.h" +#include "libcrypt-util.h" #include "main-func.h" #include "memory-util.h" #include "missing_magic.h" diff --git a/src/home/user-record-pwquality.c b/src/home/user-record-pwquality.c index a5d632c772..08d7dc0169 100644 --- a/src/home/user-record-pwquality.c +++ b/src/home/user-record-pwquality.c @@ -3,6 +3,7 @@ #include "bus-common-errors.h" #include "errno-util.h" #include "home-util.h" +#include "libcrypt-util.h" #include "pwquality-util.h" #include "strv.h" #include "user-record-pwquality.h" diff --git a/src/home/user-record-util.c b/src/home/user-record-util.c index 0bbe44ce26..6928427730 100644 --- a/src/home/user-record-util.c +++ b/src/home/user-record-util.c @@ -806,20 +806,13 @@ int user_record_make_hashed_password(UserRecord *h, char **secret, bool extend) } STRV_FOREACH(i, secret) { - _cleanup_free_ char *salt = NULL; - struct crypt_data cd = {}; - char *k; + _cleanup_(erase_and_freep) char *hashed = NULL; - r = make_salt(&salt); + r = hash_password(*i, &hashed); if (r < 0) return r; - errno = 0; - k = crypt_r(*i, salt, &cd); - if (!k) - return errno_or_else(EINVAL); - - r = strv_extend(&np, k); + r = strv_consume(&np, TAKE_PTR(hashed)); if (r < 0) return r; } diff --git a/src/shared/libcrypt-util.c b/src/shared/libcrypt-util.c index bf6605508a..c5d98671bd 100644 --- a/src/shared/libcrypt-util.c +++ b/src/shared/libcrypt-util.c @@ -1,12 +1,29 @@ /* SPDX-License-Identifier: LGPL-2.1+ */ +#if HAVE_CRYPT_H +/* libxcrypt is a replacement for glibc's libcrypt, and libcrypt might be + * removed from glibc at some point. As part of the removal, defines for + * crypt(3) are dropped from unistd.h, and we must include crypt.h instead. + * + * Newer versions of glibc (v2.0+) already ship crypt.h with a definition + * of crypt(3) as well, so we simply include it if it is present. MariaDB, + * MySQL, PostgreSQL, Perl and some other wide-spread packages do it the + * same way since ages without any problems. + */ +# include +#else +# include +#endif + #include #include #include "alloc-util.h" +#include "errno-util.h" #include "libcrypt-util.h" #include "log.h" #include "macro.h" +#include "memory-util.h" #include "missing_stdlib.h" #include "random-util.h" #include "string-util.h" @@ -14,12 +31,12 @@ int make_salt(char **ret) { -#ifdef XCRYPT_VERSION_MAJOR +#if HAVE_CRYPT_GENSALT_RA const char *e; char *salt; - /* If we have libxcrypt we default to the "preferred method" (i.e. usually yescrypt), and generate it - * with crypt_gensalt_ra(). */ + /* If we have crypt_gensalt_ra() we default to the "preferred method" (i.e. usually yescrypt). + * crypt_gensalt_ra() is usually provided by libxcrypt. */ e = secure_getenv("SYSTEMD_CRYPT_PREFIX"); if (!e) @@ -34,8 +51,7 @@ int make_salt(char **ret) { *ret = salt; return 0; #else - /* If libxcrypt is not used, we use SHA512 and generate the salt on our own since crypt_gensalt_ra() - * is not available. */ + /* If crypt_gensalt_ra() is not available, we use SHA512 and generate the salt on our own. */ static const char table[] = "abcdefghijklmnopqrstuvwxyz" @@ -53,6 +69,8 @@ int make_salt(char **ret) { assert_cc(sizeof(table) == 64U + 1U); + log_debug("Generating fallback salt for hash prefix: $6$"); + /* Insist on the best randomness by setting RANDOM_BLOCK, this is about keeping passwords secret after all. */ r = genuine_random_bytes(raw, sizeof(raw), RANDOM_BLOCK); if (r < 0) @@ -74,6 +92,73 @@ int make_salt(char **ret) { #endif } +#if HAVE_CRYPT_RA +# define CRYPT_RA_NAME "crypt_ra" +#else +# define CRYPT_RA_NAME "crypt_r" + +/* Provide a poor man's fallback that uses a fixed size buffer. */ + +static char* systemd_crypt_ra(const char *phrase, const char *setting, void **data, int *size) { + assert(data); + assert(size); + + /* We allocate the buffer because crypt(3) says: struct crypt_data may be quite large (32kB in this + * implementation of libcrypt; over 128kB in some other implementations). This is large enough that + * it may be unwise to allocate it on the stack. */ + + if (!*data) { + *data = new0(struct crypt_data, 1); + if (!*data) { + errno = -ENOMEM; + return NULL; + } + + *size = (int) (sizeof(struct crypt_data)); + } + + char *t = crypt_r(phrase, setting, *data); + if (!t) + return NULL; + + /* crypt_r may return a pointer to an invalid hashed password on error. Our callers expect NULL on + * error, so let's just return that. */ + if (t[0] == '*') + return NULL; + + return t; +} + +#define crypt_ra systemd_crypt_ra + +#endif + +int hash_password_full(const char *password, void **cd_data, int *cd_size, char **ret) { + _cleanup_free_ char *salt = NULL; + _cleanup_(erase_and_freep) void *_cd_data = NULL; + char *p; + int r, _cd_size = 0; + + assert(!!cd_data == !!cd_size); + + r = make_salt(&salt); + if (r < 0) + return log_debug_errno(r, "Failed to generate salt: %m"); + + errno = 0; + p = crypt_ra(password, salt, cd_data ?: &_cd_data, cd_size ?: &_cd_size); + if (!p) + return log_debug_errno(errno_or_else(SYNTHETIC_ERRNO(EINVAL)), + CRYPT_RA_NAME "() failed: %m"); + + p = strdup(p); + if (!p) + return -ENOMEM; + + *ret = p; + return 0; +} + bool looks_like_hashed_password(const char *s) { /* Returns false if the specified string is certainly not a hashed UNIX password. crypt(5) lists * various hashing methods. We only reject (return false) strings which are documented to have @@ -89,3 +174,35 @@ bool looks_like_hashed_password(const char *s) { return !STR_IN_SET(s, "x", "*"); } + +int test_password_one(const char *hashed_password, const char *password) { + _cleanup_(erase_and_freep) void *cd_data = NULL; + int cd_size = 0; + const char *k; + + errno = 0; + k = crypt_ra(password, hashed_password, &cd_data, &cd_size); + if (!k) { + if (errno == ENOMEM) + return -ENOMEM; + /* Unknown or unavailable hashing method or string too short */ + return 0; + } + + return streq(k, hashed_password); +} + +int test_password_many(char **hashed_password, const char *password) { + char **hpw; + int r; + + STRV_FOREACH(hpw, hashed_password) { + r = test_password_one(*hpw, password); + if (r < 0) + return r; + if (r > 0) + return true; + } + + return false; +} diff --git a/src/shared/libcrypt-util.h b/src/shared/libcrypt-util.h index 8a860ceb0d..924a35d3e1 100644 --- a/src/shared/libcrypt-util.h +++ b/src/shared/libcrypt-util.h @@ -1,22 +1,13 @@ /* SPDX-License-Identifier: LGPL-2.1+ */ #pragma once -#if HAVE_CRYPT_H -/* libxcrypt is a replacement for glibc's libcrypt, and libcrypt might be - * removed from glibc at some point. As part of the removal, defines for - * crypt(3) are dropped from unistd.h, and we must include crypt.h instead. - * - * Newer versions of glibc (v2.0+) already ship crypt.h with a definition - * of crypt(3) as well, so we simply include it if it is present. MariaDB, - * MySQL, PostgreSQL, Perl and some other wide-spread packages do it the - * same way since ages without any problems. - */ -#include -#endif - #include -#include int make_salt(char **ret); - +int hash_password_full(const char *password, void **cd_data, int *cd_size, char **ret); +static inline int hash_password(const char *password, char **ret) { + return hash_password_full(password, NULL, NULL, ret); +} bool looks_like_hashed_password(const char *s); +int test_password_one(const char *hashed_password, const char *password); +int test_password_many(char **hashed_password, const char *password); diff --git a/src/test/meson.build b/src/test/meson.build index 7eb343116a..34d64a0f42 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -301,6 +301,10 @@ tests += [ [], []], + [['src/test/test-libcrypt-util.c'], + [], + []], + [['src/test/test-offline-passwd.c', 'src/shared/offline-passwd.c', 'src/shared/offline-passwd.h'], diff --git a/src/test/test-libcrypt-util.c b/src/test/test-libcrypt-util.c new file mode 100644 index 0000000000..58b83b6866 --- /dev/null +++ b/src/test/test-libcrypt-util.c @@ -0,0 +1,102 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#if HAVE_CRYPT_H +# include +#else +# include +#endif + +#include "strv.h" +#include "tests.h" +#include "libcrypt-util.h" + +static int test_hash_password(void) { + log_info("/* %s */", __func__); + + /* As a warmup exercise, check if we can hash passwords. */ + + bool have_sane_hash = false; + const char *hash; + + FOREACH_STRING(hash, + "ew3bU1.hoKk4o", + "$1$gc5rWpTB$wK1aul1PyBn9AX1z93stk1", + "$2b$12$BlqcGkB/7BFvNMXKGxDea.5/8D6FTny.cbNcHW/tqcrcyo6ZJd8u2", + "$5$lGhDrcrao9zb5oIK$05KlOVG3ocknx/ThreqXE/gk.XzFFBMTksc4t2CPDUD", + "$6$c7wB/3GiRk0VHf7e$zXJ7hN0aLZapE.iO4mn/oHu6.prsXTUG/5k1AxpgR85ELolyAcaIGRgzfwJs3isTChMDBjnthZyaMCfCNxo9I.", + "$y$j9T$$9cKOWsAm4m97WiYk61lPPibZpy3oaGPIbsL4koRe/XD") { + int b; + + b = test_password_one(hash, "ppp"); + log_info("%s: %s", hash, yes_no(b)); +#if defined(XCRYPT_VERSION_MAJOR) + /* xcrypt is supposed to always implement all methods. */ + assert_se(b); +#endif + + if (b && IN_SET(hash[1], '6', 'y')) + have_sane_hash = true; + } + + return have_sane_hash; +} + +static void test_hash_password_full(void) { + log_info("/* %s */", __func__); + + _cleanup_free_ void *cd_data = NULL; + const char *i; + int cd_size = 0; + + log_info("sizeof(struct crypt_data): %zu bytes", sizeof(struct crypt_data)); + + for (unsigned c = 0; c < 2; c++) + FOREACH_STRING(i, "abc123", "h⸿sło") { + _cleanup_free_ char *hashed; + + if (c == 0) + assert_se(hash_password_full(i, &cd_data, &cd_size, &hashed) == 0); + else + assert_se(hash_password_full(i, NULL, NULL, &hashed) == 0); + log_debug("\"%s\" → \"%s\"", i, hashed); + log_info("crypt_r[a] buffer size: %i bytes", cd_size); + + assert_se(test_password_one(hashed, i) == true); + assert_se(test_password_one(i, hashed) <= 0); /* We get an error for non-utf8 */ + assert_se(test_password_one(hashed, "foobar") == false); + assert_se(test_password_many(STRV_MAKE(hashed), i) == true); + assert_se(test_password_many(STRV_MAKE(hashed), "foobar") == false); + assert_se(test_password_many(STRV_MAKE(hashed, hashed, hashed), "foobar") == false); + assert_se(test_password_many(STRV_MAKE("$y$j9T$dlCXwkX0GC5L6B8Gf.4PN/$VCyEH", + hashed, + "$y$j9T$SAayASazWZIQeJd9AS02m/$"), + i) == true); + assert_se(test_password_many(STRV_MAKE("$W$j9T$dlCXwkX0GC5L6B8Gf.4PN/$VCyEH", /* no such method exists... */ + hashed, + "$y$j9T$SAayASazWZIQeJd9AS02m/$"), + i) == true); + assert_se(test_password_many(STRV_MAKE("$y$j9T$dlCXwkX0GC5L6B8Gf.4PN/$VCyEH", + hashed, + "$y$j9T$SAayASazWZIQeJd9AS02m/$"), + "") == false); + assert_se(test_password_many(STRV_MAKE("$W$j9T$dlCXwkX0GC5L6B8Gf.4PN/$VCyEH", /* no such method exists... */ + hashed, + "$y$j9T$SAayASazWZIQeJd9AS02m/$"), + "") == false); + } +} + +int main(int argc, char *argv[]) { + test_setup_logging(LOG_DEBUG); + +#if defined(__powerpc__) && !defined(XCRYPT_VERSION_MAJOR) + return log_tests_skipped("crypt_r() causes a buffer overflow on ppc64el, see https://github.com/systemd/systemd/pull/16981#issuecomment-691203787"); +#endif + + if (!test_hash_password()) + return log_tests_skipped("crypt doesn't support yescrypt or sha512crypt"); + + test_hash_password_full(); + + return 0; +}