blob: bd5a9f8fc7598cc6c5bfad24104adc8ea8e2b6fc [file]
/****************************************************************************
* 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 <name> --password <pass> [options] [-o <output>]
*
* Options:
* --user <str> Username (required)
* --password <str> Plaintext password (required, not stored in output)
* --uid <int> User ID (default: 0)
* --gid <int> Group ID (default: 0)
* --home <str> Home directory (default: /)
* --iterations <int> PBKDF2 iterations (default: 10000)
* -o <path> Output file (default: stdout)
*
* Output format (matches NuttX passwd file format):
* username:$pbkdf2-sha256$<iter>$<salt>$<hash>:uid:gid:home
*
****************************************************************************/
/****************************************************************************
* Included Files
****************************************************************************/
#ifndef _POSIX_C_SOURCE
# define _POSIX_C_SOURCE 200809L
#endif
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#ifndef CONFIG_WINDOWS_NATIVE
# include <fcntl.h>
# include <sys/random.h>
# include <sys/stat.h>
# include <unistd.h>
#else
# include <direct.h>
#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 <name> --password <pass> [options] [-o <file>]\n"
"\n"
"Options:\n"
" --user <str> Username (required)\n"
" --password <str> Plaintext password (required)\n"
" --uid <int> User ID (default: 0)\n"
" --gid <int> Group ID (default: 0)\n"
" --home <str> Home directory (default: /)\n"
" --iterations <int> PBKDF2 iterations (default: %d)\n"
" -o <path> 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;
}