/*
* Copyright (C) 2000-2002, 2007, 2008 Red Hat, Inc.
*
* This is free software; you can redistribute it and/or modify it under
* the terms of the GNU Library General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include <config.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <crypt.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#ifdef WITH_SELINUX
#include <selinux/selinux.h>
#include <selinux/label.h>
#endif
#define LU_DEFAULT_SALT_TYPE "$1$"
#define LU_DEFAULT_SALT_LEN 8
#define LU_MAX_LOCK_ATTEMPTS 6
#define LU_LOCK_TIMEOUT 2
#include "user_private.h"
#include "internal.h"
#define HASH_ROUNDS_MIN 1000
#define HASH_ROUNDS_MAX 999999999
#if (defined CRYPT_GENSALT_IMPLEMENTS_AUTO_ENTROPY && \
CRYPT_GENSALT_IMPLEMENTS_AUTO_ENTROPY)
#define USE_XCRYPT_GENSALT 1
#else
#define USE_XCRYPT_GENSALT 0
#endif
#if ((defined XCRYPT_VERSION_NUM && \
XCRYPT_VERSION_NUM >= ((4 << 16) | 3)) && \
USE_XCRYPT_GENSALT)
#define HAVE_YESCRYPT 1
#else
#define HAVE_YESCRYPT 0
#endif
struct lu_lock {
int fd;
struct flock lock;
};
/* A wrapper for strcasecmp(). */
gint
lu_strcasecmp(gconstpointer v1, gconstpointer v2)
{
g_return_val_if_fail(v1 != NULL, 0);
g_return_val_if_fail(v2 != NULL, 0);
return g_ascii_strcasecmp((char *) v1, (char *) v2);
}
/* A wrapper for strcmp(). */
gint
lu_strcmp(gconstpointer v1, gconstpointer v2)
{
g_return_val_if_fail(v1 != NULL, 0);
g_return_val_if_fail(v2 != NULL, 0);
return strcmp((char *) v1, (char *) v2);
}
#if !USE_XCRYPT_GENSALT
/* A list of allowed salt characters, according to SUSv2. */
#define ACCEPTABLE "ABCDEFGHIJKLMNOPQRSTUVWXYZ" \
"abcdefghijklmnopqrstuvwxyz" \
"./0123456789"
static gboolean
is_acceptable(const char c)
{
if (c == 0) {
return FALSE;
}
return (strchr(ACCEPTABLE, c) != NULL);
}
static gboolean
fill_urandom(char *output, size_t length)
{
int fd;
size_t got = 0;
fd = open("/dev/urandom", O_RDONLY);
if (fd == -1)
return FALSE;
memset(output, '\0', length);
while (got < length) {
ssize_t len;
len = read(fd, output + got, length - got);
if (len == -1) {
if (errno == EINTR)
continue;
else {
close(fd);
return FALSE;
}
}
while (len != 0 && isprint((unsigned char)output[got])
&& !isspace((unsigned char)output[got])
&& is_acceptable(output[got])) {
got++;
len--;
}
}
close(fd);
return TRUE;
}
#endif
static const struct {
const char initial[5];
char separator[2];
size_t salt_length;
gboolean sha_rounds;
} salt_type_info[] = {
{"$1$", "$", 8, FALSE },
/* FIXME: number of rounds, base64 of 128 bits */
{"$2b$", "$", 8, FALSE },
{"$5$", "$", 16, TRUE },
{"$6$", "$", 16, TRUE },
#if HAVE_YESCRYPT
{"$y$", "$", 24, FALSE },
#endif
{ "", "", 2 },
};
const char *
lu_make_crypted(const char *plain, const char *previous)
{
char salt[2048];
size_t i, len = 0;
#if USE_XCRYPT_GENSALT
unsigned long rounds = 0;
#endif
if (previous == NULL) {
previous = LU_DEFAULT_SALT_TYPE;
}
for (i = 0; i < G_N_ELEMENTS(salt_type_info); i++) {
len = strlen(salt_type_info[i].initial);
if (strncmp(previous, salt_type_info[i].initial, len) == 0) {
break;
}
}
g_assert(i < G_N_ELEMENTS(salt_type_info));
if (salt_type_info[i].sha_rounds != FALSE
&& strncmp(previous + len, "rounds=", strlen("rounds=")) == 0) {
#if USE_XCRYPT_GENSALT
const char *start;
char *end;
start = previous + len + strlen("rounds=");
rounds = strtoul (start, &end, 10);
if (rounds < HASH_ROUNDS_MIN)
rounds = HASH_ROUNDS_MIN;
else if (rounds > HASH_ROUNDS_MAX)
rounds = HASH_ROUNDS_MAX;
}
g_assert(CRYPT_GENSALT_OUTPUT_SIZE <= sizeof(salt));
crypt_gensalt_rn(previous, rounds, NULL, 0, salt, sizeof(salt));
#else
const char *start, *end;
start = previous + len + strlen("rounds=");
end = strchr(start, '$');
if (end != NULL
&& end <= start + strlen(G_STRINGIFY(HASH_ROUNDS_MAX)))
len = (end + 1) - previous;
}
g_assert(len + salt_type_info[i].salt_length
+ strlen(salt_type_info[i].separator) < sizeof(salt));
memcpy(salt, previous, len);
if (fill_urandom(salt + len, salt_type_info[i].salt_length) == FALSE)
return NULL;
strcpy(salt + len + salt_type_info[i].salt_length,
salt_type_info[i].separator);
#endif
return crypt(plain, salt);
}
static const char *
parse_hash_rounds(struct lu_context *context, const char *key,
unsigned long *value)
{
const char *s;
s = lu_cfg_read_single(context, key, NULL);
if (s != NULL) {
char *end;
errno = 0;
*value = strtoul(s, &end, 10);
if (errno != 0 || *end != 0 || end == s) {
g_warning("Invalid %s value '%s'", key, s);
s = NULL;
}
}
return s;
}
static unsigned long
select_hash_rounds(struct lu_context *context)
{
const char *min_s, *max_s;
unsigned long min, max, rounds;
min_s = parse_hash_rounds(context, "defaults/hash_rounds_min", &min);
max_s = parse_hash_rounds(context, "defaults/hash_rounds_max", &max);
if (min_s == NULL && max_s == NULL)
return 0;
if (min_s != NULL && max_s != NULL) {
if (min <= max) {
if (max > HASH_ROUNDS_MAX)
/* To avoid overflow in (max + 1) below */
max = HASH_ROUNDS_MAX;
rounds = g_random_int_range(min, max + 1);
} else
rounds = min;
} else if (min_s != NULL)
rounds = min;
else /* max_s != NULL */
rounds = max;
if (rounds < HASH_ROUNDS_MIN)
rounds = HASH_ROUNDS_MIN;
else if (rounds > HASH_ROUNDS_MAX)
rounds = HASH_ROUNDS_MAX;
return rounds;
}
char *
lu_util_default_salt_specifier(struct lu_context *context)
{
static const struct {
const char *name, *initializer;
gboolean sha_rounds;
} salt_types[] = {
{ "des", "", FALSE },
{ "md5", "$1$", FALSE },
{ "blowfish", "$2b$", FALSE },
{ "sha256", "$5$", TRUE },
{ "sha512", "$6$", TRUE },
#if HAVE_YESCRYPT
{ "yescrypt", "$y$", FALSE },
#endif
};
const char *salt_type;
size_t i;
g_return_val_if_fail(context != NULL, g_strdup(""));
salt_type = lu_cfg_read_single(context, "defaults/crypt_style", "des");
for (i = 0; i < G_N_ELEMENTS(salt_types); i++) {
if (strcasecmp(salt_types[i].name, salt_type) == 0)
goto found;
}
return g_strdup("");
found:
if (salt_types[i].sha_rounds != FALSE) {
unsigned long rounds = 0;
rounds = select_hash_rounds(context);
#if USE_XCRYPT_GENSALT
return g_strdup(crypt_gensalt(salt_types[i].initializer,
rounds, NULL, 0));
#else
if (rounds != 0)
return g_strdup_printf("%srounds=%lu$",
salt_types[i].initializer,
rounds);
#endif
}
return g_strdup(salt_types[i].initializer);
}
gpointer
lu_util_lock_obtain(int fd, struct lu_error ** error)
{
int i;
int maxtries = LU_MAX_LOCK_ATTEMPTS;
int delay = LU_LOCK_TIMEOUT;
struct lu_lock *ret;
LU_ERROR_CHECK(error);
g_assert(fd != -1);
ret = g_malloc0(sizeof(*ret));
for (;;) {
struct timeval tv;
ret->fd = fd;
ret->lock.l_type = F_RDLCK;
if (write(ret->fd, NULL, 0) == 0)
ret->lock.l_type = F_WRLCK;
i = fcntl(ret->fd, F_SETLK, &ret->lock);
if (i != -1)
return ret;
if (errno != EINTR && errno != EAGAIN)
break;
if (maxtries-- <= 0)
break;
memset(&tv, 0, sizeof(tv));
tv.tv_usec = (delay *= 2);
select(0, NULL, NULL, NULL, &tv);
}
lu_error_new(error, lu_error_lock,
_("error locking file: %s"), strerror(errno));
g_free(ret);
return NULL;
}
void
lu_util_lock_free(gpointer lock)
{
struct lu_lock *ret;
int i;
g_return_if_fail(lock != NULL);
ret = (struct lu_lock*) lock;
do {
ret->lock.l_type = F_UNLCK;
i = fcntl(ret->fd, F_SETLK, &ret->lock);
} while ((i == -1) && ((errno == EINTR) || (errno == EAGAIN)));
g_free(ret);
}
char *
lu_util_line_get_matchingx(int fd, const char *part, int field,
struct lu_error **error)
{
char *contents, *contents_end;
struct stat st;
off_t offset;
char *ret = NULL, *line;
gboolean mapped = FALSE;
size_t part_len;
LU_ERROR_CHECK(error);
g_assert(fd != -1);
g_assert(part != NULL);
g_assert(field > 0);
offset = lseek(fd, 0, SEEK_CUR);
if (offset == -1) {
lu_error_new(error, lu_error_read, NULL);
return NULL;
}
if (fstat(fd, &st) == -1) {
lu_error_new(error, lu_error_stat, NULL);
return NULL;
}
contents = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
if (contents == MAP_FAILED) {
contents = g_malloc(st.st_size);
if (lseek(fd, 0, SEEK_SET) == -1
|| read(fd, contents, st.st_size) != st.st_size
|| lseek(fd, offset, SEEK_SET) == -1) {
lu_error_new(error, lu_error_read, NULL);
g_free(contents);
return NULL;
}
} else {
mapped = TRUE;
}
contents_end = contents + st.st_size;
part_len = strlen(part);
line = contents;
for (;;) {
char *line_end, *field_start;
line_end = memchr(line, '\n', contents_end - line);
if (field == 1)
field_start = line;
else {
int i;
char *p;
field_start = NULL;
i = 1;
for (p = line; p < contents_end && *p != '\n'; p++) {
if (*p == ':') {
i++;
if (i >= field) {
field_start = p + 1;
break;
}
}
}
}
if (field_start != NULL
&& contents_end - field_start >= part_len) {
char *expected_field_end;
expected_field_end = field_start + part_len;
if (strncmp(field_start, part, part_len) == 0
&& (expected_field_end == contents_end
|| *expected_field_end == ':'
|| *expected_field_end == '\n')) {
if (line_end == NULL)
line_end = contents_end;
ret = g_strndup(line, line_end - line);
break;
}
}
if (line_end == NULL)
break;
line = line_end + 1;
}
if (mapped) {
munmap(contents, st.st_size);
} else {
g_free(contents);
}
return ret;
}
char *
lu_util_line_get_matching1(int fd, const char *part,
struct lu_error **error)
{
LU_ERROR_CHECK(error);
return lu_util_line_get_matchingx(fd, part, 1, error);
}
char *
lu_util_line_get_matching3(int fd, const char *part,
struct lu_error **error)
{
LU_ERROR_CHECK(error);
return lu_util_line_get_matchingx(fd, part, 3, error);
}
char *
lu_util_field_read(int fd, const char *first, unsigned int field,
struct lu_error **error)
{
struct stat st;
char *buf, *buf_end;
char *pattern;
char *line, *start = NULL;
char *ret;
size_t len;
gboolean mapped = FALSE;
LU_ERROR_CHECK(error);
g_assert(fd != -1);
g_assert(first != NULL);
g_assert(strlen(first) != 0);
g_assert(field >= 1);
if (fstat(fd, &st) == -1) {
lu_error_new(error, lu_error_stat, NULL);
return NULL;
}
buf = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
if (buf == MAP_FAILED) {
buf = g_malloc(st.st_size);
if (lseek(fd, 0, SEEK_SET) == -1
|| read(fd, buf, st.st_size) != st.st_size) {
lu_error_new(error, lu_error_read, NULL);
g_free(buf);
return NULL;
}
} else {
mapped = TRUE;
}
buf_end = buf + st.st_size;
pattern = g_strdup_printf("%s:", first);
len = strlen(pattern);
line = buf;
for (;;) {
if (buf_end - line >= len && memcmp (line, pattern, len) == 0)
goto found_line;
line = memchr(line, '\n', buf_end - line);
if (line == NULL)
break;
line++;
}
lu_error_new(error, lu_error_search, NULL);
ret = NULL;
goto err;
found_line:
/* find the start of the field */
if (field == 1)
start = line;
else {
unsigned i = 1;
char *p;
start = NULL;
for (p = line; p < buf_end && *p != '\n'; p++) {
if (*p == ':') {
i++;
if (i >= field) {
start = p + 1;
break;
}
}
}
}
/* find the end of the field */
if (start != NULL) {
char *end;
end = start;
while (end < buf_end && *end != '\n' && *end != ':')
end++;
g_assert(end == buf_end || *end == '\n' || *end == ':');
ret = g_strndup(start, end - start);
} else {
ret = g_strdup("");
}
err:
g_free(pattern);
if (mapped) {
munmap(buf, st.st_size);
} else {
g_free(buf);
}
return ret;
}
gboolean
lu_util_field_write(int fd, const char *first, unsigned int field,
const char *value, struct lu_error ** error)
{
struct stat st;
char *buf;
char *pattern;
char *line, *start = NULL, *end = NULL;
gboolean ret = FALSE;
unsigned fi = 1;
size_t len;
LU_ERROR_CHECK(error);
g_assert(fd != -1);
g_assert(field >= 1);
first = first ? : "";
value = value ? : "";
if (fstat(fd, &st) == -1) {
lu_error_new(error, lu_error_stat, NULL);
return FALSE;
}
if (lseek(fd, 0, SEEK_SET) == -1) {
lu_error_new(error, lu_error_read, NULL);
return FALSE;
}
buf = g_malloc0(st.st_size + 1 + strlen(value) + field);
if (read(fd, buf, st.st_size) != st.st_size) {
lu_error_new(error, lu_error_read, NULL);
goto err_buf;
}
pattern = g_strdup_printf("\n%s:", first);
if (strncmp(buf, pattern + 1, strlen(pattern) - 1) == 0) {
/* found it on the first line */
line = buf;
} else if ((line = strstr(buf, pattern)) != NULL) {
/* found it somewhere in the middle */
line++;
}
if (line == NULL) {
lu_error_new(error, lu_error_search, NULL);
goto err_pattern;
}
/* find the start of the field */
if (fi == field)
start = line;
else {
char *p;
start = NULL;
for (p = line; fi < field && *p != '\n' && *p != '\0'; p++) {
if (*p == ':') {
fi++;
if (fi >= field) {
start = p + 1;
break;
}
}
}
}
/* find the end of the field */
if (start != NULL) {
end = start;
while ((*end != '\0') && (*end != '\n') && (*end != ':')) {
end++;
}
} else {
lu_error_new(error, lu_error_search, NULL);
goto err_pattern;
}
if (start != NULL) {
/* insert the text here, after moving the data around */
memmove(start + strlen(value), end,
st.st_size - (end - buf) + 1);
memcpy(start, value, strlen(value));
} else {
/* FIXME: this code currently can't execute */
/* fi contains the number of fields, so the difference between
* field and fi is the number of colons we need to add to the
* end of the line to create the field */
for (end = line; *end != '\0' && *end != '\n'; end++)
;
start = end;
memmove(start + strlen(value) + (field - fi), end,
st.st_size - (end - buf) + 1);
memset(start, ':', field - fi);
memcpy(start + (field - fi), value, strlen(value));
}
if (lseek(fd, 0, SEEK_SET) == -1) {
lu_error_new(error, lu_error_write, NULL);
goto err_pattern;
}
len = strlen(buf);
if (write(fd, buf, len) != len) {
lu_error_new(error, lu_error_write, NULL);
goto err_pattern;
}
if (ftruncate(fd, len) == -1) {
lu_error_new(error, lu_error_write, NULL);
goto err_pattern;
}
ret = TRUE;
err_pattern:
g_free(pattern);
err_buf:
g_free(buf);
return ret;
}
/* Return current date in days since the epoch (suitable for LU_SHADOW*),
or -1 if the current date is unknown or obviously implausible (e.g. on a
system without a RTC). */
long
lu_util_shadow_current_date_or_minus_1(void)
{
const struct tm *gmt;
time_t now;
GDate *today, *epoch;
long days;
now = time(NULL);
if (now == (time_t)-1)
return -1;
gmt = gmtime(&now);
today = g_date_new_dmy(gmt->tm_mday, gmt->tm_mon + 1,
gmt->tm_year + 1900);
epoch = g_date_new_dmy(1, 1, 1970);
days = g_date_get_julian(today) - g_date_get_julian(epoch);
g_date_free(today);
g_date_free(epoch);
/* Refuse to return 0 (Jan 1, 1970): it is unquestionably incorrect in
the real world, which is not really libuser's concern, but it is
also a special case for LU_SHADOWLASTCHANGE (marking the account for
forced password change) and LU_SHADOWEXPIRE (forbidden in
shadow(5)). In both cases, setting the value to -1 deactivates the
real-time-related actions, which is a reasonable thing to do when
the only available RTC is incorrect. */
if (days == 0)
return -1;
return days;
}
/* Set the shadow last-changed field to today's date. */
void
lu_util_update_shadow_last_change(struct lu_ent *ent)
{
lu_ent_set_long(ent, LU_SHADOWLASTCHANGE,
lu_util_shadow_current_date_or_minus_1());
}
#ifdef WITH_SELINUX
/* Store current fscreate context to ctx. */
gboolean
lu_util_fscreate_save(char **ctx, struct lu_error **error)
{
*ctx = NULL;
if (is_selinux_enabled() > 0 && getfscreatecon(ctx) < 0) {
lu_error_new(error, lu_error_generic,
_("couldn't get default security context: %s"),
strerror(errno));
return FALSE;
}
return TRUE;
}
/* Restore fscreate context from ctx, and free it. */
void
lu_util_fscreate_restore(char *ctx)
{
if (is_selinux_enabled() > 0) {
(void)setfscreatecon(ctx);
if (ctx)
freecon(ctx);
}
}
/* Set fscreate context from context of fd. Use path only for diagnostics. */
gboolean
lu_util_fscreate_from_fd(int fd, const char *path, struct lu_error **error)
{
if (is_selinux_enabled() > 0) {
char *ctx;
if (fgetfilecon(fd, &ctx) < 0) {
lu_error_new(error, lu_error_stat,
_("couldn't get security context of "
"`%s': %s"), path, strerror(errno));
return FALSE;
}
if (setfscreatecon(ctx) < 0) {
lu_error_new(error, lu_error_generic,
_("couldn't set default security context "
"to `%s': %s"), ctx, strerror(errno));
freecon(ctx);
return FALSE;
}
freecon(ctx);
}
return TRUE;
}
/* Set fscreate context from context of file. */
gboolean
lu_util_fscreate_from_file(const char *file, struct lu_error **error)
{
if (is_selinux_enabled() > 0) {
char *ctx;
if (getfilecon(file, &ctx) < 0) {
lu_error_new(error, lu_error_stat,
_("couldn't get security context of "
"`%s': %s"), file, strerror(errno));
return FALSE;
}
if (setfscreatecon(ctx) < 0) {
lu_error_new(error, lu_error_generic,
_("couldn't set default security context "
"to `%s': %s"), ctx, strerror(errno));
freecon(ctx);
return FALSE;
}
freecon(ctx);
}
return TRUE;
}
/* Set fscreate context from context of file, not resolving it if it is a
symlink. */
gboolean
lu_util_fscreate_from_lfile(const char *file, struct lu_error **error)
{
if (is_selinux_enabled() > 0) {
char *ctx;
if (lgetfilecon(file, &ctx) < 0) {
lu_error_new(error, lu_error_stat,
_("couldn't get security context of "
"`%s': %s"), file, strerror(errno));
return FALSE;
}
if (setfscreatecon(ctx) < 0) {
lu_error_new(error, lu_error_generic,
_("couldn't set default security context "
"to `%s': %s"), ctx, strerror(errno));
freecon(ctx);
return FALSE;
}
freecon(ctx);
}
return TRUE;
}
/* Set fscreate context for creating a file at path, with file type specified
by mode. */
gboolean
lu_util_fscreate_for_path(const char *path, mode_t mode,
struct lu_error **error)
{
if (is_selinux_enabled() > 0) {
char *ctx;
struct selabel_handle *label_handle = NULL;
label_handle = selabel_open(SELABEL_CTX_FILE, NULL, 0);
if (!label_handle) {
lu_error_new(error, lu_error_open,
_("couldn't obtain selabel file "
"context handle: %s"),
strerror(errno));
return FALSE;
}
if (selabel_lookup(label_handle, &ctx, path, mode) < 0) {
if (errno == ENOENT)
ctx = NULL;
else {
lu_error_new(error, lu_error_stat,
_("couldn't determine security "
"context for `%s': %s"), path,
strerror(errno));
selabel_close(label_handle);
return FALSE;
}
}
selabel_close(label_handle);
if (setfscreatecon(ctx) < 0) {
lu_error_new(error, lu_error_generic,
_("couldn't set default security context "
"to `%s': %s"),
ctx != NULL ? ctx : "<<none>>",
strerror(errno));
freecon(ctx);
return FALSE;
}
freecon(ctx);
}
return TRUE;
}
#endif
/* Append a copy of VALUES to DEST */
void
lu_util_append_values(GValueArray *dest, GValueArray *values)
{
size_t i;
for (i = 0; i < values->n_values; i++)
g_value_array_append(dest, g_value_array_get_nth(values, i));
}