From 5371bd09d18fac005fb05587c5786a9a2dd48ff8 Mon Sep 17 00:00:00 2001 From: "Roger A. Light" Date: Wed, 23 Sep 2020 22:57:26 +0100 Subject: [PATCH] Add support for PBKDF2-SHA512 password hashing. --- ChangeLog.txt | 1 + man/mosquitto_passwd.1.xml | 18 +++++++ src/mosquitto_broker_internal.h | 10 +++- src/mosquitto_passwd.c | 94 ++++++++++++++++++++++----------- src/security_default.c | 64 +++++++++++++--------- 5 files changed, 129 insertions(+), 58 deletions(-) diff --git a/ChangeLog.txt b/ChangeLog.txt index ccc3807b..52e303c3 100644 --- a/ChangeLog.txt +++ b/ChangeLog.txt @@ -46,6 +46,7 @@ Broker: - Add `mosquitto_kick_client_by_clientid()` and `mosquitto_kick_client_by_username()` functions, which can be used by plugins to disconnect clients. - Add support for handling $CONTROL/ topics in plugins. +- Add support for PBKDF2-SHA512 password hashing. Client library: - Client no longer generates random client ids for v3.1.1 clients, these are diff --git a/man/mosquitto_passwd.1.xml b/man/mosquitto_passwd.1.xml index 5f144bf9..7c8ab7de 100644 --- a/man/mosquitto_passwd.1.xml +++ b/man/mosquitto_passwd.1.xml @@ -17,6 +17,9 @@ mosquitto_passwd + + hash + @@ -26,6 +29,9 @@ mosquitto_passwd + + hash + passwordfile username @@ -74,6 +80,18 @@ file. + + + + Choose the hash to use. Can be one of + sha512-pbkdf2 or + sha512. Defaults to + sha512-pbkdf2. The + sha512 option is provided for + creating password files for use with Mosquitto 1.6 + and earlier. + + diff --git a/src/mosquitto_broker_internal.h b/src/mosquitto_broker_internal.h index 9b6cc1b8..f4494b0c 100644 --- a/src/mosquitto_broker_internal.h +++ b/src/mosquitto_broker_internal.h @@ -174,6 +174,11 @@ enum mosquitto_msg_origin{ mosq_mo_broker = 1 }; +enum mosquitto_pwhash_type{ + pw_sha512 = 6, + pw_sha512_pbkdf2 = 7 +}; + struct mosquitto__auth_plugin{ void *lib; void *user_data; @@ -449,14 +454,15 @@ struct mosquitto_client_msg{ }; struct mosquitto__unpwd{ + UT_hash_handle hh; char *username; char *password; #ifdef WITH_TLS + unsigned char *salt; unsigned int password_len; unsigned int salt_len; - unsigned char *salt; #endif - UT_hash_handle hh; + enum mosquitto_pwhash_type hashtype; }; struct mosquitto__acl{ diff --git a/src/mosquitto_passwd.c b/src/mosquitto_passwd.c index 155ef0af..a815b51f 100644 --- a/src/mosquitto_passwd.c +++ b/src/mosquitto_passwd.c @@ -54,6 +54,11 @@ Contributors: #include "misc_mosq.h" +enum pwhash{ + pw_sha512 = 6, + pw_sha512_pbkdf2 = 7, +}; + struct cb_helper { const char *line; const char *username; @@ -69,6 +74,7 @@ static FILE *mpw_tmpfile(void) #else static char alphanum[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; +static enum pwhash hashtype = pw_sha512_pbkdf2; static unsigned char tmpfile_path[36]; static FILE *mpw_tmpfile(void) @@ -132,12 +138,14 @@ int base64_encode(unsigned char *in, unsigned int in_len, char **encoded) void print_usage(void) { printf("mosquitto_passwd is a tool for managing password files for mosquitto.\n\n"); - printf("Usage: mosquitto_passwd [-c | -D] passwordfile username\n"); - printf(" mosquitto_passwd -b passwordfile username password\n"); + printf("Usage: mosquitto_passwd [-H sha512 | -H sha512-pbkdf2] [-c | -D] passwordfile username\n"); + printf(" mosquitto_passwd [-H sha512 | -H sha512-pbkdf2] -b passwordfile username password\n"); printf(" mosquitto_passwd -U passwordfile\n"); printf(" -b : run in batch mode to allow passing passwords on the command line.\n"); printf(" -c : create a new password file. This will overwrite existing files.\n"); printf(" -D : delete the username rather than adding/updating its password.\n"); + printf(" -H : specify the hashing algorithm. Defaults to sha512-pbkdf2, which is recommended.\n"); + printf(" Mosquitto 1.6 and earlier defaulted to sha512.\n"); printf(" -U : update a plain text password file to use hashed passwords.\n"); printf("\nSee https://mosquitto.org/ for more information.\n\n"); } @@ -177,21 +185,28 @@ int output_new_password(FILE *fptr, const char *username, const char *password) return 1; } + if(hashtype == pw_sha512){ #if OPENSSL_VERSION_NUMBER < 0x10100000L - EVP_MD_CTX_init(&context); - EVP_DigestInit_ex(&context, digest, NULL); - EVP_DigestUpdate(&context, password, strlen(password)); - EVP_DigestUpdate(&context, salt, SALT_LEN); - EVP_DigestFinal_ex(&context, hash, &hash_len); - EVP_MD_CTX_cleanup(&context); + EVP_MD_CTX_init(&context); + EVP_DigestInit_ex(&context, digest, NULL); + EVP_DigestUpdate(&context, password, strlen(password)); + EVP_DigestUpdate(&context, salt, SALT_LEN); + EVP_DigestFinal_ex(&context, hash, &hash_len); + EVP_MD_CTX_cleanup(&context); #else - context = EVP_MD_CTX_new(); - EVP_DigestInit_ex(context, digest, NULL); - EVP_DigestUpdate(context, password, strlen(password)); - EVP_DigestUpdate(context, salt, SALT_LEN); - EVP_DigestFinal_ex(context, hash, &hash_len); - EVP_MD_CTX_free(context); + context = EVP_MD_CTX_new(); + EVP_DigestInit_ex(context, digest, NULL); + EVP_DigestUpdate(context, password, strlen(password)); + EVP_DigestUpdate(context, salt, SALT_LEN); + EVP_DigestFinal_ex(context, hash, &hash_len); + EVP_MD_CTX_free(context); #endif + }else{ + hash_len = sizeof(hash); + PKCS5_PBKDF2_HMAC(password, strlen(password), + salt, SALT_LEN, 20000, + digest, hash_len, hash); + } rc = base64_encode(hash, hash_len, &hash64); if(rc){ @@ -201,7 +216,7 @@ int output_new_password(FILE *fptr, const char *username, const char *password) return 1; } - fprintf(fptr, "%s:$6$%s$%s\n", username, salt64, hash64); + fprintf(fptr, "%s:$%d$%s$%s\n", username, hashtype, salt64, hash64); free(salt64); free(hash64); @@ -331,10 +346,8 @@ static int update_pwuser_cb(FILE *fptr, FILE *ftmp, const char *username, const { int rc = 0; - printf("%s\n", username); if(strcmp(username, helper->username)){ /* If this isn't the matching user, then writing out the exiting line */ - printf("%s\n", line); fprintf(ftmp, "%s", line); }else{ /* Write out a new line for our matching username */ @@ -520,6 +533,7 @@ int main(int argc, char *argv[]) int rc; bool do_update_file = false; char *backup_file; + int idx; signal(SIGINT, handle_sigint); signal(SIGTERM, handle_sigint); @@ -537,45 +551,63 @@ int main(int argc, char *argv[]) return 1; } - if(!strcmp(argv[1], "-c")){ + idx = 1; + if(!strcmp(argv[1], "-H")){ + if(argc < 5){ + fprintf(stderr, "Error: -H argument given but not enough other arguments.\n"); + return 1; + } + if(!strcmp(argv[2], "sha512")){ + hashtype = pw_sha512; + }else if(!strcmp(argv[2], "sha512-pbkdf2")){ + hashtype = pw_sha512_pbkdf2; + }else{ + fprintf(stderr, "Error: Unknown hash type '%s'\n", argv[2]); + return 1; + } + idx += 2; + argc -= 2; + } + + if(!strcmp(argv[idx], "-c")){ create_new = true; if(argc != 4){ fprintf(stderr, "Error: -c argument given but password file or username missing.\n"); return 1; }else{ - password_file_tmp = argv[2]; - username = argv[3]; + password_file_tmp = argv[idx+1]; + username = argv[idx+2]; } - }else if(!strcmp(argv[1], "-D")){ + }else if(!strcmp(argv[idx], "-D")){ delete_user = true; if(argc != 4){ fprintf(stderr, "Error: -D argument given but password file or username missing.\n"); return 1; }else{ - password_file_tmp = argv[2]; - username = argv[3]; + password_file_tmp = argv[idx+1]; + username = argv[idx+2]; } - }else if(!strcmp(argv[1], "-b")){ + }else if(!strcmp(argv[idx], "-b")){ batch_mode = true; if(argc != 5){ fprintf(stderr, "Error: -b argument given but password file, username or password missing.\n"); return 1; }else{ - password_file_tmp = argv[2]; - username = argv[3]; - password_cmd = argv[4]; + password_file_tmp = argv[idx+1]; + username = argv[idx+2]; + password_cmd = argv[idx+3]; } - }else if(!strcmp(argv[1], "-U")){ + }else if(!strcmp(argv[idx], "-U")){ if(argc != 3){ fprintf(stderr, "Error: -U argument given but password file missing.\n"); return 1; }else{ do_update_file = true; - password_file_tmp = argv[2]; + password_file_tmp = argv[idx+1]; } }else if(argc == 3){ - password_file_tmp = argv[1]; - username = argv[2]; + password_file_tmp = argv[idx]; + username = argv[idx+1]; }else{ print_usage(); return 1; diff --git a/src/security_default.c b/src/security_default.c index 2d6e7a93..3ee8287a 100644 --- a/src/security_default.c +++ b/src/security_default.c @@ -33,7 +33,7 @@ static int acl__cleanup(struct mosquitto_db *db, bool reload); static int unpwd__cleanup(struct mosquitto__unpwd **unpwd, bool reload); static int psk__file_parse(struct mosquitto_db *db, struct mosquitto__unpwd **psk_id, const char *psk_file); #ifdef WITH_TLS -static int pw__digest(const char *password, const unsigned char *salt, unsigned int salt_len, unsigned char *hash, unsigned int *hash_len); +static int pw__digest(const char *password, const unsigned char *salt, unsigned int salt_len, unsigned char *hash, unsigned int *hash_len, enum mosquitto_pwhash_type hashtype); static int base64__decode(char *in, unsigned char **decoded, unsigned int *decoded_len); static int mosquitto__memcmp_const(const void *ptr1, const void *b, size_t len); #endif @@ -773,12 +773,22 @@ static int unpwd__decode_passwords(struct mosquitto__unpwd **unpwd) unsigned char *password; unsigned int password_len; int rc; + int hashtype; HASH_ITER(hh, *unpwd, u, tmp){ /* Need to decode password into hashed data + salt. */ if(u->password){ token = strtok(u->password, "$"); - if(token && !strcmp(token, "6")){ + if(token){ + if(!strcmp(token, "6")){ + hashtype = pw_sha512; + }else if(!strcmp(token, "7")){ + hashtype = pw_sha512_pbkdf2; + }else{ + log__printf(NULL, MOSQ_LOG_ERR, "Error: Invalid password hash type for user %s, removing entry.", u->username); + unpwd__free_item(unpwd, u); + continue; + } token = strtok(NULL, "$"); if(token){ rc = base64__decode(token, &salt, &salt_len); @@ -792,6 +802,7 @@ static int unpwd__decode_passwords(struct mosquitto__unpwd **unpwd) mosquitto__free(u->password); u->password = (char *)password; u->password_len = password_len; + u->hashtype = hashtype; }else{ log__printf(NULL, MOSQ_LOG_ERR, "Error: Unable to decode password for user %s, removing entry.", u->username); unpwd__free_item(unpwd, u); @@ -953,7 +964,7 @@ int mosquitto_unpwd_check_default(struct mosquitto_db *db, struct mosquitto *con if(u->password){ if(context->password){ #ifdef WITH_TLS - rc = pw__digest(context->password, u->salt, u->salt_len, hash, &hash_len); + rc = pw__digest(context->password, u->salt, u->salt_len, hash, &hash_len, u->hashtype); if(rc == MOSQ_ERR_SUCCESS){ if(hash_len == u->password_len && !mosquitto__memcmp_const(u->password, hash, hash_len)){ return MOSQ_ERR_SUCCESS; @@ -1250,27 +1261,14 @@ int mosquitto_psk_key_get_default(struct mosquitto_db *db, struct mosquitto *con } #ifdef WITH_TLS -int pw__digest(const char *password, const unsigned char *salt, unsigned int salt_len, unsigned char *hash, unsigned int *hash_len) +int pw__digest(const char *password, const unsigned char *salt, unsigned int salt_len, unsigned char *hash, unsigned int *hash_len, enum mosquitto_pwhash_type hashtype) { const EVP_MD *digest; #if OPENSSL_VERSION_NUMBER < 0x10100000L EVP_MD_CTX context; - - digest = EVP_get_digestbyname("sha512"); - if(!digest){ - // FIXME fprintf(stderr, "Error: Unable to create openssl digest.\n"); - return 1; - } - - EVP_MD_CTX_init(&context); - EVP_DigestInit_ex(&context, digest, NULL); - EVP_DigestUpdate(&context, password, strlen(password)); - EVP_DigestUpdate(&context, salt, salt_len); - /* hash is assumed to be EVP_MAX_MD_SIZE bytes long. */ - EVP_DigestFinal_ex(&context, hash, hash_len); - EVP_MD_CTX_cleanup(&context); #else EVP_MD_CTX *context; +#endif digest = EVP_get_digestbyname("sha512"); if(!digest){ @@ -1278,14 +1276,30 @@ int pw__digest(const char *password, const unsigned char *salt, unsigned int sal return 1; } - context = EVP_MD_CTX_new(); - EVP_DigestInit_ex(context, digest, NULL); - EVP_DigestUpdate(context, password, strlen(password)); - EVP_DigestUpdate(context, salt, salt_len); - /* hash is assumed to be EVP_MAX_MD_SIZE bytes long. */ - EVP_DigestFinal_ex(context, hash, hash_len); - EVP_MD_CTX_free(context); + if(hashtype == pw_sha512){ +#if OPENSSL_VERSION_NUMBER < 0x10100000L + EVP_MD_CTX_init(&context); + EVP_DigestInit_ex(&context, digest, NULL); + EVP_DigestUpdate(&context, password, strlen(password)); + EVP_DigestUpdate(&context, salt, salt_len); + /* hash is assumed to be EVP_MAX_MD_SIZE bytes long. */ + EVP_DigestFinal_ex(&context, hash, hash_len); + EVP_MD_CTX_cleanup(&context); +#else + context = EVP_MD_CTX_new(); + EVP_DigestInit_ex(context, digest, NULL); + EVP_DigestUpdate(context, password, strlen(password)); + EVP_DigestUpdate(context, salt, salt_len); + /* hash is assumed to be EVP_MAX_MD_SIZE bytes long. */ + EVP_DigestFinal_ex(context, hash, hash_len); + EVP_MD_CTX_free(context); #endif + }else{ + *hash_len = EVP_MAX_MD_SIZE; + PKCS5_PBKDF2_HMAC(password, strlen(password), + salt, salt_len, 20000, + digest, *hash_len, hash); + } return MOSQ_ERR_SUCCESS; }