/**************************************************************************** * tools/mkpasswd.c * * SPDX-License-Identifier: Apache-2.0 * * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. The * ASF licenses this file to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance with the * License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. * ****************************************************************************/ /**************************************************************************** * Description: * Host build tool that generates a NuttX /etc/passwd entry with a * PBKDF2-HMAC-SHA256 password hash. This is a pure C replacement for the * former tools/mkpasswd.py, removing the Python dependency from the build. * * The hash format is identical to that used at runtime by: * apps/fsutils/passwd/passwd_encrypt.c * apps/fsutils/passwd/passwd_verify.c * * Usage: * mkpasswd --user --password [options] [-o ] * * Options: * --user Username (required) * --password Plaintext password (required, not stored in output) * --uid User ID (default: 0) * --gid Group ID (default: 0) * --home Home directory (default: /) * --iterations PBKDF2 iterations (default: 10000) * -o Output file (default: stdout) * * Output format (matches NuttX passwd file format): * username:$pbkdf2-sha256$$$:uid:gid:home * ****************************************************************************/ /**************************************************************************** * Included Files ****************************************************************************/ #ifndef _POSIX_C_SOURCE # define _POSIX_C_SOURCE 200809L #endif #include #include #include #include #include #include #ifndef CONFIG_WINDOWS_NATIVE # include # include # include # include #else # include #endif /**************************************************************************** * Pre-processor Definitions ****************************************************************************/ #define MKPASSWD_NL "\n\n" #define PASSWD_MCF_PREFIX "$pbkdf2-sha256$" #define PASSWD_SALT_BYTES 16 #define PASSWD_HASH_BYTES 32 #define MAX_ENCRYPTED 96 #define MAX_PASSWORD 256 #define MIN_PASSWORD 8 #define DEFAULT_ITERATIONS 10000 #define MIN_ITERATIONS 1000 #define MAX_ITERATIONS 200000 static const char g_base64url[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; /**************************************************************************** * Private Types (minimal SHA-256) ****************************************************************************/ struct sha256_ctx { uint32_t state[8]; uint64_t bitlen; uint8_t data[64]; uint32_t datalen; }; /**************************************************************************** * Private Functions (SHA-256 + HMAC-SHA256 + PBKDF2) ****************************************************************************/ static uint32_t rotr32(uint32_t x, uint32_t n) { return (x >> n) | (x << (32 - n)); } static void sha256_transform(struct sha256_ctx *ctx, const uint8_t data[64]) { static const uint32_t k[64] = { 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 }; uint32_t m[64]; uint32_t a; uint32_t b; uint32_t c; uint32_t d; uint32_t e; uint32_t f; uint32_t g; uint32_t h; uint32_t t1; uint32_t t2; uint32_t s0; uint32_t s1; int i; for (i = 0; i < 16; i++) { m[i] = ((uint32_t)data[i * 4] << 24) | ((uint32_t)data[i * 4 + 1] << 16) | ((uint32_t)data[i * 4 + 2] << 8) | ((uint32_t)data[i * 4 + 3]); } for (i = 16; i < 64; i++) { s0 = rotr32(m[i - 15], 7) ^ rotr32(m[i - 15], 18) ^ (m[i - 15] >> 3); s1 = rotr32(m[i - 2], 17) ^ rotr32(m[i - 2], 19) ^ (m[i - 2] >> 10); m[i] = m[i - 16] + s0 + m[i - 7] + s1; } a = ctx->state[0]; b = ctx->state[1]; c = ctx->state[2]; d = ctx->state[3]; e = ctx->state[4]; f = ctx->state[5]; g = ctx->state[6]; h = ctx->state[7]; for (i = 0; i < 64; i++) { t1 = h + (rotr32(e, 6) ^ rotr32(e, 11) ^ rotr32(e, 25)) + ((e & f) ^ ((~e) & g)) + k[i] + m[i]; t2 = (rotr32(a, 2) ^ rotr32(a, 13) ^ rotr32(a, 22)) + ((a & b) ^ (a & c) ^ (b & c)); h = g; g = f; f = e; e = d + t1; d = c; c = b; b = a; a = t1 + t2; } ctx->state[0] += a; ctx->state[1] += b; ctx->state[2] += c; ctx->state[3] += d; ctx->state[4] += e; ctx->state[5] += f; ctx->state[6] += g; ctx->state[7] += h; } static void sha256_init(struct sha256_ctx *ctx) { ctx->datalen = 0; ctx->bitlen = 0; ctx->state[0] = 0x6a09e667; ctx->state[1] = 0xbb67ae85; ctx->state[2] = 0x3c6ef372; ctx->state[3] = 0xa54ff53a; ctx->state[4] = 0x510e527f; ctx->state[5] = 0x9b05688c; ctx->state[6] = 0x1f83d9ab; ctx->state[7] = 0x5be0cd19; } static void sha256_update(struct sha256_ctx *ctx, const uint8_t *data, size_t len) { size_t i; for (i = 0; i < len; i++) { ctx->data[ctx->datalen] = data[i]; ctx->datalen++; if (ctx->datalen == 64) { sha256_transform(ctx, ctx->data); ctx->bitlen += 512; ctx->datalen = 0; } } } static void sha256_final(struct sha256_ctx *ctx, uint8_t hash[32]) { uint32_t i; uint32_t j; i = ctx->datalen; if (ctx->datalen < 56) { ctx->data[i++] = 0x80; while (i < 56) { ctx->data[i++] = 0x00; } } else { ctx->data[i++] = 0x80; while (i < 64) { ctx->data[i++] = 0x00; } sha256_transform(ctx, ctx->data); memset(ctx->data, 0, 56); } ctx->bitlen += (uint64_t)ctx->datalen * 8; ctx->data[63] = (uint8_t)(ctx->bitlen); ctx->data[62] = (uint8_t)(ctx->bitlen >> 8); ctx->data[61] = (uint8_t)(ctx->bitlen >> 16); ctx->data[60] = (uint8_t)(ctx->bitlen >> 24); ctx->data[59] = (uint8_t)(ctx->bitlen >> 32); ctx->data[58] = (uint8_t)(ctx->bitlen >> 40); ctx->data[57] = (uint8_t)(ctx->bitlen >> 48); ctx->data[56] = (uint8_t)(ctx->bitlen >> 56); sha256_transform(ctx, ctx->data); for (i = 0; i < 4; i++) { for (j = 0; j < 8; j++) { hash[i + (j * 4)] = (uint8_t)((ctx->state[j] >> (24 - i * 8)) & 0xff); } } } static void hmac_sha256(const uint8_t *key, size_t keylen, const uint8_t *data, size_t datalen, uint8_t mac[32]) { struct sha256_ctx ctx; uint8_t k_ipad[64]; uint8_t k_opad[64]; uint8_t tk[32]; size_t i; if (keylen > 64) { sha256_init(&ctx); sha256_update(&ctx, key, keylen); sha256_final(&ctx, tk); key = tk; keylen = 32; } memset(k_ipad, 0, sizeof(k_ipad)); memset(k_opad, 0, sizeof(k_opad)); memcpy(k_ipad, key, keylen); memcpy(k_opad, key, keylen); for (i = 0; i < 64; i++) { k_ipad[i] ^= 0x36; k_opad[i] ^= 0x5c; } sha256_init(&ctx); sha256_update(&ctx, k_ipad, 64); sha256_update(&ctx, data, datalen); sha256_final(&ctx, mac); sha256_init(&ctx); sha256_update(&ctx, k_opad, 64); sha256_update(&ctx, mac, 32); sha256_final(&ctx, mac); } static int pbkdf2_hmac_sha256(const uint8_t *pass, size_t passlen, const uint8_t *salt, size_t saltlen, uint32_t iterations, uint8_t *out, size_t outlen) { uint8_t u[32]; uint8_t t[32]; uint8_t saltblk[64]; size_t generated = 0; uint32_t block; uint32_t i; uint32_t j; if (iterations == 0 || outlen == 0 || saltlen + 4 > sizeof(saltblk)) { return -1; } for (block = 1; generated < outlen; block++) { memcpy(saltblk, salt, saltlen); saltblk[saltlen + 0] = (uint8_t)((block >> 24) & 0xff); saltblk[saltlen + 1] = (uint8_t)((block >> 16) & 0xff); saltblk[saltlen + 2] = (uint8_t)((block >> 8) & 0xff); saltblk[saltlen + 3] = (uint8_t)(block & 0xff); hmac_sha256(pass, passlen, saltblk, saltlen + 4, u); memcpy(t, u, sizeof(t)); for (i = 1; i < iterations; i++) { hmac_sha256(pass, passlen, u, sizeof(u), u); for (j = 0; j < 32; j++) { t[j] ^= u[j]; } } if (outlen - generated >= 32) { memcpy(out + generated, t, 32); generated += 32; } else { memcpy(out + generated, t, outlen - generated); generated = outlen; } } return 0; } static int fill_random(uint8_t *buf, size_t len) { #ifndef CONFIG_WINDOWS_NATIVE ssize_t nread; int fd; # ifdef SYS_getrandom nread = getrandom(buf, len, 0); if (nread == (ssize_t)len) { return 0; } # endif fd = open("/dev/urandom", O_RDONLY); if (fd < 0) { return -1; } nread = read(fd, buf, len); close(fd); return nread == (ssize_t)len ? 0 : -1; #else (void)buf; (void)len; return -1; #endif } static int base64url_encode(const uint8_t *in, size_t inlen, char *out, size_t outlen) { uint32_t acc = 0; size_t i; size_t o = 0; int bits = 0; for (i = 0; i < inlen; i++) { acc = (acc << 8) | in[i]; bits += 8; while (bits >= 6) { if (o + 1 >= outlen) { return -1; } bits -= 6; out[o++] = g_base64url[(acc >> bits) & 0x3f]; } } if (bits > 0) { if (o + 1 >= outlen) { return -1; } out[o++] = g_base64url[(acc << (6 - bits)) & 0x3f]; } if (o >= outlen) { return -1; } out[o] = '\0'; return 0; } static int validate_password_complexity(const char *password) { const char *specials = "!@#$%^&*()_+-=[]{}|;:,.<>?"; const char *p; int has_upper = 0; int has_lower = 0; int has_digit = 0; int has_special = 0; if (strlen(password) < MIN_PASSWORD) { fprintf(stderr, "\nError: password must be at least 8 characters\n\n"); return -1; } if (strlen(password) > MAX_PASSWORD) { fprintf(stderr, "\nError: password must be at most %d characters\n\n", MAX_PASSWORD); return -1; } for (p = password; *p; p++) { if (isupper((unsigned char)*p)) { has_upper = 1; } else if (islower((unsigned char)*p)) { has_lower = 1; } else if (isdigit((unsigned char)*p)) { has_digit = 1; } else if (strchr(specials, *p)) { has_special = 1; } } if (!has_upper) { fprintf(stderr, "\nError: password must contain at least one uppercase " "letter (A-Z)\n\n"); return -1; } if (!has_lower) { fprintf(stderr, "\nError: password must contain at least one lowercase " "letter (a-z)\n\n"); return -1; } if (!has_digit) { fprintf(stderr, "\nError: password must contain at least one digit " "(0-9)\n\n"); return -1; } if (!has_special) { fprintf(stderr, "\nError: password must contain at least one special " "character (!@#$%%^&*()_+-=[]{}|;:,.<>?)\n\n"); return -1; } return 0; } static int passwd_hash(const char *password, uint32_t iterations, char encrypted[MAX_ENCRYPTED + 1]) { uint8_t salt[PASSWD_SALT_BYTES]; uint8_t hash[PASSWD_HASH_BYTES]; char salt_b64[32]; char hash_b64[48]; size_t passlen; int ret; passlen = strlen(password); if (fill_random(salt, sizeof(salt)) < 0) { fputs(MKPASSWD_NL, stderr); fprintf(stderr, "mkpasswd: cannot obtain random salt\n"); return -1; } if (pbkdf2_hmac_sha256((const uint8_t *)password, passlen, salt, sizeof(salt), iterations, hash, sizeof(hash)) < 0) { return -1; } if (base64url_encode(salt, sizeof(salt), salt_b64, sizeof(salt_b64)) < 0 || base64url_encode(hash, sizeof(hash), hash_b64, sizeof(hash_b64)) < 0) { return -1; } ret = snprintf(encrypted, MAX_ENCRYPTED + 1, PASSWD_MCF_PREFIX "%u$%s$%s", iterations, salt_b64, hash_b64); if (ret < 0 || (size_t)ret > MAX_ENCRYPTED) { return -1; } return 0; } static int mkdir_p(const char *path) { char *tmp; char *p; size_t len; tmp = strdup(path); if (tmp == NULL) { return -1; } len = strlen(tmp); if (len > 0 && tmp[len - 1] == '/') { tmp[len - 1] = '\0'; } for (p = tmp + 1; *p != '\0'; p++) { if (*p == '/') { *p = '\0'; #ifndef CONFIG_WINDOWS_NATIVE mkdir(tmp, 0755); #else mkdir(tmp); #endif *p = '/'; } } #ifndef CONFIG_WINDOWS_NATIVE mkdir(tmp, 0755); #else mkdir(tmp); #endif free(tmp); return 0; } static void show_usage(const char *progname) { fprintf(stderr, "Usage: %s --user --password [options] [-o ]\n" "\n" "Options:\n" " --user Username (required)\n" " --password Plaintext password (required)\n" " --uid User ID (default: 0)\n" " --gid Group ID (default: 0)\n" " --home Home directory (default: /)\n" " --iterations PBKDF2 iterations (default: %d)\n" " -o Output file (default: stdout)\n" "\n" "Output format: username:$pbkdf2-sha256$...:uid:gid:home\n", progname, DEFAULT_ITERATIONS); } /**************************************************************************** * Public Functions ****************************************************************************/ int main(int argc, char **argv) { const char *user; const char *password; const char *home; const char *outpath; FILE *out; char encrypted[MAX_ENCRYPTED + 1]; int uid; int gid; uint32_t iterations; int i; int ret; user = NULL; password = NULL; home = "/"; outpath = NULL; uid = 0; gid = 0; iterations = DEFAULT_ITERATIONS; for (i = 1; i < argc; i++) { if (strcmp(argv[i], "--user") == 0 && i + 1 < argc) { user = argv[++i]; } else if (strcmp(argv[i], "--password") == 0 && i + 1 < argc) { password = argv[++i]; } else if (strcmp(argv[i], "--uid") == 0 && i + 1 < argc) { uid = atoi(argv[++i]); } else if (strcmp(argv[i], "--gid") == 0 && i + 1 < argc) { gid = atoi(argv[++i]); } else if (strcmp(argv[i], "--home") == 0 && i + 1 < argc) { home = argv[++i]; } else if (strcmp(argv[i], "--iterations") == 0 && i + 1 < argc) { iterations = (uint32_t)strtoul(argv[++i], NULL, 10); } else if ((strcmp(argv[i], "-o") == 0 || strcmp(argv[i], "--output") == 0) && i + 1 < argc) { outpath = argv[++i]; } else if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) { show_usage(argv[0]); return 0; } else { fputs(MKPASSWD_NL, stderr); fprintf(stderr, "mkpasswd: unknown option: %s\n", argv[i]); show_usage(argv[0]); return 1; } } if (user == NULL) { fputs(MKPASSWD_NL, stderr); fprintf(stderr, "mkpasswd: --user is required\n"); show_usage(argv[0]); return 1; } if (password == NULL) { fputs(MKPASSWD_NL, stderr); fprintf(stderr, "mkpasswd: --password is required\n"); show_usage(argv[0]); return 1; } if (validate_password_complexity(password) < 0) { return 1; } if (iterations < MIN_ITERATIONS || iterations > MAX_ITERATIONS) { fputs(MKPASSWD_NL, stderr); fprintf(stderr, "mkpasswd: --iterations must be between %d and %d\n", MIN_ITERATIONS, MAX_ITERATIONS); return 1; } ret = passwd_hash(password, iterations, encrypted); if (ret < 0) { return 1; } if (outpath != NULL) { char *dir; char *last; dir = strdup(outpath); last = strrchr(dir, '/'); if (last != NULL && last != dir) { *last = '\0'; mkdir_p(dir); } free(dir); out = fopen(outpath, "w"); if (out == NULL) { fputs(MKPASSWD_NL, stderr); fprintf(stderr, "mkpasswd: cannot open output file '%s': %s\n", outpath, strerror(errno)); return 1; } } else { out = stdout; } fprintf(out, "%s:%s:%d:%d:%s\n", user, encrypted, uid, gid, home); if (outpath != NULL) { fclose(out); } return 0; }